feat: 文件转换逻辑延迟到读取文件的时候 see #218

This commit is contained in:
涵曦 2024-10-09 01:40:44 +08:00
parent e25e1748c4
commit 9d9939be9f
4 changed files with 128 additions and 97 deletions

View File

@ -98,7 +98,7 @@ class Config:
"XIAOMUSIC_ACTIVE_CMD", "XIAOMUSIC_ACTIVE_CMD",
"play,set_random_play,playlocal,play_music_list,play_music_list_index,stop_after_minute,stop", "play,set_random_play,playlocal,play_music_list,play_music_list_index,stop_after_minute,stop",
) )
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir") exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir,tmp")
music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10")) music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10"))
disable_httpauth: bool = ( disable_httpauth: bool = (
os.getenv("XIAOMUSIC_DISABLE_HTTPAUTH", "true").lower() == "true" os.getenv("XIAOMUSIC_DISABLE_HTTPAUTH", "true").lower() == "true"

View File

@ -24,7 +24,7 @@ from fastapi import (
status, status,
) )
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse from fastapi.responses import RedirectResponse, StreamingResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel from pydantic import BaseModel
@ -33,12 +33,16 @@ from starlette.responses import FileResponse, Response
from xiaomusic import __version__ from xiaomusic import __version__
from xiaomusic.utils import ( from xiaomusic.utils import (
convert_file_to_mp3,
deepcopy_data_no_sensitive_info, deepcopy_data_no_sensitive_info,
download_one_music, download_one_music,
download_playlist, download_playlist,
downloadfile, downloadfile,
get_latest_version, get_latest_version,
is_mp3,
remove_common_prefix, remove_common_prefix,
remove_id3_tags,
try_add_access_control_param,
) )
xiaomusic = None xiaomusic = None
@ -507,6 +511,26 @@ async def music_file(request: Request, file_path: str, key: str = "", code: str
if not os.path.exists(absolute_file_path): if not os.path.exists(absolute_file_path):
raise HTTPException(status_code=404, detail="File not found") raise HTTPException(status_code=404, detail="File not found")
# 移除MP3 ID3 v2标签和填充减少播放前延迟
if config.remove_id3tag and is_mp3(file_path):
log.info(f"remove_id3tag:{config.remove_id3tag}, is_mp3:True ")
temp_mp3_file = remove_id3_tags(absolute_file_path, config)
if temp_mp3_file:
log.info(f"ID3 tag removed {absolute_file_path} to {temp_mp3_file}")
url = try_add_access_control_param(config, f"/music/{temp_mp3_file}")
return RedirectResponse(url=url)
else:
log.info(f"No ID3 tag remove needed: {absolute_file_path}")
if config.convert_to_mp3 and not is_mp3(file_path):
temp_mp3_file = convert_file_to_mp3(absolute_file_path, config)
if temp_mp3_file:
log.info(f"Converted file: {absolute_file_path} to {temp_mp3_file}")
url = try_add_access_control_param(config, f"/music/{temp_mp3_file}")
return RedirectResponse(url=url)
else:
log.warning(f"Failed to convert file to MP3 format: {absolute_file_path}")
file_size = os.path.getsize(absolute_file_path) file_size = os.path.getsize(absolute_file_path)
range_start, range_end = 0, file_size - 1 range_start, range_end = 0, file_size - 1

View File

@ -17,6 +17,7 @@ import shutil
import string import string
import subprocess import subprocess
import tempfile import tempfile
import urllib.parse
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
@ -38,6 +39,8 @@ from requests.utils import cookiejar_from_dict
from xiaomusic.const import SUPPORT_MUSIC_TYPE from xiaomusic.const import SUPPORT_MUSIC_TYPE
log = logging.getLogger(__package__)
cc = OpenCC("t2s") # convert from Traditional Chinese to Simplified Chinese cc = OpenCC("t2s") # convert from Traditional Chinese to Simplified Chinese
@ -263,7 +266,7 @@ async def _get_web_music_duration(session, url, ffmpeg_location, start=0, end=50
m = mutagen.File(tmp) m = mutagen.File(tmp)
duration = m.info.length duration = m.info.length
except Exception as e: except Exception as e:
logging.error(f"Error _get_web_music_duration: {e}") log.error(f"Error _get_web_music_duration: {e}")
return duration return duration
@ -295,7 +298,7 @@ async def get_web_music_duration(url, ffmpeg_location="./ffmpeg/bin"):
session, url, ffmpeg_location, start=0, end=3000 session, url, ffmpeg_location, start=0, end=3000
) )
except Exception as e: except Exception as e:
logging.error(f"Error get_web_music_duration: {e}") log.error(f"Error get_web_music_duration: {e}")
return duration, url return duration, url
@ -313,7 +316,7 @@ async def get_local_music_duration(filename, ffmpeg_location="./ffmpeg/bin"):
m = await loop.run_in_executor(None, mutagen.File, filename) m = await loop.run_in_executor(None, mutagen.File, filename)
duration = m.info.length duration = m.info.length
except Exception as e: except Exception as e:
logging.error(f"Error getting local music {filename} duration: {e}") log.error(f"Error getting local music {filename} duration: {e}")
return duration return duration
@ -397,48 +400,76 @@ def no_padding(info):
return 0 return 0
def remove_id3_tags(file_path): def get_temp_dir(music_path: str):
audio = MP3(file_path, ID3=ID3) # 指定临时文件的目录为 music_path 目录下的 tmp 文件夹
change = False temp_dir = os.path.join(music_path, "tmp")
if not os.path.exists(temp_dir):
os.makedirs(temp_dir) # 确保目录存在
return temp_dir
def remove_id3_tags(input_file: str, config) -> str:
music_path = config.music_path
temp_dir = get_temp_dir(music_path)
# 构造新文件的路径
out_file_name = os.path.splitext(os.path.basename(input_file))[0]
out_file_path = os.path.join(temp_dir, f"{out_file_name}.mp3")
relative_path = os.path.relpath(out_file_path, music_path)
# 路径相同的情况
input_absolute_path = os.path.abspath(input_file)
output_absolute_path = os.path.abspath(out_file_path)
if input_absolute_path == output_absolute_path:
log.info(f"File {input_file} = {out_file_path} . Skipping remove_id3_tags.")
return None
# 检查目标文件是否存在
if os.path.exists(out_file_path):
log.info(f"File {out_file_path} already exists. Skipping remove_id3_tags.")
return relative_path
audio = MP3(input_file, ID3=ID3)
# 检查是否存在ID3 v2.3或v2.4标签 # 检查是否存在ID3 v2.3或v2.4标签
if audio.tags and ( if audio.tags and (
audio.tags.version == (2, 3, 0) or audio.tags.version == (2, 4, 0) audio.tags.version == (2, 3, 0) or audio.tags.version == (2, 4, 0)
): ):
# 构造新文件的路径 # 拷贝文件
new_file_path = file_path + ".bak" shutil.copy(input_file, out_file_path)
outaudio = MP3(out_file_path, ID3=ID3)
# 备份原始文件为新文件
shutil.copy(file_path, new_file_path)
# 删除ID3标签 # 删除ID3标签
audio.delete() outaudio.delete()
# 删除padding
audio.save(padding=no_padding)
# 保存修改后的文件 # 保存修改后的文件
audio.save() outaudio.save(padding=no_padding)
log.info(f"File {out_file_path} remove_id3_tags ok.")
return relative_path
change = True return relative_path
return change
def convert_file_to_mp3(input_file: str, ffmpeg_location: str, music_path: str) -> str: def convert_file_to_mp3(input_file: str, config) -> str:
""" music_path = config.music_path
Convert the music file to MP3 format and return the path of the temporary MP3 file. temp_dir = get_temp_dir(music_path)
"""
# 指定临时文件的目录为 music_path 目录下的 tmp 文件夹
temp_dir = os.path.join(music_path, "tmp")
if not os.path.exists(temp_dir):
os.makedirs(temp_dir) # 确保目录存在
out_file_name = os.path.splitext(os.path.basename(input_file))[0] out_file_name = os.path.splitext(os.path.basename(input_file))[0]
out_file_path = os.path.join(temp_dir, f"{out_file_name}.mp3") out_file_path = os.path.join(temp_dir, f"{out_file_name}.mp3")
relative_path = os.path.relpath(out_file_path, music_path)
# 路径相同的情况
input_absolute_path = os.path.abspath(input_file)
output_absolute_path = os.path.abspath(out_file_path)
if input_absolute_path == output_absolute_path:
log.info(f"File {input_file} = {out_file_path} . Skipping convert_file_to_mp3.")
return None
# 检查目标文件是否存在
if os.path.exists(out_file_path):
log.info(f"File {out_file_path} already exists. Skipping convert_file_to_mp3.")
return relative_path
command = [ command = [
os.path.join(ffmpeg_location, "ffmpeg"), os.path.join(config.ffmpeg_location, "ffmpeg"),
"-i", "-i",
input_file, input_file,
"-f", "-f",
@ -451,10 +482,10 @@ def convert_file_to_mp3(input_file: str, ffmpeg_location: str, music_path: str)
try: try:
subprocess.run(command, check=True) subprocess.run(command, check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
logging.exception(f"Error during conversion: {e}") log.exception(f"Error during conversion: {e}")
return None return None
relative_path = os.path.relpath(out_file_path, music_path) log.info(f"File {input_file} to {out_file_path} convert_file_to_mp3 ok.")
return relative_path return relative_path
@ -567,7 +598,7 @@ def _save_picture(picture_data, save_root, file_path):
try: try:
_resize_save_image(picture_data, picture_path) _resize_save_image(picture_data, picture_path)
except Exception as e: except Exception as e:
logging.exception(f"Error _resize_save_image: {e}") log.exception(f"Error _resize_save_image: {e}")
return picture_path return picture_path
@ -704,7 +735,7 @@ async def download_playlist(config, url, dirname):
sbp_args += (url,) sbp_args += (url,)
cmd = " ".join(sbp_args) cmd = " ".join(sbp_args)
logging.info(f"download_playlist: {cmd}") log.info(f"download_playlist: {cmd}")
download_proc = await asyncio.create_subprocess_exec(*sbp_args) download_proc = await asyncio.create_subprocess_exec(*sbp_args)
return download_proc return download_proc
@ -737,7 +768,7 @@ async def download_one_music(config, url, name=""):
sbp_args += (url,) sbp_args += (url,)
cmd = " ".join(sbp_args) cmd = " ".join(sbp_args)
logging.info(f"download_one_music: {cmd}") log.info(f"download_one_music: {cmd}")
download_proc = await asyncio.create_subprocess_exec(*sbp_args) download_proc = await asyncio.create_subprocess_exec(*sbp_args)
return download_proc return download_proc
@ -766,7 +797,7 @@ def remove_common_prefix(directory):
# 获取所有文件的前缀 # 获取所有文件的前缀
common_prefix = _longest_common_prefix(files) common_prefix = _longest_common_prefix(files)
logging.info(f'Common prefix identified: "{common_prefix}"') log.info(f'Common prefix identified: "{common_prefix}"')
for filename in files: for filename in files:
if filename == common_prefix: if filename == common_prefix:
@ -781,4 +812,33 @@ def remove_common_prefix(directory):
# 重命名文件 # 重命名文件
os.rename(old_file_path, new_file_path) os.rename(old_file_path, new_file_path)
logging.debug(f'Renamed: "{filename}" to "{new_filename}"') log.debug(f'Renamed: "{filename}" to "{new_filename}"')
def try_add_access_control_param(config, url):
if config.disable_httpauth:
return url
url_parts = urllib.parse.urlparse(url)
file_path = urllib.parse.unquote(url_parts.path)
correct_code = hashlib.sha256(
(file_path + config.httpauth_username + config.httpauth_password).encode(
"utf-8"
)
).hexdigest()
log.debug(f"rewrite url: [{file_path}, {correct_code}]")
# make new url
parsed_get_args = dict(urllib.parse.parse_qsl(url_parts.query))
parsed_get_args.update({"code": correct_code})
encoded_get_args = urllib.parse.urlencode(parsed_get_args, doseq=True)
new_url = urllib.parse.ParseResult(
url_parts.scheme,
url_parts.netloc,
url_parts.path,
url_parts.params,
encoded_get_args,
url_parts.fragment,
).geturl()
return new_url

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import asyncio import asyncio
import copy import copy
import hashlib
import json import json
import logging import logging
import math import math
@ -39,7 +38,6 @@ from xiaomusic.plugin import PluginManager
from xiaomusic.utils import ( from xiaomusic.utils import (
Metadata, Metadata,
chinese_to_number, chinese_to_number,
convert_file_to_mp3,
custom_sort_key, custom_sort_key,
deepcopy_data_no_sensitive_info, deepcopy_data_no_sensitive_info,
extract_audio_metadata, extract_audio_metadata,
@ -47,12 +45,11 @@ from xiaomusic.utils import (
fuzzyfinder, fuzzyfinder,
get_local_music_duration, get_local_music_duration,
get_web_music_duration, get_web_music_duration,
is_mp3,
list2str, list2str,
parse_cookie_string, parse_cookie_string,
parse_str_to_dict, parse_str_to_dict,
remove_id3_tags,
traverse_music_directory, traverse_music_directory,
try_add_access_control_param,
) )
@ -108,36 +105,6 @@ class XiaoMusic:
if self.config.conf_path == self.music_path: if self.config.conf_path == self.music_path:
self.log.warning("配置文件目录和音乐目录建议设置为不同的目录") self.log.warning("配置文件目录和音乐目录建议设置为不同的目录")
def try_add_access_control_param(self, url):
if self.config.disable_httpauth:
return url
url_parts = urllib.parse.urlparse(url)
file_path = urllib.parse.unquote(url_parts.path)
correct_code = hashlib.sha256(
(
file_path
+ self.config.httpauth_username
+ self.config.httpauth_password
).encode("utf-8")
).hexdigest()
self.log.debug(f"rewrite url: [{file_path}, {correct_code}]")
# make new url
parsed_get_args = dict(urllib.parse.parse_qsl(url_parts.query))
parsed_get_args.update({"code": correct_code})
encoded_get_args = urllib.parse.urlencode(parsed_get_args, doseq=True)
new_url = urllib.parse.ParseResult(
url_parts.scheme,
url_parts.netloc,
url_parts.path,
url_parts.params,
encoded_get_args,
url_parts.fragment,
).geturl()
return new_url
def init_config(self): def init_config(self):
self.music_path = self.config.music_path self.music_path = self.config.music_path
self.download_path = self.config.download_path self.download_path = self.config.download_path
@ -158,8 +125,6 @@ class XiaoMusic:
self.active_cmd = self.config.active_cmd.split(",") self.active_cmd = self.config.active_cmd.split(",")
self.exclude_dirs = set(self.config.exclude_dirs.split(",")) self.exclude_dirs = set(self.config.exclude_dirs.split(","))
self.music_path_depth = self.config.music_path_depth self.music_path_depth = self.config.music_path_depth
self.remove_id3tag = self.config.remove_id3tag
self.convert_to_mp3 = self.config.convert_to_mp3
self.continue_play = self.config.continue_play self.continue_play = self.config.continue_play
def update_devices(self): def update_devices(self):
@ -439,7 +404,8 @@ class XiaoMusic:
if picture.startswith("/"): if picture.startswith("/"):
picture = picture[1:] picture = picture[1:]
encoded_name = urllib.parse.quote(picture) encoded_name = urllib.parse.quote(picture)
tags["picture"] = self.try_add_access_control_param( tags["picture"] = try_add_access_control_param(
self.config,
f"{self.hostname}:{self.public_port}/picture/{encoded_name}", f"{self.hostname}:{self.public_port}/picture/{encoded_name}",
) )
return tags return tags
@ -451,26 +417,6 @@ class XiaoMusic:
return url return url
filename = self.get_filename(name) filename = self.get_filename(name)
# 移除MP3 ID3 v2标签和填充减少播放前延迟
if self.remove_id3tag and is_mp3(filename):
self.log.info(f"remove_id3tag:{self.remove_id3tag}, is_mp3:True ")
change = remove_id3_tags(filename)
if change:
self.log.info("ID3 tag removed, orgin mp3 file saved as bak")
else:
self.log.info("No ID3 tag remove needed")
# 如果开启了MP3转换功能且文件不是MP3格式则进行转换
if self.convert_to_mp3 and not is_mp3(filename):
self.log.info(f"convert_to_mp3 is enabled. Checking file: {filename}")
temp_mp3_file = convert_file_to_mp3(
filename, self.config.ffmpeg_location, self.config.music_path
)
if temp_mp3_file:
self.log.info(f"Converted file: {filename} to {temp_mp3_file}")
filename = temp_mp3_file
else:
self.log.warning(f"Failed to convert file to MP3 format: {filename}")
# 构造音乐文件的URL # 构造音乐文件的URL
if filename.startswith(self.config.music_path): if filename.startswith(self.config.music_path):
@ -482,7 +428,8 @@ class XiaoMusic:
self.log.info(f"get_music_url local music. name:{name}, filename:{filename}") self.log.info(f"get_music_url local music. name:{name}, filename:{filename}")
encoded_name = urllib.parse.quote(filename) encoded_name = urllib.parse.quote(filename)
return self.try_add_access_control_param( return try_add_access_control_param(
self.config,
f"{self.hostname}:{self.public_port}/music/{encoded_name}", f"{self.hostname}:{self.public_port}/music/{encoded_name}",
) )