feat: 歌曲信息中的图片改为url #190
This commit is contained in:
parent
d214cc8df3
commit
baf9a83e50
@ -4,7 +4,7 @@ from xiaomusic.const import (
|
||||
SUPPORT_MUSIC_TYPE,
|
||||
)
|
||||
from xiaomusic.utils import (
|
||||
get_audio_metadata,
|
||||
extract_audio_metadata,
|
||||
traverse_music_directory,
|
||||
)
|
||||
|
||||
@ -20,12 +20,8 @@ from xiaomusic.utils import (
|
||||
async def test_one_music(filename):
|
||||
# 获取播放时长
|
||||
try:
|
||||
metadata = get_audio_metadata(filename)
|
||||
print(metadata.title, metadata.album)
|
||||
if metadata:
|
||||
lyrics = metadata.lyrics
|
||||
if lyrics:
|
||||
print(f"歌曲 : {filename} 的 {lyrics}")
|
||||
metadata = extract_audio_metadata(filename, "cache/picture_cache")
|
||||
print(metadata)
|
||||
except Exception as e:
|
||||
print(f"歌曲 : {filename} no tag {e}")
|
||||
traceback.print_exc()
|
||||
@ -34,13 +30,14 @@ async def test_one_music(filename):
|
||||
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)
|
||||
print(file)
|
||||
# await test_one_music(file)
|
||||
pass
|
||||
|
||||
await test_one_music("./music/一生何求.mp3")
|
||||
await test_one_music("./music/程响-人间烟火.flac")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -255,3 +255,10 @@ class Config:
|
||||
os.makedirs(self.cache_dir)
|
||||
filename = os.path.join(self.cache_dir, "tag_cache.json")
|
||||
return filename
|
||||
|
||||
@property
|
||||
def picture_cache_path(self):
|
||||
cache_path = os.path.join(self.cache_dir, "picture_cache")
|
||||
if not os.path.exists(cache_path):
|
||||
os.makedirs(cache_path)
|
||||
return cache_path
|
||||
|
@ -429,3 +429,18 @@ async def music_options():
|
||||
"Accept-Ranges": "bytes",
|
||||
}
|
||||
return Response(headers=headers)
|
||||
|
||||
|
||||
@app.get("/picture/{file_path:path}")
|
||||
async def get_picture(request: Request, file_path: str):
|
||||
absolute_path = os.path.abspath(config.picture_cache_path)
|
||||
absolute_file_path = os.path.normpath(os.path.join(absolute_path, file_path))
|
||||
if not absolute_file_path.startswith(absolute_path):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
if not os.path.exists(absolute_file_path):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(absolute_file_path)
|
||||
if mime_type is None:
|
||||
mime_type = "image/jpeg"
|
||||
return FileResponse(absolute_file_path, media_type=mime_type)
|
||||
|
@ -5,6 +5,7 @@ import asyncio
|
||||
import base64
|
||||
import copy
|
||||
import difflib
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
@ -22,13 +23,14 @@ from urllib.parse import urlparse
|
||||
|
||||
import aiohttp
|
||||
import mutagen
|
||||
from mutagen.asf import ASF
|
||||
from mutagen.flac import FLAC
|
||||
from mutagen.id3 import APIC, ID3
|
||||
from mutagen.monkeysaudio import MonkeysAudio
|
||||
from mutagen.id3 import ID3, Encoding, TextFrame, TimeStampTextFrame
|
||||
from mutagen.mp3 import MP3
|
||||
from mutagen.mp4 import MP4
|
||||
from mutagen.oggvorbis import OggVorbis
|
||||
from mutagen.wave import WAVE
|
||||
from mutagen.wavpack import WavPack
|
||||
from opencc import OpenCC
|
||||
from requests.utils import cookiejar_from_dict
|
||||
|
||||
@ -492,149 +494,6 @@ def chinese_to_number(chinese):
|
||||
return result
|
||||
|
||||
|
||||
def get_audio_metadata(file_path):
|
||||
ret = Metadata()
|
||||
if file_path.endswith(".mp3"):
|
||||
ret = get_mp3_metadata(file_path)
|
||||
elif file_path.endswith(".flac"):
|
||||
ret = get_flac_metadata(file_path)
|
||||
elif file_path.endswith(".wav"):
|
||||
ret = get_wav_metadata(file_path)
|
||||
elif file_path.endswith(".ape"):
|
||||
ret = get_ape_metadata(file_path)
|
||||
elif file_path.endswith(".ogg"):
|
||||
ret = get_ogg_metadata(file_path)
|
||||
elif file_path.endswith(".m4a"):
|
||||
ret = get_m4a_metadata(file_path)
|
||||
return {k: str(v) for k, v in asdict(ret).items()}
|
||||
|
||||
|
||||
@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 Metadata()
|
||||
|
||||
# 处理编码
|
||||
def get_tag_value(tags, k):
|
||||
if k not in tags:
|
||||
return ""
|
||||
v = tags[k]
|
||||
if isinstance(v, mutagen.id3.TextFrame) and not isinstance(
|
||||
v, mutagen.id3.TimeStampTextFrame
|
||||
):
|
||||
old_ts = "".join(v.text)
|
||||
if v.encoding == mutagen.id3.Encoding.LATIN1:
|
||||
bs = old_ts.encode("latin1")
|
||||
ts = bs.decode("GBK", errors="ignore")
|
||||
return ts
|
||||
return old_ts
|
||||
return v
|
||||
|
||||
metadata = Metadata(
|
||||
title=get_tag_value(tags, "TIT2"),
|
||||
artist=get_tag_value(tags, "TPE1"),
|
||||
album=get_tag_value(tags, "TALB"),
|
||||
year=get_tag_value(tags, "TDRC"),
|
||||
genre=get_tag_value(tags, "TCON"),
|
||||
)
|
||||
|
||||
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 = 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")
|
||||
|
||||
if "lyrics" in audio:
|
||||
metadata.lyrics = audio["lyrics"][0]
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def get_wav_metadata(file_path):
|
||||
audio = WAVE(file_path)
|
||||
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 = 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 = 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 = 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")
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def list2str(li, verbose=False):
|
||||
if len(li) > 5 and not verbose:
|
||||
return f"{li[:2]} ... {li[-2:]} with len: {len(li)}"
|
||||
@ -651,3 +510,145 @@ async def get_latest_version(package_name: str) -> str:
|
||||
return data["info"]["version"]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Metadata:
|
||||
title: str = ""
|
||||
artist: str = ""
|
||||
album: str = ""
|
||||
year: str = ""
|
||||
genre: str = ""
|
||||
picture: str = ""
|
||||
lyrics: str = ""
|
||||
|
||||
|
||||
def _get_alltag_value(tags, k):
|
||||
v = tags.getall(k)
|
||||
if len(v) > 0:
|
||||
return _to_utf8(v[0])
|
||||
return ""
|
||||
|
||||
|
||||
def _get_tag_value(tags, k):
|
||||
if k not in tags:
|
||||
return ""
|
||||
v = tags[k]
|
||||
return _to_utf8(v)
|
||||
|
||||
|
||||
def _to_utf8(v):
|
||||
if isinstance(v, TextFrame) and not isinstance(v, TimeStampTextFrame):
|
||||
old_ts = "".join(v.text)
|
||||
if v.encoding == Encoding.LATIN1:
|
||||
bs = old_ts.encode("latin1")
|
||||
ts = bs.decode("GBK", errors="ignore")
|
||||
return ts
|
||||
return old_ts
|
||||
elif isinstance(v, list):
|
||||
return "".join(v)
|
||||
return str(v)
|
||||
|
||||
|
||||
def _save_picture(picture_data, save_root, file_path):
|
||||
# 计算文件名的哈希值
|
||||
file_hash = hashlib.md5(file_path.encode("utf-8")).hexdigest()
|
||||
# 创建目录结构
|
||||
dir_path = os.path.join(save_root, file_hash[-6:])
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
|
||||
# 检测图片格式
|
||||
if picture_data[:3] == b"\xff\xd8\xff":
|
||||
ext = "jpg"
|
||||
elif picture_data[:8] == b"\x89PNG\r\n\x1a\n":
|
||||
ext = "png"
|
||||
else:
|
||||
ext = "bin" # 未知格式
|
||||
|
||||
# 保存图片
|
||||
filename = os.path.basename(file_path)
|
||||
(name, _) = os.path.splitext(filename)
|
||||
picture_path = os.path.join(dir_path, f"{name}.{ext}")
|
||||
with open(picture_path, "wb") as img:
|
||||
img.write(picture_data)
|
||||
return picture_path
|
||||
|
||||
|
||||
def extract_audio_metadata(file_path, save_root):
|
||||
audio = mutagen.File(file_path)
|
||||
metadata = Metadata()
|
||||
tags = audio.tags
|
||||
if tags is None:
|
||||
return asdict(metadata)
|
||||
|
||||
if isinstance(audio, MP3):
|
||||
metadata.title = _get_tag_value(tags, "TIT2")
|
||||
metadata.artist = _get_tag_value(tags, "TPE1")
|
||||
metadata.album = _get_tag_value(tags, "TALB")
|
||||
metadata.year = _get_tag_value(tags, "TDRC")
|
||||
metadata.genre = _get_tag_value(tags, "TCON")
|
||||
if "APIC:" in tags:
|
||||
metadata.picture = _save_picture(tags["APIC:"].data, save_root, file_path)
|
||||
metadata.lyrics = _get_alltag_value(tags, "USLT")
|
||||
|
||||
elif isinstance(audio, FLAC):
|
||||
metadata.title = _get_tag_value(tags, "TITLE")
|
||||
metadata.artist = _get_tag_value(tags, "ARTIST")
|
||||
metadata.album = _get_tag_value(tags, "ALBUM")
|
||||
metadata.year = _get_tag_value(tags, "DATE")
|
||||
metadata.genre = _get_tag_value(tags, "GENRE")
|
||||
if audio.pictures:
|
||||
metadata.picture = _save_picture(
|
||||
audio.pictures[0].data, save_root, file_path
|
||||
)
|
||||
if "lyrics" in audio:
|
||||
metadata.lyrics = audio["lyrics"][0]
|
||||
|
||||
elif isinstance(audio, MP4):
|
||||
metadata.title = _get_tag_value(tags, "\xa9nam")
|
||||
metadata.artist = _get_tag_value(tags, "\xa9ART")
|
||||
metadata.album = _get_tag_value(tags, "\xa9alb")
|
||||
metadata.year = _get_tag_value(tags, "\xa9day")
|
||||
metadata.genre = _get_tag_value(tags, "\xa9gen")
|
||||
if "covr" in tags:
|
||||
metadata.picture = _save_picture(tags["covr"][0], save_root, file_path)
|
||||
|
||||
elif isinstance(audio, OggVorbis):
|
||||
metadata.title = _get_tag_value(tags, "TITLE")
|
||||
metadata.artist = _get_tag_value(tags, "ARTIST")
|
||||
metadata.album = _get_tag_value(tags, "ALBUM")
|
||||
metadata.year = _get_tag_value(tags, "DATE")
|
||||
metadata.genre = _get_tag_value(tags, "GENRE")
|
||||
if "metadata_block_picture" in tags:
|
||||
picture = json.loads(base64.b64decode(tags["metadata_block_picture"][0]))
|
||||
metadata.picture = _save_picture(
|
||||
base64.b64decode(picture["data"]), save_root, file_path
|
||||
)
|
||||
|
||||
elif isinstance(audio, ASF):
|
||||
metadata.title = _get_tag_value(tags, "Title")
|
||||
metadata.artist = _get_tag_value(tags, "Author")
|
||||
metadata.album = _get_tag_value(tags, "WM/AlbumTitle")
|
||||
metadata.year = _get_tag_value(tags, "WM/Year")
|
||||
metadata.genre = _get_tag_value(tags, "WM/Genre")
|
||||
if "WM/Picture" in tags:
|
||||
metadata.picture = _save_picture(
|
||||
tags["WM/Picture"][0].value, save_root, file_path
|
||||
)
|
||||
|
||||
elif isinstance(audio, WavPack):
|
||||
metadata.title = _get_tag_value(tags, "Title")
|
||||
metadata.artist = _get_tag_value(tags, "Artist")
|
||||
metadata.album = _get_tag_value(tags, "Album")
|
||||
metadata.year = _get_tag_value(tags, "Year")
|
||||
metadata.genre = _get_tag_value(tags, "Genre")
|
||||
if audio.pictures:
|
||||
metadata.picture = _save_picture(
|
||||
audio.pictures[0].data, save_root, file_path
|
||||
)
|
||||
|
||||
elif isinstance(audio, WAVE):
|
||||
metadata.title = _get_tag_value(tags, "Title")
|
||||
metadata.artist = _get_tag_value(tags, "Artist")
|
||||
|
||||
return asdict(metadata)
|
||||
|
@ -41,9 +41,9 @@ from xiaomusic.utils import (
|
||||
convert_file_to_mp3,
|
||||
custom_sort_key,
|
||||
deepcopy_data_no_sensitive_info,
|
||||
extract_audio_metadata,
|
||||
find_best_match,
|
||||
fuzzyfinder,
|
||||
get_audio_metadata,
|
||||
get_local_music_duration,
|
||||
get_web_music_duration,
|
||||
is_mp3,
|
||||
@ -399,7 +399,19 @@ class XiaoMusic:
|
||||
return sec, url
|
||||
|
||||
def get_music_tags(self, name):
|
||||
return self.all_music_tags.get(name, asdict(Metadata()))
|
||||
tags = copy.copy(self.all_music_tags.get(name, asdict(Metadata())))
|
||||
picture = tags["picture"]
|
||||
if picture:
|
||||
picture = picture.replace("\\", "/")
|
||||
if picture.startswith(self.config.picture_cache_path):
|
||||
picture = picture[len(self.config.picture_cache_path) :]
|
||||
if picture.startswith("/"):
|
||||
picture = picture[1:]
|
||||
encoded_name = urllib.parse.quote(picture)
|
||||
tags["picture"] = (
|
||||
f"{self.hostname}:{self.public_port}/picture/{encoded_name}"
|
||||
)
|
||||
return tags
|
||||
|
||||
def get_music_url(self, name):
|
||||
if self.is_web_music(name):
|
||||
@ -454,6 +466,8 @@ class XiaoMusic:
|
||||
else:
|
||||
self.log.info("刷新:tag cache 未启用")
|
||||
# TODO: 优化性能?
|
||||
# TODO 如何安全的清空 picture_cache_path
|
||||
self.all_music_tags = {} # 需要清空内存残留
|
||||
self.try_gen_all_music_tag()
|
||||
self.log.info("刷新:已启动重建 tag cache")
|
||||
|
||||
@ -512,11 +526,13 @@ class XiaoMusic:
|
||||
# TODO: 网络歌曲获取歌曲额外信息
|
||||
pass
|
||||
elif os.path.exists(file_or_url):
|
||||
all_music_tags[name] = get_audio_metadata(file_or_url)
|
||||
all_music_tags[name] = extract_audio_metadata(
|
||||
file_or_url, self.config.picture_cache_path
|
||||
)
|
||||
else:
|
||||
self.log.info(f"{name}/{file_or_url} 无法更新 tag")
|
||||
except BaseException as e:
|
||||
self.log.info(f"{e} {file_or_url} error {type(file_or_url)}!")
|
||||
self.log.exception(f"{e} {file_or_url} error {type(file_or_url)}!")
|
||||
# 全部更新结束后,一次性赋值
|
||||
self.all_music_tags = all_music_tags
|
||||
# 刷新 tag cache
|
||||
|
Loading…
Reference in New Issue
Block a user