diff --git a/xiaomusic/config.py b/xiaomusic/config.py index ca0cb11..c0de842 100644 --- a/xiaomusic/config.py +++ b/xiaomusic/config.py @@ -98,7 +98,7 @@ class Config: "XIAOMUSIC_ACTIVE_CMD", "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")) disable_httpauth: bool = ( os.getenv("XIAOMUSIC_DISABLE_HTTPAUTH", "true").lower() == "true" diff --git a/xiaomusic/httpserver.py b/xiaomusic/httpserver.py index 7253033..3099cec 100644 --- a/xiaomusic/httpserver.py +++ b/xiaomusic/httpserver.py @@ -24,7 +24,7 @@ from fastapi import ( status, ) 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.staticfiles import StaticFiles from pydantic import BaseModel @@ -33,12 +33,16 @@ from starlette.responses import FileResponse, Response from xiaomusic import __version__ from xiaomusic.utils import ( + convert_file_to_mp3, deepcopy_data_no_sensitive_info, download_one_music, download_playlist, downloadfile, get_latest_version, + is_mp3, remove_common_prefix, + remove_id3_tags, + try_add_access_control_param, ) 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): 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) range_start, range_end = 0, file_size - 1 diff --git a/xiaomusic/utils.py b/xiaomusic/utils.py index a442f3b..2fe4c7d 100644 --- a/xiaomusic/utils.py +++ b/xiaomusic/utils.py @@ -17,6 +17,7 @@ import shutil import string import subprocess import tempfile +import urllib.parse from collections.abc import AsyncIterator from dataclasses import asdict, dataclass from http.cookies import SimpleCookie @@ -38,6 +39,8 @@ from requests.utils import cookiejar_from_dict from xiaomusic.const import SUPPORT_MUSIC_TYPE +log = logging.getLogger(__package__) + 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) duration = m.info.length except Exception as e: - logging.error(f"Error _get_web_music_duration: {e}") + log.error(f"Error _get_web_music_duration: {e}") 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 ) 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 @@ -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) duration = m.info.length 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 @@ -397,48 +400,76 @@ def no_padding(info): return 0 -def remove_id3_tags(file_path): - audio = MP3(file_path, ID3=ID3) - change = False +def get_temp_dir(music_path: str): + # 指定临时文件的目录为 music_path 目录下的 tmp 文件夹 + 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标签 if audio.tags and ( audio.tags.version == (2, 3, 0) or audio.tags.version == (2, 4, 0) ): - # 构造新文件的路径 - new_file_path = file_path + ".bak" - - # 备份原始文件为新文件 - shutil.copy(file_path, new_file_path) - + # 拷贝文件 + shutil.copy(input_file, out_file_path) + outaudio = MP3(out_file_path, ID3=ID3) # 删除ID3标签 - audio.delete() - - # 删除padding - audio.save(padding=no_padding) - + outaudio.delete() # 保存修改后的文件 - audio.save() + outaudio.save(padding=no_padding) + log.info(f"File {out_file_path} remove_id3_tags ok.") + return relative_path - change = True - - return change + return relative_path -def convert_file_to_mp3(input_file: str, ffmpeg_location: str, music_path: str) -> str: - """ - Convert the music file to MP3 format and return the path of the temporary MP3 file. - """ - # 指定临时文件的目录为 music_path 目录下的 tmp 文件夹 - temp_dir = os.path.join(music_path, "tmp") - if not os.path.exists(temp_dir): - os.makedirs(temp_dir) # 确保目录存在 +def convert_file_to_mp3(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 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 = [ - os.path.join(ffmpeg_location, "ffmpeg"), + os.path.join(config.ffmpeg_location, "ffmpeg"), "-i", input_file, "-f", @@ -451,10 +482,10 @@ def convert_file_to_mp3(input_file: str, ffmpeg_location: str, music_path: str) try: subprocess.run(command, check=True) except subprocess.CalledProcessError as e: - logging.exception(f"Error during conversion: {e}") + log.exception(f"Error during conversion: {e}") 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 @@ -567,7 +598,7 @@ def _save_picture(picture_data, save_root, file_path): try: _resize_save_image(picture_data, picture_path) except Exception as e: - logging.exception(f"Error _resize_save_image: {e}") + log.exception(f"Error _resize_save_image: {e}") return picture_path @@ -704,7 +735,7 @@ async def download_playlist(config, url, dirname): sbp_args += (url,) 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) return download_proc @@ -737,7 +768,7 @@ async def download_one_music(config, url, name=""): sbp_args += (url,) 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) return download_proc @@ -766,7 +797,7 @@ def remove_common_prefix(directory): # 获取所有文件的前缀 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: if filename == common_prefix: @@ -781,4 +812,33 @@ def remove_common_prefix(directory): # 重命名文件 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 diff --git a/xiaomusic/xiaomusic.py b/xiaomusic/xiaomusic.py index aff5d6f..35b2576 100644 --- a/xiaomusic/xiaomusic.py +++ b/xiaomusic/xiaomusic.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import asyncio import copy -import hashlib import json import logging import math @@ -39,7 +38,6 @@ from xiaomusic.plugin import PluginManager from xiaomusic.utils import ( Metadata, chinese_to_number, - convert_file_to_mp3, custom_sort_key, deepcopy_data_no_sensitive_info, extract_audio_metadata, @@ -47,12 +45,11 @@ from xiaomusic.utils import ( fuzzyfinder, get_local_music_duration, get_web_music_duration, - is_mp3, list2str, parse_cookie_string, parse_str_to_dict, - remove_id3_tags, traverse_music_directory, + try_add_access_control_param, ) @@ -108,36 +105,6 @@ class XiaoMusic: if self.config.conf_path == self.music_path: 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): self.music_path = self.config.music_path self.download_path = self.config.download_path @@ -158,8 +125,6 @@ class XiaoMusic: self.active_cmd = self.config.active_cmd.split(",") self.exclude_dirs = set(self.config.exclude_dirs.split(",")) 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 def update_devices(self): @@ -439,7 +404,8 @@ class XiaoMusic: if picture.startswith("/"): picture = picture[1:] 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}", ) return tags @@ -451,26 +417,6 @@ class XiaoMusic: return url 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 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}") 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}", )