From 44860d495e6a0a43584bc779a63ff66e4bce0496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B6=B5=E6=9B=A6?= Date: Mon, 23 Sep 2024 01:10:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20musicinfos=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=94=A8=E4=BA=8E=E6=89=B9=E9=87=8F=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=AD=8C=E6=9B=B2=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pdm.lock | 13 +++- xiaomusic/httpserver.py | 21 +++++- xiaomusic/utils.py | 146 ++++++++++++++++++++-------------------- xiaomusic/xiaomusic.py | 48 +++++++++---- 4 files changed, 139 insertions(+), 89 deletions(-) diff --git a/pdm.lock b/pdm.lock index 147bac4..854380d 100644 --- a/pdm.lock +++ b/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" diff --git a/xiaomusic/httpserver.py b/xiaomusic/httpserver.py index 220beb1..3bbd30e 100644 --- a/xiaomusic/httpserver.py +++ b/xiaomusic/httpserver.py @@ -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): diff --git a/xiaomusic/utils.py b/xiaomusic/utils.py index c7da990..ab563cf 100644 --- a/xiaomusic/utils.py +++ b/xiaomusic/utils.py @@ -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 diff --git a/xiaomusic/xiaomusic.py b/xiaomusic/xiaomusic.py index 4281a66..b91b09e 100644 --- a/xiaomusic/xiaomusic.py +++ b/xiaomusic/xiaomusic.py @@ -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