diff --git a/downloader/extensions/yandex/domain.py b/downloader/extensions/yandex/domain.py index f7fa1da..b361d4c 100644 --- a/downloader/extensions/yandex/domain.py +++ b/downloader/extensions/yandex/domain.py @@ -40,6 +40,7 @@ class Yandex(Domain, Fetchable): """ def __init__(self) -> None: self.quality = TrackQuality.STANDARD + self.displace = "_" @staticmethod def match(url: str) -> bool: @@ -53,12 +54,15 @@ def activate(self, common_options: list[str], kwargs_options: dict[str, str]) -> case _: Logger.warning("Option %s is not supported by Yandex domain", option) + self.displace = kwargs_options.get("Displace", "_") + def fetch_from(self, url: str) -> Target: for item in FETCH_MODELS: if (target := item.from_url(url)) is None: continue target.quality = self.quality + target.displace = self.displace return target Logger.error("Yandex domain unable to recognize url: %s", url) diff --git a/downloader/extensions/yandex/models/album.py b/downloader/extensions/yandex/models/album.py index 6b3882b..372204a 100644 --- a/downloader/extensions/yandex/models/album.py +++ b/downloader/extensions/yandex/models/album.py @@ -6,6 +6,7 @@ import logging import re +from collections.abc import Sequence from dataclasses import dataclass, field from typing import Optional @@ -30,6 +31,7 @@ class Album(Expandable): alone: bool = True available: bool = False quality: TrackQuality = field(default=TrackQuality.STANDARD, repr=False) + displace: str = "_" @staticmethod def from_url(url: str) -> Optional[Album]: @@ -38,7 +40,7 @@ def from_url(url: str) -> Optional[Album]: return Album(album.group(1)) return None - async def expand(self, session: ClientSession, system: FileSystem) -> list[ExpandedTargets]: + async def expand(self, session: ClientSession, system: FileSystem) -> Sequence[ExpandedTargets]: if not self.available: Logger.warning("Album %s is not available", self) @@ -50,12 +52,12 @@ async def expand(self, session: ClientSession, system: FileSystem) -> list[Expan raise RuntimeError(f"{self} wasn't prepared by `prepare` method") expanded = [] - async with system.into(self.title) as album: + async with system.into(self.title, self.displace) as album: if len(self.volumes) == 1: return [ExpandedTargets(album, self.volumes[0])] for part, tracks in enumerate(self.volumes, 1): - async with album.into(f"CD{part}") as subdir: + async with album.into(f"CD{part}", self.displace) as subdir: expanded.append(ExpandedTargets(subdir, tracks)) return expanded @@ -71,15 +73,16 @@ async def prepare(self, session: ClientSession) -> None: self.available = meta_info["available"] - self.title = meta_info["title"] + self.title = str(meta_info["title"]) if "version" in meta_info: self.title = f"{self.title} ({meta_info['version']})" if self.alone: artists = ", ".join((artist["name"] for artist in meta_info["artists"])) - self.title = artists + " - " + self.title # type: ignore + self.title = artists + " - " + self.title for volume in meta_info["volumes"]: self.volumes.append([ - Track(self.id, str(track["id"]), alone=False, quality=self.quality) for track in volume + Track(self.id, str(track["id"]), alone=False, quality=self.quality, displace=self.displace) + for track in volume ]) diff --git a/downloader/extensions/yandex/models/artist.py b/downloader/extensions/yandex/models/artist.py index 1eaeef5..d506f52 100644 --- a/downloader/extensions/yandex/models/artist.py +++ b/downloader/extensions/yandex/models/artist.py @@ -29,6 +29,7 @@ class Artist(Expandable): name: str | None = field(default=None) available: bool = False quality: TrackQuality = field(default=TrackQuality.STANDARD, repr=False) + displace: str = "_" @staticmethod def from_url(url: str) -> Optional[Artist]: @@ -48,7 +49,7 @@ async def expand(self, session: ClientSession, system: FileSystem) -> list[Expan Logger.error("%s wasn't prepared by `prepare` method", self) raise RuntimeError(f"{self} wasn't prepared by `prepare` method") - async with system.into(self.name) as artist: + async with system.into(self.name, self.displace) as artist: return [ExpandedTargets(artist, self.albums)] async def prepare(self, session: ClientSession) -> None: @@ -65,4 +66,4 @@ async def prepare(self, session: ClientSession) -> None: self.name = meta_info["artist"]["name"] for album in meta_info["albums"]: - self.albums.append(Album(str(album["id"]), alone=False, quality=self.quality)) + self.albums.append(Album(str(album["id"]), alone=False, quality=self.quality, displace=self.displace)) diff --git a/downloader/extensions/yandex/models/label.py b/downloader/extensions/yandex/models/label.py index 6357c31..610682d 100644 --- a/downloader/extensions/yandex/models/label.py +++ b/downloader/extensions/yandex/models/label.py @@ -28,6 +28,7 @@ class Label(Expandable): albums: list[Album] = field(default_factory=list) name: str | None = field(default=None) # name quality: TrackQuality = field(default=TrackQuality.STANDARD, repr=False) + displace: str = "_" @staticmethod def from_url(url: str) -> Optional[Label]: @@ -40,7 +41,7 @@ async def expand(self, session: ClientSession, system: FileSystem) -> list[Expan Logger.error("%s wasn't prepared by `prepare` method", self) raise RuntimeError(f"{self} wasn't prepared by `prepare` method") - async with system.into(self.name) as artist: + async with system.into(self.name, self.displace) as artist: return [ExpandedTargets(artist, self.albums)] async def prepare(self, session: ClientSession) -> None: @@ -56,4 +57,4 @@ async def prepare(self, session: ClientSession) -> None: self.name = meta_info["label"]["name"] for album in meta_info["albums"]: - self.albums.append(Album(str(album["id"]), quality=self.quality)) + self.albums.append(Album(str(album["id"]), quality=self.quality, displace=self.displace)) diff --git a/downloader/extensions/yandex/models/playlist.py b/downloader/extensions/yandex/models/playlist.py index 2cc3e00..6ab230c 100644 --- a/downloader/extensions/yandex/models/playlist.py +++ b/downloader/extensions/yandex/models/playlist.py @@ -30,6 +30,7 @@ class Playlist(Expandable): name: str | None = field(default=None) available: bool = False quality: TrackQuality = field(default=TrackQuality.STANDARD) + displace: str = "_" @staticmethod def from_url(url: str) -> Optional[Playlist]: @@ -49,7 +50,7 @@ async def expand(self, session: ClientSession, system: FileSystem) -> list[Expan Logger.error("%s wasn't prepared by `prepare` method", self) raise RuntimeError(f"{self} wasn't prepared by `prepare` method") - async with system.into(self.name) as playlist: + async with system.into(self.name, self.displace) as playlist: return [ExpandedTargets(playlist, self.tracks)] async def prepare(self, session: ClientSession) -> None: diff --git a/downloader/extensions/yandex/models/track/core.py b/downloader/extensions/yandex/models/track/core.py index 5eb2706..4dbfbe2 100644 --- a/downloader/extensions/yandex/models/track/core.py +++ b/downloader/extensions/yandex/models/track/core.py @@ -11,7 +11,8 @@ from typing import Optional from aiohttp import ClientSession -from defusedxml import ElementTree +# Note: Defused xml doesn't provide types for mypy +from defusedxml import ElementTree # type: ignore from downloader.fetcher import Downloadable from downloader.filesystem import FileSystem, IgnoredException @@ -33,6 +34,7 @@ class Track(Downloadable): available: bool = False alone: bool = True quality: TrackQuality = field(default=TrackQuality.STANDARD, repr=False) + displace: str = "_" @staticmethod def from_url(url: str) -> Optional[Track]: @@ -54,7 +56,8 @@ async def download(self, session: ClientSession, system: FileSystem) -> None: raise RuntimeError(f"{self} wasn't prepared by `prepare` method") # Receiving and processing download info - src = self.file.resource.removeprefix("//") + # Safe: Value self.file was checked above + src = self.file.resource.removeprefix("//") # type: ignore request = (api.TRACK_DOWN_REQUEST .with_url_fields(parameters={"src": src})) @@ -81,12 +84,13 @@ async def download(self, session: ClientSession, system: FileSystem) -> None: .with_section_fields("params", parameters={ "track-id": self.id})) - title = self.meta.info.title_from(self.alone) - filename = title + "." + self.file.codec + # Safe: Values self.meta, self.file were checked above + title = self.meta.info.title_from(self.alone) # type: ignore + filename = title + "." + self.file.codec # type: ignore # Trying to open file before downloading, because file may exist # In that case IgnoredException or FileExistError occurs - async with system.open(filename).to_file() as file: + async with system.open(filename, self.displace).to_file() as file: async with request.make(session) as response: if response.status != 200: Logger.error("Bad response %s for %s", response.status, self) @@ -102,8 +106,9 @@ async def download(self, session: ClientSession, system: FileSystem) -> None: print("DONE", title) Logger.info("Begin applying tags for track %s", title) - async with system.open(filename).to_track() as track: - self.meta.apply(track) + async with system.open(filename, self.displace).to_track() as track: + # Safe: Values self.meta, self.file were checked above + self.meta.apply(track) # type: ignore async def prepare(self, session: ClientSession) -> None: # Both under must have been prepared in following order: @@ -120,11 +125,12 @@ async def prepare_cover(self, session: ClientSession) -> None: Args: session: The client session instance that will be used for making requests. """ - if not all((self.meta, self.meta.cover)): + if self.meta is None or self.meta.cover is None: Logger.error("%s wasn't prepared by `prepare_meta` method", self) raise RuntimeError(f"{self} wasn't prepared by `prepare_meta` method") - src = self.meta.cover.resource.replace("%%", self.quality.cover()) + # Safe: Values self.meta was checked above + src = self.meta.cover.resource.replace("%%", self.quality.cover()) # type: ignore request = (api.TRACK_COVER_REQUEST .with_url_fields(parameters={"src": src})) @@ -142,8 +148,9 @@ async def prepare_cover(self, session: ClientSession) -> None: Logger.error("Bad content type `application/octet-stream` (RFC2616) for %s", self) raise RuntimeError("Bad content type `application/octet-stream` (RFC2616)") - self.meta.cover.content = await response.read() - self.meta.cover.mimetype = response.headers["CONTENT-TYPE"] + # Safe: Values self.meta was checked above + self.meta.cover.content = await response.read() # type: ignore + self.meta.cover.mimetype = response.headers["CONTENT-TYPE"] # type: ignore Logger.info("Cover was successfully prepared for %s", self) async def prepare_file(self, session: ClientSession) -> None: @@ -161,7 +168,7 @@ async def prepare_file(self, session: ClientSession) -> None: request = (api.TRACK_META_REQUEST .with_url_fields(parameters={"album": self.album, "track": self.id}) - .with_section_fields("params", parameters={"hq": self.quality.hq()})) + .with_section_fields("params", parameters={"hq": str(self.quality.hq())})) async with request.make(session) as response: if response.status != 200: diff --git a/downloader/extensions/yandex/models/track/models.py b/downloader/extensions/yandex/models/track/models.py index d4c0b67..6545138 100644 --- a/downloader/extensions/yandex/models/track/models.py +++ b/downloader/extensions/yandex/models/track/models.py @@ -37,6 +37,10 @@ def post_init(self): self.title = f"{self.title} ({self.version})" def apply(self, file: FileType) -> None: + if file.tags is None: + Logger.error("FileType hasn't provided tags. At least empty tags must be set") + raise RuntimeError("FileType.tags attribute is None, but must be at least an empty instance") + self.post_init() # The chosen one album translates all information in track @@ -87,6 +91,10 @@ def post_init(self): self.title = f"{self.title} ({self.version})" def apply(self, file: FileType) -> None: + if file.tags is None: + Logger.error("FileType hasn't provided tags. At least empty tags must be set") + raise RuntimeError("FileType.tags attribute is None, but must be at least an empty instance") + self.post_init() artists = [artist.name for artist in self.artists if not artist.composer] or [""] @@ -151,6 +159,10 @@ def apply(self, file: FileType) -> None: Logger.warning("Apply was calling for track but no cover provided") return + if file.tags is None: + Logger.error("FileType hasn't provided tags. At least empty tags must be set") + raise RuntimeError("FileType.tags attribute is None, but must be at least an empty instance") + file.tags.add(id3.APIC( data=self.content, desc="Cover", @@ -182,17 +194,24 @@ def apply(self, file: FileType) -> None: """ if self.text is None: Logger.warning("Apply was calling for track but no lyrics provided") + return + if self.authors is None: Logger.warning("Apply was calling for track but no lyrics authors provided") + return + + if file.tags is None: + Logger.error("FileType hasn't provided tags. At least empty tags must be set") + raise RuntimeError("FileType.tags attribute is None, but must be at least an empty instance") file.tags.add(id3.TEXT( encoding=3, # 3 for UTF-8 # see mutagen _specs.py - text=(self.authors or "").split(", "))) + text=self.authors.split(", "))) file.tags.add(id3.USLT( encoding=3, # 3 for UTF-8 # see mutagen _specs.py - text=(self.text or ""))) + text=self.text)) @dataclass