Initial import: Music_Server, MusicFree, catalog-sync
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user