mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-18 00:54:50 -04:00
added interactive mode
This commit is contained in:
parent
c79cbbd6f4
commit
5abe14aeb9
5 changed files with 77 additions and 14 deletions
|
@ -20,6 +20,7 @@ if not os.path.isdir(CACHE_DIR):
|
||||||
os.makedirs(CONFIG_DIR)
|
os.makedirs(CONFIG_DIR)
|
||||||
|
|
||||||
config = Config(CONFIG_PATH)
|
config = Config(CONFIG_PATH)
|
||||||
|
core = MusicDL(config)
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
|
@ -82,7 +83,6 @@ def download(ctx, **kwargs):
|
||||||
init_log()
|
init_log()
|
||||||
|
|
||||||
config.update_from_cli(**ctx.params)
|
config.update_from_cli(**ctx.params)
|
||||||
core = MusicDL(config, database=list() if kwargs["no_db"] else None)
|
|
||||||
for item in kwargs["items"]:
|
for item in kwargs["items"]:
|
||||||
try:
|
try:
|
||||||
if os.path.isfile(item):
|
if os.path.isfile(item):
|
||||||
|
|
|
@ -359,6 +359,7 @@ class DeezerClient(ClientInterface):
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.logged_in = True
|
self.logged_in = True
|
||||||
|
|
||||||
|
@region.cache_on_arguments(expiration_time=RELEASE_CACHE_TIME)
|
||||||
def search(self, query: str, media_type: str = "album", limit: int = 200) -> dict:
|
def search(self, query: str, media_type: str = "album", limit: int = 200) -> dict:
|
||||||
"""Search API for query.
|
"""Search API for query.
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import mutagen.id3 as id3
|
import mutagen.id3 as id3
|
||||||
|
@ -11,6 +12,8 @@ CONFIG_PATH = os.path.join(CONFIG_DIR, "config.yaml")
|
||||||
LOG_DIR = click.get_app_dir(APPNAME)
|
LOG_DIR = click.get_app_dir(APPNAME)
|
||||||
DB_PATH = os.path.join(LOG_DIR, "music-dl.db")
|
DB_PATH = os.path.join(LOG_DIR, "music-dl.db")
|
||||||
|
|
||||||
|
DOWNLOADS_DIR = os.path.join(Path.home(), "Music Downloads")
|
||||||
|
|
||||||
AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"
|
AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"
|
||||||
|
|
||||||
TIDAL_COVER_URL = "https://resources.tidal.com/images/{uuid}/{width}x{height}.jpg"
|
TIDAL_COVER_URL = "https://resources.tidal.com/images/{uuid}/{width}x{height}.jpg"
|
||||||
|
|
|
@ -2,9 +2,11 @@ import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
|
from string import Formatter
|
||||||
from typing import Generator, Optional, Tuple, Union
|
from typing import Generator, Optional, Tuple, Union
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
from simple_term_menu import TerminalMenu
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from .clients import DeezerClient, QobuzClient, TidalClient
|
from .clients import DeezerClient, QobuzClient, TidalClient
|
||||||
|
@ -62,12 +64,12 @@ class MusicDL(list):
|
||||||
:type source: str
|
:type source: str
|
||||||
"""
|
"""
|
||||||
click.secho(f"Enter {capitalize(source)} email:", fg="green")
|
click.secho(f"Enter {capitalize(source)} email:", fg="green")
|
||||||
self.config[source]["email"] = input()
|
self.config.file[source]["email"] = input()
|
||||||
click.secho(
|
click.secho(
|
||||||
f"Enter {capitalize(source)} password (will not show on screen):",
|
f"Enter {capitalize(source)} password (will not show on screen):",
|
||||||
fg="green",
|
fg="green",
|
||||||
)
|
)
|
||||||
self.config[source]["password"] = getpass(
|
self.config.file[source]["password"] = getpass(
|
||||||
prompt=""
|
prompt=""
|
||||||
) # does hashing work for tidal?
|
) # does hashing work for tidal?
|
||||||
|
|
||||||
|
@ -81,8 +83,8 @@ class MusicDL(list):
|
||||||
return
|
return
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.config[source]["email"] is None
|
self.config.file[source]["email"] is None
|
||||||
or self.config[source]["password"] is None
|
or self.config.file[source]["password"] is None
|
||||||
):
|
):
|
||||||
self.prompt_creds(source)
|
self.prompt_creds(source)
|
||||||
|
|
||||||
|
@ -110,8 +112,7 @@ class MusicDL(list):
|
||||||
"embed_cover": self.config.metadata["embed_cover"],
|
"embed_cover": self.config.metadata["embed_cover"],
|
||||||
}
|
}
|
||||||
|
|
||||||
client = self.clients[source]
|
client = self.get_client(source)
|
||||||
self.login(client)
|
|
||||||
|
|
||||||
item = MEDIA_CLASS[media_type](client=client, id=item_id)
|
item = MEDIA_CLASS[media_type](client=client, id=item_id)
|
||||||
self.append(item)
|
self.append(item)
|
||||||
|
@ -128,6 +129,13 @@ class MusicDL(list):
|
||||||
click.secho(f"Downloading {item!s}", fg="bright_green")
|
click.secho(f"Downloading {item!s}", fg="bright_green")
|
||||||
item.download(**arguments)
|
item.download(**arguments)
|
||||||
|
|
||||||
|
def get_client(self, source: str):
|
||||||
|
client = self.clients[source]
|
||||||
|
if not client.logged_in:
|
||||||
|
self.assert_creds(source)
|
||||||
|
self.login(client)
|
||||||
|
return client
|
||||||
|
|
||||||
def convert_all(self, codec, **kwargs):
|
def convert_all(self, codec, **kwargs):
|
||||||
click.secho("Converting the downloaded tracks...", fg="cyan")
|
click.secho("Converting the downloaded tracks...", fg="cyan")
|
||||||
for item in self:
|
for item in self:
|
||||||
|
@ -198,14 +206,65 @@ class MusicDL(list):
|
||||||
self.handle_url(line)
|
self.handle_url(line)
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self, query: str, media_type: str = "album", limit: int = 200
|
self, source: str, query: str, media_type: str = "album", limit: int = 200
|
||||||
) -> Generator:
|
) -> Generator:
|
||||||
results = self.client.search(query, media_type, limit)
|
client = self.get_client(source)
|
||||||
|
results = client.search(query, media_type)
|
||||||
|
|
||||||
|
i = 0
|
||||||
if isinstance(results, Generator): # QobuzClient
|
if isinstance(results, Generator): # QobuzClient
|
||||||
for page in results:
|
for page in results:
|
||||||
for item in page[f"{media_type}s"]["items"]:
|
for item in page[f"{media_type}s"]["items"]:
|
||||||
yield MEDIA_CLASS[media_type].from_api(item, self.client)
|
yield MEDIA_CLASS[media_type].from_api(item, client)
|
||||||
|
i += 1
|
||||||
|
if i > limit:
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
for item in results.get("data") or results.get("items"):
|
for item in results.get("data") or results.get("items"):
|
||||||
yield MEDIA_CLASS[media_type].from_api(item, self.client)
|
yield MEDIA_CLASS[media_type].from_api(item, client)
|
||||||
|
i += 1
|
||||||
|
if i > limit:
|
||||||
|
return
|
||||||
|
|
||||||
|
def preview_media(self, media):
|
||||||
|
if isinstance(media, Album):
|
||||||
|
fmt = (
|
||||||
|
"{albumartist} - {title}\n"
|
||||||
|
"Released on {year}\n{tracktotal} tracks\n"
|
||||||
|
"{bit_depth} bit / {sampling_rate} Hz\n"
|
||||||
|
"Version: {version}"
|
||||||
|
)
|
||||||
|
fields = (fname for _, fname, _, _ in Formatter().parse(fmt) if fname)
|
||||||
|
ret = fmt.format(**{k: media.get(k, "Unknown") for k in fields})
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def interactive_search(
|
||||||
|
self, query: str, source: str = "qobuz", media_type: str = "album"
|
||||||
|
):
|
||||||
|
results = tuple(self.search(source, query, media_type, limit=30))
|
||||||
|
|
||||||
|
def title(res):
|
||||||
|
return f"{res[0]+1}. {res[1].title}"
|
||||||
|
|
||||||
|
def from_title(s):
|
||||||
|
num = []
|
||||||
|
for char in s:
|
||||||
|
if char.isdigit():
|
||||||
|
num.append(char)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return self.preview_media(results[int("".join(num)) - 1])
|
||||||
|
|
||||||
|
menu = TerminalMenu(
|
||||||
|
map(title, enumerate(results)),
|
||||||
|
preview_command=from_title,
|
||||||
|
preview_size=0.5,
|
||||||
|
title=f"{capitalize(source)} {media_type} search",
|
||||||
|
cycle_cursor=True,
|
||||||
|
clear_screen=True,
|
||||||
|
)
|
||||||
|
choice = menu.show()
|
||||||
|
return results[choice]
|
||||||
|
|
|
@ -692,7 +692,7 @@ class Album(Tracklist):
|
||||||
"title": resp.get("title"),
|
"title": resp.get("title"),
|
||||||
"_artist": safe_get(resp, "artist", "name"),
|
"_artist": safe_get(resp, "artist", "name"),
|
||||||
"albumartist": safe_get(resp, "artist", "name"),
|
"albumartist": safe_get(resp, "artist", "name"),
|
||||||
"year": str(resp.get("year"))[:4],
|
"year": str(resp.get("year"))[:4] or "Unknown",
|
||||||
# version not given by API
|
# version not given by API
|
||||||
"cover_urls": {
|
"cover_urls": {
|
||||||
sk: resp.get(rk) # size key, resp key
|
sk: resp.get(rk) # size key, resp key
|
||||||
|
@ -705,7 +705,7 @@ class Album(Tracklist):
|
||||||
"quality": 6, # all tracks are 16/44.1 streamable
|
"quality": 6, # all tracks are 16/44.1 streamable
|
||||||
"bit_depth": 16,
|
"bit_depth": 16,
|
||||||
"sampling_rate": 44100,
|
"sampling_rate": 44100,
|
||||||
"tracktotal": resp.get("track_total"),
|
"tracktotal": resp.get("track_total") or resp.get("nb_tracks"),
|
||||||
}
|
}
|
||||||
|
|
||||||
raise InvalidSourceError(client.source)
|
raise InvalidSourceError(client.source)
|
||||||
|
@ -733,7 +733,7 @@ class Album(Tracklist):
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
album_title = self._title
|
album_title = self._title
|
||||||
if isinstance(self.version, str):
|
if hasattr(self, "version") and isinstance(self.version, str):
|
||||||
if self.version.lower() not in album_title.lower():
|
if self.version.lower() not in album_title.lower():
|
||||||
album_title = f"{album_title} ({self.version})"
|
album_title = f"{album_title} ({self.version})"
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue