diff --git a/src/music_kraken/pages/abstract.py b/src/music_kraken/pages/abstract.py index 87f2027..b2d3be3 100644 --- a/src/music_kraken/pages/abstract.py +++ b/src/music_kraken/pages/abstract.py @@ -26,6 +26,7 @@ from ..utils.string_processing import fit_to_file_system LOGGER = logging.getLogger("this shouldn't be used") + @dataclass class DefaultTarget: genre: str = DEFAULT_VALUES["genre"] @@ -33,23 +34,25 @@ class DefaultTarget: artist: str = DEFAULT_VALUES["artist"] album: str = DEFAULT_VALUES["album"] song: str = DEFAULT_VALUES["song"] - + def __setattr__(self, __name: str, __value: str) -> None: if __name in DEFAULT_VALUES: if self.__getattribute__(__name) == DEFAULT_VALUES[__name]: super().__setattr__(__name, fit_to_file_system(__value)) return - + super().__setattr__(__name, __value) - + @property def target(self) -> Target: return Target( relative_to_music_dir=True, - path=DOWNLOAD_PATH.format(genre=self.genre, label=self.label, artist=self.artist, album=self.album, song=self.song), - file=DOWNLOAD_FILE.format(genre=self.genre, label=self.label, artist=self.artist, album=self.album, song=self.song) + path=DOWNLOAD_PATH.format(genre=self.genre, label=self.label, artist=self.artist, album=self.album, + song=self.song), + file=DOWNLOAD_FILE.format(genre=self.genre, label=self.label, artist=self.artist, album=self.album, + song=self.song) ) - + class Page: """ @@ -61,11 +64,12 @@ class Page: TIMEOUT = 5 TRIES = 5 LOGGER = LOGGER - + SOURCE_TYPE: SourcePages @classmethod - def get_request(cls, url: str, stream: bool = False, accepted_response_codes: set = set((200,)), trie: int = 0) -> Optional[ + def get_request(cls, url: str, stream: bool = False, accepted_response_codes: set = set((200,)), trie: int = 0) -> \ + Optional[ requests.Response]: retry = False try: @@ -200,16 +204,17 @@ class Page: this gets ignored :return detailed_music_object: IT MODIFIES THE INPUT OBJ """ - + new_music_object: DatabaseObject = type(music_object)() had_sources = False source: Source for source in music_object.source_collection.get_sources_from_page(cls.SOURCE_TYPE): - new_music_object.merge(cls._fetch_object_from_source(source=source, obj_type=type(music_object), stop_at_level=stop_at_level)) + new_music_object.merge( + cls._fetch_object_from_source(source=source, obj_type=type(music_object), stop_at_level=stop_at_level)) had_sources = True - + if not had_sources: music_object.compile(merge_into=True) return music_object @@ -220,23 +225,23 @@ class Page: Album: Collection(element_type=Album), Song: Collection(element_type=Song) } - + cls._clean_music_object(new_music_object, collections) - - music_object.merge(new_music_object) - + + music_object.merge(new_music_object) + music_object.compile(merge_into=True) return music_object - + @classmethod def fetch_object_from_source(cls, source: Source, stop_at_level: int = 2): obj_type = cls._get_type_of_url(source.url) if obj_type is None: return None - + music_object = cls._fetch_object_from_source(source=source, obj_type=obj_type, stop_at_level=stop_at_level) - + collections = { Label: Collection(element_type=Label), Artist: Collection(element_type=Artist), @@ -244,27 +249,29 @@ class Page: Song: Collection(element_type=Song) } - cls._clean_music_object(music_object, collections) + cls._clean_music_object(music_object, collections) music_object.compile(merge_into=True) return music_object - @classmethod - def _fetch_object_from_source(cls, source: Source, obj_type: Union[Type[Song], Type[Album], Type[Artist], Type[Label]], stop_at_level: int = 1) -> Union[Song, Album, Artist, Label]: + def _fetch_object_from_source(cls, source: Source, + obj_type: Union[Type[Song], Type[Album], Type[Artist], Type[Label]], + stop_at_level: int = 1) -> Union[Song, Album, Artist, Label]: if obj_type == Artist: return cls._fetch_artist_from_source(source=source, stop_at_level=stop_at_level) - + if obj_type == Song: return cls._fetch_song_from_source(source=source, stop_at_level=stop_at_level) - + if obj_type == Album: return cls._fetch_album_from_source(source=source, stop_at_level=stop_at_level) - + if obj_type == Label: return cls._fetch_label_from_source(source=source, stop_at_level=stop_at_level) @classmethod - def _clean_music_object(cls, music_object: Union[Label, Album, Artist, Song], collections: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): + def _clean_music_object(cls, music_object: Union[Label, Album, Artist, Song], + collections: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): if type(music_object) == Label: return cls._clean_label(label=music_object, collections=collections) if type(music_object) == Artist: @@ -275,50 +282,55 @@ class Page: return cls._clean_song(song=music_object, collections=collections) @classmethod - def _clean_collection(cls, collection: Collection, collection_dict: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): + def _clean_collection(cls, collection: Collection, + collection_dict: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): if collection.element_type not in collection_dict: return for i, element in enumerate(collection): r = collection_dict[collection.element_type].append(element, merge_into_existing=True) collection[i] = r.current_element - + if not r.was_the_same: cls._clean_music_object(r.current_element, collection_dict) @classmethod - def _clean_label(cls, label: Label, collections: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): + def _clean_label(cls, label: Label, + collections: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): cls._clean_collection(label.current_artist_collection, collections) cls._clean_collection(label.album_collection, collections) @classmethod - def _clean_artist(cls, artist: Artist, collections: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): + def _clean_artist(cls, artist: Artist, + collections: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): cls._clean_collection(artist.main_album_collection, collections) cls._clean_collection(artist.feature_song_collection, collections) cls._clean_collection(artist.label_collection, collections) @classmethod - def _clean_album(cls, album: Album, collections: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): + def _clean_album(cls, album: Album, + collections: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): cls._clean_collection(album.label_collection, collections) cls._clean_collection(album.song_collection, collections) cls._clean_collection(album.artist_collection, collections) @classmethod - def _clean_song(cls, song: Song, collections: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): + def _clean_song(cls, song: Song, + collections: Dict[Union[Type[Song], Type[Album], Type[Artist], Type[Label]], Collection]): cls._clean_collection(song.album_collection, collections) cls._clean_collection(song.feature_artist_collection, collections) cls._clean_collection(song.main_artist_collection, collections) @classmethod def download( - cls, - music_object: Union[Song, Album, Artist, Label], - download_features: bool = True, - default_target: DefaultTarget = None - ) -> bool: + cls, + music_object: Union[Song, Album, Artist, Label], + download_features: bool = True, + default_target: DefaultTarget = None + ) -> bool: if default_target is None: default_target = DefaultTarget() - + if type(music_object) is Song: return cls.download_song(music_object, default_target) if type(music_object) is Album: @@ -327,26 +339,29 @@ class Page: return cls.download_artist(music_object, default_target) if type(music_object) is Label: return cls.download_label(music_object, download_features=download_features, default_target=default_target) - + return False - + @classmethod - def download_label(cls, label: Label, download_features: bool = True, override_existing: bool = False, default_target: DefaultTarget = None): + def download_label(cls, label: Label, download_features: bool = True, override_existing: bool = False, + default_target: DefaultTarget = None): if default_target is None: default_target = DefaultTarget() else: default_target = copy(default_target) default_target.label = label.name - + cls.fetch_details(label) for artist in label.current_artist_collection: - cls.download_artist(artist, download_features=download_features, override_existing=override_existing, default_target=default_target) - + cls.download_artist(artist, download_features=download_features, override_existing=override_existing, + default_target=default_target) + for album in label.album_collection: cls.download_album(album, override_existing=override_existing, default_target=default_target) @classmethod - def download_artist(cls, artist: Artist, download_features: bool = True, override_existing: bool = False, default_target: DefaultTarget = None): + def download_artist(cls, artist: Artist, download_features: bool = True, override_existing: bool = False, + default_target: DefaultTarget = None): if default_target is None: default_target = DefaultTarget() else: @@ -354,11 +369,11 @@ class Page: default_target.artist = artist.name if not artist.label_collection.empty: default_target.label = artist.label_collection[0].name - + cls.fetch_details(artist) for album in artist.main_album_collection: cls.download_album(album, override_existing=override_existing, default_target=default_target) - + if download_features: for song in artist.feature_album.song_collection: cls.download_song(song, override_existing=override_existing, default_target=default_target) @@ -374,15 +389,16 @@ class Page: default_target.artist = album.artist_collection[0].name if not album.label_collection.empty: default_target.label = album.label_collection[0].name - + cls.fetch_details(album) - + album.update_tracksort() for song in album.song_collection: cls.download_song(song, override_existing=override_existing, default_target=default_target) @classmethod - def download_song(cls, song: Song, override_existing: bool = False, create_target_on_demand: bool = True, default_target: DefaultTarget = None): + def download_song(cls, song: Song, override_existing: bool = False, create_target_on_demand: bool = True, + default_target: DefaultTarget = None): if default_target is None: default_target = DefaultTarget() else: @@ -393,64 +409,63 @@ class Page: if not song.main_artist_collection.empty: artist: Artist = song.main_artist_collection[0] default_target.artist = artist.name - + if not artist.label_collection.empty: default_target.label = artist.label_collection[0].name - + cls.fetch_details(song) - + if song.target_collection.empty: if create_target_on_demand and not song.main_artist_collection.empty and not song.album_collection.empty: song.target_collection.append(default_target.target) else: return - + target: Target if any(target.exists for target in song.target_collection) and not override_existing: existing_target: Target for existing_target in song.target_collection: if existing_target.exists: break - + for target in song.target_collection: if target is existing_target: continue - + existing_target.copy_content(target) return True - + sources = song.source_collection.get_sources_from_page(cls.SOURCE_TYPE) if len(sources) == 0: return False - + temp_target: Target = Target( path=shared.TEMP_DIR, file=str(random.randint(0, 999999)) ) - + success = True - + if not cls._download_song_to_targets(source=sources[0], target=temp_target): success = False - + if not cls._post_process_targets(song, temp_target): success = False - + return success - + @classmethod def _post_process_targets(cls, song: Song, temp_target: Target): write_metadata_to_target(song.metadata, temp_target) - + target: Target for target in song.target_collection: temp_target.copy_content(target) - @classmethod def _fetch_song_from_source(cls, source: Source, stop_at_level: int = 1) -> Song: return Song() - + @classmethod def _fetch_album_from_source(cls, source: Source, stop_at_level: int = 1) -> Album: return Album() @@ -466,7 +481,7 @@ class Page: @classmethod def _get_type_of_url(cls, url: str) -> Optional[Union[Type[Song], Type[Album], Type[Artist], Type[Label]]]: return None - + @classmethod - def _download_song_to_targets(cls, source: Source) -> Target: + def _download_song_to_targets(cls, source: Source, target: Target) -> Target: return Target() diff --git a/src/music_kraken/pages/download_center/search.py b/src/music_kraken/pages/download_center/search.py index 65835a4..a6f1543 100644 --- a/src/music_kraken/pages/download_center/search.py +++ b/src/music_kraken/pages/download_center/search.py @@ -29,7 +29,7 @@ class Search(Download): self._option_history: List[MultiPageOptions] = [] - self._current_option: MultiPageOptions = self.next_options + self._current_option: MultiPageOptions = self.next_options() def __repr__(self): diff --git a/src/music_kraken/pages/musify.py b/src/music_kraken/pages/musify.py index 5e551e4..6c77e59 100644 --- a/src/music_kraken/pages/musify.py +++ b/src/music_kraken/pages/musify.py @@ -847,6 +847,61 @@ class Musify(Page): source_list=source_list ) + @classmethod + def _parse_album(cls, soup: BeautifulSoup) -> Album: + name: str = None + source_list: List[Source] = [] + artist_list: List[Artist] = [] + + """ + if breadcrumb list has 4 elements, then + the -2 is the artist link, + the -1 is the album + """ + breadcrumb_soup: BeautifulSoup = soup.find("ol", {"class", "breadcrumb"}) + breadcrumb_elements: List[BeautifulSoup] = breadcrumb_soup.find_all("li", {"class": "breadcrumb-item"}) + if len(breadcrumb_elements) == 4: + # album + album_crumb: BeautifulSoup = breadcrumb_elements[-1] + name = album_crumb.text.strip() + + # artist + artist_crumb: BeautifulSoup = breadcrumb_elements[-2] + anchor: BeautifulSoup = artist_crumb.find("a") + if anchor is not None: + href = anchor.get("href") + artist_source_list: List[Source] = [] + + if href is not None: + artist_source_list.append(Source(cls.SOURCE_TYPE, cls.HOST + href.strip())) + + span: BeautifulSoup = anchor.find("span") + if span is not None: + artist_list.append(Artist( + name=span.get_text(strip=True), + source_list=artist_source_list + )) + else: + cls.LOGGER.debug("there are not 4 breadcrumb items, which shouldn't be the case") + + meta_url: BeautifulSoup = soup.find("meta", {"itemprop": "url"}) + if meta_url is not None: + url = meta_url.get("content") + if url is not None: + source_list.append(Source(cls.SOURCE_TYPE, cls.HOST + url)) + + meta_name: BeautifulSoup = soup.find("meta", {"itemprop": "name"}) + if meta_name is not None: + _name = meta_name.get("content") + if _name is not None: + name = _name + + return Album( + title=name, + source_list=source_list, + artist_list=artist_list + ) + @classmethod def _fetch_album_from_source(cls, source: Source, stop_at_level: int = 1) -> Album: """ @@ -862,17 +917,19 @@ class Musify(Page): :param source: :return: """ - album = Album(title="Hi :)", source_list=[source]) url = cls.parse_url(source.url) endpoint = cls.HOST + "/release/" + url.name_with_id r = cls.get_request(endpoint) if r is None: - return album + return Album() soup = BeautifulSoup(r.content, "html.parser") + album = cls._parse_album(soup) + print(album) + #
...
cards_soup: BeautifulSoup = soup.find("div", {"class": "card-body"}) if cards_soup is not None: @@ -890,9 +947,6 @@ class Musify(Page): album.update_tracksort() - print(album.song_collection) - print() - return album @classmethod @@ -908,17 +962,16 @@ class Musify(Page): return None @classmethod - def _download_song_to_targets(cls, source: Source, target: Target) -> Path: + def _download_song_to_targets(cls, source: Source, target: Target) -> bool: """ https://musify.club/track/im-in-a-coffin-life-never-was-waste-of-skin-16360302 https://musify.club/track/dl/16360302/im-in-a-coffin-life-never-was-waste-of-skin.mp3 """ + url: MusifyUrl = cls.parse_url(source.url) if url.source_type != MusifyTypes.SONG: - return - - + return False endpoint = f"https://musify.club/track/dl/{url.musify_id}/{url.name_without_id}.mp3" print(endpoint) - target.stream_into(cls.get_request(endpoint, stream=True)) + return target.stream_into(cls.get_request(endpoint, stream=True))