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",
"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"

View File

@ -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

View File

@ -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

View File

@ -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}",
)