mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-23 03:27:14 -04:00
Changes to config artwork field
Also when converting, the track is kept in the temp_file and moved after conversion. Formatting.
This commit is contained in:
parent
de1db82da8
commit
6ed5f77464
8 changed files with 110 additions and 94 deletions
|
@ -1,4 +1,4 @@
|
|||
'''streamrip: the all in one music downloader.
|
||||
'''
|
||||
"""streamrip: the all in one music downloader.
|
||||
"""
|
||||
|
||||
__version__ = "0.4"
|
||||
|
|
|
@ -489,7 +489,11 @@ class TidalClient(ClientInterface):
|
|||
"assetpresentation": "FULL",
|
||||
}
|
||||
resp = self._api_request(f"tracks/{track_id}/playbackinfopostpaywall", params)
|
||||
try:
|
||||
manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8"))
|
||||
except KeyError:
|
||||
raise Exception("You must have a TIDAL Hi-Fi account to download tracks.")
|
||||
|
||||
logger.debug(manifest)
|
||||
return {
|
||||
"url": manifest["urls"][0],
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'''A config class that manages arguments between the config file and CLI.'''
|
||||
"""A config class that manages arguments between the config file and CLI."""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
@ -77,10 +77,8 @@ class Config:
|
|||
"downloads": {"folder": DOWNLOADS_DIR, "source_subdirectories": False},
|
||||
"artwork": {
|
||||
"embed": True,
|
||||
"embed_size": "large",
|
||||
"download_size": "original",
|
||||
"keep_embedded_cover": False,
|
||||
"keep_downloaded_cover": True,
|
||||
"size": "large",
|
||||
"keep_hires_cover": True,
|
||||
},
|
||||
"metadata": {
|
||||
"set_playlist_to_album": False,
|
||||
|
|
|
@ -87,7 +87,6 @@ class Converter:
|
|||
|
||||
shutil.move(self.tempfile, self.final_fn)
|
||||
logger.debug("Moved: %s -> %s", self.tempfile, self.final_fn)
|
||||
logger.debug("Converted: %s -> %s", self.filename, self.final_fn)
|
||||
else:
|
||||
raise ConversionError("No file was returned from conversion")
|
||||
|
||||
|
|
|
@ -153,18 +153,15 @@ class MusicDL(list):
|
|||
"parent_folder": self.config.session["downloads"]["folder"],
|
||||
"folder_format": self.config.session["path_format"]["folder"],
|
||||
"track_format": self.config.session["path_format"]["track"],
|
||||
"keep_downloaded_cover": self.config.session["artwork"][
|
||||
"keep_downloaded_cover"
|
||||
],
|
||||
"keep_embedded_cover": self.config.session["artwork"][
|
||||
"keep_embedded_cover"
|
||||
],
|
||||
|
||||
"embed_cover": self.config.session["artwork"]["embed"],
|
||||
"embed_cover_size": self.config.session["artwork"]["embed_size"],
|
||||
"download_cover_size": self.config.session["artwork"]["download_size"],
|
||||
"embed_cover_size": self.config.session["artwork"]["size"],
|
||||
"keep_hires_cover": self.config.session['artwork']['keep_hires_cover'],
|
||||
|
||||
"set_playlist_to_album": self.config.session["metadata"][
|
||||
"set_playlist_to_album"
|
||||
],
|
||||
"stay_temp": self.config.session["conversion"]["enabled"],
|
||||
}
|
||||
logger.debug("Arguments from config: %s", arguments)
|
||||
for item in self:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'''A simple wrapper over an sqlite database that stores
|
||||
"""A simple wrapper over an sqlite database that stores
|
||||
the downloaded media IDs.
|
||||
'''
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'''These classes parse information from Clients into a universal,
|
||||
"""These classes parse information from Clients into a universal,
|
||||
downloadable form.
|
||||
'''
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
@ -53,7 +53,7 @@ TIDAL_Q_MAP = {
|
|||
}
|
||||
|
||||
# used to homogenize cover size keys
|
||||
COVER_SIZES = ("thumbnail", "small", "large")
|
||||
COVER_SIZES = ("thumbnail", "small", "large", "original")
|
||||
|
||||
TYPE_REGEXES = {
|
||||
"remaster": re.compile(r"(?i)(re)?master(ed)?"),
|
||||
|
@ -178,22 +178,26 @@ class Track:
|
|||
|
||||
self.file_format = kwargs.get("track_format", TRACK_FORMAT)
|
||||
self.folder = sanitize_filepath(self.folder, platform="auto")
|
||||
self.format_final_path()
|
||||
|
||||
os.makedirs(self.folder, exist_ok=True)
|
||||
|
||||
if database is not None:
|
||||
if isinstance(database, MusicDB):
|
||||
if self.id in database:
|
||||
self.downloaded = True
|
||||
self.tagged = True
|
||||
self.path = self.final_path
|
||||
|
||||
click.secho(
|
||||
f"{self['title']} already logged in database, skipping.",
|
||||
fg="magenta",
|
||||
)
|
||||
return False # because the track was not downloaded
|
||||
|
||||
if os.path.isfile(self.format_final_path()): # track already exists
|
||||
if os.path.isfile(self.final_path): # track already exists
|
||||
self.downloaded = True
|
||||
self.tagged = True
|
||||
self.path = self.final_path
|
||||
click.secho(f"Track already downloaded: {self.final_path}", fg="magenta")
|
||||
return False
|
||||
|
||||
|
@ -202,14 +206,15 @@ class Track:
|
|||
self.download_cover()
|
||||
|
||||
if self.client.source == "soundcloud":
|
||||
# soundcloud client needs whole dict to get file url
|
||||
url_id = self.resp
|
||||
else:
|
||||
url_id = self.id
|
||||
|
||||
dl_info = self.client.get_file_url(url_id, self.quality)
|
||||
|
||||
temp_file = os.path.join(gettempdir(), f"~{hash(self.id)}_{quality}.tmp")
|
||||
logger.debug("Temporary file path: %s", temp_file)
|
||||
self.path = os.path.join(gettempdir(), f"{hash(self.id)}_{self.quality}.tmp")
|
||||
logger.debug("Temporary file path: %s", self.path)
|
||||
|
||||
if self.client.source == "qobuz":
|
||||
if not (dl_info.get("sampling_rate") and dl_info.get("url")) or dl_info.get(
|
||||
|
@ -224,35 +229,43 @@ class Track:
|
|||
|
||||
click.secho(f"\nDownloading {self!s}", fg="blue")
|
||||
|
||||
# --------- Download Track ----------
|
||||
if self.client.source in ("qobuz", "tidal"):
|
||||
logger.debug("Downloadable URL found: %s", dl_info.get("url"))
|
||||
tqdm_download(dl_info["url"], temp_file) # downloads file
|
||||
tqdm_download(dl_info["url"], self.path) # downloads file
|
||||
|
||||
elif self.client.source == "deezer": # Deezer
|
||||
logger.debug("Downloadable URL found: %s", dl_info)
|
||||
try:
|
||||
tqdm_download(dl_info, temp_file) # downloads file
|
||||
tqdm_download(dl_info, self.path) # downloads file
|
||||
except NonStreamable:
|
||||
logger.debug("Track is not downloadable %s", dl_info)
|
||||
click.secho("Track is not available for download", fg="red")
|
||||
return False
|
||||
|
||||
elif self.client.source == "soundcloud":
|
||||
temp_file = self._soundcloud_download(dl_info, temp_file)
|
||||
self._soundcloud_download(dl_info, self.path)
|
||||
|
||||
else:
|
||||
raise InvalidSourceError(self.client.source)
|
||||
|
||||
if isinstance(dl_info, dict) and dl_info.get("enc_key"):
|
||||
decrypt_mqa_file(temp_file, self.final_path, dl_info["enc_key"])
|
||||
else:
|
||||
shutil.move(temp_file, self.final_path)
|
||||
if (
|
||||
self.client.source == "tidal"
|
||||
and isinstance(dl_info, dict)
|
||||
and dl_info.get("enc_key", False)
|
||||
):
|
||||
out_path = f"{self.path}_dec"
|
||||
decrypt_mqa_file(self.path, out_path, dl_info["enc_key"])
|
||||
self.path = out_path
|
||||
|
||||
if not kwargs.get("stay_temp", False):
|
||||
self.move(self.final_path)
|
||||
|
||||
if isinstance(database, MusicDB):
|
||||
database.add(self.id)
|
||||
logger.debug(f"{self.id} added to database")
|
||||
|
||||
logger.debug("Downloaded: %s -> %s", temp_file, self.final_path)
|
||||
logger.debug("Downloaded: %s -> %s", self.path, self.final_path)
|
||||
|
||||
self.downloaded = True
|
||||
|
||||
|
@ -264,9 +277,14 @@ class Track:
|
|||
|
||||
return True
|
||||
|
||||
def _soundcloud_download(self, dl_info: dict, temp_file: str) -> str:
|
||||
def move(self, path: str):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
shutil.move(self.path, path)
|
||||
self.path = path
|
||||
|
||||
def _soundcloud_download(self, dl_info: dict) -> str:
|
||||
if dl_info["type"] == "mp3":
|
||||
temp_file += ".mp3"
|
||||
self.path += ".mp3"
|
||||
# convert hls stream to mp3
|
||||
subprocess.call(
|
||||
[
|
||||
|
@ -276,24 +294,22 @@ class Track:
|
|||
"-c",
|
||||
"copy",
|
||||
"-y",
|
||||
temp_file,
|
||||
self.path,
|
||||
"-loglevel",
|
||||
"fatal",
|
||||
]
|
||||
)
|
||||
elif dl_info["type"] == "original":
|
||||
tqdm_download(dl_info["url"], temp_file)
|
||||
tqdm_download(dl_info["url"], self.path)
|
||||
|
||||
# if a wav is returned, convert to flac
|
||||
engine = converter.FLAC(temp_file)
|
||||
temp_file = f"{temp_file}.flac"
|
||||
engine.convert(custom_fn=temp_file)
|
||||
engine = converter.FLAC(self.path)
|
||||
self.path = f"{self.path}.flac"
|
||||
engine.convert(custom_fn=self.path)
|
||||
|
||||
self.final_path = self.final_path.replace(".mp3", ".flac")
|
||||
self.quality = 2
|
||||
|
||||
return temp_file
|
||||
|
||||
def download_cover(self):
|
||||
"""Downloads the cover art, if cover_url is given."""
|
||||
|
||||
|
@ -405,15 +421,15 @@ class Track:
|
|||
if self.quality in (2, 3, 4):
|
||||
self.container = "FLAC"
|
||||
logger.debug("Tagging file with %s container", self.container)
|
||||
audio = FLAC(self.final_path)
|
||||
audio = FLAC(self.path)
|
||||
elif self.quality <= 1:
|
||||
if self.client.source == "tidal":
|
||||
self.container = "AAC"
|
||||
audio = MP4(self.final_path)
|
||||
audio = MP4(self.path)
|
||||
else:
|
||||
self.container = "MP3"
|
||||
try:
|
||||
audio = ID3(self.final_path)
|
||||
audio = ID3(self.path)
|
||||
except ID3NoHeaderError:
|
||||
audio = ID3()
|
||||
|
||||
|
@ -439,7 +455,7 @@ class Track:
|
|||
elif isinstance(audio, ID3):
|
||||
if embed_cover:
|
||||
audio.add(cover)
|
||||
audio.save(self.final_path, "v2_version=3")
|
||||
audio.save(self.path, "v2_version=3")
|
||||
elif isinstance(audio, MP4):
|
||||
audio["covr"] = [cover]
|
||||
audio.save()
|
||||
|
@ -485,18 +501,27 @@ class Track:
|
|||
if not hasattr(self, "final_path"):
|
||||
self.format_final_path()
|
||||
|
||||
if not os.path.isfile(self.final_path):
|
||||
logger.debug(f"File {self.final_path} does not exist. Skipping conversion.")
|
||||
if not os.path.isfile(self.path):
|
||||
logger.info("File %s does not exist. Skipping conversion.", self.path)
|
||||
click.secho(f"{self!s} does not exist. Skipping conversion.", fg="red")
|
||||
return
|
||||
|
||||
engine = CONV_CLASS[codec.upper()](
|
||||
filename=self.final_path,
|
||||
assert (
|
||||
self.container in CONV_CLASS
|
||||
), f"Invalid codec {codec}. Must be in {CONV_CLASS.keys()}"
|
||||
|
||||
engine = CONV_CLASS[self.container](
|
||||
filename=self.path,
|
||||
sampling_rate=kwargs.get("sampling_rate"),
|
||||
remove_source=kwargs.get("remove_source", True),
|
||||
)
|
||||
click.secho(f"Converting {self!s}", fg="blue")
|
||||
engine.convert()
|
||||
self.path = engine.final_fn
|
||||
self.final_path = self.final_path.replace(ext(self.quality, self.client.source), f".{engine.container}")
|
||||
|
||||
if not kwargs.get("stay_temp", False):
|
||||
self.move(self.final_path)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
|
@ -574,6 +599,7 @@ class Tracklist(list):
|
|||
>>> tlist[2]
|
||||
IndexError
|
||||
"""
|
||||
|
||||
essence_regex = re.compile(r"([^\(]+)(?:\s*[\(\[][^\)][\)\]])*")
|
||||
|
||||
def get(self, key: Union[str, int], default=None):
|
||||
|
@ -816,7 +842,7 @@ class Album(Tracklist):
|
|||
"version": resp.get("version"),
|
||||
"cover_urls": {
|
||||
size: tidal_cover_url(resp.get("cover"), x)
|
||||
for size, x in zip(COVER_SIZES, (160, 320, 1280))
|
||||
for size, x in zip(COVER_SIZES, (160, 320, 640, 1280))
|
||||
},
|
||||
"streamable": resp.get("allowStreaming"),
|
||||
"quality": TIDAL_Q_MAP[resp.get("audioQuality")],
|
||||
|
@ -844,7 +870,8 @@ class Album(Tracklist):
|
|||
"cover_urls": {
|
||||
sk: resp.get(rk) # size key, resp key
|
||||
for sk, rk in zip(
|
||||
COVER_SIZES, ("cover", "cover_medium", "cover_xl")
|
||||
COVER_SIZES,
|
||||
("cover", "cover_medium", "cover_large", "cover_xl"),
|
||||
)
|
||||
},
|
||||
"url": resp.get("link"),
|
||||
|
@ -911,7 +938,7 @@ class Album(Tracklist):
|
|||
):
|
||||
"""Download all of the tracks in the album.
|
||||
|
||||
:param quality: (5, 6, 7, 27)
|
||||
:param quality: (0, 1, 2, 3, 4)
|
||||
:type quality: int
|
||||
:param parent_folder: the folder to download the album to
|
||||
:type parent_folder: Union[str, os.PathLike]
|
||||
|
@ -927,40 +954,34 @@ class Album(Tracklist):
|
|||
quality = min(quality, self.client.max_quality)
|
||||
folder = self._get_formatted_folder(parent_folder, quality)
|
||||
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
logger.debug("Directory created: %s", folder)
|
||||
|
||||
# choose optimal cover size and download it
|
||||
cover = None
|
||||
cover_path = os.path.join(folder, "cover.jpg")
|
||||
|
||||
self.download_message()
|
||||
|
||||
click.secho("Downloading cover art", fg="magenta")
|
||||
download_cover_size = kwargs.get("download_cover_size", "original")
|
||||
cover_path = os.path.join(gettempdir(), f"cover_{hash(self)}.jpg")
|
||||
embed_cover_size = kwargs.get("embed_cover_size", "large")
|
||||
if not os.path.isfile(cover_path):
|
||||
if embed_cover_size not in self.cover_urls:
|
||||
embed_cover_size = "large"
|
||||
|
||||
assert (
|
||||
embed_cover_size in self.cover_urls
|
||||
), f"Invalid cover size. Must be in {self.cover_urls.keys()}"
|
||||
|
||||
tqdm_download(self.cover_urls[embed_cover_size], cover_path)
|
||||
if (
|
||||
self.cover_urls.get(download_cover_size, embed_cover_size)
|
||||
!= embed_cover_size
|
||||
or os.path.getsize(cover_path) > FLAC_MAX_BLOCKSIZE
|
||||
):
|
||||
# download cover at another resolution but don't use for embed
|
||||
embed_cover_path = cover_path.replace(".jpg", "_embed.jpg")
|
||||
shutil.move(cover_path, embed_cover_path)
|
||||
tqdm_download(self.cover_urls[download_cover_size], cover_path)
|
||||
else:
|
||||
embed_cover_path = cover_path
|
||||
else:
|
||||
embed_cover_path = cover_path
|
||||
|
||||
if kwargs.get("keep_hires_cover", True):
|
||||
tqdm_download(self.cover_urls['original'], os.path.join(folder, 'cover.jpg'))
|
||||
|
||||
cover_size = os.path.getsize(cover_path)
|
||||
if cover_size > FLAC_MAX_BLOCKSIZE: # 16.77 MB
|
||||
click.secho(
|
||||
"Downgrading embedded cover size, too large ({cover_size}).",
|
||||
fg="bright_yellow",
|
||||
)
|
||||
# large is about 600x600px which is guaranteed < 16.7 MB
|
||||
tqdm_download(self.cover_urls["large"], cover_path)
|
||||
|
||||
embed_cover = kwargs.get("embed_cover", True) # embed by default
|
||||
if self.client.source != "deezer" and embed_cover:
|
||||
cover = self.get_cover_obj(embed_cover_path, quality, self.client.source)
|
||||
cover = self.get_cover_obj(cover_path, quality, self.client.source)
|
||||
|
||||
download_args = {
|
||||
"quality": quality,
|
||||
|
@ -968,6 +989,7 @@ class Album(Tracklist):
|
|||
"progress_bar": kwargs.get("progress_bar", True),
|
||||
"database": database,
|
||||
"track_format": kwargs.get("track_format", TRACK_FORMAT),
|
||||
"stay_temp": kwargs.get("stay_temp")
|
||||
}
|
||||
for track in self:
|
||||
logger.debug("Downloading track to %s", folder)
|
||||
|
@ -975,23 +997,13 @@ class Album(Tracklist):
|
|||
disc_folder = os.path.join(folder, f"Disc {track.meta.discnumber}")
|
||||
download_args["parent_folder"] = disc_folder
|
||||
|
||||
track.download(**download_args)
|
||||
track.download(quality=quality, parent_folder=folder, database=database, **kwargs)
|
||||
|
||||
# deezer tracks come tagged
|
||||
if kwargs.get("tag_tracks", True) and self.client.source != "deezer":
|
||||
track.tag(cover=cover, embed_cover=embed_cover)
|
||||
|
||||
if not kwargs.get("keep_embedded_cover", False):
|
||||
try:
|
||||
os.remove(embed_cover_path)
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
# TODO: fix this, bad solution
|
||||
if not kwargs.get("keep_downloaded_cover", True):
|
||||
try:
|
||||
os.remove(cover_path)
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
self.downloaded = True
|
||||
|
||||
|
@ -1046,6 +1058,9 @@ class Album(Tracklist):
|
|||
def __len__(self) -> int:
|
||||
return self.tracktotal
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.id)
|
||||
|
||||
|
||||
class Playlist(Tracklist):
|
||||
"""Represents a downloadable playlist.
|
||||
|
@ -1219,7 +1234,7 @@ class Playlist(Tracklist):
|
|||
track.meta["tracknumber"] = str(i + 1)
|
||||
|
||||
if (
|
||||
track.download(parent_folder=folder, quality=quality, database=database)
|
||||
track.download(parent_folder=folder, quality=quality, database=database, **kwargs)
|
||||
and self.client.source != "deezer"
|
||||
):
|
||||
|
||||
|
@ -1540,6 +1555,9 @@ class Artist(Tracklist):
|
|||
"""
|
||||
return self.name
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
|
||||
|
||||
class Label(Artist):
|
||||
def load_meta(self):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'''Manages the information that will be embeded in the audio file. '''
|
||||
"""Manages the information that will be embeded in the audio file. """
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue