1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2026-05-14 10:47:51 +08:00

重构: 网络歌曲获取播放时长问题

This commit is contained in:
涵曦
2026-01-16 17:07:30 +08:00
parent 02f6862b76
commit 2a1a13dea6
8 changed files with 187 additions and 208 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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