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",
# and "albumcomposer"
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
[lastfm]
@ -160,4 +163,4 @@ progress_bar = "dainty"
[misc]
# 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"]
return {
"restrict_filenames": filepaths["restrict_characters"],
"parent_folder": session["downloads"]["folder"],
"folder_format": filepaths["folder_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.id3 import APIC, ID3, ID3NoHeaderError
from mutagen.mp4 import MP4, MP4Cover
from pathvalidate import sanitize_filename, sanitize_filepath
from pathvalidate import sanitize_filepath
from . import converter
from .clients import Client, DeezloaderClient
@ -50,6 +50,7 @@ from .exceptions import (
from .metadata import TrackMetadata
from .utils import (
DownloadStream,
clean_filename,
clean_format,
decrypt_mqa_file,
downsize_image,
@ -247,13 +248,16 @@ class Track(Media):
clean_format(
kwargs.get("folder_format", FOLDER_FORMAT),
self.meta.get_album_formatter(self.quality),
restrict=kwargs.get("restrict_filenames", False),
),
)
self.file_format = kwargs.get("track_format", TRACK_FORMAT)
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)
@ -364,7 +368,9 @@ class Track(Media):
if self.quality != stream_quality:
# The chosen quality is not available
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:
for chunk in tqdm_stream(stream, desc=self._progress_desc):
@ -503,16 +509,17 @@ class Track(Media):
logger.debug("Cover already exists, skipping download")
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.
This uses the `get_formatter` method of TrackMetadata, which returns
a dict with the keys allowed in formatter strings, and their values in
the TrackMetadata object.
"""
print(f"{restrict=}")
formatter = self.meta.get_formatter(max_quality=self.quality)
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)[
:250
].strip() + ext(self.quality, self.client.source)
@ -725,7 +732,7 @@ class Track(Media):
exit()
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):
logger.info(
@ -1079,9 +1086,11 @@ class Booklet:
:type parent_folder: str
:param kwargs:
"""
filepath = os.path.join(
parent_folder, f"{sanitize_filename(self.description)}.pdf"
fn = clean_filename(
self.description, restrict=kwargs.get("restrict_filenames")
)
filepath = os.path.join(parent_folder, f"{fn}.pdf")
_quick_download(self.url, filepath, "Booklet")
def type(self) -> str:
@ -1468,7 +1477,9 @@ class Album(Tracklist, Media):
parent_folder = kwargs.get("parent_folder", "StreamripDownloads")
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:
self.folder = parent_folder
@ -1639,7 +1650,9 @@ class Album(Tracklist, Media):
logger.debug("Formatter: %s", 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.
:param parent_folder:
@ -1650,7 +1663,9 @@ class Album(Tracklist, Media):
"""
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)
@ -1843,7 +1858,9 @@ class Playlist(Tracklist, Media):
self, parent_folder: str = "StreamripDownloads", **kwargs
):
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)
else:
self.folder = parent_folder
@ -2039,7 +2056,9 @@ class Artist(Tracklist, Media):
:rtype: Iterable
"""
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)
else:
self.folder = parent_folder

View file

@ -191,6 +191,17 @@ def safe_get(d: dict, *keys: Hashable, default=None):
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]]]] = {
"qobuz": {
1: 5,
@ -274,21 +285,24 @@ def get_stats_from_quality(
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.
:param formatter:
:type formatter: str
: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)
clean_dict = dict()
for key in fmt_keys:
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
clean_dict[key] = f"{format_info[key]:02}"
else: