Initial import: Music_Server, MusicFree, catalog-sync

This commit is contained in:
2026-05-23 16:51:14 +08:00
commit 069af30dba
847 changed files with 179878 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
'''
Function:
Implementation of SongInfo
Author:
Zhenchao Jin
WeChat Official Account (微信公众号):
Charles的皮卡丘
'''
from __future__ import annotations
import os
from typing import Any, Dict, Optional
from dataclasses import dataclass, field, fields
from .misc import sanitize_filepath, safeextractfromdict, AudioLinkTester
def remove_prefix(value: str, prefix: str) -> str:
if prefix and value.startswith(prefix):
return value[len(prefix):]
return value
def remove_suffix(value: str, suffix: str) -> str:
if suffix and value.endswith(suffix):
return value[: -len(suffix)]
return value
'''SongInfo'''
@dataclass
class SongInfo:
# raw data replied by requested APIs
raw_data: Dict[str, Any] = field(default_factory=dict)
# from which music client
source: Optional[str] = None
root_source: Optional[str] = None
# song information
song_name: Optional[str] = None
singers: Optional[str] = None
album: Optional[str] = None
ext: Optional[str] = None
file_size_bytes: Optional[int] = None
file_size: Optional[str] = None
duration_s: Optional[int] = None
duration: Optional[str] = None
bitrate: Optional[int] = None
codec: Optional[str] = None
samplerate: Optional[int] = None
channels: Optional[int] = None
# lyric
lyric: Optional[str] = None
# cover
cover_url: Optional[str] = None
# episodes, each item in episodes is SongInfo object, used by FM site like XimalayaMusicClient
episodes: Optional[list[SongInfo]] = None
# download url related variables
download_url: Optional[Any] = None
download_url_status: Optional[Any] = None
default_download_headers: Dict[str, Any] = field(default_factory=dict)
downloaded_contents: Optional[Any] = None
chunk_size: Optional[int] = 1024 * 1024
protocol: Optional[str] = 'HTTP' # should be in {'HTTP', 'HLS'}
@property
def with_valid_download_url(self) -> bool:
if self.episodes: return all([eps.with_valid_download_url for eps in self.episodes])
if isinstance(self.download_url, str): is_valid_download_url_format = self.download_url and self.download_url.startswith('http')
else: is_valid_download_url_format = bool(self.download_url)
with_downloaded_contents = bool(self.downloaded_contents)
is_downloadable = isinstance(self.download_url_status, dict) and self.download_url_status.get('ok')
if not is_downloadable and (safeextractfromdict(self.download_url_status, ['probe_status', 'ext'], None) in AudioLinkTester.VALID_AUDIO_EXTS): is_downloadable = True
return bool((is_valid_download_url_format or with_downloaded_contents) and is_downloadable)
# save info
work_dir: Optional[str] = './'
_save_path: Optional[str] = None
@property
def save_path(self) -> str:
if self._save_path is not None: return self._save_path
ext = remove_prefix(str(self.ext or ""), ".")
sp, same_name_file_idx = os.path.join(self.work_dir, f"{self.song_name} - {self.identifier}.{ext}"), 1
while os.path.exists(sp):
sp = os.path.join(self.work_dir, f"{self.song_name} - {self.identifier} ({same_name_file_idx}).{ext}")
same_name_file_idx += 1
sp = sanitize_filepath(sp)
self._save_path = sp
return sp
# identifier
identifier: Optional[str] = None
'''fieldnames'''
@classmethod
def fieldnames(cls) -> set[str]:
return {f.name for f in fields(cls)}
'''fromdict'''
@classmethod
def fromdict(cls, data: Dict[str, Any]) -> "SongInfo":
field_names = cls.fieldnames()
filtered = {k: v for k, v in data.items() if k in field_names}
if "episodes" in filtered and filtered["episodes"] and isinstance(filtered["episodes"], list):
episodes = [cls.fromdict(e) if isinstance(e, dict) else e for e in filtered["episodes"]]
filtered["episodes"] = episodes
return cls(**filtered)
'''todict'''
def todict(self) -> Dict[str, Any]:
converted_dict = {f.name: getattr(self, f.name) for f in fields(self)}
if self.episodes and isinstance(self.episodes, list): converted_dict['episodes'] = [e.todict() for e in self.episodes]
return converted_dict
'''update'''
def update(self, data: Dict[str, Any] = None, **kwargs: Any) -> "SongInfo":
if data is None: data = {}
merged: Dict[str, Any] = {**data, **kwargs}
field_names = self.fieldnames()
for k, v in merged.items():
if k in field_names: setattr(self, k, v)
return self
'''getitem'''
def __getitem__(self, key: str) -> Any:
field_names = self.fieldnames()
if key not in field_names: raise KeyError(key)
return getattr(self, key)
'''setitem'''
def __setitem__(self, key: str, value: Any) -> None:
field_names = self.fieldnames()
if key not in field_names: raise KeyError(key)
setattr(self, key, value)
'''contains'''
def __contains__(self, key: object) -> bool:
return isinstance(key, str) and key in self.fieldnames()
'''get'''
def get(self, key: str, default: Any = None) -> Any:
if key in self.fieldnames(): return getattr(self, key)
return default
'''largerthan'''
def largerthan(self, song_info: SongInfo):
# file_size_a
try: file_size_a = float(remove_suffix(str(self.file_size), 'MB').strip())
except Exception: file_size_a = 0.0
if not isinstance(file_size_a, (int, float)): file_size_a = 0.0
# file_size_b
try: file_size_b = float(remove_suffix(str(song_info.file_size), 'MB').strip())
except Exception: file_size_b = 0.0
if not isinstance(file_size_b, (int, float)): file_size_b = 0.0
# compare
return bool(file_size_a > file_size_b)