feat: 文件转换逻辑延迟到读取文件的时候 see #218
This commit is contained in:
parent
e25e1748c4
commit
9d9939be9f
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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}",
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user