feat: 新增 musicinfos 接口用于批量查询歌曲信息
This commit is contained in:
parent
7c9576874b
commit
44860d495e
13
pdm.lock
13
pdm.lock
@ -5,7 +5,7 @@
|
||||
groups = ["default", "dev", "lint"]
|
||||
strategy = ["inherit_metadata"]
|
||||
lock_version = "4.5.0"
|
||||
content_hash = "sha256:743f0a2ac59e1902f4f5389375ec5df7e2469502b0dff7cef40d391febd1ad92"
|
||||
content_hash = "sha256:d7209c5b89041122b16847a761d8f522ea404543d50b859fb283f9061d4e9f36"
|
||||
|
||||
[[metadata.targets]]
|
||||
requires_python = "==3.10.12"
|
||||
@ -757,6 +757,17 @@ files = [
|
||||
{file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opencc-python-reimplemented"
|
||||
version = "0.1.7"
|
||||
summary = "OpenCC made with Python"
|
||||
groups = ["default"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
files = [
|
||||
{file = "opencc-python-reimplemented-0.1.7.tar.gz", hash = "sha256:4f777ea3461a25257a7b876112cfa90bb6acabc6dfb843bf4d11266e43579dee"},
|
||||
{file = "opencc_python_reimplemented-0.1.7-py2.py3-none-any.whl", hash = "sha256:41b3b92943c7bed291f448e9c7fad4b577c8c2eae30fcfe5a74edf8818493aa6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.1"
|
||||
|
@ -12,7 +12,7 @@ from dataclasses import asdict
|
||||
from typing import Annotated
|
||||
|
||||
import aiofiles
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request, status
|
||||
from fastapi import Depends, FastAPI, HTTPException, Query, Request, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
@ -241,6 +241,25 @@ async def musicinfo(
|
||||
return info
|
||||
|
||||
|
||||
@app.get("/musicinfos")
|
||||
async def musicinfos(
|
||||
name: list[str] = Query(None),
|
||||
musictag: bool = False,
|
||||
Verifcation=Depends(verification),
|
||||
):
|
||||
ret = []
|
||||
for music_name in name:
|
||||
url = xiaomusic.get_music_url(music_name)
|
||||
info = {
|
||||
"name": music_name,
|
||||
"url": url,
|
||||
}
|
||||
if musictag:
|
||||
info["tags"] = xiaomusic.get_music_tags(music_name)
|
||||
ret.append(info)
|
||||
return ret
|
||||
|
||||
|
||||
@app.get("/curplaylist")
|
||||
async def curplaylist(did: str = "", Verifcation=Depends(verification)):
|
||||
if not xiaomusic.did_exist(did):
|
||||
|
@ -16,6 +16,7 @@ import string
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections.abc import AsyncIterator
|
||||
from dataclasses import dataclass
|
||||
from http.cookies import SimpleCookie
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@ -28,13 +29,12 @@ from mutagen.mp3 import MP3
|
||||
from mutagen.mp4 import MP4
|
||||
from mutagen.oggvorbis import OggVorbis
|
||||
from mutagen.wave import WAVE
|
||||
from opencc import OpenCC
|
||||
from requests.utils import cookiejar_from_dict
|
||||
|
||||
from xiaomusic.const import SUPPORT_MUSIC_TYPE
|
||||
from opencc import OpenCC
|
||||
|
||||
|
||||
cc = OpenCC('t2s') # convert from Traditional Chinese to Simplified Chinese
|
||||
cc = OpenCC("t2s") # convert from Traditional Chinese to Simplified Chinese
|
||||
|
||||
|
||||
### HELP FUNCTION ###
|
||||
@ -90,8 +90,9 @@ def validate_proxy(proxy_str: str) -> bool:
|
||||
|
||||
# 模糊搜索
|
||||
def fuzzyfinder(user_input, collection, extra_search_index=None):
|
||||
return find_best_match(user_input, collection, cutoff=0.1, n=10,
|
||||
extra_search_index=extra_search_index)
|
||||
return find_best_match(
|
||||
user_input, collection, cutoff=0.1, n=10, extra_search_index=extra_search_index
|
||||
)
|
||||
|
||||
|
||||
def traditional_to_simple(to_convert: str):
|
||||
@ -107,11 +108,11 @@ def keyword_detection(user_input, str_list, n):
|
||||
matched.append(item)
|
||||
else:
|
||||
remains.append(item)
|
||||
|
||||
|
||||
# 如果 n 是 -1,如果 n 大于匹配的数量,返回所有匹配的结果
|
||||
if n == -1 or n > len(matched):
|
||||
return matched, remains
|
||||
|
||||
|
||||
# 随机选择 n 个匹配的结果
|
||||
return random.sample(matched, n), remains
|
||||
|
||||
@ -120,14 +121,14 @@ def real_search(prompt, candidates, cutoff, n):
|
||||
matches, remains = keyword_detection(prompt, candidates, n=n)
|
||||
if len(matches) < n:
|
||||
# 如果没有准确关键词匹配,开始模糊匹配
|
||||
matches += difflib.get_close_matches(
|
||||
prompt, remains, n=n, cutoff=cutoff
|
||||
)
|
||||
matches += difflib.get_close_matches(prompt, remains, n=n, cutoff=cutoff)
|
||||
return matches
|
||||
|
||||
|
||||
def find_best_match(user_input, collection, cutoff=0.6, n=1, extra_search_index=None):
|
||||
lower_collection = {traditional_to_simple(item.lower()): item for item in collection}
|
||||
lower_collection = {
|
||||
traditional_to_simple(item.lower()): item for item in collection
|
||||
}
|
||||
user_input = traditional_to_simple(user_input.lower())
|
||||
matches = real_search(user_input, lower_collection.keys(), cutoff, n)
|
||||
cur_matched_collection = [lower_collection[match] for match in matches]
|
||||
@ -141,8 +142,8 @@ def find_best_match(user_input, collection, cutoff=0.6, n=1, extra_search_index=
|
||||
matches = real_search(user_input, lower_extra_search_index.keys(), cutoff, n)
|
||||
cur_matched_collection += [lower_extra_search_index[match] for match in matches]
|
||||
return cur_matched_collection[:n]
|
||||
|
||||
|
||||
|
||||
|
||||
# 歌曲排序
|
||||
def custom_sort_key(s):
|
||||
# 使用正则表达式分别提取字符串的数字前缀和数字后缀
|
||||
@ -506,113 +507,112 @@ def get_audio_metadata(file_path):
|
||||
raise ValueError("Unsupported file type")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Metadata:
|
||||
title: str = ""
|
||||
artist: str = ""
|
||||
album: str = ""
|
||||
year: str = ""
|
||||
genre: str = ""
|
||||
picture: str = ""
|
||||
lyrics: str = ""
|
||||
|
||||
|
||||
def get_mp3_metadata(file_path):
|
||||
audio = MP3(file_path, ID3=ID3)
|
||||
tags = audio.tags
|
||||
if tags is None:
|
||||
return None
|
||||
return Metadata()
|
||||
|
||||
metadata = {
|
||||
"title": tags.get("TIT2", [""])[0] if "TIT2" in tags else "",
|
||||
"artist": tags.get("TPE1", [""])[0] if "TPE1" in tags else "",
|
||||
"album": tags.get("TALB", [""])[0] if "TALB" in tags else "",
|
||||
"year": tags.get("TDRC", [""])[0] if "TDRC" in tags else "",
|
||||
"genre": tags.get("TCON", [""])[0] if "TCON" in tags else "",
|
||||
"picture": "",
|
||||
"lyrics": "",
|
||||
}
|
||||
metadata = Metadata(
|
||||
title=tags.get("TIT2", [""])[0] if "TIT2" in tags else "",
|
||||
artist=tags.get("TPE1", [""])[0] if "TPE1" in tags else "",
|
||||
album=tags.get("TALB", [""])[0] if "TALB" in tags else "",
|
||||
year=tags.get("TDRC", [""])[0] if "TDRC" in tags else "",
|
||||
genre=tags.get("TCON", [""])[0] if "TCON" in tags else "",
|
||||
)
|
||||
|
||||
for tag in tags.values():
|
||||
if isinstance(tag, APIC):
|
||||
metadata["picture"] = base64.b64encode(tag.data).decode("utf-8")
|
||||
metadata.picture = base64.b64encode(tag.data).decode("utf-8")
|
||||
break
|
||||
|
||||
lyrics = tags.getall("USLT")
|
||||
if lyrics:
|
||||
metadata["lyrics"] = lyrics[0]
|
||||
metadata.lyrics = lyrics[0]
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def get_flac_metadata(file_path):
|
||||
audio = FLAC(file_path)
|
||||
metadata = {
|
||||
"title": audio.get("title", [""])[0],
|
||||
"artist": audio.get("artist", [""])[0],
|
||||
"album": audio.get("album", [""])[0],
|
||||
"year": audio.get("date", [""])[0],
|
||||
"genre": audio.get("genre", [""])[0],
|
||||
"picture": "",
|
||||
"lyrics": "",
|
||||
}
|
||||
metadata = Metadata(
|
||||
title=audio.get("title", [""])[0],
|
||||
artist=audio.get("artist", [""])[0],
|
||||
album=audio.get("album", [""])[0],
|
||||
year=audio.get("date", [""])[0],
|
||||
genre=audio.get("genre", [""])[0],
|
||||
)
|
||||
|
||||
if audio.pictures:
|
||||
picture = audio.pictures[0]
|
||||
metadata["picture"] = base64.b64encode(picture.data).decode("utf-8")
|
||||
metadata.picture = base64.b64encode(picture.data).decode("utf-8")
|
||||
|
||||
if "lyrics" in audio:
|
||||
metadata["lyrics"] = audio["lyrics"][0]
|
||||
metadata.lyrics = audio["lyrics"][0]
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def get_wav_metadata(file_path):
|
||||
audio = WAVE(file_path)
|
||||
metadata = {
|
||||
"title": audio.get("TIT2", [""])[0],
|
||||
"artist": audio.get("TPE1", [""])[0],
|
||||
"album": audio.get("TALB", [""])[0],
|
||||
"year": audio.get("TDRC", [""])[0],
|
||||
"genre": audio.get("TCON", [""])[0],
|
||||
"picture": "",
|
||||
"lyrics": "",
|
||||
}
|
||||
metadata = Metadata(
|
||||
title=audio.get("TIT2", [""])[0],
|
||||
artist=audio.get("TPE1", [""])[0],
|
||||
album=audio.get("TALB", [""])[0],
|
||||
year=audio.get("TDRC", [""])[0],
|
||||
genre=audio.get("TCON", [""])[0],
|
||||
)
|
||||
return metadata
|
||||
|
||||
|
||||
def get_ape_metadata(file_path):
|
||||
audio = MonkeysAudio(file_path)
|
||||
metadata = {
|
||||
"title": audio.get("TIT2", [""])[0],
|
||||
"artist": audio.get("TPE1", [""])[0],
|
||||
"album": audio.get("TALB", [""])[0],
|
||||
"year": audio.get("TDRC", [""])[0],
|
||||
"genre": audio.get("TCON", [""])[0],
|
||||
"picture": "",
|
||||
"lyrics": "",
|
||||
}
|
||||
metadata = Metadata(
|
||||
title=audio.get("TIT2", [""])[0],
|
||||
artist=audio.get("TPE1", [""])[0],
|
||||
album=audio.get("TALB", [""])[0],
|
||||
year=audio.get("TDRC", [""])[0],
|
||||
genre=audio.get("TCON", [""])[0],
|
||||
)
|
||||
return metadata
|
||||
|
||||
|
||||
def get_ogg_metadata(file_path):
|
||||
audio = OggVorbis(file_path)
|
||||
metadata = {
|
||||
"title": audio.get("title", [""])[0],
|
||||
"artist": audio.get("artist", [""])[0],
|
||||
"album": audio.get("album", [""])[0],
|
||||
"year": audio.get("date", [""])[0],
|
||||
"genre": audio.get("genre", [""])[0],
|
||||
"picture": "",
|
||||
"lyrics": "",
|
||||
}
|
||||
metadata = Metadata(
|
||||
title=audio.get("title", [""])[0],
|
||||
artist=audio.get("artist", [""])[0],
|
||||
album=audio.get("album", [""])[0],
|
||||
year=audio.get("date", [""])[0],
|
||||
genre=audio.get("genre", [""])[0],
|
||||
)
|
||||
return metadata
|
||||
|
||||
|
||||
def get_m4a_metadata(file_path):
|
||||
audio = MP4(file_path)
|
||||
metadata = {
|
||||
"title": audio.tags.get("\xa9nam", [""])[0],
|
||||
"artist": audio.tags.get("\xa9ART", [""])[0],
|
||||
"album": audio.tags.get("\xa9alb", [""])[0],
|
||||
"year": audio.tags.get("\xa9day", [""])[0],
|
||||
"genre": audio.tags.get("\xa9gen", [""])[0],
|
||||
"picture": "",
|
||||
"lyrics": "",
|
||||
}
|
||||
metadata = Metadata(
|
||||
title=audio.tags.get("\xa9nam", [""])[0],
|
||||
artist=audio.tags.get("\xa9ART", [""])[0],
|
||||
album=audio.tags.get("\xa9alb", [""])[0],
|
||||
year=audio.tags.get("\xa9day", [""])[0],
|
||||
genre=audio.tags.get("\xa9gen", [""])[0],
|
||||
)
|
||||
|
||||
if "covr" in audio.tags:
|
||||
cover = audio.tags["covr"][0]
|
||||
metadata["picture"] = base64.b64encode(cover).decode("utf-8")
|
||||
metadata.picture = base64.b64encode(cover).decode("utf-8")
|
||||
|
||||
return metadata
|
||||
|
||||
|
@ -9,10 +9,10 @@ import random
|
||||
import re
|
||||
import time
|
||||
import urllib.parse
|
||||
from collections import OrderedDict
|
||||
from dataclasses import asdict
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
from collections import OrderedDict
|
||||
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
from miservice import MiAccount, MiNAService
|
||||
@ -36,6 +36,7 @@ from xiaomusic.const import (
|
||||
from xiaomusic.crontab import Crontab
|
||||
from xiaomusic.plugin import PluginManager
|
||||
from xiaomusic.utils import (
|
||||
Metadata,
|
||||
chinese_to_number,
|
||||
convert_file_to_mp3,
|
||||
custom_sort_key,
|
||||
@ -46,11 +47,11 @@ from xiaomusic.utils import (
|
||||
get_local_music_duration,
|
||||
get_web_music_duration,
|
||||
is_mp3,
|
||||
list2str,
|
||||
parse_cookie_string,
|
||||
parse_str_to_dict,
|
||||
remove_id3_tags,
|
||||
traverse_music_directory,
|
||||
list2str,
|
||||
)
|
||||
|
||||
|
||||
@ -395,7 +396,7 @@ class XiaoMusic:
|
||||
return sec, url
|
||||
|
||||
def get_music_tags(self, name):
|
||||
return self.all_music_tags.get(name, {})
|
||||
return self.all_music_tags.get(name, Metadata())
|
||||
|
||||
def get_music_url(self, name):
|
||||
if self.is_web_music(name):
|
||||
@ -731,14 +732,20 @@ class XiaoMusic:
|
||||
|
||||
all_music_list = list(self.all_music.keys())
|
||||
real_names = find_best_match(
|
||||
name, all_music_list, cutoff=self.config.fuzzy_match_cutoff, n=n,
|
||||
name,
|
||||
all_music_list,
|
||||
cutoff=self.config.fuzzy_match_cutoff,
|
||||
n=n,
|
||||
extra_search_index=self._extra_index_search,
|
||||
)
|
||||
if real_names:
|
||||
if n > 1:
|
||||
# 扩大范围再找,最后保留随机 n 个
|
||||
real_names = find_best_match(
|
||||
name, all_music_list, cutoff=self.config.fuzzy_match_cutoff, n=n * 2,
|
||||
name,
|
||||
all_music_list,
|
||||
cutoff=self.config.fuzzy_match_cutoff,
|
||||
n=n * 2,
|
||||
extra_search_index=self._extra_index_search,
|
||||
)
|
||||
random.shuffle(real_names)
|
||||
@ -793,7 +800,9 @@ class XiaoMusic:
|
||||
|
||||
# 模糊搜一个播放列表(只需要一个,不需要 extra index)
|
||||
real_name = find_best_match(
|
||||
list_name, self.music_list, cutoff=self.config.fuzzy_match_cutoff,
|
||||
list_name,
|
||||
self.music_list,
|
||||
cutoff=self.config.fuzzy_match_cutoff,
|
||||
n=1,
|
||||
)[0]
|
||||
if real_name:
|
||||
@ -1095,9 +1104,10 @@ class XiaoMusicDevice:
|
||||
# 没有重置 list 且非初始化
|
||||
if self.device.cur_playlist == "临时搜索列表" and len(self._play_list) > 0:
|
||||
# 更新总播放列表,为了UI显示
|
||||
self.xiaomusic.music_list['临时搜索列表'] = copy.copy(self._play_list)
|
||||
elif (self.device.cur_playlist == "临时搜索列表" and len(self._play_list) == 0
|
||||
) or (self.device.cur_playlist not in self.xiaomusic.music_list):
|
||||
self.xiaomusic.music_list["临时搜索列表"] = copy.copy(self._play_list)
|
||||
elif (
|
||||
self.device.cur_playlist == "临时搜索列表" and len(self._play_list) == 0
|
||||
) or (self.device.cur_playlist not in self.xiaomusic.music_list):
|
||||
self.device.cur_playlist = "全部"
|
||||
else:
|
||||
pass # 指定了已知的播放列表名称
|
||||
@ -1108,12 +1118,18 @@ class XiaoMusicDevice:
|
||||
if reorder:
|
||||
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)}")
|
||||
self.log.info(
|
||||
f"随机打乱 {list_name} {list2str(self._play_list, self.config.verbose)}"
|
||||
)
|
||||
else:
|
||||
self._play_list = sorted(self._play_list)
|
||||
self.log.info(f"没打乱 {list_name} {list2str(self._play_list, self.config.verbose)}")
|
||||
self.log.info(
|
||||
f"没打乱 {list_name} {list2str(self._play_list, self.config.verbose)}"
|
||||
)
|
||||
else:
|
||||
self.log.info(f"更新 {list_name} {list2str(self._play_list, self.config.verbose)}")
|
||||
self.log.info(
|
||||
f"更新 {list_name} {list2str(self._play_list, self.config.verbose)}"
|
||||
)
|
||||
|
||||
# 播放歌曲
|
||||
async def play(self, name="", search_key=""):
|
||||
@ -1144,7 +1160,9 @@ class XiaoMusicDevice:
|
||||
self.device.cur_playlist = "临时搜索列表"
|
||||
self.update_playlist(reorder=False)
|
||||
name = names[0]
|
||||
self.log.debug(f"当前播放列表为:{list2str(self._play_list, self.config.verbose)}")
|
||||
self.log.debug(
|
||||
f"当前播放列表为:{list2str(self._play_list, self.config.verbose)}"
|
||||
)
|
||||
elif not self.xiaomusic.is_music_exist(name):
|
||||
if self.config.disable_download:
|
||||
await self.do_tts(f"本地不存在歌曲{name}")
|
||||
@ -1222,7 +1240,9 @@ class XiaoMusicDevice:
|
||||
self.device.cur_playlist = "临时搜索列表"
|
||||
self.update_playlist(reorder=False)
|
||||
name = names[0]
|
||||
self.log.debug(f"当前播放列表为:{list2str(self._play_list, self.config.verbose)}")
|
||||
self.log.debug(
|
||||
f"当前播放列表为:{list2str(self._play_list, self.config.verbose)}"
|
||||
)
|
||||
elif not self.xiaomusic.is_music_exist(name):
|
||||
await self.do_tts(f"本地不存在歌曲{name}")
|
||||
return
|
||||
|
Loading…
Reference in New Issue
Block a user