mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-13 06:34:45 -04:00
Add option to restrict filenames to ASCII #161
This commit is contained in:
parent
cddbd98224
commit
372a755215
4 changed files with 54 additions and 17 deletions
|
@ -145,6 +145,9 @@ folder_format = "{albumartist} - {title} ({year}) [{container}] [{bit_depth}B-{s
|
||||||
# Available keys: "tracknumber", "artist", "albumartist", "composer", "title",
|
# Available keys: "tracknumber", "artist", "albumartist", "composer", "title",
|
||||||
# and "albumcomposer"
|
# and "albumcomposer"
|
||||||
track_format = "{tracknumber}. {artist} - {title}"
|
track_format = "{tracknumber}. {artist} - {title}"
|
||||||
|
# Only allow printable ASCII characters in filenames.
|
||||||
|
restrict_characters = false
|
||||||
|
|
||||||
|
|
||||||
# Last.fm playlists are downloaded by searching for the titles of the tracks
|
# Last.fm playlists are downloaded by searching for the titles of the tracks
|
||||||
[lastfm]
|
[lastfm]
|
||||||
|
@ -160,4 +163,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.4"
|
version = "1.5"
|
||||||
|
|
|
@ -223,6 +223,7 @@ class RipCore(list):
|
||||||
)
|
)
|
||||||
concurrency = session["downloads"]["concurrency"]
|
concurrency = session["downloads"]["concurrency"]
|
||||||
return {
|
return {
|
||||||
|
"restrict_filenames": filepaths["restrict_characters"],
|
||||||
"parent_folder": session["downloads"]["folder"],
|
"parent_folder": session["downloads"]["folder"],
|
||||||
"folder_format": filepaths["folder_format"],
|
"folder_format": filepaths["folder_format"],
|
||||||
"track_format": filepaths["track_format"],
|
"track_format": filepaths["track_format"],
|
||||||
|
|
|
@ -29,7 +29,7 @@ from click import echo, secho, style
|
||||||
from mutagen.flac import FLAC, Picture
|
from mutagen.flac import FLAC, Picture
|
||||||
from mutagen.id3 import APIC, ID3, ID3NoHeaderError
|
from mutagen.id3 import APIC, ID3, ID3NoHeaderError
|
||||||
from mutagen.mp4 import MP4, MP4Cover
|
from mutagen.mp4 import MP4, MP4Cover
|
||||||
from pathvalidate import sanitize_filename, sanitize_filepath
|
from pathvalidate import sanitize_filepath
|
||||||
|
|
||||||
from . import converter
|
from . import converter
|
||||||
from .clients import Client, DeezloaderClient
|
from .clients import Client, DeezloaderClient
|
||||||
|
@ -50,6 +50,7 @@ from .exceptions import (
|
||||||
from .metadata import TrackMetadata
|
from .metadata import TrackMetadata
|
||||||
from .utils import (
|
from .utils import (
|
||||||
DownloadStream,
|
DownloadStream,
|
||||||
|
clean_filename,
|
||||||
clean_format,
|
clean_format,
|
||||||
decrypt_mqa_file,
|
decrypt_mqa_file,
|
||||||
downsize_image,
|
downsize_image,
|
||||||
|
@ -247,13 +248,16 @@ class Track(Media):
|
||||||
clean_format(
|
clean_format(
|
||||||
kwargs.get("folder_format", FOLDER_FORMAT),
|
kwargs.get("folder_format", FOLDER_FORMAT),
|
||||||
self.meta.get_album_formatter(self.quality),
|
self.meta.get_album_formatter(self.quality),
|
||||||
|
restrict=kwargs.get("restrict_filenames", False),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.file_format = kwargs.get("track_format", TRACK_FORMAT)
|
self.file_format = kwargs.get("track_format", TRACK_FORMAT)
|
||||||
|
|
||||||
self.folder = sanitize_filepath(self.folder, platform="auto")
|
self.folder = sanitize_filepath(self.folder, platform="auto")
|
||||||
self.format_final_path() # raises: ItemExists
|
self.format_final_path(
|
||||||
|
restrict=kwargs.get("restrict_filenames", False)
|
||||||
|
) # raises: ItemExists
|
||||||
|
|
||||||
os.makedirs(self.folder, exist_ok=True)
|
os.makedirs(self.folder, exist_ok=True)
|
||||||
|
|
||||||
|
@ -364,7 +368,9 @@ class Track(Media):
|
||||||
if self.quality != stream_quality:
|
if self.quality != stream_quality:
|
||||||
# The chosen quality is not available
|
# The chosen quality is not available
|
||||||
self.quality = stream_quality
|
self.quality = stream_quality
|
||||||
self.format_final_path() # If the extension is different
|
self.format_final_path(
|
||||||
|
restrict=kwargs.get("restrict_filenames", False)
|
||||||
|
) # If the extension is different
|
||||||
|
|
||||||
with open(self.path, "wb") as file:
|
with open(self.path, "wb") as file:
|
||||||
for chunk in tqdm_stream(stream, desc=self._progress_desc):
|
for chunk in tqdm_stream(stream, desc=self._progress_desc):
|
||||||
|
@ -503,16 +509,17 @@ class Track(Media):
|
||||||
logger.debug("Cover already exists, skipping download")
|
logger.debug("Cover already exists, skipping download")
|
||||||
raise ItemExists(self.cover_path)
|
raise ItemExists(self.cover_path)
|
||||||
|
|
||||||
def format_final_path(self) -> str:
|
def format_final_path(self, restrict: bool = False) -> str:
|
||||||
"""Return the final filepath of the downloaded file.
|
"""Return the final filepath of the downloaded file.
|
||||||
|
|
||||||
This uses the `get_formatter` method of TrackMetadata, which returns
|
This uses the `get_formatter` method of TrackMetadata, which returns
|
||||||
a dict with the keys allowed in formatter strings, and their values in
|
a dict with the keys allowed in formatter strings, and their values in
|
||||||
the TrackMetadata object.
|
the TrackMetadata object.
|
||||||
"""
|
"""
|
||||||
|
print(f"{restrict=}")
|
||||||
formatter = self.meta.get_formatter(max_quality=self.quality)
|
formatter = self.meta.get_formatter(max_quality=self.quality)
|
||||||
logger.debug("Track meta formatter %s", formatter)
|
logger.debug("Track meta formatter %s", formatter)
|
||||||
filename = clean_format(self.file_format, formatter)
|
filename = clean_format(self.file_format, formatter, restrict=restrict)
|
||||||
self.final_path = os.path.join(self.folder, filename)[
|
self.final_path = os.path.join(self.folder, filename)[
|
||||||
:250
|
:250
|
||||||
].strip() + ext(self.quality, self.client.source)
|
].strip() + ext(self.quality, self.client.source)
|
||||||
|
@ -725,7 +732,7 @@ class Track(Media):
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
if not hasattr(self, "final_path"):
|
if not hasattr(self, "final_path"):
|
||||||
self.format_final_path()
|
self.format_final_path(kwargs.get("restrict_filenames", False))
|
||||||
|
|
||||||
if not os.path.isfile(self.path):
|
if not os.path.isfile(self.path):
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -1079,9 +1086,11 @@ class Booklet:
|
||||||
:type parent_folder: str
|
:type parent_folder: str
|
||||||
:param kwargs:
|
:param kwargs:
|
||||||
"""
|
"""
|
||||||
filepath = os.path.join(
|
fn = clean_filename(
|
||||||
parent_folder, f"{sanitize_filename(self.description)}.pdf"
|
self.description, restrict=kwargs.get("restrict_filenames")
|
||||||
)
|
)
|
||||||
|
filepath = os.path.join(parent_folder, f"{fn}.pdf")
|
||||||
|
|
||||||
_quick_download(self.url, filepath, "Booklet")
|
_quick_download(self.url, filepath, "Booklet")
|
||||||
|
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
|
@ -1468,7 +1477,9 @@ class Album(Tracklist, Media):
|
||||||
|
|
||||||
parent_folder = kwargs.get("parent_folder", "StreamripDownloads")
|
parent_folder = kwargs.get("parent_folder", "StreamripDownloads")
|
||||||
if self.folder_format:
|
if self.folder_format:
|
||||||
self.folder = self._get_formatted_folder(parent_folder)
|
self.folder = self._get_formatted_folder(
|
||||||
|
parent_folder, restrict=kwargs.get("restrict_filenames", False)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.folder = parent_folder
|
self.folder = parent_folder
|
||||||
|
|
||||||
|
@ -1639,7 +1650,9 @@ 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) -> str:
|
def _get_formatted_folder(
|
||||||
|
self, parent_folder: str, restrict: bool = False
|
||||||
|
) -> str:
|
||||||
"""Generate the folder name for this album.
|
"""Generate the folder name for this album.
|
||||||
|
|
||||||
:param parent_folder:
|
:param parent_folder:
|
||||||
|
@ -1650,7 +1663,9 @@ class Album(Tracklist, Media):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
formatted_folder = clean_format(
|
formatted_folder = clean_format(
|
||||||
self.folder_format, self._get_formatter()
|
self.folder_format,
|
||||||
|
self._get_formatter(),
|
||||||
|
restrict=restrict,
|
||||||
)
|
)
|
||||||
|
|
||||||
return os.path.join(parent_folder, formatted_folder)
|
return os.path.join(parent_folder, formatted_folder)
|
||||||
|
@ -1843,7 +1858,9 @@ class Playlist(Tracklist, Media):
|
||||||
self, parent_folder: str = "StreamripDownloads", **kwargs
|
self, parent_folder: str = "StreamripDownloads", **kwargs
|
||||||
):
|
):
|
||||||
if kwargs.get("folder_format"):
|
if kwargs.get("folder_format"):
|
||||||
fname = sanitize_filename(self.name)
|
fname = clean_filename(
|
||||||
|
self.name, kwargs.get("restrict_filenames", False)
|
||||||
|
)
|
||||||
self.folder = os.path.join(parent_folder, fname)
|
self.folder = os.path.join(parent_folder, fname)
|
||||||
else:
|
else:
|
||||||
self.folder = parent_folder
|
self.folder = parent_folder
|
||||||
|
@ -2039,7 +2056,9 @@ class Artist(Tracklist, Media):
|
||||||
:rtype: Iterable
|
:rtype: Iterable
|
||||||
"""
|
"""
|
||||||
if kwargs.get("folder_format"):
|
if kwargs.get("folder_format"):
|
||||||
folder = sanitize_filename(self.name)
|
folder = clean_filename(
|
||||||
|
self.name, kwargs.get("restrict_filenames", False)
|
||||||
|
)
|
||||||
self.folder = os.path.join(parent_folder, folder)
|
self.folder = os.path.join(parent_folder, folder)
|
||||||
else:
|
else:
|
||||||
self.folder = parent_folder
|
self.folder = parent_folder
|
||||||
|
|
|
@ -191,6 +191,17 @@ def safe_get(d: dict, *keys: Hashable, default=None):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def clean_filename(fn: str, restrict=False) -> str:
|
||||||
|
path = sanitize_filename(fn)
|
||||||
|
if restrict:
|
||||||
|
from string import printable
|
||||||
|
|
||||||
|
allowed_chars = set(printable)
|
||||||
|
path = "".join(c for c in path if c in allowed_chars)
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
__QUALITY_MAP: Dict[str, Dict[int, Union[int, str, Tuple[int, str]]]] = {
|
__QUALITY_MAP: Dict[str, Dict[int, Union[int, str, Tuple[int, str]]]] = {
|
||||||
"qobuz": {
|
"qobuz": {
|
||||||
1: 5,
|
1: 5,
|
||||||
|
@ -274,21 +285,24 @@ def get_stats_from_quality(
|
||||||
raise InvalidQuality(quality_id)
|
raise InvalidQuality(quality_id)
|
||||||
|
|
||||||
|
|
||||||
def clean_format(formatter: str, format_info):
|
def clean_format(formatter: str, format_info, restrict: bool = False):
|
||||||
"""Format track or folder names sanitizing every formatter key.
|
"""Format track or folder names sanitizing every formatter key.
|
||||||
|
|
||||||
:param formatter:
|
:param formatter:
|
||||||
:type formatter: str
|
:type formatter: str
|
||||||
:param kwargs:
|
:param kwargs:
|
||||||
"""
|
"""
|
||||||
fmt_keys = [i[1] for i in Formatter().parse(formatter) if i[1] is not None]
|
fmt_keys = filter(None, (i[1] for i in Formatter().parse(formatter)))
|
||||||
|
# fmt_keys = (i[1] for i in Formatter().parse(formatter) if i[1] is not None)
|
||||||
|
|
||||||
logger.debug("Formatter keys: %s", fmt_keys)
|
logger.debug("Formatter keys: %s", fmt_keys)
|
||||||
|
|
||||||
clean_dict = dict()
|
clean_dict = dict()
|
||||||
for key in fmt_keys:
|
for key in fmt_keys:
|
||||||
if isinstance(format_info.get(key), (str, float)):
|
if isinstance(format_info.get(key), (str, float)):
|
||||||
clean_dict[key] = sanitize_filename(str(format_info[key]))
|
clean_dict[key] = clean_filename(
|
||||||
|
str(format_info[key]), restrict=restrict
|
||||||
|
)
|
||||||
elif isinstance(format_info.get(key), int): # track/discnumber
|
elif isinstance(format_info.get(key), int): # track/discnumber
|
||||||
clean_dict[key] = f"{format_info[key]:02}"
|
clean_dict[key] = f"{format_info[key]:02}"
|
||||||
else:
|
else:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue