mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-25 04:24:49 -04:00
Merge branch 'dev'
This commit is contained in:
commit
b9a9e24162
8 changed files with 82 additions and 22 deletions
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "streamrip"
|
name = "streamrip"
|
||||||
version = "1.1"
|
version = "1.2"
|
||||||
description = "A fast, all-in-one music ripper for Qobuz, Deezer, Tidal, and SoundCloud"
|
description = "A fast, all-in-one music ripper for Qobuz, Deezer, Tidal, and SoundCloud"
|
||||||
authors = ["nathom <nathanthomas707@gmail.com>"]
|
authors = ["nathom <nathanthomas707@gmail.com>"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
@ -29,9 +29,9 @@ click = "^8.0.1"
|
||||||
tqdm = "^4.61.1"
|
tqdm = "^4.61.1"
|
||||||
tomlkit = "^0.7.2"
|
tomlkit = "^0.7.2"
|
||||||
pathvalidate = "^2.4.1"
|
pathvalidate = "^2.4.1"
|
||||||
simple-term-menu = {version = "^1.2.1", platform = 'linux or darwin'}
|
simple-term-menu = {version = "^1.2.1", platform = 'darwin|linux'}
|
||||||
pick = {version = "^1.0.0", platform = 'win32 or cygwin'}
|
pick = {version = "^1.0.0", platform = 'win32 or cygwin'}
|
||||||
windows-curses = {version = "^2.2.0", platform = 'win32 or cygwin'}
|
windows-curses = {version = "^2.2.0", platform = 'win32|cygwin'}
|
||||||
Pillow = "^8.3.0"
|
Pillow = "^8.3.0"
|
||||||
deezer-py = "^1.0.4"
|
deezer-py = "^1.0.4"
|
||||||
pycryptodomex = "^3.10.1"
|
pycryptodomex = "^3.10.1"
|
||||||
|
|
29
rip/cli.py
29
rip/cli.py
|
@ -276,12 +276,34 @@ class ConfigCommand(Command):
|
||||||
self.line("<info>Credentials saved to config.</info>")
|
self.line("<info>Credentials saved to config.</info>")
|
||||||
|
|
||||||
if self.option("deezer"):
|
if self.option("deezer"):
|
||||||
|
from streamrip.clients import DeezerClient
|
||||||
|
from streamrip.exceptions import AuthenticationError
|
||||||
|
|
||||||
self.line(
|
self.line(
|
||||||
"Follow the instructions at <url>https://github.com"
|
"Follow the instructions at <url>https://github.com"
|
||||||
"/nathom/streamrip/wiki/Finding-your-Deezer-ARL-Cookie</url>"
|
"/nathom/streamrip/wiki/Finding-your-Deezer-ARL-Cookie</url>"
|
||||||
)
|
)
|
||||||
|
|
||||||
config.file["deezer"]["arl"] = self.ask("Paste your ARL here: ")
|
given_arl = self.ask("Paste your ARL here: ").strip()
|
||||||
|
self.line("<comment>Validating arl...</comment>")
|
||||||
|
|
||||||
|
try:
|
||||||
|
DeezerClient().login(arl=given_arl)
|
||||||
|
config.file["deezer"]["arl"] = given_arl
|
||||||
|
config.save()
|
||||||
|
self.line("<b>Sucessfully logged in!</b>")
|
||||||
|
|
||||||
|
except AuthenticationError:
|
||||||
|
self.line("<error>Could not log in. Double check your ARL</error>")
|
||||||
|
|
||||||
|
if self.option("qobuz"):
|
||||||
|
import hashlib
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
config.file["qobuz"]["email"] = self.ask("Qobuz email:")
|
||||||
|
config.file["qobuz"]["password"] = hashlib.md5(
|
||||||
|
getpass.getpass("Qobuz password (won't show on screen): ").encode()
|
||||||
|
).hexdigest()
|
||||||
config.save()
|
config.save()
|
||||||
|
|
||||||
|
|
||||||
|
@ -412,7 +434,12 @@ class Application(BaseApplication):
|
||||||
|
|
||||||
def _run(self, io):
|
def _run(self, io):
|
||||||
if io.is_debug():
|
if io.is_debug():
|
||||||
|
from .constants import CONFIG_DIR
|
||||||
|
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
fh = logging.FileHandler(os.path.join(CONFIG_DIR, "streamrip.log"))
|
||||||
|
fh.setLevel(logging.DEBUG)
|
||||||
|
logger.addHandler(fh)
|
||||||
|
|
||||||
super()._run(io)
|
super()._run(io)
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ class Config:
|
||||||
self.load()
|
self.load()
|
||||||
else:
|
else:
|
||||||
logger.debug("Creating toml config file at '%s'", self._path)
|
logger.debug("Creating toml config file at '%s'", self._path)
|
||||||
|
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
||||||
shutil.copy(self.default_config_path, self._path)
|
shutil.copy(self.default_config_path, self._path)
|
||||||
self.load()
|
self.load()
|
||||||
self.file["downloads"]["folder"] = DOWNLOADS_DIR
|
self.file["downloads"]["folder"] = DOWNLOADS_DIR
|
||||||
|
|
|
@ -156,4 +156,4 @@ progress_bar = "dainty"
|
||||||
|
|
||||||
[misc]
|
[misc]
|
||||||
# Metadata to identify this config file. Do not change.
|
# Metadata to identify this config file. Do not change.
|
||||||
version = "1.1"
|
version = "1.2"
|
||||||
|
|
19
rip/core.py
19
rip/core.py
|
@ -488,6 +488,14 @@ class RipCore(list):
|
||||||
self._config_corrupted_message(err)
|
self._config_corrupted_message(err)
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
|
# Do not include tracks that have (re)mix, live, karaoke in their titles
|
||||||
|
# within parentheses or brackets
|
||||||
|
# This will match somthing like "Test (Person Remix]" though, so its not perfect
|
||||||
|
banned_words_plain = re.compile(r"(?i)(?:(?:re)?mix|live|karaoke)")
|
||||||
|
banned_words = re.compile(
|
||||||
|
rf"(?i)[\(\[][^\)\]]*?(?:(?:re)?mix|live|karaoke)[^\)\]]*[\]\)]"
|
||||||
|
)
|
||||||
|
|
||||||
def search_query(title, artist, playlist) -> bool:
|
def search_query(title, artist, playlist) -> bool:
|
||||||
"""Search for a query and add the first result to playlist.
|
"""Search for a query and add the first result to playlist.
|
||||||
|
|
||||||
|
@ -503,7 +511,16 @@ class RipCore(list):
|
||||||
query = QUERY_FORMAT[lastfm_source].format(
|
query = QUERY_FORMAT[lastfm_source].format(
|
||||||
title=title, artist=artist
|
title=title, artist=artist
|
||||||
)
|
)
|
||||||
track = next(self.search(source, query, media_type="track"))
|
query_is_clean = banned_words_plain.search(query) is None
|
||||||
|
|
||||||
|
search_results = self.search(source, query, media_type="track")
|
||||||
|
track = next(search_results)
|
||||||
|
|
||||||
|
if query_is_clean:
|
||||||
|
while banned_words.search(track["title"]) is not None:
|
||||||
|
logger.debug("Track title banned for query=%s", query)
|
||||||
|
track = next(search_results)
|
||||||
|
|
||||||
# Because the track is searched as a single we need to set
|
# Because the track is searched as a single we need to set
|
||||||
# this manually
|
# this manually
|
||||||
track.part_of_tracklist = True
|
track.part_of_tracklist = True
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""streamrip: the all in one music downloader."""
|
"""streamrip: the all in one music downloader."""
|
||||||
|
|
||||||
__version__ = "1.1"
|
__version__ = "1.2"
|
||||||
|
|
||||||
from . import clients, constants, converter, media
|
from . import clients, constants, converter, media
|
||||||
|
|
|
@ -118,6 +118,7 @@ class QobuzClient(Client):
|
||||||
:type pwd: str
|
:type pwd: str
|
||||||
:param kwargs: app_id: str, secrets: list, return_secrets: bool
|
:param kwargs: app_id: str, secrets: list, return_secrets: bool
|
||||||
"""
|
"""
|
||||||
|
# TODO: make this faster
|
||||||
secho(f"Logging into {self.source}", fg="green")
|
secho(f"Logging into {self.source}", fg="green")
|
||||||
email: str = kwargs["email"]
|
email: str = kwargs["email"]
|
||||||
pwd: str = kwargs["pwd"]
|
pwd: str = kwargs["pwd"]
|
||||||
|
@ -128,7 +129,7 @@ class QobuzClient(Client):
|
||||||
logger.debug("Already logged in")
|
logger.debug("Already logged in")
|
||||||
return
|
return
|
||||||
|
|
||||||
if (kwargs.get("app_id") or kwargs.get("secrets")) in (None, [], ""):
|
if not (kwargs.get("app_id") or kwargs.get("secrets")):
|
||||||
secho("Fetching tokens — this may take a few seconds.", fg="magenta")
|
secho("Fetching tokens — this may take a few seconds.", fg="magenta")
|
||||||
logger.info("Fetching tokens from Qobuz")
|
logger.info("Fetching tokens from Qobuz")
|
||||||
spoofer = Spoofer()
|
spoofer = Spoofer()
|
||||||
|
|
|
@ -1409,9 +1409,18 @@ class Album(Tracklist, Media):
|
||||||
self.folder_format = kwargs.get("folder_format", FOLDER_FORMAT)
|
self.folder_format = kwargs.get("folder_format", FOLDER_FORMAT)
|
||||||
self.quality = min(kwargs.get("quality", 3), self.client.max_quality)
|
self.quality = min(kwargs.get("quality", 3), self.client.max_quality)
|
||||||
|
|
||||||
self.folder = self._get_formatted_folder(
|
self.container = get_container(self.quality, self.client.source)
|
||||||
kwargs.get("parent_folder", "StreamripDownloads"), self.quality
|
# necessary to format the folder
|
||||||
)
|
if self.container in ("AAC", "MP3"):
|
||||||
|
# lossy codecs don't have these metrics
|
||||||
|
self.bit_depth = self.sampling_rate = None
|
||||||
|
|
||||||
|
parent_folder = kwargs.get("parent_folder", "StreamripDownloads")
|
||||||
|
if self.folder_format:
|
||||||
|
self.folder = self._get_formatted_folder(parent_folder)
|
||||||
|
else:
|
||||||
|
self.folder = parent_folder
|
||||||
|
|
||||||
os.makedirs(self.folder, exist_ok=True)
|
os.makedirs(self.folder, exist_ok=True)
|
||||||
|
|
||||||
self.download_message()
|
self.download_message()
|
||||||
|
@ -1483,7 +1492,11 @@ class Album(Tracklist, Media):
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
logger.debug("Downloading track to %s", self.folder)
|
logger.debug("Downloading track to %s", self.folder)
|
||||||
if self.disctotal > 1 and isinstance(item, Track):
|
if (
|
||||||
|
self.disctotal > 1
|
||||||
|
and isinstance(item, Track)
|
||||||
|
and kwargs.get("folder_format")
|
||||||
|
):
|
||||||
disc_folder = os.path.join(self.folder, f"Disc {item.meta.discnumber}")
|
disc_folder = os.path.join(self.folder, f"Disc {item.meta.discnumber}")
|
||||||
kwargs["parent_folder"] = disc_folder
|
kwargs["parent_folder"] = disc_folder
|
||||||
else:
|
else:
|
||||||
|
@ -1566,7 +1579,7 @@ class Album(Tracklist, Media):
|
||||||
logger.debug("Formatter: %s", fmt)
|
logger.debug("Formatter: %s", fmt)
|
||||||
return fmt
|
return fmt
|
||||||
|
|
||||||
def _get_formatted_folder(self, parent_folder: str, quality: int) -> str:
|
def _get_formatted_folder(self, parent_folder: str) -> str:
|
||||||
"""Generate the folder name for this album.
|
"""Generate the folder name for this album.
|
||||||
|
|
||||||
:param parent_folder:
|
:param parent_folder:
|
||||||
|
@ -1575,11 +1588,6 @@ class Album(Tracklist, Media):
|
||||||
:type quality: int
|
:type quality: int
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
# necessary to format the folder
|
|
||||||
self.container = get_container(quality, self.client.source)
|
|
||||||
if self.container in ("AAC", "MP3"):
|
|
||||||
# lossy codecs don't have these metrics
|
|
||||||
self.bit_depth = self.sampling_rate = None
|
|
||||||
|
|
||||||
formatted_folder = clean_format(self.folder_format, self._get_formatter())
|
formatted_folder = clean_format(self.folder_format, self._get_formatter())
|
||||||
|
|
||||||
|
@ -1764,8 +1772,11 @@ class Playlist(Tracklist, Media):
|
||||||
logger.debug(f"Loaded {len(self)} tracks from playlist {self.name}")
|
logger.debug(f"Loaded {len(self)} tracks from playlist {self.name}")
|
||||||
|
|
||||||
def _prepare_download(self, parent_folder: str = "StreamripDownloads", **kwargs):
|
def _prepare_download(self, parent_folder: str = "StreamripDownloads", **kwargs):
|
||||||
fname = sanitize_filename(self.name)
|
if kwargs.get("folder_format"):
|
||||||
self.folder = os.path.join(parent_folder, fname)
|
fname = sanitize_filename(self.name)
|
||||||
|
self.folder = os.path.join(parent_folder, fname)
|
||||||
|
else:
|
||||||
|
self.folder = parent_folder
|
||||||
|
|
||||||
# Used for safe concurrency with tracknumbers instead of an object
|
# Used for safe concurrency with tracknumbers instead of an object
|
||||||
# level that stores an index
|
# level that stores an index
|
||||||
|
@ -1953,8 +1964,11 @@ class Artist(Tracklist, Media):
|
||||||
:param kwargs:
|
:param kwargs:
|
||||||
:rtype: Iterable
|
:rtype: Iterable
|
||||||
"""
|
"""
|
||||||
folder = sanitize_filename(self.name)
|
if kwargs.get("folder_format"):
|
||||||
self.folder = os.path.join(parent_folder, folder)
|
folder = sanitize_filename(self.name)
|
||||||
|
self.folder = os.path.join(parent_folder, folder)
|
||||||
|
else:
|
||||||
|
self.folder = parent_folder
|
||||||
|
|
||||||
logger.debug("Artist folder: %s", folder)
|
logger.debug("Artist folder: %s", folder)
|
||||||
logger.debug(f"Length of tracklist {len(self)}")
|
logger.debug(f"Length of tracklist {len(self)}")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue