diff --git a/xiaomusic/api/routers/music.py b/xiaomusic/api/routers/music.py
index 0c08417..3d6c10a 100644
--- a/xiaomusic/api/routers/music.py
+++ b/xiaomusic/api/routers/music.py
@@ -133,16 +133,20 @@ async def get_plugin_source_url(
)
if media_source and media_source.get("url"):
source_url = media_source.get("url")
+ log.info(f"plugin-url 成功解析: {json_data} -> {source_url}")
+ return RedirectResponse(url=source_url)
else:
- source_url = xiaomusic.default_url()
- log.info(f"plugin-url {json_data} {source_url}")
- # 直接重定向到真实URL
- return RedirectResponse(url=source_url)
+ # 没有有效链接时,直接抛出 404 错误!
+ log.warning(f"plugin-url 解析失败(链接为空): {json_data}")
+ raise HTTPException(status_code=404, detail="获取真实音频链接为空")
+
+ except HTTPException:
+ # 允许 HTTPException 继续向上传递,确保 404 能被前线捕获
+ raise
except Exception as e:
log.error(f"获取真实音乐URL失败: {e}")
- # 如果代理获取失败,重定向到原始URL
- source_url = xiaomusic.default_url()
- return RedirectResponse(url=source_url)
+ # 发生其他未知异常时,同样抛出错误
+ raise HTTPException(status_code=404, detail=str(e)) from e
@router.post("/api/play/getMediaSource")
diff --git a/xiaomusic/config.py b/xiaomusic/config.py
index 18add18..d89c7d2 100644
--- a/xiaomusic/config.py
+++ b/xiaomusic/config.py
@@ -96,6 +96,7 @@ class Config:
download_path: str = os.getenv("XIAOMUSIC_DOWNLOAD_PATH", "music/download")
conf_path: str = os.getenv("XIAOMUSIC_CONF_PATH", "conf")
cache_dir: str = os.getenv("XIAOMUSIC_CACHE_DIR", "music/cache")
+ cache_max_size_mb: int = int(os.getenv("XIAOMUSIC_CACHE_MAX_SIZE_MB", "500"))
hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "http://192.168.2.5")
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) # 监听端口
public_port: int = int(os.getenv("XIAOMUSIC_PUBLIC_PORT", 58090)) # 歌曲访问端口
diff --git a/xiaomusic/device_player.py b/xiaomusic/device_player.py
index fd8b809..db50403 100644
--- a/xiaomusic/device_player.py
+++ b/xiaomusic/device_player.py
@@ -70,7 +70,6 @@ class XiaoMusicDevice:
self._duration = 0
self._paused_time = 0
self._play_failed_cnt = 0
- self._url_failed_cnt = 0 # 新增:URL探路失败专属计数器
self._play_list = []
@@ -85,6 +84,8 @@ class XiaoMusicDevice:
self._add_song_timer = None
# TTS 播放定时器
self._tts_timer = None
+ # 用于预缓存下一首的定时器
+ self._prefetch_timer = None
@property
def did(self):
@@ -167,31 +168,83 @@ class XiaoMusicDevice:
"""播放音乐(外部接口)"""
return await self._playmusic(name)
- def update_playlist(self):
- """初始化/更新播放列表"""
- # 没有重置 list 且非初始化
+ def update_playlist(self, force_reshuffle=False):
+ """
+ 初始化或更新播放列表。
+
+ 【核心架构特点】:
+ 1. 状态保持 (Stateful Shuffle):随机模式下,生成一次乱序列表后永久保持,避免反复洗牌导致预缓存断链和歌曲无限循环。
+ 2. 洗牌置顶 (Pin-to-Top):当发生全量重洗时,将当前正在播放的歌曲强行“钉”在列表最顶端(index 0),完美闭环预缓存机制。
+ 3. 增量更新 (Incremental Update):歌单发生变化(如自动追加了歌手新歌)时,不打乱原有播放顺序,仅将新歌洗牌后追加到队尾。
+
+ Args:
+ force_reshuffle (bool): 是否强制彻底重新洗牌(用于切换模式、切换歌单、或一轮播放触底时)
+ """
+ # 1. 兜底保护:如果没有重置 list 且当前歌单在系统里不存在,默认切到"全部"
if self.device.cur_playlist not in self.xiaomusic.music_library.music_list:
self.device.cur_playlist = "全部"
list_name = self.device.cur_playlist
- self._play_list = copy.copy(self.xiaomusic.music_library.music_list[list_name])
+ # 获取大管家(Library)里最新鲜的歌单数据
+ latest_list = self.xiaomusic.music_library.music_list[list_name]
+ # ==========================================
+ # 随机播放模式 (PLAY_TYPE_RND) 的调度
+ # ==========================================
if self.device.play_type == PLAY_TYPE_RND:
- random.shuffle(self._play_list)
- self.log.info(
- f"随机打乱 {list_name} {list2str(self._play_list, self.config.verbose)}"
- )
+ # 判断是否需要【全量重洗牌】的三个条件:
+ # A. 外部明确要求强洗 (force_reshuffle=True)
+ # B. 当前播放列表是空的 (系统刚启动)
+ # C. 当前播放列表和最新的歌单毫无交集 (说明用户切了全新的歌单)
+ if (
+ force_reshuffle
+ or not self._play_list
+ or not set(self._play_list).intersection(set(latest_list))
+ ):
+ self._play_list = copy.copy(latest_list)
+ random.shuffle(self._play_list)
+
+ # 2:洗牌置顶 (Pin-to-Top)
+ # 防止洗牌后当前歌曲位置丢失,导致下一首乱跳和预缓存错位
+ cur_music = self.get_cur_music()
+ if cur_music and cur_music in self._play_list:
+ self._play_list.remove(cur_music)
+ self._play_list.insert(0, cur_music)
+
+ self.log.info(f"彻底重新洗牌 {list_name},并将当前歌曲置顶")
+
+ # 【增量更新牌库】
+ else:
+ # 3:增量更新 (Incremental Update)
+ old_list = self._play_list
+ # A. 剔除云端已经被删除的歌,保留依然存在的歌(绝对不改变它们的相对顺序!)
+ self._play_list = [s for s in old_list if s in latest_list]
+
+ # B. 找出最新歌单里多出来的新歌(比如 auto_add_song 追加进来的)
+ new_songs = [s for s in latest_list if s not in old_list]
+ if new_songs:
+ # 把新来的歌单独洗乱,然后悄悄垫在牌堆的最底下
+ random.shuffle(new_songs)
+ self._play_list.extend(new_songs)
+ self.log.info(
+ f"歌单有更新,保持原顺序并追加了 {len(new_songs)} 首新歌"
+ )
+
+ # ==========================================
+ # 顺序/循环模式的处理
+ # ==========================================
else:
+ self._play_list = copy.copy(latest_list)
is_online = self.xiaomusic.music_library.is_online_music(list_name)
+
+ # 如果是本地目录歌单,且列表都是纯字符串,执行本地特定的字母自然排序
if not is_online and len(self._play_list) > 0:
has_non_str_item = any(
not isinstance(item, str) for item in self._play_list
)
if not has_non_str_item:
self._play_list.sort(key=custom_sort_key)
- self.log.info(
- f"没打乱 {list_name} {list2str(self._play_list, self.config.verbose)}"
- )
+ self.log.info(f"顺序模式更新,不打乱 {list_name}")
async def play(self, name="", search_key=""):
"""播放歌曲(外部接口)"""
@@ -362,6 +415,36 @@ class XiaoMusicDevice:
self._last_cmd = "playlocal"
return await self._play_internal(name=name, search_key="", allow_download=False)
+ async def prefetch_next_song(self, sleep_sec):
+ """延时后台预加载(缓存)下一首歌曲"""
+ if self._prefetch_timer:
+ self._prefetch_timer.cancel()
+
+ async def _do_prefetch():
+ try:
+ await asyncio.sleep(sleep_sec)
+
+ # 拿下一首歌的名字
+ next_music = self.get_next_music()
+ if not next_music:
+ return
+
+ # 如果是网络音乐,触发预下载
+ if self.xiaomusic.music_library.is_web_music(next_music):
+ self.log.info(f"开始后台预先缓存下一首: {next_music}")
+ cur_playlist = self.device.cur_playlist
+ # 巧妙利用我们重构过的时长函数,底层会自动走:没缓存 -> 去下载 -> 存硬盘 的闭环
+ await self.xiaomusic.music_library.get_music_duration(
+ next_music, cur_playlist
+ )
+ self.log.info(f"后台预先缓存完成: {next_music}")
+ except asyncio.CancelledError:
+ pass
+ except Exception as e:
+ self.log.error(f"预加载下一首歌曲失败: {e}")
+
+ self._prefetch_timer = asyncio.create_task(_do_prefetch())
+
async def _playmusic(self, name):
"""播放音乐的核心实现"""
# 取消组内所有的下一首歌曲的定时器
@@ -373,91 +456,130 @@ class XiaoMusicDevice:
cur_playlist = self.device.cur_playlist
self.log.info(f"cur_music {self.get_cur_music()}")
- # 把 cur_playlist 传过去,要求拿该歌单下的专属 URL!
+ # 获取该歌单下的播放 URL
url, _ = await self.xiaomusic.music_library.get_music_url(name, cur_playlist)
- # 代理 URL 极速探路与 5 次死链熔断
- if url and url.startswith("http") and "/proxy/" in url:
+ # 1. 命中硬盘级负向缓存墓碑(url为空)的秒切拦截
+ if not url:
+ self._play_failed_cnt = getattr(self, "_play_failed_cnt", 0) + 1
+ self.log.warning(
+ f"【{name}】命中了死链墓碑标记,立刻拦截跳过!连续失败次数: {self._play_failed_cnt}"
+ )
+
+ if self._play_failed_cnt >= 5:
+ self.log.error("连续获取歌曲失败达到5次,触发系统第一层熔断保护!")
+ self._play_failed_cnt = 0
+ await self.xiaomusic.handle_fatal_error(
+ self.did, "连续多次获取歌曲失败,已为您停止播放。"
+ )
+ else:
+ await self.set_next_music_timeout(0.5)
+ return
+
+ # 2. 统一系统提示音/TTS 的白名单免探路、免墓碑机制
+ is_system_or_tts = (
+ "/music/tmp/" in url or "silence.mp3" in url or "xiaomusic_" in url
+ )
+
+ # 3. 极速探路器:帮小爱吃下所有的 404/401 炸弹
+ if not is_system_or_tts and url and url.startswith("http") and "/proxy/" in url:
self.log.info(f"极速探路启动,触发后端代理解析: {url}")
is_url_ok = False
try:
- # 给了 3.0 秒,因为触发 JS 插件去目标平台请求真实 URL 需要一点时间
+ # 给了 3.0 秒超时,让本地解析服务有足够时间反应
timeout = aiohttp.ClientTimeout(total=3.0)
async with aiohttp.ClientSession(timeout=timeout) as session:
- # 只要拿到 200/206 状态码,瞬间挂断不下载!
async with session.get(url) as resp:
- # 200是正常,206是部分内容(也是正常)
+ # 如果 music.py 报了 404,在这里会直接被抓个正着!
if resp.status in (200, 206):
is_url_ok = True
- self.log.info(
- f"探路成功!代理服务器存活,状态码: {resp.status}"
- )
+ self.log.info(f"探路成功!接口畅通,状态码: {resp.status}")
else:
self.log.warning(
- f"探路发现死链!代理服务器报错,状态码: {resp.status}"
+ f"探路发现死链!接口报错,状态码: {resp.status}"
)
except Exception as e:
self.log.warning(f"探路超时或网络异常(插件解析失败): {e}")
- # --- 探路失败处理逻辑 ---
+ # --- 探路失败(吃下404)处理逻辑 ---
if not is_url_ok:
- self._url_failed_cnt += 1
- self.log.warning(f"当前连续死链次数: {self._url_failed_cnt}")
+ # 统一步调!所有的失败全部使用全局唯一的 _play_failed_cnt 累计!
+ self._play_failed_cnt = getattr(self, "_play_failed_cnt", 0) + 1
+ self.log.warning(f"当前连续失败次数: {self._play_failed_cnt}")
- if self._url_failed_cnt >= 5:
- self.log.error("连续 5 次获取歌曲死链,触发第一层终极熔断!")
- self._url_failed_cnt = 0
- # 调用大管家的万能报错机制!
+ if self._play_failed_cnt >= 5:
+ self.log.error("连续 5 次获取歌曲死链,触发系统第二层熔断保护!")
+ self._play_failed_cnt = 0
await self.xiaomusic.handle_fatal_error(
- self.did, "连续多次获取歌曲失败,已为您停止"
+ self.did, "连续多次获取歌曲失败,已为您停止播放。"
)
return
- # 没到 5 次,静默切下一首
+ # 没到 5 次,静默 0.5 秒直接切下一首。小爱甚至都不知道发生过什么!
if self.is_playing and self._last_cmd != "stop":
await asyncio.sleep(0.5)
await self._play_next()
return
+ # 4. 真正安全的下发播放阶段
await self.group_force_stop_xiaoai()
- self.log.info(f"播放 {url}")
+ self.log.info(f"发送指令给小爱,开始播放: {url}")
- # 设备指令异常拦截 (完全保持原样)
results = await self.group_player_play(url, name)
if all(ele is None for ele in results):
- self.log.info(f"播放 {name} 失败. 失败次数: {self._play_failed_cnt}")
+ self._play_failed_cnt = getattr(self, "_play_failed_cnt", 0) + 1
+ self.log.info(f"播放指令发送失败. 连续失败次数: {self._play_failed_cnt}")
await asyncio.sleep(1)
if (
self.is_playing
and self._last_cmd != "stop"
- and self._play_failed_cnt < 10
+ and self._play_failed_cnt < 5
):
- self._play_failed_cnt = self._play_failed_cnt + 1
await self._play_next()
return
- # 重置播放失败次数
- self._play_failed_cnt = 0
- self._url_failed_cnt = 0
-
self.log.info(f"【{name}】已经开始播放了")
# 记录歌曲开始播放的时间
self._start_time = time.time()
self._paused_time = 0
- # 获取时长也传 cur_playlist
+ # 获取音频时长
sec = await self.xiaomusic.music_library.get_music_duration(name, cur_playlist)
- # 存储真实歌曲时长
self._duration = sec
await self.xiaomusic.analytics.send_play_event(name, sec, self.hardware)
- # 设置下一首歌曲的播放定时器
+ is_radio = self.xiaomusic.music_library.is_web_radio_music(name)
+
+ # 5. 时长质检阶段:拦截下载回来的残次品
if sec <= 0.1:
- self.log.info(f"【{name}】不会设置下一首歌的定时器")
+ if is_radio:
+ self.log.info(f"【{name}】是电台流,无限时长,免跳过")
+ self._play_failed_cnt = 0
+ else:
+ self._play_failed_cnt = getattr(self, "_play_failed_cnt", 0) + 1
+ self.log.warning(
+ f"【{name}】资源无效(获取时长为 {sec}),触发自动跳过。连续失败次数: {self._play_failed_cnt}"
+ )
+
+ if self._play_failed_cnt >= 5:
+ self.log.error(
+ "连续获取歌曲失败达到 5 次,触发第一层终极熔断保护!"
+ )
+ self._play_failed_cnt = 0
+ asyncio.ensure_future(
+ self.xiaomusic.handle_fatal_error(
+ self.did, "连续多次获取歌曲失败,已为您停止播放。"
+ )
+ )
+ else:
+ await self.set_next_music_timeout(0.5)
return
- # 计算自动添加歌曲的延迟时间,为当前歌曲时长的一半,但不超过60秒
+ # 只有通过了 404 探路存活 -> 发送指令成功 -> 质检测出时长正常,才允许重置清零!
+ self._play_failed_cnt = 0
+
+ # 计算自动添加歌曲的延迟时间
if sec > 30:
sleep_sec = min(sec / 2, 60)
await self.auto_add_song(cur_playlist, sleep_sec)
@@ -477,6 +599,11 @@ class XiaoMusicDevice:
if self.event_bus:
self.event_bus.publish(DEVICE_CONFIG_CHANGED)
+ # --- 🌟 新增:触发预缓存下一首 🌟 ---
+ # 如果当前歌曲大于 2 秒,则在播放 20 秒后悄悄去下载下一首歌
+ if sec > 20:
+ await self.prefetch_next_song(20)
+
async def do_tts(self, value):
"""执行TTS(文字转语音)"""
self.log.info(f"try do_tts value:{value}")
@@ -637,7 +764,13 @@ class XiaoMusicDevice:
self.log.info("顺序播放结束")
return ""
if new_index >= play_list_len:
- new_index = 0
+ if self.device.play_type == PLAY_TYPE_RND:
+ self.log.info("当前随机列表已播放一轮,触发重新洗牌!")
+ self.update_playlist(force_reshuffle=True)
+ # 洗完牌后,当前歌曲被强行置顶在了 0,下一首必定是 1
+ new_index = 1
+ else:
+ new_index = 0
elif direction == "prev":
new_index = index - 1
if new_index < 0:
@@ -818,6 +951,15 @@ class XiaoMusicDevice:
if not (self.config.use_music_api or self.config.continue_play):
return str(audio_id)
+ # 如果 name 为空(如播放 TTS 时),坚决不请求小米接口,会导致小米账号报错。
+ name = name.strip() if name else ""
+ if not name:
+ self.log.debug(
+ "歌名为空(可能是TTS播报),直接使用默认 audio_id,跳过小米接口查询。"
+ )
+ return str(audio_id)
+ # 修复结束
+
try:
params = {
"query": name,
@@ -959,12 +1101,15 @@ class XiaoMusicDevice:
tts = self.config.get_play_type_tts(play_type)
await self.do_tts(tts)
self.update_playlist()
+ # 切换模式,强制重新洗牌
+ self.update_playlist(force_reshuffle=True)
async def play_music_list(self, list_name, music_name):
"""播放指定播放列表"""
self._last_cmd = "play_music_list"
self.device.cur_playlist = list_name
- self.update_playlist()
+ # 切换歌单,强制重新洗牌
+ self.update_playlist(force_reshuffle=True)
if not music_name:
music_name = self.device.playlist2music.get(list_name, "")
self.log.info(f"开始播放列表{list_name} {music_name}")
@@ -1053,6 +1198,11 @@ class XiaoMusicDevice:
self._tts_timer = None
self.log.info("cancel_all_timer _tts_timer.cancel")
+ if self._prefetch_timer:
+ self._prefetch_timer.cancel()
+ self._prefetch_timer = None
+ self.log.info("cancel_all_timer _prefetch_timer.cancel")
+
@classmethod
def dict_clear(cls, d):
"""清空设备字典并取消所有定时器"""
diff --git a/xiaomusic/music_library.py b/xiaomusic/music_library.py
index fe8b484..6634080 100644
--- a/xiaomusic/music_library.py
+++ b/xiaomusic/music_library.py
@@ -12,13 +12,19 @@ import time
import urllib.parse
from collections import OrderedDict
from dataclasses import asdict
-from urllib.parse import urlparse
+from urllib.parse import parse_qs, urlparse
from xiaomusic.const import SUPPORT_MUSIC_TYPE
from xiaomusic.events import CONFIG_CHANGED
-from xiaomusic.utils.file_utils import not_in_dirs, traverse_music_directory
+from xiaomusic.utils.file_utils import (
+ clean_old_caches,
+ is_cache_valid,
+ not_in_dirs,
+ traverse_music_directory,
+)
from xiaomusic.utils.music_utils import (
Metadata,
+ build_cache_file_path,
extract_audio_metadata,
get_local_music_duration,
get_web_music_duration,
@@ -804,7 +810,28 @@ class MusicLibrary:
self.try_save_tag_cache()
return "OK"
- # 🌟 修改参数,增加 playlist_name
+ def _extract_cache_path_from_url(self, url: str, name: str) -> str:
+ """从代理 URL 中提取 datab64 并计算物理缓存路径"""
+ if not url or not url.startswith("self:///api/proxy/plugin-url"):
+ return ""
+ try:
+ query = urlparse(url).query
+ params = parse_qs(query)
+ datab64 = params.get("data", [""])[0]
+
+ if datab64:
+ actual_cache_dir = self.config.cache_dir
+ if not actual_cache_dir.startswith(self.config.music_path):
+ actual_cache_dir = os.path.join(
+ self.config.music_path, actual_cache_dir.lstrip("\\/")
+ )
+
+ # 将纠正后的 actual_cache_dir 传给底层
+ return build_cache_file_path(datab64, name, actual_cache_dir)
+ except Exception as e:
+ self.log.debug(f"提取缓存路径失败: {e}")
+ return ""
+
async def get_music_duration(self, name: str, playlist_name: str = None) -> float:
"""获取歌曲时长
@@ -827,26 +854,79 @@ class MusicLibrary:
self.log.info(f"电台 {name} 不会有播放时长")
return 0
- # 网络音乐:使用内存缓存
+ # --- 优化后的网络音乐时长获取及持久化缓存 ---
if self.is_web_music(name):
- # 先检查内存缓存
- if name in self._web_music_duration_cache:
- duration = self._web_music_duration_cache[name]
- self.log.debug(f"从内存缓存读取网络音乐 {name} 时长: {duration} 秒")
- return duration
-
- # 缓存中没有,获取时长
try:
- url, _ = await self._get_web_music_url(name, playlist_name) # 传参
- duration, _ = await get_web_music_duration(url, self.config)
- self.log.info(f"网络音乐 {name} 时长: {duration} 秒")
+ # 直接从字典里拿原始带 base64 的数据,防止被下层函数截胡
+ raw_url = None
+ if playlist_name:
+ raw_url = self.playlist_music_urls.get(f"{playlist_name}::{name}")
+ if not raw_url:
+ raw_url = self.all_music.get(name)
- # 存入内存缓存(不持久化)
+ # 尝试计算缓存路径 (利用最原始的 URL)
+ cache_path = None
+ if getattr(self.config, "cache_max_size_mb", 0) > 0 and raw_url:
+ cache_path = self._extract_cache_path_from_url(raw_url, name)
+
+ # 分支判断:检查物理缓存
+ # 如果物理文件在,直接测
+ if cache_path:
+ cache_status = is_cache_valid(cache_path)
+
+ if cache_status == -1:
+ self.log.warning(
+ f"获取时长时命中负向缓存(死链),直接返回0: {name}"
+ )
+ return 0
+ elif cache_status == 1:
+ os.utime(cache_path, None)
+
+ if name in self._web_music_duration_cache:
+ self.log.debug(f"物理文件存在,命中内存时长: {name}")
+ return self._web_music_duration_cache[name]
+
+ duration = await get_local_music_duration(
+ cache_path, self.config
+ )
+ self.log.info(
+ f"命中物理缓存: {cache_path}, 测得时长: {duration}"
+ )
+ if duration > 0:
+ self._web_music_duration_cache[name] = duration
+ return duration
+
+ # 4. 如果走到这里,说明【没开启缓存】或者【文件不存在/预下载失败】
+ # 此时我们才需要真正去拿能够下载的最终 URL
+ url, origin_url = await self._get_web_music_url(name, playlist_name)
+
+ # 【情况 A】没开缓存功能,走兜底临时文件下载
+ if not cache_path:
+ if name in self._web_music_duration_cache:
+ return self._web_music_duration_cache[name]
+ duration, _ = await get_web_music_duration(url, self.config, None)
+ if duration > 0:
+ self._web_music_duration_cache[name] = duration
+ return duration
+
+ # 【情况 B】开启了缓存,但物理文件缺失,强制下载并缓存
+ self.log.info(f"缓存文件缺失,强制触发重新下载: {name}")
+ duration, _ = await get_web_music_duration(url, self.config, cache_path)
+
+ # 下载完后后台清理与记账
if duration > 0:
self._web_music_duration_cache[name] = duration
- self.log.info(f"已缓存网络音乐 {name} 时长到内存: {duration} 秒")
+ cleanup_task = asyncio.create_task(
+ asyncio.to_thread(
+ clean_old_caches,
+ self.config.cache_dir,
+ getattr(self.config, "cache_max_size_mb", 0),
+ )
+ )
+ cleanup_task.add_done_callback(lambda t: t.exception())
return duration
+
except Exception as e:
self.log.exception(f"获取网络音乐 {name} 时长失败: {e}")
return 0
@@ -1100,6 +1180,26 @@ class MusicLibrary:
self.log.info(f"get_music_url web music. name:{name}, url:{url}")
+ # --- 小爱音箱端缓存截胡逻辑 ---
+ if getattr(self.config, "cache_max_size_mb", 0) > 0:
+ cache_path = self._extract_cache_path_from_url(url, name)
+ if cache_path:
+ cache_status = is_cache_valid(cache_path)
+ if cache_status == -1:
+ # 确诊死链,直接打回空链接,触发秒切
+ self.log.warning(
+ f"命中负向缓存(死链墓碑),拒绝网络请求,直接跳过: {name}"
+ )
+ return "", None
+
+ if cache_status == 1:
+ # 正常命中缓存
+ os.utime(cache_path, None) # 刷新保命时间
+ local_url = self._get_file_url(cache_path)
+ self.log.info(f"音箱端命中本地缓存,直链下发: {local_url}")
+ return local_url, None
+ # --- 音箱截胡逻辑结束 ---
+
# 需要通过API获取真实播放地址
if self.is_need_use_play_music_api(name):
url = await self._get_url_from_api(name, url)
diff --git a/xiaomusic/static/default/setting.html b/xiaomusic/static/default/setting.html
index 0321b47..48e1d04 100644
--- a/xiaomusic/static/default/setting.html
+++ b/xiaomusic/static/default/setting.html
@@ -8,9 +8,9 @@
-
-
-
+
+
+