mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-09 14:11:55 -04:00
More docs
This commit is contained in:
parent
1c2bd2545c
commit
963881ca27
10 changed files with 270 additions and 31 deletions
|
@ -69,7 +69,7 @@ build-backend = "poetry.core.masonry.api"
|
|||
|
||||
[tool.ruff.lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
select = ["E4", "E7", "E9", "F", "I", "ASYNC", "N", "RUF", "ERA001"]
|
||||
select = ["E4", "E7", "E9", "F", "I", "ASYNC", "N", "RUF", "ERA001", "D"]
|
||||
ignore = []
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
|
|
|
@ -22,7 +22,8 @@ logging.captureWarnings(True)
|
|||
class DeezerClient(Client):
|
||||
"""Client to handle deezer API. Does not do rate limiting.
|
||||
|
||||
Attributes:
|
||||
Attributes
|
||||
----------
|
||||
global_config: Entire config object
|
||||
client: client from deezer py used for API requests
|
||||
logged_in: True if logged in
|
||||
|
|
|
@ -242,7 +242,6 @@ class TidalDownloadable(Downloadable):
|
|||
:param out_path:
|
||||
:param encryption_key:
|
||||
"""
|
||||
|
||||
# Do not change this
|
||||
master_key = "UIlTTEMmmLfGowo/UC60x2H45W6MdGgTRfo/umg4754="
|
||||
|
||||
|
|
|
@ -1,3 +1,37 @@
|
|||
"""Module providing a QobuzClient class for interacting with the Qobuz API.
|
||||
|
||||
The QobuzClient class extends the Client class and includes methods for
|
||||
authentication, metadata retrieval, search, and downloading content from Qobuz.
|
||||
|
||||
Classes:
|
||||
- QobuzClient: Main class for interacting with the Qobuz API.
|
||||
|
||||
Usage:
|
||||
Example usage of the QobuzClient class:
|
||||
|
||||
```python
|
||||
from qobuz_client import QobuzClient
|
||||
|
||||
# Initialize the QobuzClient with a configuration object
|
||||
qobuz_client = QobuzClient(config)
|
||||
|
||||
# Log in to the Qobuz API
|
||||
await qobuz_client.login()
|
||||
|
||||
# Retrieve metadata for a track
|
||||
metadata = await qobuz_client.get_metadata("123456", "track")
|
||||
|
||||
# Search for albums by an artist
|
||||
search_results = await qobuz_client.search("artist", "John Doe", limit=5)
|
||||
|
||||
# Get user favorites for tracks
|
||||
user_favorites = await qobuz_client.get_user_favorites("track", limit=10)
|
||||
|
||||
# Download a track
|
||||
downloadable = await qobuz_client.get_downloadable("789012", quality=3)
|
||||
await downloadable.download("output_path")
|
||||
```
|
||||
"""
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
|
@ -64,6 +98,7 @@ class QobuzSpoofer:
|
|||
self.session = None
|
||||
|
||||
async def get_app_id_and_secrets(self) -> tuple[str, list[str]]:
|
||||
"""Request the relevant pages and return app ID and secrets."""
|
||||
assert self.session is not None
|
||||
async with self.session.get("https://play.qobuz.com/login") as req:
|
||||
login_page = await req.text()
|
||||
|
@ -124,20 +159,57 @@ class QobuzSpoofer:
|
|||
return app_id, secrets_list
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Enter context manager and create async client session."""
|
||||
self.session = aiohttp.ClientSession()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_):
|
||||
"""Close client session on context manager exit."""
|
||||
if self.session is not None:
|
||||
await self.session.close()
|
||||
self.session = None
|
||||
|
||||
|
||||
class QobuzClient(Client):
|
||||
"""QobuzClient class for interacting with the Qobuz API.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
source (str): The source identifier for Qobuz.
|
||||
max_quality (int): The maximum quality level supported by Qobuz.
|
||||
|
||||
Methods
|
||||
-------
|
||||
__init__(self, config: Config): Initialize the QobuzClient instance.
|
||||
login(self): Log in to the Qobuz API.
|
||||
get_metadata(self, item_id: str, media_type: str): Get metadata for a specified item.
|
||||
get_label(self, label_id: str) -> dict: Get details for a label.
|
||||
search(self, media_type: str, query: str, limit: int = 500) -> list[dict]: Search for items on Qobuz.
|
||||
get_featured(self, query, limit: int = 500) -> list[dict]: Get featured items on Qobuz.
|
||||
get_user_favorites(self, media_type: str, limit: int = 500) -> list[dict]: Get user favorites for a media type.
|
||||
get_user_playlists(self, limit: int = 500) -> list[dict]: Get user playlists on Qobuz.
|
||||
get_downloadable(self, item_id: str, quality: int) -> Downloadable: Get downloadable content details.
|
||||
|
||||
Private Methods
|
||||
_paginate(self, epoint: str, params: dict, limit: int = 500) -> list[dict]: Paginate search results.
|
||||
_get_app_id_and_secrets(self) -> tuple[str, list[str]]: Get Qobuz app ID and secrets.
|
||||
_get_valid_secret(self, secrets: list[str]) -> str: Get a valid secret for authentication.
|
||||
_test_secret(self, secret: str) -> Optional[str]: Test the validity of a secret.
|
||||
_request_file_url(self, track_id: str, quality: int, secret: str) -> tuple[int, dict]: Request file URL for downloading.
|
||||
_api_request(self, epoint: str, params: dict) -> tuple[int, dict]: Make a request to the Qobuz API.
|
||||
get_quality(quality: int): Map the quality level to Qobuz format.
|
||||
"""
|
||||
|
||||
source = "qobuz"
|
||||
max_quality = 4
|
||||
|
||||
def __init__(self, config: Config):
|
||||
"""Initialize a new QobuzClient instance.
|
||||
|
||||
Args:
|
||||
----
|
||||
config (Config): Configuration object containing session details.
|
||||
"""
|
||||
self.logged_in = False
|
||||
self.config = config
|
||||
self.rate_limiter = self.get_rate_limiter(
|
||||
|
@ -146,6 +218,15 @@ class QobuzClient(Client):
|
|||
self.secret: Optional[str] = None
|
||||
|
||||
async def login(self):
|
||||
"""Log in to the Qobuz API.
|
||||
|
||||
Raises
|
||||
------
|
||||
MissingCredentialsError: If email/user ID or password/token is missing.
|
||||
AuthenticationError: If invalid credentials are provided.
|
||||
InvalidAppIdError: If the app ID is invalid.
|
||||
IneligibleError: If the user has a free account that is not eligible for downloading tracks.
|
||||
"""
|
||||
self.session = await self.get_session()
|
||||
c = self.config.session.qobuz
|
||||
if not c.email_or_userid or not c.password_or_token:
|
||||
|
@ -198,6 +279,17 @@ class QobuzClient(Client):
|
|||
self.logged_in = True
|
||||
|
||||
async def get_metadata(self, item_id: str, media_type: str):
|
||||
"""Get metadata for a specified item.
|
||||
|
||||
Args:
|
||||
----
|
||||
item_id (str): The ID of the item.
|
||||
media_type (str): The type of media (e.g., artist, album, track).
|
||||
|
||||
Raises:
|
||||
------
|
||||
NonStreamableError: If there is an error fetching metadata.
|
||||
"""
|
||||
if media_type == "label":
|
||||
return await self.get_label(item_id)
|
||||
|
||||
|
@ -233,6 +325,16 @@ class QobuzClient(Client):
|
|||
return resp
|
||||
|
||||
async def get_label(self, label_id: str) -> dict:
|
||||
"""Get details for a label.
|
||||
|
||||
Args:
|
||||
----
|
||||
label_id (str): The ID of the label.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
dict: Details of the label.
|
||||
"""
|
||||
c = self.config.session.qobuz
|
||||
page_limit = 500
|
||||
params = {
|
||||
|
@ -273,6 +375,18 @@ class QobuzClient(Client):
|
|||
return label_resp
|
||||
|
||||
async def search(self, media_type: str, query: str, limit: int = 500) -> list[dict]:
|
||||
"""Search for items on Qobuz.
|
||||
|
||||
Args:
|
||||
----
|
||||
media_type (str): The type of media to search for (e.g., artist, album, track, playlist).
|
||||
query (str): The search query.
|
||||
limit (int): The maximum number of results to retrieve.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
list[dict]: List of search results.
|
||||
"""
|
||||
if media_type not in ("artist", "album", "track", "playlist"):
|
||||
raise Exception(f"{media_type} not available for search on qobuz")
|
||||
|
||||
|
@ -284,6 +398,21 @@ class QobuzClient(Client):
|
|||
return await self._paginate(epoint, params, limit=limit)
|
||||
|
||||
async def get_featured(self, query, limit: int = 500) -> list[dict]:
|
||||
"""Get featured items on Qobuz.
|
||||
|
||||
Args:
|
||||
----
|
||||
query: The type of featured items to retrieve.
|
||||
limit (int): The maximum number of results to retrieve.
|
||||
|
||||
Raises:
|
||||
------
|
||||
AssertionError: If the provided query is invalid.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
list[dict]: List of featured items.
|
||||
"""
|
||||
params = {
|
||||
"type": query,
|
||||
}
|
||||
|
@ -292,6 +421,21 @@ class QobuzClient(Client):
|
|||
return await self._paginate(epoint, params, limit=limit)
|
||||
|
||||
async def get_user_favorites(self, media_type: str, limit: int = 500) -> list[dict]:
|
||||
"""Get user favorites for a specific media type on Qobuz.
|
||||
|
||||
Args:
|
||||
----
|
||||
media_type (str): The type of media (e.g., track, artist, album).
|
||||
limit (int): The maximum number of results to retrieve.
|
||||
|
||||
Raises:
|
||||
------
|
||||
AssertionError: If the provided media type is invalid.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
list[dict]: List of user favorites for the specified media type.
|
||||
"""
|
||||
assert media_type in ("track", "artist", "album")
|
||||
params = {"type": f"{media_type}s"}
|
||||
epoint = "favorite/getUserFavorites"
|
||||
|
@ -299,10 +443,36 @@ class QobuzClient(Client):
|
|||
return await self._paginate(epoint, params, limit=limit)
|
||||
|
||||
async def get_user_playlists(self, limit: int = 500) -> list[dict]:
|
||||
"""Get user playlists on Qobuz.
|
||||
|
||||
Args:
|
||||
----
|
||||
limit (int): The maximum number of playlists to retrieve.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
list[dict]: List of user playlists.
|
||||
"""
|
||||
epoint = "playlist/getUserPlaylists"
|
||||
return await self._paginate(epoint, {}, limit=limit)
|
||||
|
||||
async def get_downloadable(self, item_id: str, quality: int) -> Downloadable:
|
||||
"""Get details of a downloadable item on Qobuz.
|
||||
|
||||
Args:
|
||||
----
|
||||
item_id (str): The ID of the item to download.
|
||||
quality (int): The quality level of the download.
|
||||
|
||||
Raises:
|
||||
------
|
||||
AssertionError: If the secret is not valid, not logged in, or quality level is out of bounds.
|
||||
NonStreamableError: If the item is not streamable or there is an error.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
Downloadable: Downloadable item details.
|
||||
"""
|
||||
assert self.secret is not None and self.logged_in and 1 <= quality <= 4
|
||||
status, resp_json = await self._request_file_url(item_id, quality, self.secret)
|
||||
assert status == 200
|
||||
|
@ -332,13 +502,15 @@ class QobuzClient(Client):
|
|||
) -> list[dict]:
|
||||
"""Paginate search results.
|
||||
|
||||
params:
|
||||
limit: If None, all the results are yielded. Otherwise a maximum
|
||||
of `limit` results are yielded.
|
||||
Args:
|
||||
----
|
||||
epoint (str): The API endpoint.
|
||||
params (dict): Parameters for the API request.
|
||||
limit (int): The maximum number of results to retrieve.
|
||||
|
||||
Returns
|
||||
Returns:
|
||||
-------
|
||||
Generator that yields (status code, response) tuples
|
||||
list[dict]: List of paginated search results.
|
||||
"""
|
||||
params.update({"limit": limit})
|
||||
status, page = await self._api_request(epoint, params)
|
||||
|
@ -408,7 +580,7 @@ class QobuzClient(Client):
|
|||
quality: int,
|
||||
secret: str,
|
||||
) -> tuple[int, dict]:
|
||||
quality = self.get_quality(quality)
|
||||
quality = self._get_quality(quality)
|
||||
unix_ts = time.time()
|
||||
r_sig = f"trackgetFileUrlformat_id{quality}intentstreamtrack_id{track_id}{unix_ts}{secret}"
|
||||
logger.debug("Raw request signature: %s", r_sig)
|
||||
|
@ -425,6 +597,7 @@ class QobuzClient(Client):
|
|||
|
||||
async def _api_request(self, epoint: str, params: dict) -> tuple[int, dict]:
|
||||
"""Make a request to the API.
|
||||
|
||||
returns: status code, json parsed response
|
||||
"""
|
||||
url = f"{QOBUZ_BASE_URL}/{epoint}"
|
||||
|
@ -434,6 +607,6 @@ class QobuzClient(Client):
|
|||
return response.status, await response.json()
|
||||
|
||||
@staticmethod
|
||||
def get_quality(quality: int):
|
||||
def _get_quality(quality: int):
|
||||
quality_map = (5, 6, 7, 27)
|
||||
return quality_map[quality - 1]
|
||||
|
|
|
@ -56,12 +56,12 @@ class SoundcloudClient(Client):
|
|||
"""Fetch metadata for an item in Soundcloud API.
|
||||
|
||||
Args:
|
||||
|
||||
----
|
||||
item_id (str): Plain soundcloud item ID (e.g 1633786176)
|
||||
media_type (str): track or playlist
|
||||
|
||||
Returns:
|
||||
|
||||
-------
|
||||
API response. The item IDs for the tracks in the playlist are modified to
|
||||
include resolution status.
|
||||
"""
|
||||
|
@ -141,9 +141,11 @@ class SoundcloudClient(Client):
|
|||
usage.
|
||||
|
||||
Args:
|
||||
----
|
||||
url (str): Url to resolve.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
API response for item.
|
||||
"""
|
||||
resp, status = await self._api_request("resolve", params={"url": url})
|
||||
|
|
|
@ -20,6 +20,8 @@ CURRENT_CONFIG_VERSION = "2.0.3"
|
|||
|
||||
@dataclass(slots=True)
|
||||
class QobuzConfig:
|
||||
"""Stores configuration related to Qobuz."""
|
||||
|
||||
use_auth_token: bool
|
||||
email_or_userid: str
|
||||
# This is an md5 hash of the plaintext password
|
||||
|
@ -35,6 +37,8 @@ class QobuzConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class TidalConfig:
|
||||
"""Stores configuration related to Tidal."""
|
||||
|
||||
# Do not change any of the fields below
|
||||
user_id: str
|
||||
country_code: str
|
||||
|
@ -52,6 +56,8 @@ class TidalConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class DeezerConfig:
|
||||
"""Stores configuration related to Deezer."""
|
||||
|
||||
# An authentication cookie that allows streamrip to use your Deezer account
|
||||
# See https://github.com/nathom/streamrip/wiki/Finding-Your-Deezer-ARL-Cookie
|
||||
# for instructions on how to find this
|
||||
|
@ -70,6 +76,8 @@ class DeezerConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class SoundcloudConfig:
|
||||
"""Stores configuration related to Soundcloud."""
|
||||
|
||||
# This changes periodically, so it needs to be updated
|
||||
client_id: str
|
||||
app_version: str
|
||||
|
@ -79,6 +87,8 @@ class SoundcloudConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class YoutubeConfig:
|
||||
"""Stores configuration related to Youtube."""
|
||||
|
||||
# The path to download the videos to
|
||||
video_downloads_folder: str
|
||||
# Only 0 is available for now
|
||||
|
@ -89,6 +99,8 @@ class YoutubeConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class DatabaseConfig:
|
||||
"""Stores configuration related to databases."""
|
||||
|
||||
downloads_enabled: bool
|
||||
downloads_path: str
|
||||
failed_downloads_enabled: bool
|
||||
|
@ -97,6 +109,8 @@ class DatabaseConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class ConversionConfig:
|
||||
"""Stores configuration related to audio coversion."""
|
||||
|
||||
enabled: bool
|
||||
# FLAC, ALAC, OPUS, MP3, VORBIS, or AAC
|
||||
codec: str
|
||||
|
@ -112,6 +126,8 @@ class ConversionConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class QobuzDiscographyFilterConfig:
|
||||
"""Stores configuration related to qobuz discography filters."""
|
||||
|
||||
# Remove Collectors Editions, live recordings, etc.
|
||||
extras: bool
|
||||
# Picks the highest quality out of albums with identical titles.
|
||||
|
@ -128,6 +144,8 @@ class QobuzDiscographyFilterConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class ArtworkConfig:
|
||||
"""Stores configuration related to Album artwork."""
|
||||
|
||||
# Write the image to the audio file
|
||||
embed: bool
|
||||
# The size of the artwork to embed. Options: thumbnail, small, large, original.
|
||||
|
@ -146,6 +164,8 @@ class ArtworkConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class MetadataConfig:
|
||||
"""Stores configuration related to Metadata."""
|
||||
|
||||
# Sets the value of the 'ALBUM' field in the metadata to the playlist's name.
|
||||
# This is useful if your music library software organizes tracks based on album name.
|
||||
set_playlist_to_album: bool
|
||||
|
@ -159,6 +179,8 @@ class MetadataConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class FilepathsConfig:
|
||||
"""Stores configuration related to Filepaths."""
|
||||
|
||||
# Create folders for single tracks within the downloads directory using the folder_format
|
||||
# template
|
||||
add_singles_to_folder: bool
|
||||
|
@ -177,6 +199,8 @@ class FilepathsConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class DownloadsConfig:
|
||||
"""Stores configuration related to downloads."""
|
||||
|
||||
# Folder where tracks are downloaded to
|
||||
folder: str
|
||||
# Put Qobuz albums in a 'Qobuz' folder, Tidal albums in 'Tidal' etc.
|
||||
|
@ -194,6 +218,8 @@ class DownloadsConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class LastFmConfig:
|
||||
"""Stores configuration related to last.fm."""
|
||||
|
||||
# The source on which to search for the tracks.
|
||||
source: str
|
||||
# If no results were found with the primary source, the item is searched for
|
||||
|
@ -203,6 +229,8 @@ class LastFmConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class CliConfig:
|
||||
"""Stores configuration related to the command line interface."""
|
||||
|
||||
# Print "Downloading {Album name}" etc. to screen
|
||||
text_output: bool
|
||||
# Show resolve, download progress bars
|
||||
|
@ -213,6 +241,8 @@ class CliConfig:
|
|||
|
||||
@dataclass(slots=True)
|
||||
class MiscConfig:
|
||||
"""Stores miscellaneous configuration."""
|
||||
|
||||
version: str
|
||||
check_for_updates: bool
|
||||
|
||||
|
@ -231,6 +261,8 @@ assert os.path.isfile(BLANK_CONFIG_PATH), "Template config not found"
|
|||
|
||||
@dataclass(slots=True)
|
||||
class ConfigData:
|
||||
"""Stores all the configuration data."""
|
||||
|
||||
toml: TOMLDocument
|
||||
downloads: DownloadsConfig
|
||||
|
||||
|
@ -256,6 +288,7 @@ class ConfigData:
|
|||
|
||||
@classmethod
|
||||
def from_toml(cls, toml_str: str):
|
||||
"""Create a ConfigData instance from valid TOML."""
|
||||
# TODO: handle the mistake where Windows people forget to escape backslash
|
||||
toml = parse(toml_str)
|
||||
if (v := toml["misc"]["version"]) != CURRENT_CONFIG_VERSION: # type: ignore
|
||||
|
@ -300,36 +333,55 @@ class ConfigData:
|
|||
|
||||
@classmethod
|
||||
def defaults(cls):
|
||||
"""Return a ConfigData object filled with default values."""
|
||||
with open(BLANK_CONFIG_PATH) as f:
|
||||
return cls.from_toml(f.read())
|
||||
|
||||
def set_modified(self):
|
||||
"""Set the config data as modified for saving to disk."""
|
||||
self._modified = True
|
||||
|
||||
@property
|
||||
def modified(self):
|
||||
"""Get whether the config was modified for saving to disk."""
|
||||
return self._modified
|
||||
|
||||
def update_toml(self):
|
||||
update_toml_section_from_config(self.toml["downloads"], self.downloads)
|
||||
update_toml_section_from_config(self.toml["qobuz"], self.qobuz)
|
||||
update_toml_section_from_config(self.toml["tidal"], self.tidal)
|
||||
update_toml_section_from_config(self.toml["deezer"], self.deezer)
|
||||
update_toml_section_from_config(self.toml["soundcloud"], self.soundcloud)
|
||||
update_toml_section_from_config(self.toml["youtube"], self.youtube)
|
||||
update_toml_section_from_config(self.toml["lastfm"], self.lastfm)
|
||||
update_toml_section_from_config(self.toml["artwork"], self.artwork)
|
||||
update_toml_section_from_config(self.toml["filepaths"], self.filepaths)
|
||||
update_toml_section_from_config(self.toml["metadata"], self.metadata)
|
||||
update_toml_section_from_config(self.toml["qobuz_filters"], self.qobuz_filters)
|
||||
update_toml_section_from_config(self.toml["cli"], self.cli)
|
||||
update_toml_section_from_config(self.toml["database"], self.database)
|
||||
update_toml_section_from_config(self.toml["conversion"], self.conversion)
|
||||
"""Write the current state to the TOML object, which will be synced with disk."""
|
||||
_update_toml_section_from_config(self.toml["downloads"], self.downloads)
|
||||
_update_toml_section_from_config(self.toml["qobuz"], self.qobuz)
|
||||
_update_toml_section_from_config(self.toml["tidal"], self.tidal)
|
||||
_update_toml_section_from_config(self.toml["deezer"], self.deezer)
|
||||
_update_toml_section_from_config(self.toml["soundcloud"], self.soundcloud)
|
||||
_update_toml_section_from_config(self.toml["youtube"], self.youtube)
|
||||
_update_toml_section_from_config(self.toml["lastfm"], self.lastfm)
|
||||
_update_toml_section_from_config(self.toml["artwork"], self.artwork)
|
||||
_update_toml_section_from_config(self.toml["filepaths"], self.filepaths)
|
||||
_update_toml_section_from_config(self.toml["metadata"], self.metadata)
|
||||
_update_toml_section_from_config(self.toml["qobuz_filters"], self.qobuz_filters)
|
||||
_update_toml_section_from_config(self.toml["cli"], self.cli)
|
||||
_update_toml_section_from_config(self.toml["database"], self.database)
|
||||
_update_toml_section_from_config(self.toml["conversion"], self.conversion)
|
||||
|
||||
def get_source(
|
||||
self,
|
||||
source: str,
|
||||
) -> QobuzConfig | DeezerConfig | SoundcloudConfig | TidalConfig:
|
||||
"""Return the configuration for the source.
|
||||
|
||||
Args:
|
||||
----
|
||||
source (str): One of the available sources
|
||||
|
||||
Returns:
|
||||
-------
|
||||
A Config dataclass.
|
||||
|
||||
Raises:
|
||||
------
|
||||
Exception: If the source is invalid
|
||||
|
||||
"""
|
||||
d = {
|
||||
"qobuz": self.qobuz,
|
||||
"deezer": self.deezer,
|
||||
|
@ -342,13 +394,21 @@ class ConfigData:
|
|||
return res
|
||||
|
||||
|
||||
def update_toml_section_from_config(toml_section, config):
|
||||
def _update_toml_section_from_config(toml_section, config):
|
||||
for field in fields(config):
|
||||
toml_section[field.name] = getattr(config, field.name)
|
||||
|
||||
|
||||
class Config:
|
||||
"""Manages the synchronization between the config data and the file stored on disk.
|
||||
|
||||
It contains 2 copies of the data: one that will be synced with disk (self.file),
|
||||
and another that will be read during program execution, but not synced with
|
||||
disk (self.session).
|
||||
"""
|
||||
|
||||
def __init__(self, path: str, /):
|
||||
"""Create Config object."""
|
||||
self.path = path
|
||||
|
||||
with open(path) as toml_file:
|
||||
|
@ -357,6 +417,7 @@ class Config:
|
|||
self.session: ConfigData = copy.deepcopy(self.file)
|
||||
|
||||
def save_file(self):
|
||||
"""Save the file config copy to disk."""
|
||||
if not self.file.modified:
|
||||
return
|
||||
|
||||
|
@ -366,12 +427,15 @@ class Config:
|
|||
|
||||
@classmethod
|
||||
def defaults(cls):
|
||||
"""Return a Config object with default values."""
|
||||
return cls(BLANK_CONFIG_PATH)
|
||||
|
||||
def __enter__(self):
|
||||
"""Enter context manager."""
|
||||
return self
|
||||
|
||||
def __exit__(self, *_):
|
||||
"""Save to disk when context manager exits."""
|
||||
self.save_file()
|
||||
|
||||
|
||||
|
|
|
@ -261,6 +261,7 @@ class PendingLastfmPlaylist(Pending):
|
|||
if that fails.
|
||||
|
||||
Args:
|
||||
----
|
||||
query (str): Query to search
|
||||
s (Status):
|
||||
callback: function to call after each query completes
|
||||
|
|
|
@ -281,9 +281,8 @@ class AlbumMetadata:
|
|||
|
||||
@classmethod
|
||||
def from_tidal(cls, resp) -> AlbumMetadata | None:
|
||||
"""
|
||||
|
||||
Args:
|
||||
"""Args:
|
||||
----
|
||||
resp: API response containing album metadata.
|
||||
|
||||
Returns: AlbumMetadata instance if the album is streamable, otherwise None.
|
||||
|
|
|
@ -353,7 +353,7 @@ async def search(ctx, first, output_file, num_results, source, media_type, query
|
|||
"""Search for content using a specific source.
|
||||
|
||||
Example:
|
||||
|
||||
-------
|
||||
rip search qobuz album 'rumours'
|
||||
"""
|
||||
if first and output_file:
|
||||
|
|
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue