200 lines
6.3 KiB
Python
200 lines
6.3 KiB
Python
from abc import ABC
|
|
from typing import Dict
|
|
from typing import Generic
|
|
from typing import List
|
|
from typing import TypeVar
|
|
|
|
from ytdl_sub.downloaders.downloader import Downloader
|
|
from ytdl_sub.downloaders.downloader import DownloaderValidator
|
|
from ytdl_sub.entries.soundcloud import SoundcloudAlbum
|
|
from ytdl_sub.entries.soundcloud import SoundcloudTrack
|
|
from ytdl_sub.validators.validators import BoolValidator
|
|
from ytdl_sub.validators.validators import StringValidator
|
|
|
|
###############################################################################
|
|
# Abstract Soundcloud downloader + options
|
|
|
|
|
|
class SoundcloudDownloaderOptions(DownloaderValidator, ABC):
|
|
"""
|
|
Abstract source validator for all soundcloud sources.
|
|
"""
|
|
|
|
_optional_keys = {"skip_premiere_tracks"}
|
|
|
|
def __init__(self, name: str, value: dict):
|
|
super().__init__(name=name, value=value)
|
|
self._skip_premiere_tracks = self._validate_key(
|
|
"skip_premiere_tracks", BoolValidator, default=True
|
|
)
|
|
|
|
@property
|
|
def skip_premiere_tracks(self) -> bool:
|
|
"""
|
|
Optional. True to skip tracks that require purchasing. False otherwise. Defaults to True.
|
|
"""
|
|
return self._skip_premiere_tracks.value
|
|
|
|
|
|
SoundcloudDownloaderOptionsT = TypeVar(
|
|
"SoundcloudDownloaderOptionsT", bound=SoundcloudDownloaderOptions
|
|
)
|
|
|
|
|
|
class SoundcloudDownloader(
|
|
Downloader[SoundcloudDownloaderOptionsT, SoundcloudTrack],
|
|
Generic[SoundcloudDownloaderOptionsT],
|
|
ABC,
|
|
):
|
|
"""
|
|
Class that handles downloading soundcloud entries via ytdl and converting them into
|
|
SoundcloudTrack / SoundcloudAlbumTrack objects
|
|
"""
|
|
|
|
downloader_entry_type = SoundcloudTrack
|
|
|
|
@classmethod
|
|
def ytdl_option_defaults(cls) -> Dict:
|
|
"""Returns default format to be best mp3"""
|
|
return {
|
|
"format": "bestaudio[ext=mp3]",
|
|
}
|
|
|
|
@classmethod
|
|
def artist_url(cls, artist_name: str) -> str:
|
|
"""Returns full artist url"""
|
|
return f"https://soundcloud.com/{artist_name}"
|
|
|
|
@classmethod
|
|
def artist_albums_url(cls, artist_name: str) -> str:
|
|
"""
|
|
Returns
|
|
-------
|
|
Full artist album url
|
|
"""
|
|
return cls.artist_url(artist_name) + "/albums"
|
|
|
|
@classmethod
|
|
def artist_tracks_url(cls, artist_name: str) -> str:
|
|
"""
|
|
Returns
|
|
-------
|
|
Full artist tracks url
|
|
"""
|
|
return cls.artist_url(artist_name) + "/tracks"
|
|
|
|
|
|
###############################################################################
|
|
# Soundcloud albums and singles downloader + options
|
|
|
|
|
|
class SoundcloudAlbumsAndSinglesDownloadOptions(SoundcloudDownloaderOptions):
|
|
"""
|
|
Downloads a soundcloud user's entire discography. Groups together album tracks and considers
|
|
any track not in an album as a single. Also includes any collaboration tracks.
|
|
|
|
Usage:
|
|
|
|
.. code-block:: yaml
|
|
|
|
presets:
|
|
my_example_preset:
|
|
soundcloud:
|
|
# required
|
|
download_strategy: "albums_and_singles"
|
|
username: soundcloud_username_from_url
|
|
# optional
|
|
skip_premiere_tracks: True
|
|
|
|
"""
|
|
|
|
_required_keys = {"username"}
|
|
|
|
def __init__(self, name, value):
|
|
super().__init__(name, value)
|
|
self._username = self._validate_key(key="username", validator=StringValidator)
|
|
|
|
@property
|
|
def username(self) -> str:
|
|
"""
|
|
Required. The Soundcloud username found in the url of their page.
|
|
"""
|
|
return self._username.value
|
|
|
|
|
|
class SoundcloudAlbumsAndSinglesDownloader(
|
|
SoundcloudDownloader[SoundcloudAlbumsAndSinglesDownloadOptions]
|
|
):
|
|
downloader_options_type = SoundcloudAlbumsAndSinglesDownloadOptions
|
|
|
|
def _get_albums(self, entry_dicts: List[Dict]) -> List[SoundcloudAlbum]:
|
|
"""
|
|
Parameters
|
|
----------
|
|
entry_dicts
|
|
Entry dicts from extracting info jsons
|
|
|
|
Returns
|
|
-------
|
|
Dict containing album_id: album class
|
|
"""
|
|
albums: Dict[str, SoundcloudAlbum] = {}
|
|
|
|
# First, get the albums themselves
|
|
for entry_dict in entry_dicts:
|
|
if entry_dict.get("extractor") == "soundcloud:set":
|
|
albums[entry_dict["id"]] = SoundcloudAlbum(
|
|
entry_dict=entry_dict, working_directory=self.working_directory
|
|
)
|
|
|
|
# Then, get all tracks that belong to the album
|
|
for entry_dict in entry_dicts:
|
|
album_id = entry_dict.get("playlist_id")
|
|
if entry_dict.get("extractor") == "soundcloud" and album_id in albums:
|
|
albums[album_id].tracks.append(
|
|
SoundcloudTrack(entry_dict=entry_dict, working_directory=self.working_directory)
|
|
)
|
|
|
|
return list(albums.values())
|
|
|
|
def _get_singles(
|
|
self, entry_dicts: List[Dict], albums: List[SoundcloudAlbum]
|
|
) -> List[SoundcloudTrack]:
|
|
tracks: List[SoundcloudTrack] = []
|
|
|
|
# Get all tracks that are not part of an album
|
|
for entry_dict in entry_dicts:
|
|
if entry_dict.get("extractor") == "soundcloud" and not any(
|
|
entry_dict in album for album in albums
|
|
):
|
|
tracks.append(
|
|
SoundcloudTrack(entry_dict=entry_dict, working_directory=self.working_directory)
|
|
)
|
|
|
|
return tracks
|
|
|
|
def download(self) -> List[SoundcloudTrack]:
|
|
"""
|
|
Soundcloud subscription to download albums and tracks as singles.
|
|
"""
|
|
artist_albums_url = self.artist_albums_url(artist_name=self.download_options.username)
|
|
artist_tracks_url = self.artist_tracks_url(artist_name=self.download_options.username)
|
|
|
|
album_entry_dicts = self.extract_info_via_info_json(url=artist_albums_url)
|
|
tracks_entry_dicts = self.extract_info_via_info_json(url=artist_tracks_url)
|
|
|
|
# Get all of the artist's albums
|
|
albums = self._get_albums(entry_dicts=album_entry_dicts)
|
|
|
|
# Then, get all singles
|
|
tracks = self._get_singles(entry_dicts=tracks_entry_dicts, albums=albums)
|
|
|
|
# Append all album tracks as SoundcloudAlbumTrack classes to the singles
|
|
for album in albums:
|
|
tracks += album.album_tracks()
|
|
|
|
# Filter any premiere tracks if specified
|
|
if self.download_options.skip_premiere_tracks:
|
|
tracks = [track for track in tracks if not track.is_premiere()]
|
|
|
|
return tracks
|