diff --git a/Dockerfile b/Dockerfile index aedcd4b..a70aac0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,9 @@ COPY install_dependencies.sh . RUN bash install_dependencies.sh FROM python:3.10-slim +RUN pip install pydub +RUN python3 -m venv /app/.venv +RUN /app/.venv/bin/pip install pydub WORKDIR /app COPY --from=builder /app/.venv /app/.venv COPY --from=builder /app/ffmpeg /app/ffmpeg diff --git a/config-example.json b/config-example.json index 1444065..36c11e2 100644 --- a/config-example.json +++ b/config-example.json @@ -12,7 +12,7 @@ "public_port": 0, "proxy": null, "search_prefix": "bilisearch:", - "ffmpeg_location": "./ffmpeg/bin", + "ffmpeg_location": "./ffmpeg/bin/ffmpeg", "active_cmd": "play,set_random_play,playlocal,play_music_list,stop", "exclude_dirs": "@eaDir", "music_path_depth": 10, @@ -77,5 +77,6 @@ }, "enable_force_stop": false, "devices": {}, - "group_list": "" -} \ No newline at end of file + "group_list": "", + "convert_to_mp3": false +} diff --git a/xiaomusic/config.py b/xiaomusic/config.py index cac3e5a..33544d9 100644 --- a/xiaomusic/config.py +++ b/xiaomusic/config.py @@ -83,7 +83,7 @@ class Config: search_prefix: str = os.getenv( "XIAOMUSIC_SEARCH", "bilisearch:" ) # "bilisearch:" or "ytsearch:" - ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin") + ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin/ffmpeg") active_cmd: str = os.getenv( "XIAOMUSIC_ACTIVE_CMD", "play,set_random_play,playlocal,play_music_list,stop" ) @@ -136,6 +136,9 @@ class Config: remove_id3tag: bool = ( os.getenv("XIAOMUSIC_REMOVE_ID3TAG", "false").lower() == "true" ) + convert_to_mp3: bool = ( + os.getenv("CONVERT_TO_MP3", "false").lower() == "true" + ) delay_sec: int = int(os.getenv("XIAOMUSIC_DELAY_SEC", 3)) # 下一首歌延迟播放秒数 def append_keyword(self, keys, action): diff --git a/xiaomusic/convert_to_mp3.py b/xiaomusic/convert_to_mp3.py new file mode 100644 index 0000000..959f3b3 --- /dev/null +++ b/xiaomusic/convert_to_mp3.py @@ -0,0 +1,59 @@ +# convert_to_mp3.py +import os +import subprocess +import tempfile +from pydub import AudioSegment +from pydub.playback import play +from xiaomusic.config import Config + +class Convert_To_MP3: + def __init__(self, config: Config): + self.config = config + self.music_path = self.config.music_path + self.ffmpeg_location = self.config.ffmpeg_location + + @staticmethod + def convert_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) # 确保目录存在 + + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp3', dir=temp_dir) + temp_file.close() + temp_file_path = temp_file.name + + command = [ + ffmpeg_location, + '-i', input_file, + '-f', 'mp3', + '-y', + temp_file_path + ] + + try: + subprocess.run(command, check=True) + except subprocess.CalledProcessError as e: + print(f"Error during conversion: {e}") + return None + + return temp_file_path + + @classmethod + def convert_and_play(cls, input_file: str, ffmpeg_location: str): + """ + 将音乐文件转码为 MP3 格式,播放,然后不删除临时文件,依赖于 xiaomusic 启动时的清理逻辑。 + """ + temp_mp3_file = cls.convert_to_mp3(input_file, ffmpeg_location, cls.music_path) + if temp_mp3_file: + try: + # 假设 xiaomusic_playmusic 是一个播放 MP3 文件的函数 + cls.xiaomusic.xiaomusic_playmusic(temp_mp3_file) + finally: + # 此处不再删除临时文件,依赖 xiaomusic 的清理逻辑 + pass + else: + print("Conversion failed") \ No newline at end of file diff --git a/xiaomusic/static/setting.html b/xiaomusic/static/setting.html index 07208de..30af917 100644 --- a/xiaomusic/static/setting.html +++ b/xiaomusic/static/setting.html @@ -98,7 +98,13 @@ var vConsole = new window.VConsole(); + + + diff --git a/xiaomusic/utils.py b/xiaomusic/utils.py index e1810c7..aa3b512 100644 --- a/xiaomusic/utils.py +++ b/xiaomusic/utils.py @@ -12,6 +12,7 @@ import re import shutil import string import tempfile +import subprocess from collections.abc import AsyncIterator from http.cookies import SimpleCookie from urllib.parse import urlparse diff --git a/xiaomusic/xiaomusic.py b/xiaomusic/xiaomusic.py index 94b7f39..140667c 100644 --- a/xiaomusic/xiaomusic.py +++ b/xiaomusic/xiaomusic.py @@ -45,6 +45,8 @@ from xiaomusic.utils import ( traverse_music_directory, ) +from xiaomusic.convert_to_mp3 import Convert_To_MP3 + class XiaoMusic: def __init__(self, config: Config): @@ -64,6 +66,12 @@ class XiaoMusic: self.devices = {} # key 为 did self.running_task = [] + # 在程序启动时调用清理函数 + self.cleanup_old_temp_files() + + self.convert_to_mp3 = self.config.convert_to_mp3 + self.ffmpeg_location = self.config.ffmpeg_location + self.music_path = self.config.music_path # 初始化配置 self.init_config() @@ -107,6 +115,7 @@ class XiaoMusic: 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 def update_devices(self): self.device_id_did = {} # key 为 device_id @@ -318,6 +327,23 @@ class XiaoMusic: return filename return "" + def cleanup_old_temp_files(self): + """ + 清理在 /tmp 目录下旧的临时 MP3 文件。 + """ + temp_dir = '/tmp' # 临时文件存储的目录 + file_ext = '.mp3' # 临时文件的扩展名 + try: + for filename in os.listdir(temp_dir): + if filename.endswith(file_ext): + file_path = os.path.join(temp_dir, filename) + # 如果文件是超过一天的旧文件,则删除它 + if (time.time() - os.path.getmtime(file_path)) > 86400: + os.remove(file_path) + self.log.info(f"Deleted old temporary file: {file_path}") + except Exception as e: + self.log.error(f"Failed to cleanup old temp files: {e}") + # 判断本地音乐是否存在,网络歌曲不判断 def is_music_exist(self, name): if name not in self.all_music: @@ -381,15 +407,37 @@ class XiaoMusic: 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 = self.convert_file_to_mp3(filename) + if temp_mp3_file: + # 转换成功后,修改文件名为music_path/tmp下的相对路径 + relative_path = os.path.relpath(temp_mp3_file, self.config.music_path) + self.log.info(f"Converted file: {temp_mp3_file} to {relative_path}") + filename = relative_path + else: + self.log.warning(f"Failed to convert file to MP3 format: {filename}") + return "" # 转换失败,返回空字符串表示无法获取播放URL + + # 构造音乐文件的URL filename = filename.replace("\\", "/") if filename.startswith(self.config.music_path): - filename = filename[len(self.config.music_path) :] + filename = filename[len(self.config.music_path):] if filename.startswith("/"): filename = filename[1:] - self.log.info(f"get_music_url local music. name:{name}, filename:{filename}") encoded_name = urllib.parse.quote(filename) return f"http://{self.hostname}:{self.public_port}/music/{encoded_name}" + def convert_file_to_mp3(self, input_file): + """ + Convert the file to MP3 format using convert_to_mp3.py. + """ + # 创建 Convert_To_MP3 类的实例,只传递 config 对象 + converter = Convert_To_MP3(self.config) + # 调用静态方法 convert_to_mp3,并传递所需的文件路径和 ffmpeg 位置 + return converter.convert_to_mp3(input_file, self.ffmpeg_location, self.music_path) + # 获取目录下所有歌曲,生成随机播放列表 def _gen_all_music_list(self): self.all_music = {} @@ -404,7 +452,7 @@ class XiaoMusic: if len(files) == 0: continue if dir_name == os.path.basename(self.music_path): - dir_name = "其他" + dir_name = "默认" if self.music_path != self.download_path and dir_name == os.path.basename( self.download_path ):