mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-09 14:11:55 -04:00
Merge branch 'dev'
# Conflicts: # setup.py # streamrip/__init__.py
This commit is contained in:
commit
48fb99494b
9 changed files with 130 additions and 27 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -13,3 +13,9 @@ StreamripDownloads
|
|||
*.wav
|
||||
*.log
|
||||
*.mp4
|
||||
*.opus
|
||||
*.mkv
|
||||
*.aac
|
||||
*.pyc
|
||||
*test.py
|
||||
/.mypy_cache
|
||||
|
|
|
@ -165,6 +165,14 @@ can be accessed with `rip config --open`.
|
|||
|
||||
```
|
||||
|
||||
## Integration with macOS Music app
|
||||
|
||||
`streamrip` was designed to be used seamlessly with the macOS Music app. To set it up, you need to find the `Automatically Add to Music.localized` folder inside the file given at `Music.app -> Preferences -> Files -> Music Media folder location`. Set the downloads folder to the path in the config file.
|
||||
|
||||
Next, enable `conversion` and set the `codec` to `alac`. If you want to save space, set `sampling_rate` to `48000`. Finally, set `keep_hires_cover` to `false`.
|
||||
|
||||
Now, you can download anything and it will appear in your Library!
|
||||
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
|
2
setup.py
2
setup.py
|
@ -14,7 +14,7 @@ requirements = read_file("requirements.txt").strip().split()
|
|||
# https://github.com/pypa/sampleproject/blob/main/setup.py
|
||||
setup(
|
||||
name=pkg_name,
|
||||
version="0.4.4",
|
||||
version="0.5",
|
||||
author="Nathan",
|
||||
author_email="nathanthomas707@gmail.com",
|
||||
keywords="lossless, hi-res, qobuz, tidal, deezer, audio, convert, soundcloud, mp3",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"""streamrip: the all in one music downloader.
|
||||
"""
|
||||
|
||||
__version__ = "0.4.4"
|
||||
__version__ = "0.5"
|
||||
|
|
|
@ -765,7 +765,7 @@ class Tracklist(list):
|
|||
|
||||
else:
|
||||
for item in self:
|
||||
if self.client.source != 'soundcloud':
|
||||
if self.client.source != "soundcloud":
|
||||
# soundcloud only gets metadata after `target` is called
|
||||
# message will be printed in `target`
|
||||
click.secho(f'\nDownloading "{item!s}"', fg="blue")
|
||||
|
@ -951,3 +951,62 @@ class Tracklist(list):
|
|||
|
||||
if isinstance(key, int):
|
||||
super().__setitem__(key, val)
|
||||
|
||||
|
||||
class YoutubeVideo:
|
||||
"""Dummy class implemented for consistency with the Media API."""
|
||||
|
||||
class DummyClient:
|
||||
source = "youtube"
|
||||
|
||||
def __init__(self, url: str):
|
||||
self.url = url
|
||||
self.client = self.DummyClient()
|
||||
|
||||
def download(
|
||||
self,
|
||||
parent_folder="StreamripDownloads",
|
||||
download_youtube_videos=False,
|
||||
youtube_video_downloads_folder="StreamripDownloads",
|
||||
**kwargs,
|
||||
):
|
||||
click.secho(f"Downloading url {self.url}", fg="blue")
|
||||
filename_formatter = "%(track_number)s.%(track)s.%(container)s"
|
||||
filename = os.path.join(parent_folder, filename_formatter)
|
||||
|
||||
p = subprocess.Popen(
|
||||
[
|
||||
"youtube-dl",
|
||||
"-x", # audio only
|
||||
"-q", # quiet mode
|
||||
"--add-metadata",
|
||||
"--audio-format",
|
||||
"mp3",
|
||||
"--embed-thumbnail",
|
||||
"-o",
|
||||
filename,
|
||||
self.url,
|
||||
]
|
||||
)
|
||||
|
||||
if download_youtube_videos:
|
||||
click.secho("Downloading video stream", fg='blue')
|
||||
pv = subprocess.Popen(
|
||||
[
|
||||
"youtube-dl",
|
||||
"-q",
|
||||
"-o",
|
||||
os.path.join(
|
||||
youtube_video_downloads_folder, "%(title)s.%(container)s"
|
||||
),
|
||||
self.url,
|
||||
]
|
||||
)
|
||||
pv.wait()
|
||||
p.wait()
|
||||
|
||||
def load_meta(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def tag(self, *args, **kwargs):
|
||||
pass
|
||||
|
|
|
@ -68,6 +68,11 @@ class Config:
|
|||
"soundcloud": {
|
||||
"quality": 0,
|
||||
},
|
||||
"youtube": {
|
||||
"quality": 0,
|
||||
"download_videos": False,
|
||||
"video_downloads_folder": DOWNLOADS_DIR,
|
||||
},
|
||||
"database": {"enabled": True, "path": None},
|
||||
"conversion": {
|
||||
"enabled": False,
|
||||
|
@ -225,6 +230,10 @@ class ConfigDocumentation:
|
|||
quality: 0, 1, or 2
|
||||
soundcloud:
|
||||
quality: Only 0 is available
|
||||
youtube:
|
||||
quality: Only 0 is available for now
|
||||
download_videos: Download the video along with the audio
|
||||
video_downloads_folder: The path to download the videos to
|
||||
database: This stores a list of item IDs so that repeats are not downloaded.
|
||||
filters: Filter a Qobuz artist's discography. Set to 'true' to turn on a filter.
|
||||
extras: Remove Collectors Editions, live recordings, etc.
|
||||
|
|
|
@ -12,7 +12,8 @@ CONFIG_PATH = os.path.join(CONFIG_DIR, "config.yaml")
|
|||
LOG_DIR = click.get_app_dir(APPNAME)
|
||||
DB_PATH = os.path.join(LOG_DIR, "downloads.db")
|
||||
|
||||
DOWNLOADS_DIR = os.path.join(Path.home(), "StreamripDownloads")
|
||||
HOME = Path.home()
|
||||
DOWNLOADS_DIR = os.path.join(HOME, "StreamripDownloads")
|
||||
|
||||
AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0"
|
||||
|
||||
|
@ -145,7 +146,7 @@ LASTFM_URL_REGEX = r"https://www.last.fm/user/\w+/playlists/\w+"
|
|||
QOBUZ_INTERPRETER_URL_REGEX = (
|
||||
r"https?://www\.qobuz\.com/\w\w-\w\w/interpreter/[-\w]+/[-\w]+"
|
||||
)
|
||||
|
||||
YOUTUBE_URL_REGEX = r"https://www\.youtube\.com/watch\?v=[-\w]+"
|
||||
|
||||
TIDAL_MAX_Q = 7
|
||||
|
||||
|
|
|
@ -12,13 +12,14 @@ import click
|
|||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
from .bases import Track, Video
|
||||
from .bases import Track, Video, YoutubeVideo
|
||||
from .clients import DeezerClient, QobuzClient, SoundCloudClient, TidalClient
|
||||
from .config import Config
|
||||
from .constants import (
|
||||
CONFIG_PATH,
|
||||
DB_PATH,
|
||||
LASTFM_URL_REGEX,
|
||||
YOUTUBE_URL_REGEX,
|
||||
MEDIA_TYPES,
|
||||
QOBUZ_INTERPRETER_URL_REGEX,
|
||||
SOUNDCLOUD_URL_REGEX,
|
||||
|
@ -58,6 +59,7 @@ class MusicDL(list):
|
|||
self.soundcloud_url_parse = re.compile(SOUNDCLOUD_URL_REGEX)
|
||||
self.lastfm_url_parse = re.compile(LASTFM_URL_REGEX)
|
||||
self.interpreter_url_parse = re.compile(QOBUZ_INTERPRETER_URL_REGEX)
|
||||
self.youtube_url_parse = re.compile(YOUTUBE_URL_REGEX)
|
||||
|
||||
self.config = config
|
||||
if self.config is None:
|
||||
|
@ -89,7 +91,17 @@ class MusicDL(list):
|
|||
:raises ParsingError
|
||||
"""
|
||||
|
||||
for source, url_type, item_id in self.parse_urls(url):
|
||||
# youtube is handled by youtube-dl, so much of the
|
||||
# processing is not necessary
|
||||
youtube_urls = self.youtube_url_parse.findall(url)
|
||||
if youtube_urls != []:
|
||||
self.extend(YoutubeVideo(u) for u in youtube_urls)
|
||||
|
||||
parsed = self.parse_urls(url)
|
||||
if not parsed and len(self) == 0:
|
||||
raise ParsingError(url)
|
||||
|
||||
for source, url_type, item_id in parsed:
|
||||
if item_id in self.db:
|
||||
logger.info(
|
||||
f"ID {item_id} already downloaded, use --no-db to override."
|
||||
|
@ -135,6 +147,12 @@ class MusicDL(list):
|
|||
],
|
||||
"download_videos": self.config.session["tidal"]["download_videos"],
|
||||
"download_booklets": self.config.session["qobuz"]["download_booklets"],
|
||||
"download_youtube_videos": self.config.session["youtube"][
|
||||
"download_videos"
|
||||
],
|
||||
"youtube_video_downloads_folder": self.config.session["youtube"][
|
||||
"video_downloads_folder"
|
||||
],
|
||||
}
|
||||
|
||||
def download(self):
|
||||
|
@ -157,7 +175,7 @@ class MusicDL(list):
|
|||
)
|
||||
click.secho("rip config --reset ", fg="yellow", nl=False)
|
||||
click.secho("to reset it. You will need to log in again.", fg="red")
|
||||
logger.debug(err)
|
||||
click.secho(err, fg='red')
|
||||
exit()
|
||||
|
||||
logger.debug("Arguments from config: %s", arguments)
|
||||
|
@ -170,6 +188,10 @@ class MusicDL(list):
|
|||
item.client.source
|
||||
)
|
||||
|
||||
if item is YoutubeVideo:
|
||||
item.download(**arguments)
|
||||
continue
|
||||
|
||||
arguments["quality"] = self.config.session[item.client.source]["quality"]
|
||||
if isinstance(item, Artist):
|
||||
filters_ = tuple(
|
||||
|
@ -266,10 +288,7 @@ class MusicDL(list):
|
|||
|
||||
logger.debug(f"Parsed urls: {parsed}")
|
||||
|
||||
if parsed != []:
|
||||
return parsed
|
||||
|
||||
raise ParsingError(f"Error parsing URL: `{url}`")
|
||||
return parsed
|
||||
|
||||
def handle_lastfm_urls(self, urls):
|
||||
# https://www.last.fm/user/nathan3895/playlists/12058911
|
||||
|
|
|
@ -7,7 +7,7 @@ import logging
|
|||
import os
|
||||
import re
|
||||
from tempfile import gettempdir
|
||||
from typing import Generator, Iterable, Union
|
||||
from typing import Dict, Generator, Iterable, Union
|
||||
|
||||
import click
|
||||
from pathvalidate import sanitize_filename
|
||||
|
@ -28,11 +28,6 @@ from .utils import (
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TYPE_REGEXES = {
|
||||
"remaster": re.compile(r"(?i)(re)?master(ed)?"),
|
||||
"extra": re.compile(r"(?i)(anniversary|deluxe|live|collector|demo|expanded|remix)"),
|
||||
}
|
||||
|
||||
|
||||
class Album(Tracklist):
|
||||
"""Represents a downloadable album.
|
||||
|
@ -415,10 +410,10 @@ class Playlist(Tracklist):
|
|||
self.download_message()
|
||||
|
||||
def _download_item(self, item: Track, **kwargs):
|
||||
kwargs['parent_folder'] = self.folder
|
||||
kwargs["parent_folder"] = self.folder
|
||||
if self.client.source == "soundcloud":
|
||||
item.load_meta()
|
||||
click.secho(f"Downloading {item!s}", fg='blue')
|
||||
click.secho(f"Downloading {item!s}", fg="blue")
|
||||
|
||||
if kwargs.get("set_playlist_to_album", False):
|
||||
item["album"] = self.name
|
||||
|
@ -649,6 +644,13 @@ class Artist(Tracklist):
|
|||
|
||||
# ----------- Filters --------------
|
||||
|
||||
TYPE_REGEXES = {
|
||||
"remaster": re.compile(r"(?i)(re)?master(ed)?"),
|
||||
"extra": re.compile(
|
||||
r"(?i)(anniversary|deluxe|live|collector|demo|expanded|remix)"
|
||||
),
|
||||
}
|
||||
|
||||
def _remove_repeats(self, bit_depth=max, sampling_rate=max) -> Generator:
|
||||
"""Remove the repeated albums from self. May remove different
|
||||
versions of the same album.
|
||||
|
@ -656,7 +658,7 @@ class Artist(Tracklist):
|
|||
:param bit_depth: either max or min functions
|
||||
:param sampling_rate: either max or min functions
|
||||
"""
|
||||
groups = dict()
|
||||
groups: Dict[str, list] = {}
|
||||
for album in self:
|
||||
if (t := self.essence(album.title)) not in groups:
|
||||
groups[t] = []
|
||||
|
@ -683,7 +685,7 @@ class Artist(Tracklist):
|
|||
"""
|
||||
return (
|
||||
album["albumartist"] != "Various Artists"
|
||||
and TYPE_REGEXES["extra"].search(album.title) is None
|
||||
and self.TYPE_REGEXES["extra"].search(album.title) is None
|
||||
)
|
||||
|
||||
def _features(self, album: Album) -> bool:
|
||||
|
@ -709,7 +711,7 @@ class Artist(Tracklist):
|
|||
:type album: Album
|
||||
:rtype: bool
|
||||
"""
|
||||
return TYPE_REGEXES["extra"].search(album.title) is None
|
||||
return self.TYPE_REGEXES["extra"].search(album.title) is None
|
||||
|
||||
def _non_remasters(self, album: Album) -> bool:
|
||||
"""Passed as a parameter by the user.
|
||||
|
@ -721,7 +723,7 @@ class Artist(Tracklist):
|
|||
:type album: Album
|
||||
:rtype: bool
|
||||
"""
|
||||
return TYPE_REGEXES["remaster"].search(album.title) is not None
|
||||
return self.TYPE_REGEXES["remaster"].search(album.title) is not None
|
||||
|
||||
def _non_albums(self, album: Album) -> bool:
|
||||
"""This will ignore non-album releases.
|
||||
|
@ -731,8 +733,7 @@ class Artist(Tracklist):
|
|||
:type album: Album
|
||||
:rtype: bool
|
||||
"""
|
||||
# Doesn't work yet
|
||||
return album["release_type"] == "album"
|
||||
return len(album) > 1
|
||||
|
||||
# --------- Magic Methods --------
|
||||
|
||||
|
@ -751,7 +752,7 @@ class Artist(Tracklist):
|
|||
"""
|
||||
return self.name
|
||||
|
||||
def __hash__(self) -> int:
|
||||
def __hash__(self):
|
||||
return hash(self.id)
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue