mirror of
https://github.com/hanxi/xiaomusic.git
synced 2026-05-14 10:47:51 +08:00
重构: 网络歌曲获取播放时长问题
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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 音频
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user