From 2a1a13dea6708ae8c7fa34910c8f9b160c22e30d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B6=B5=E6=9B=A6?= Date: Fri, 16 Jan 2026 17:07:30 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E6=AD=8C=E6=9B=B2=E8=8E=B7=E5=8F=96=E6=92=AD=E6=94=BE=E6=97=B6?= =?UTF-8?q?=E9=95=BF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- xiaomusic/api/routers/file.py | 4 +- xiaomusic/api/routers/music.py | 4 +- xiaomusic/auth.py | 2 +- xiaomusic/config.py | 5 + xiaomusic/device_player.py | 4 +- xiaomusic/music_library.py | 176 +++++++++++++++++++++++++++++- xiaomusic/music_url.py | 189 --------------------------------- xiaomusic/xiaomusic.py | 11 -- 8 files changed, 187 insertions(+), 208 deletions(-) delete mode 100644 xiaomusic/music_url.py diff --git a/xiaomusic/api/routers/file.py b/xiaomusic/api/routers/file.py index a431ba4..ab17129 100644 --- a/xiaomusic/api/routers/file.py +++ b/xiaomusic/api/routers/file.py @@ -302,7 +302,7 @@ async def proxy(urlb64: str): log.info(f"代理请求: {url}") parsed_url = urlparse(url) - parsed_url = xiaomusic._music_url_handler.expand_self_url(parsed_url) + parsed_url = xiaomusic._music_library.expand_self_url(parsed_url) if not parsed_url.scheme or not parsed_url.netloc: # Fixed: Use a new exception instance since 'e' from previous block is out of scope invalid_url_exc = ValueError("URL缺少协议或域名") @@ -334,7 +334,7 @@ async def proxy(urlb64: str): "upgrade-insecure-requests": "1", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36", } - if parsed_url.netloc == xiaomusic._music_url_handler.netloc: + if parsed_url.netloc == config.get_self_netloc(): headers["Authorization"] = config.get_basic_auth() return headers diff --git a/xiaomusic/api/routers/music.py b/xiaomusic/api/routers/music.py index 59576e5..ffa672a 100644 --- a/xiaomusic/api/routers/music.py +++ b/xiaomusic/api/routers/music.py @@ -196,7 +196,7 @@ async def musicinfo( name: str, musictag: bool = False, Verifcation=Depends(verification) ): """音乐信息""" - url, _ = await xiaomusic._music_url_handler.get_music_url(name) + url, _ = await xiaomusic._music_library.get_music_url(name) info = { "ret": "OK", "name": name, @@ -216,7 +216,7 @@ async def musicinfos( """批量音乐信息""" ret = [] for music_name in name: - url, _ = await xiaomusic._music_url_handler.get_music_url(music_name) + url, _ = await xiaomusic._music_library.get_music_url(music_name) info = { "name": music_name, "url": url, diff --git a/xiaomusic/auth.py b/xiaomusic/auth.py index d7e2f71..d70afef 100644 --- a/xiaomusic/auth.py +++ b/xiaomusic/auth.py @@ -174,7 +174,7 @@ class AuthManager: "userId": cookies_dict["userId"], "deviceId": self.device_id, } - self.log.info(f"设置token到account:{accout.token}") + self.log.info(f"设置token到account:{account.token}") def get_cookie(self): """获取Cookie diff --git a/xiaomusic/config.py b/xiaomusic/config.py index 1eae3da..7262f81 100644 --- a/xiaomusic/config.py +++ b/xiaomusic/config.py @@ -420,3 +420,8 @@ class Config: credentials = f"{self.httpauth_username}:{self.httpauth_password}" encoded_credentials = base64.b64encode(credentials.encode()).decode() return f"Basic {encoded_credentials}" + + def get_self_netloc(self): + """获取网络地址""" + host = self.hostname.split("//", 1)[1] + return f"{host}:{self.public_port}" diff --git a/xiaomusic/device_player.py b/xiaomusic/device_player.py index 5ab59e2..a89b60a 100644 --- a/xiaomusic/device_player.py +++ b/xiaomusic/device_player.py @@ -355,7 +355,7 @@ class XiaoMusicDevice: self.device.playlist2music[self.device.cur_playlist] = name cur_playlist = self.device.cur_playlist self.log.info(f"cur_music {self.get_cur_music()}") - sec, url = await self.xiaomusic._music_url_handler.get_music_sec_url( + sec, url = await self.xiaomusic._music_library.get_music_sec_url( name, cur_playlist ) await self.group_force_stop_xiaoai() @@ -651,7 +651,7 @@ class XiaoMusicDevice: self.log.info(f"edge-tts 生成的文件路径: {mp3_path}") # 生成播放 URL - url = self.xiaomusic._music_url_handler._get_file_url(mp3_path) + url = self.xiaomusic._music_library._get_file_url(mp3_path) self.log.info(f"TTS 播放 URL: {url}") # 播放 TTS 音频 diff --git a/xiaomusic/music_library.py b/xiaomusic/music_library.py index b9fa5e2..43080e4 100644 --- a/xiaomusic/music_library.py +++ b/xiaomusic/music_library.py @@ -4,6 +4,7 @@ """ import asyncio +import base64 import copy import json import os @@ -12,6 +13,7 @@ import time import urllib.parse from collections import OrderedDict from dataclasses import asdict +from urllib.parse import urlparse from xiaomusic.const import SUPPORT_MUSIC_TYPE from xiaomusic.events import CONFIG_CHANGED @@ -24,6 +26,7 @@ from xiaomusic.utils.music_utils import ( save_picture_by_base64, set_music_tag_to_file, ) +from xiaomusic.utils.network_utils import MusicUrlCache from xiaomusic.utils.system_utils import try_add_access_control_param from xiaomusic.utils.text_utils import custom_sort_key, find_best_match, fuzzyfinder @@ -85,6 +88,9 @@ class MusicLibrary: self._tag_generation_task = False # 标签生成任务标志 self._web_music_duration_cache = {} # 网络音乐时长缓存(仅内存) + # URL处理相关 + self.url_cache = MusicUrlCache() # URL缓存 + def gen_all_music_list(self): """生成所有音乐列表 @@ -669,6 +675,7 @@ class MusicLibrary: f"{self.config.hostname}:{self.config.public_port}/picture/{encoded_name}", ) + # 如果是网络音乐,获取时长 if self.is_web_music(name): try: duration = await self.get_music_duration(name) @@ -745,7 +752,7 @@ class MusicLibrary: # 缓存中没有,获取时长 try: - url = self.all_music[name] + url, _ = await self._get_web_music_url(name) duration, _ = await get_web_music_duration(url, self.config) self.log.info(f"网络音乐 {name} 时长: {duration} 秒") @@ -968,3 +975,170 @@ class MusicLibrary: """ self._web_music_duration_cache = {} self.log.info("已清空网络音乐时长缓存") + + # ==================== URL处理方法 ==================== + + async def get_music_sec_url(self, name, cur_playlist): + """获取歌曲播放时长和播放地址 + + Args: + name: 歌曲名称 + cur_playlist: 当前歌单名称 + Returns: + tuple: (播放时长(秒), 播放地址) + """ + url, origin_url = await self.get_music_url(name) + self.log.info( + f"get_music_sec_url. name:{name} url:{url} origin_url:{origin_url}" + ) + sec = await self.get_music_duration(name) + return sec, url + + async def get_music_url(self, name): + """获取音乐播放地址 + + Args: + name: 歌曲名称 + + Returns: + tuple: (播放地址, 原始地址) - 网络音乐时可能有原始地址 + """ + self.log.info(f"get_music_url name:{name}") + if self.is_web_music(name): + return await self._get_web_music_url(name) + return self._get_local_music_url(name), None + + async def _get_web_music_url(self, name): + """获取网络音乐播放地址 + + Args: + name: 歌曲名称 + + Returns: + tuple: (播放地址, 原始地址) + """ + self.log.info("in _get_web_music_url") + url = self.all_music[name] + self.log.info(f"get_music_url web music. name:{name}, url:{url}") + + # 需要通过API获取真实播放地址 + if self.is_need_use_play_music_api(name): + url = await self._get_url_from_api(name, url) + if not url: + return "", None + + # 是否需要代理 + if self.config.web_music_proxy or url.startswith("self://"): + proxy_url = self._get_proxy_url(url) + return proxy_url, url + + return url, None + + async def _get_url_from_api(self, name, url): + """通过API获取真实播放地址 + + Args: + name: 歌曲名称 + url: 原始URL + + Returns: + str: 真实播放地址,失败返回空字符串 + """ + headers = self._web_music_api[name].get("headers", {}) + url = await self.url_cache.get(url, headers, self.config) + if not url: + self.log.error(f"get_music_url use api fail. name:{name}, url:{url}") + return url + + def _get_proxy_url(self, origin_url): + """获取代理URL + + Args: + origin_url: 原始URL + + Returns: + str: 代理URL + """ + urlb64 = base64.b64encode(origin_url.encode("utf-8")).decode("utf-8") + proxy_url = ( + f"{self.config.hostname}:{self.config.public_port}/proxy?urlb64={urlb64}" + ) + self.log.info(f"Using proxy url: {proxy_url}") + return proxy_url + + def _get_local_music_url(self, name): + """获取本地音乐播放地址 + + Args: + name: 歌曲名称 + + Returns: + str: 本地音乐播放URL + """ + filename = self.get_filename(name) + self.log.info( + f"_get_local_music_url local music. name:{name}, filename:{filename}" + ) + return self._get_file_url(filename) + + def _get_file_url(self, filepath): + """根据文件路径生成可访问的URL + + Args: + filepath: 文件的完整路径 + + Returns: + str: 文件访问URL + """ + filename = filepath + + # 处理文件路径 + if filename.startswith(self.config.music_path): + filename = filename[len(self.config.music_path) :] + filename = filename.replace("\\", "/") + if filename.startswith("/"): + filename = filename[1:] + + self.log.info(f"_get_file_url filepath:{filepath}, filename:{filename}") + + # 构造URL + encoded_name = urlparse.quote(filename) + url = f"{self.config.hostname}:{self.config.public_port}/music/{encoded_name}" + return try_add_access_control_param(self.config, url) + + @staticmethod + async def get_play_url(proxy_url): + """获取播放URL + + Args: + proxy_url: 代理URL + + Returns: + str: 最终重定向的URL + """ + import aiohttp + + async with aiohttp.ClientSession() as session: + async with session.get(proxy_url) as response: + # 获取最终重定向的 URL + return str(response.url) + + def expand_self_url(self, parsed_url): + """扩展self协议URL + + Args: + parsed_url: 解析后的URL对象 + + Returns: + 解析后的URL对象 + """ + if parsed_url.scheme != "self": + return parsed_url + + url = f"{self.config.hostname}:{self.config.public_port}{parsed_url.path}" + if parsed_url.query: + url += f"?{parsed_url.query}" + if parsed_url.fragment: + url += f"#{parsed_url.fragment}" + + return urlparse(url) diff --git a/xiaomusic/music_url.py b/xiaomusic/music_url.py deleted file mode 100644 index b023ba3..0000000 --- a/xiaomusic/music_url.py +++ /dev/null @@ -1,189 +0,0 @@ -"""音乐URL处理模块 - -负责音乐播放地址的获取、代理和时长计算。 -""" - -import base64 -from urllib.parse import urlparse - -from xiaomusic.utils.network_utils import MusicUrlCache -from xiaomusic.utils.system_utils import try_add_access_control_param - - -class MusicUrlHandler: - """音乐URL处理器 - - 负责处理音乐播放地址的获取、代理URL生成和播放时长计算。 - """ - - def __init__( - self, - config, - log, - music_library, - ): - """初始化URL处理器 - - Args: - config: 配置对象 - log: 日志对象 - music_library: 音乐库管理模块 - """ - self.config = config - self.log = log - self.music_library = music_library - self.url_cache = MusicUrlCache() - - @property - def netloc(self): - host = self.config.hostname.split("//", 1)[1] - return f"{host}:{self.config.public_port}" - - async def get_music_sec_url(self, name, cur_playlist): - """获取歌曲播放时长和播放地址 - - Args: - name: 歌曲名称 - cur_playlist: 当前歌单名称 - Returns: - tuple: (播放时长(秒), 播放地址) - """ - url, origin_url = await self.get_music_url(name) - self.log.info( - f"get_music_sec_url. name:{name} url:{url} origin_url:{origin_url}" - ) - sec = await self.music_library.get_music_duration(name) - return sec, url - - async def get_music_url(self, name): - self.log.info(f"get_music_url name:{name}") - """获取音乐播放地址 - - Args: - name: 歌曲名称 - - Returns: - tuple: (播放地址, 原始地址) - 网络音乐时可能有原始地址 - """ - if self.music_library.is_web_music(name): - return await self._get_web_music_url(name) - return self._get_local_music_url(name), None - - async def _get_web_music_url(self, name): - self.log.info("in _get_web_music_url") - """获取网络音乐播放地址 - - Args: - name: 歌曲名称 - - Returns: - tuple: (播放地址, 原始地址) - """ - url = self.music_library.all_music[name] - self.log.info(f"get_music_url web music. name:{name}, url:{url}") - - # 需要通过API获取真实播放地址 - if self.music_library.is_need_use_play_music_api(name): - url = await self._get_url_from_api(name, url) - if not url: - return "", None - - # 是否需要代理 - if self.config.web_music_proxy: - proxy_url = self._get_proxy_url(url) - return proxy_url, url - - return url, None - - async def _get_url_from_api(self, name, url): - """通过API获取真实播放地址 - - Args: - name: 歌曲名称 - url: 原始URL - - Returns: - str: 真实播放地址,失败返回空字符串 - """ - headers = self.music_library._web_music_api[name].get("headers", {}) - url = await self.url_cache.get(url, headers, self.config) - if not url: - self.log.error(f"get_music_url use api fail. name:{name}, url:{url}") - return url - - def _get_proxy_url(self, origin_url): - """获取代理URL - - Args: - origin_url: 原始URL - - Returns: - str: 代理URL - """ - urlb64 = base64.b64encode(origin_url.encode("utf-8")).decode("utf-8") - proxy_url = ( - f"{self.config.hostname}:{self.config.public_port}/proxy?urlb64={urlb64}" - ) - self.log.info(f"Using proxy url: {proxy_url}") - return proxy_url - - def _get_local_music_url(self, name): - """获取本地音乐播放地址 - - Args: - name: 歌曲名称 - - Returns: - str: 本地音乐播放URL - """ - filename = self.music_library.get_filename(name) - self.log.info( - f"_get_local_music_url local music. name:{name}, filename:{filename}" - ) - return self._get_file_url(filename) - - def _get_file_url(self, filepath): - """根据文件路径生成可访问的URL - - Args: - filepath: 文件的完整路径 - - Returns: - str: 文件访问URL - """ - filename = filepath - - # 处理文件路径 - if filename.startswith(self.config.music_path): - filename = filename[len(self.config.music_path) :] - filename = filename.replace("\\", "/") - if filename.startswith("/"): - filename = filename[1:] - - self.log.info(f"_get_file_url filepath:{filepath}, filename:{filename}") - - # 构造URL - encoded_name = urlparse.quote(filename) - url = f"{self.config.hostname}:{self.config.public_port}/music/{encoded_name}" - return try_add_access_control_param(self.config, url) - - @staticmethod - async def get_play_url(proxy_url): - import aiohttp - - async with aiohttp.ClientSession() as session: - async with session.get(proxy_url) as response: - # 获取最终重定向的 URL - return str(response.url) - - def expand_self_url(self, parsed_url): - if parsed_url.scheme != "self": - return parsed_url - - url = f"{self.config.hostname}:{self.config.public_port}{parsed_url.path}" - if parsed_url.query: - url += f"?{parsed_url.query}" - if parsed_url.fragment: - url += f"#{parsed_url.fragment}" - - return urlparse(url) diff --git a/xiaomusic/xiaomusic.py b/xiaomusic/xiaomusic.py index 748b9d3..6d647b6 100644 --- a/xiaomusic/xiaomusic.py +++ b/xiaomusic/xiaomusic.py @@ -24,7 +24,6 @@ from xiaomusic.device_manager import DeviceManager from xiaomusic.events import CONFIG_CHANGED, DEVICE_CONFIG_CHANGED, EventBus from xiaomusic.file_watcher import FileWatcherManager from xiaomusic.music_library import MusicLibrary -from xiaomusic.music_url import MusicUrlHandler from xiaomusic.online_music import OnlineMusicService from xiaomusic.plugin import PluginManager from xiaomusic.utils.network_utils import downloadfile @@ -64,9 +63,6 @@ class XiaoMusic: # 初始化文件监控管理器 self._file_watcher = None - # 初始化音乐URL处理器(延迟初始化,在 init_config 之后) - self._music_url_handler = None - # 初始化在线音乐服务(延迟初始化,在 js_plugin_manager 之后) self._online_music_service = None @@ -123,13 +119,6 @@ class XiaoMusic: # 启动时重新生成一次播放列表 self._music_library.gen_all_music_list() - # 初始化音乐URL处理器(在配置和音乐列表准备好之后) - self._music_url_handler = MusicUrlHandler( - config=self.config, - log=self.log, - music_library=self._music_library, - ) - # 初始化在线音乐服务(在 js_plugin_manager 准备好之后) self._online_music_service = OnlineMusicService( log=self.log,