From 425214d4530776fbfca74246db064d3ed5fde31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B6=B5=E6=9B=A6?= Date: Sat, 21 Sep 2024 21:18:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20musicinfo=E6=8E=A5=E5=8F=A3=E6=96=B0?= =?UTF-8?q?=E5=A2=9Emusictag=E5=8F=82=E6=95=B0=EF=BC=8C=E7=94=A8=E4=BA=8E?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E6=AD=8C=E6=9B=B2=E9=A2=9D=E5=A4=96=E4=BF=A1?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_music_tags.py | 46 ++++++++++++++ xiaomusic/httpserver.py | 9 ++- xiaomusic/utils.py | 136 +++++++++++++++++++++++++++++++++++++++- xiaomusic/xiaomusic.py | 9 +++ 4 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 test/test_music_tags.py diff --git a/test/test_music_tags.py b/test/test_music_tags.py new file mode 100644 index 0000000..7ce4edf --- /dev/null +++ b/test/test_music_tags.py @@ -0,0 +1,46 @@ +import traceback + +from xiaomusic.const import ( + SUPPORT_MUSIC_TYPE, +) +from xiaomusic.utils import ( + get_audio_metadata, + traverse_music_directory, +) + +# title 标题 +# artist 艺术家 +# album 影集 +# year 年 +# genre 性 +# picture 图片 +# lyrics 歌词 + + +async def test_one_music(filename): + # 获取播放时长 + try: + metadata = get_audio_metadata(filename) + if metadata: + lyrics = metadata.get("lyrics") + if lyrics: + print(f"歌曲 : {filename} 的 {lyrics}") + except Exception as e: + print(f"歌曲 : {filename} no tag {e}") + traceback.print_exc() + + +async def main(directory): + # 获取所有歌曲文件 + local_musics = traverse_music_directory(directory, 10, [], SUPPORT_MUSIC_TYPE) + print(local_musics) + for _, files in local_musics.items(): + for file in files: + await test_one_music(file) + + +if __name__ == "__main__": + import asyncio + + directory = "./music" # 替换为你的音乐目录路径 + asyncio.run(main(directory)) diff --git a/xiaomusic/httpserver.py b/xiaomusic/httpserver.py index bd8fa6c..220beb1 100644 --- a/xiaomusic/httpserver.py +++ b/xiaomusic/httpserver.py @@ -227,13 +227,18 @@ async def musiclist(Verifcation=Depends(verification)): @app.get("/musicinfo") -async def musicinfo(name: str, Verifcation=Depends(verification)): +async def musicinfo( + name: str, musictag: bool = False, Verifcation=Depends(verification) +): url = xiaomusic.get_music_url(name) - return { + info = { "ret": "OK", "name": name, "url": url, } + if musictag: + info["tags"] = xiaomusic.get_music_tags(name) + return info @app.get("/curplaylist") diff --git a/xiaomusic/utils.py b/xiaomusic/utils.py index acb3129..9c1331e 100644 --- a/xiaomusic/utils.py +++ b/xiaomusic/utils.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import base64 import copy import difflib import json @@ -20,8 +21,13 @@ from urllib.parse import urlparse import aiohttp import mutagen -from mutagen.id3 import ID3 +from mutagen.flac import FLAC +from mutagen.id3 import APIC, ID3 +from mutagen.monkeysaudio import MonkeysAudio from mutagen.mp3 import MP3 +from mutagen.mp4 import MP4 +from mutagen.oggvorbis import OggVorbis +from mutagen.wave import WAVE from requests.utils import cookiejar_from_dict from xiaomusic.const import SUPPORT_MUSIC_TYPE @@ -441,3 +447,131 @@ def chinese_to_number(chinese): result += num num = 0 return result + + +def get_audio_metadata(file_path): + if file_path.endswith(".mp3"): + return get_mp3_metadata(file_path) + elif file_path.endswith(".flac"): + return get_flac_metadata(file_path) + elif file_path.endswith(".wav"): + return get_wav_metadata(file_path) + elif file_path.endswith(".ape"): + return get_ape_metadata(file_path) + elif file_path.endswith(".ogg"): + return get_ogg_metadata(file_path) + elif file_path.endswith(".m4a"): + return get_m4a_metadata(file_path) + else: + raise ValueError("Unsupported file type") + + +def get_mp3_metadata(file_path): + audio = MP3(file_path, ID3=ID3) + tags = audio.tags + if tags is None: + return None + + 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": "", + } + + for tag in tags.values(): + if isinstance(tag, APIC): + metadata["picture"] = base64.b64encode(tag.data).decode("utf-8") + break + + lyrics = tags.getall("USLT") + if lyrics: + 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": "", + } + + if audio.pictures: + picture = audio.pictures[0] + metadata["picture"] = base64.b64encode(picture.data).decode("utf-8") + + if "lyrics" in audio: + 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": "", + } + 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": "", + } + 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": "", + } + 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": "", + } + + if "covr" in audio.tags: + cover = audio.tags["covr"][0] + metadata["picture"] = base64.b64encode(cover).decode("utf-8") + + return metadata diff --git a/xiaomusic/xiaomusic.py b/xiaomusic/xiaomusic.py index 45013dd..ae73ac3 100644 --- a/xiaomusic/xiaomusic.py +++ b/xiaomusic/xiaomusic.py @@ -41,6 +41,7 @@ from xiaomusic.utils import ( deepcopy_data_no_sensitive_info, find_best_match, fuzzyfinder, + get_audio_metadata, get_local_music_duration, get_web_music_duration, is_mp3, @@ -68,6 +69,7 @@ class XiaoMusic: self.music_list = {} # 播放列表 key 为目录名, value 为 play_list self.devices = {} # key 为 did self.running_task = [] + self.all_music_tags = {} # 歌曲额外信息 # 初始化配置 self.init_config() @@ -389,6 +391,9 @@ class XiaoMusic: self.log.warning(f"获取歌曲时长失败 {name} {url}") return sec, url + def get_music_tags(self, name): + return self.all_music_tags.get(name, {}) + def get_music_url(self, name): if self.is_web_music(name): url = self.all_music[name] @@ -432,6 +437,7 @@ class XiaoMusic: # 获取目录下所有歌曲,生成随机播放列表 def _gen_all_music_list(self): self.all_music = {} + self.all_music_tags = {} all_music_by_dir = {} local_musics = traverse_music_directory( self.music_path, @@ -455,6 +461,7 @@ class XiaoMusic: filename = os.path.basename(file) (name, _) = os.path.splitext(filename) self.all_music[name] = file + self.all_music_tags[name] = get_audio_metadata(file) all_music_by_dir[dir_name][name] = True self.log.debug(f"_gen_all_music_list {name}:{dir_name}:{file}") @@ -516,6 +523,8 @@ class XiaoMusic: if (not name) or (not url): continue self.all_music[name] = url + # TODO: 网络歌曲获取歌曲额外信息 + # self.all_music_tags[name] = get_audio_metadata(url) one_music_list.append(name) # 处理电台列表