Add option to restrict filenames to ASCII #161

This commit is contained in:
Nathan Thomas 2021-08-30 12:11:45 -07:00
parent cddbd98224
commit 372a755215
4 changed files with 54 additions and 17 deletions

View file

@ -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"

View file

@ -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"],

View file

@ -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

View file

@ -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: