2024-02-26 06:04:41 +01:00
|
|
|
import math
|
2023-12-19 23:58:57 +01:00
|
|
|
import os
|
2024-11-13 09:49:33 +01:00
|
|
|
import pathlib
|
2024-11-12 22:15:11 +01:00
|
|
|
import posixpath
|
2023-12-19 23:58:57 +01:00
|
|
|
import re
|
2024-04-01 19:54:11 +02:00
|
|
|
import sys
|
2025-02-28 16:57:18 +01:00
|
|
|
from copy import deepcopy
|
2024-11-12 22:15:11 +01:00
|
|
|
from urllib.parse import unquote, urlsplit
|
2023-12-19 23:58:57 +01:00
|
|
|
|
2024-01-13 11:57:11 +01:00
|
|
|
from pathvalidate import sanitize_filename, sanitize_filepath
|
|
|
|
|
from pathvalidate.error import ValidationError
|
2024-01-12 08:15:54 +01:00
|
|
|
from tidalapi import Album, Mix, Playlist, Track, UserPlaylist, Video
|
2024-04-20 16:02:29 +02:00
|
|
|
from tidalapi.media import AudioExtensions
|
2023-12-19 23:58:57 +01:00
|
|
|
|
2024-01-14 23:09:43 +01:00
|
|
|
from tidal_dl_ng import __name_display__
|
2025-02-28 16:57:18 +01:00
|
|
|
from tidal_dl_ng.constants import FILENAME_LENGTH_MAX, FILENAME_SANITIZE_PLACEHOLDER, UNIQUIFY_THRESHOLD, MediaType
|
2024-03-22 15:02:26 +01:00
|
|
|
from tidal_dl_ng.helper.tidal import name_builder_album_artist, name_builder_artist, name_builder_title
|
2023-12-19 23:58:57 +01:00
|
|
|
|
2024-01-14 23:09:43 +01:00
|
|
|
|
|
|
|
|
def path_home() -> str:
|
2023-12-19 23:58:57 +01:00
|
|
|
if "XDG_CONFIG_HOME" in os.environ:
|
|
|
|
|
return os.environ["XDG_CONFIG_HOME"]
|
|
|
|
|
elif "HOME" in os.environ:
|
|
|
|
|
return os.environ["HOME"]
|
|
|
|
|
elif "HOMEDRIVE" in os.environ and "HOMEPATH" in os.environ:
|
|
|
|
|
return os.path.join(os.environ["HOMEDRIVE"], os.environ["HOMEPATH"])
|
|
|
|
|
else:
|
|
|
|
|
return os.path.abspath("./")
|
|
|
|
|
|
|
|
|
|
|
2024-01-23 09:51:59 +01:00
|
|
|
def path_config_base() -> str:
|
2024-11-09 13:51:39 +01:00
|
|
|
# https://wiki.archlinux.org/title/XDG_Base_Directory
|
|
|
|
|
# X11 workaround: If user specified config path is set, do not point to "~/.config"
|
|
|
|
|
path_user_custom: str = os.environ.get("XDG_CONFIG_HOME", "")
|
|
|
|
|
path_config: str = ".config" if not path_user_custom else ""
|
2024-01-14 23:09:43 +01:00
|
|
|
path_base: str = os.path.join(path_home(), path_config, __name_display__)
|
|
|
|
|
|
|
|
|
|
return path_base
|
2023-12-19 23:58:57 +01:00
|
|
|
|
|
|
|
|
|
2024-01-14 23:09:43 +01:00
|
|
|
def path_file_log() -> str:
|
2024-01-23 09:51:59 +01:00
|
|
|
return os.path.join(path_config_base(), "app.log")
|
2023-12-19 23:58:57 +01:00
|
|
|
|
|
|
|
|
|
2024-01-14 23:09:43 +01:00
|
|
|
def path_file_token() -> str:
|
2024-01-23 09:51:59 +01:00
|
|
|
return os.path.join(path_config_base(), "token.json")
|
2024-01-14 23:09:43 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def path_file_settings() -> str:
|
2024-01-23 09:51:59 +01:00
|
|
|
return os.path.join(path_config_base(), "settings.json")
|
2023-12-19 23:58:57 +01:00
|
|
|
|
|
|
|
|
|
2024-12-15 20:37:06 +01:00
|
|
|
def format_path_media(
|
|
|
|
|
fmt_template: str, media: Track | Album | Playlist | UserPlaylist | Video | Mix, album_track_num_pad_min: int = 0
|
|
|
|
|
) -> str:
|
2023-12-19 23:58:57 +01:00
|
|
|
result = fmt_template
|
|
|
|
|
|
|
|
|
|
# Search track format template for placeholder.
|
|
|
|
|
regex = r"\{(.+?)\}"
|
|
|
|
|
matches = re.finditer(regex, fmt_template, re.MULTILINE)
|
|
|
|
|
|
|
|
|
|
for _matchNum, match in enumerate(matches, start=1):
|
|
|
|
|
template_str = match.group()
|
2024-12-15 20:37:06 +01:00
|
|
|
result_fmt = format_str_media(match.group(1), media, album_track_num_pad_min)
|
2023-12-19 23:58:57 +01:00
|
|
|
|
2024-02-26 06:04:41 +01:00
|
|
|
if result_fmt != match.group(1):
|
2023-12-19 23:58:57 +01:00
|
|
|
value = sanitize_filename(result_fmt)
|
|
|
|
|
result = result.replace(template_str, value)
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
2024-12-15 20:37:06 +01:00
|
|
|
def format_str_media(
|
|
|
|
|
name: str, media: Track | Album | Playlist | UserPlaylist | Video | Mix, album_track_num_pad_min: int = 0
|
|
|
|
|
) -> str:
|
2024-02-26 06:04:41 +01:00
|
|
|
result: str = name
|
2023-12-19 23:58:57 +01:00
|
|
|
|
2024-02-26 06:04:41 +01:00
|
|
|
try:
|
2024-04-05 09:39:47 +02:00
|
|
|
match name:
|
|
|
|
|
case "artist_name":
|
2024-11-10 13:56:27 +01:00
|
|
|
if isinstance(media, Track | Video):
|
|
|
|
|
if hasattr(media, "artists"):
|
|
|
|
|
result = name_builder_artist(media)
|
|
|
|
|
elif hasattr(media, "artist"):
|
|
|
|
|
result = media.artist.name
|
2024-04-05 09:39:47 +02:00
|
|
|
case "album_artist":
|
2024-11-17 23:06:36 +01:00
|
|
|
result = name_builder_album_artist(media)
|
2024-04-05 09:39:47 +02:00
|
|
|
case "track_title":
|
|
|
|
|
if isinstance(media, Track | Video):
|
|
|
|
|
result = name_builder_title(media)
|
|
|
|
|
case "mix_name":
|
|
|
|
|
if isinstance(media, Mix):
|
|
|
|
|
result = media.title
|
|
|
|
|
case "playlist_name":
|
|
|
|
|
if isinstance(media, Playlist | UserPlaylist):
|
|
|
|
|
result = media.name
|
|
|
|
|
case "album_title":
|
|
|
|
|
if isinstance(media, Album):
|
|
|
|
|
result = media.name
|
|
|
|
|
elif isinstance(media, Track):
|
|
|
|
|
result = media.album.name
|
|
|
|
|
case "album_track_num":
|
|
|
|
|
if isinstance(media, Track | Video):
|
|
|
|
|
num_tracks: int = media.album.num_tracks if hasattr(media, "album") else 1
|
|
|
|
|
count_digits: int = int(math.log10(num_tracks)) + 1
|
2024-12-15 20:37:06 +01:00
|
|
|
count_digits_computed: int = (
|
|
|
|
|
count_digits if count_digits > album_track_num_pad_min else album_track_num_pad_min
|
|
|
|
|
)
|
|
|
|
|
result = str(media.track_num).zfill(count_digits_computed)
|
2024-04-05 09:39:47 +02:00
|
|
|
case "album_num_tracks":
|
|
|
|
|
if isinstance(media, Track | Video):
|
|
|
|
|
result = str(media.album.num_tracks if hasattr(media, "album") else 1)
|
|
|
|
|
case "track_id":
|
|
|
|
|
if isinstance(media, Track | Video):
|
2024-04-20 14:54:03 +02:00
|
|
|
result = str(media.id)
|
2024-04-05 09:39:47 +02:00
|
|
|
case "playlist_id":
|
|
|
|
|
if isinstance(media, Playlist):
|
2024-04-20 14:54:03 +02:00
|
|
|
result = str(media.id)
|
2024-04-20 11:54:46 +02:00
|
|
|
case "album_id":
|
|
|
|
|
if isinstance(media, Album):
|
2024-04-20 14:54:03 +02:00
|
|
|
result = str(media.id)
|
2024-06-16 16:18:22 +02:00
|
|
|
elif isinstance(media, Track):
|
|
|
|
|
result = str(media.album.id)
|
2024-04-05 09:39:47 +02:00
|
|
|
case "track_duration_seconds":
|
|
|
|
|
if isinstance(media, Track | Video):
|
|
|
|
|
result = str(media.duration)
|
|
|
|
|
case "track_duration_minutes":
|
|
|
|
|
if isinstance(media, Track | Video):
|
|
|
|
|
m, s = divmod(media.duration, 60)
|
|
|
|
|
result = f"{m:01d}:{s:02d}"
|
|
|
|
|
case "album_duration_seconds":
|
|
|
|
|
if isinstance(media, Album):
|
|
|
|
|
result = str(media.duration)
|
|
|
|
|
case "album_duration_minutes":
|
|
|
|
|
if isinstance(media, Album):
|
|
|
|
|
m, s = divmod(media.duration, 60)
|
|
|
|
|
result = f"{m:01d}:{s:02d}"
|
|
|
|
|
case "playlist_duration_seconds":
|
|
|
|
|
if isinstance(media, Album):
|
|
|
|
|
result = str(media.duration)
|
|
|
|
|
case "playlist_duration_minutes":
|
|
|
|
|
if isinstance(media, Album):
|
|
|
|
|
m, s = divmod(media.duration, 60)
|
|
|
|
|
result = f"{m:01d}:{s:02d}"
|
|
|
|
|
case "album_year":
|
|
|
|
|
if isinstance(media, Album):
|
2024-04-20 11:54:46 +02:00
|
|
|
result = str(media.year)
|
|
|
|
|
elif isinstance(media, Track):
|
|
|
|
|
result = str(media.album.year)
|
2024-04-05 09:39:47 +02:00
|
|
|
case "video_quality":
|
|
|
|
|
if isinstance(media, Video):
|
|
|
|
|
result = media.video_quality
|
|
|
|
|
case "track_quality":
|
|
|
|
|
if isinstance(media, Track):
|
|
|
|
|
result = ", ".join(tag for tag in media.media_metadata_tags)
|
2024-04-20 11:38:21 +02:00
|
|
|
case "track_explicit":
|
|
|
|
|
if isinstance(media, Track | Video):
|
|
|
|
|
result = " (Explicit)" if media.explicit else ""
|
|
|
|
|
case "album_explicit":
|
|
|
|
|
if isinstance(media, Album):
|
|
|
|
|
result = " (Explicit)" if media.explicit else ""
|
2024-04-20 14:01:17 +02:00
|
|
|
case "album_num_volumes":
|
|
|
|
|
if isinstance(media, Album):
|
2024-04-20 14:54:03 +02:00
|
|
|
result = str(media.num_volumes)
|
2024-04-20 14:01:17 +02:00
|
|
|
case "track_volume_num":
|
|
|
|
|
if isinstance(media, Track | Video):
|
2024-04-20 14:54:03 +02:00
|
|
|
result = str(media.volume_num)
|
2024-11-10 14:21:10 +01:00
|
|
|
case "track_volume_num_optional":
|
|
|
|
|
if isinstance(media, Track | Video):
|
|
|
|
|
num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1
|
2024-12-15 20:37:06 +01:00
|
|
|
result = "" if num_volumes == 1 else str(media.volume_num)
|
2024-11-10 14:21:10 +01:00
|
|
|
case "track_volume_num_optional_CD":
|
|
|
|
|
if isinstance(media, Track | Video):
|
|
|
|
|
num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1
|
2024-12-15 20:37:06 +01:00
|
|
|
result = "" if num_volumes == 1 else f"CD{media.volume_num!s}"
|
2025-02-19 19:07:31 +01:00
|
|
|
case "isrc":
|
|
|
|
|
if isinstance(media, Track):
|
|
|
|
|
result = media.isrc
|
2024-02-26 06:04:41 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
# TODO: Implement better exception logging.
|
|
|
|
|
print(e)
|
2024-03-13 06:38:13 +01:00
|
|
|
|
2024-02-26 06:04:41 +01:00
|
|
|
pass
|
2023-12-19 23:58:57 +01:00
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
2024-01-14 23:09:43 +01:00
|
|
|
def get_format_template(
|
|
|
|
|
media: Track | Album | Playlist | UserPlaylist | Video | Mix | MediaType, settings
|
|
|
|
|
) -> str | bool:
|
|
|
|
|
result = False
|
|
|
|
|
|
2024-01-16 06:33:23 +01:00
|
|
|
if isinstance(media, Track) or media == MediaType.TRACK:
|
2024-01-14 23:09:43 +01:00
|
|
|
result = settings.data.format_track
|
2024-05-22 06:33:40 +02:00
|
|
|
elif isinstance(media, Album) or media == MediaType.ALBUM or media == MediaType.ARTIST:
|
2024-01-14 23:09:43 +01:00
|
|
|
result = settings.data.format_album
|
2024-01-16 06:33:23 +01:00
|
|
|
elif isinstance(media, Playlist | UserPlaylist) or media == MediaType.PLAYLIST:
|
2024-01-14 23:09:43 +01:00
|
|
|
result = settings.data.format_playlist
|
2024-01-16 06:33:23 +01:00
|
|
|
elif isinstance(media, Mix) or media == MediaType.MIX:
|
2024-01-14 23:09:43 +01:00
|
|
|
result = settings.data.format_mix
|
2024-01-16 06:33:23 +01:00
|
|
|
elif isinstance(media, Video) or media == MediaType.VIDEO:
|
2024-01-14 23:09:43 +01:00
|
|
|
result = settings.data.format_video
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
2025-03-24 08:22:12 +01:00
|
|
|
def path_file_sanitize(path_file: pathlib.Path, adapt: bool = False, uniquify: bool = False) -> pathlib.Path:
|
2025-02-28 16:57:18 +01:00
|
|
|
# Get each directory name separately (first value in tuple; second value is for the file suffix).
|
2025-03-24 07:12:16 +01:00
|
|
|
to_sanitize: [[str, str]] = path_split_parts_suffix(path_file)
|
2025-03-24 07:34:13 +01:00
|
|
|
sanitized_path_file: pathlib.Path = pathlib.Path(to_sanitize.pop(0)[0])
|
2025-02-28 16:57:18 +01:00
|
|
|
|
|
|
|
|
for name, suffix in to_sanitize:
|
|
|
|
|
# Sanitize names: We need first top make sure that none file / directory name has bad chars or is longer than 255 chars.
|
|
|
|
|
try:
|
|
|
|
|
# sanitize_filename can shorten the file name actually
|
|
|
|
|
filename_sanitized: str = sanitize_filename(
|
|
|
|
|
name + suffix, replacement_text=" ", validate_after_sanitize=True, platform="auto"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Check if the file extension was removed by shortening the filename length
|
|
|
|
|
if not filename_sanitized.endswith(suffix):
|
|
|
|
|
# Add the original file extension
|
|
|
|
|
file_suffix: str = FILENAME_SANITIZE_PLACEHOLDER + path_file.suffix
|
|
|
|
|
filename_sanitized = filename_sanitized[: -len(file_suffix)] + file_suffix
|
|
|
|
|
except ValidationError as e:
|
|
|
|
|
if adapt:
|
|
|
|
|
# TODO: Implement proper exception handling and logging.
|
|
|
|
|
# Hacky stuff, since the sanitizing function does not shorten the filename (filename too long)
|
|
|
|
|
if str(e).startswith("[PV1101]"):
|
|
|
|
|
byte_ct: int = len(name.encode(sys.getfilesystemencoding())) - FILENAME_LENGTH_MAX
|
|
|
|
|
filename_sanitized = (
|
|
|
|
|
name[: -byte_ct - len(FILENAME_SANITIZE_PLACEHOLDER) - len(suffix)]
|
|
|
|
|
+ FILENAME_SANITIZE_PLACEHOLDER
|
|
|
|
|
+ suffix
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
raise
|
|
|
|
|
else:
|
|
|
|
|
raise
|
|
|
|
|
finally:
|
|
|
|
|
sanitized_path_file = sanitized_path_file / filename_sanitized
|
|
|
|
|
|
|
|
|
|
# Sanitize the whole path. The whole path with filename is not allowed to be longer then the max path length depending on the OS.
|
2023-12-19 23:58:57 +01:00
|
|
|
try:
|
2025-03-24 07:12:16 +01:00
|
|
|
sanitized_path_file: pathlib.Path = sanitize_filepath(
|
2025-02-28 16:57:18 +01:00
|
|
|
sanitized_path_file, replacement_text=" ", validate_after_sanitize=True, platform="auto"
|
2024-01-14 17:29:39 +01:00
|
|
|
)
|
2025-02-28 16:57:18 +01:00
|
|
|
except ValidationError as e:
|
2024-01-13 11:57:11 +01:00
|
|
|
# If adaption of path is allowed in case of an error set path to HOME.
|
|
|
|
|
if adapt:
|
2025-02-28 16:57:18 +01:00
|
|
|
if str(e).startswith("[PV1101]"):
|
|
|
|
|
sanitized_path_file = pathlib.Path.home() / sanitized_path_file.name
|
|
|
|
|
else:
|
|
|
|
|
raise
|
2024-01-13 11:57:11 +01:00
|
|
|
else:
|
2024-01-13 12:04:35 +01:00
|
|
|
raise
|
2023-12-19 23:58:57 +01:00
|
|
|
|
2024-03-19 07:02:00 +01:00
|
|
|
# Uniquify
|
|
|
|
|
if uniquify:
|
2025-03-24 08:22:12 +01:00
|
|
|
sanitized_path_file = path_file_uniquify(sanitized_path_file)
|
2024-03-19 07:02:00 +01:00
|
|
|
|
2025-02-28 16:57:18 +01:00
|
|
|
return sanitized_path_file
|
2023-12-19 23:58:57 +01:00
|
|
|
|
|
|
|
|
|
2025-03-24 08:22:12 +01:00
|
|
|
def path_file_uniquify(path_file: pathlib.Path) -> pathlib.Path:
|
|
|
|
|
"""Checks whether the file exists, if so it tries to return an unique name suffix.
|
|
|
|
|
|
|
|
|
|
:param path_file: Path to file name which shall be unique.
|
|
|
|
|
:type path_file: pathlib.Path
|
|
|
|
|
:return: Unique file name with path for given input.
|
|
|
|
|
:rtype: pathlib.Path
|
|
|
|
|
"""
|
|
|
|
|
unique_suffix: str = file_unique_suffix(path_file)
|
|
|
|
|
|
|
|
|
|
if unique_suffix:
|
|
|
|
|
file_suffix = unique_suffix + path_file.suffix
|
|
|
|
|
# For most OS filename has a character limit of 255.
|
|
|
|
|
path_file = (
|
|
|
|
|
path_file.parent / (str(path_file.stem)[: -len(file_suffix)] + file_suffix)
|
2025-03-24 09:01:49 +01:00
|
|
|
if len(str(path_file.parent / (path_file.stem + unique_suffix))) > FILENAME_LENGTH_MAX
|
2025-03-24 08:22:12 +01:00
|
|
|
else path_file.parent / (path_file.stem + unique_suffix)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return path_file
|
|
|
|
|
|
|
|
|
|
|
2025-02-28 16:57:18 +01:00
|
|
|
def file_unique_suffix(path_file: pathlib.Path, seperator: str = "_") -> str:
|
2024-03-19 07:02:00 +01:00
|
|
|
threshold_zfill: int = len(str(UNIQUIFY_THRESHOLD))
|
|
|
|
|
count: int = 0
|
2025-02-28 16:57:18 +01:00
|
|
|
path_file_tmp: pathlib.Path = deepcopy(path_file)
|
2024-03-19 07:02:00 +01:00
|
|
|
unique_suffix: str = ""
|
|
|
|
|
|
|
|
|
|
while check_file_exists(path_file_tmp) and count < UNIQUIFY_THRESHOLD:
|
|
|
|
|
count += 1
|
|
|
|
|
unique_suffix = seperator + str(count).zfill(threshold_zfill)
|
2025-02-28 16:57:18 +01:00
|
|
|
path_file_tmp = path_file.parent / (path_file.stem + unique_suffix + path_file.suffix)
|
2024-03-19 07:02:00 +01:00
|
|
|
|
|
|
|
|
return unique_suffix
|
|
|
|
|
|
|
|
|
|
|
2024-11-13 09:49:33 +01:00
|
|
|
def check_file_exists(path_file: pathlib.Path, extension_ignore: bool = False) -> bool:
|
2024-01-13 11:57:11 +01:00
|
|
|
if extension_ignore:
|
2024-11-13 09:49:33 +01:00
|
|
|
path_file_stem: str = pathlib.Path(path_file).stem
|
|
|
|
|
path_parent: pathlib.Path = pathlib.Path(path_file).parent
|
2024-01-17 22:08:39 +01:00
|
|
|
path_files: [str] = []
|
2023-12-19 23:58:57 +01:00
|
|
|
|
2024-01-17 22:08:39 +01:00
|
|
|
for extension in AudioExtensions:
|
2024-04-02 16:26:57 +02:00
|
|
|
path_files.append(str(path_parent.joinpath(path_file_stem + extension)))
|
2024-01-17 22:08:39 +01:00
|
|
|
else:
|
|
|
|
|
path_files: [str] = [path_file]
|
|
|
|
|
|
2024-04-02 15:21:57 +02:00
|
|
|
result = bool(sum([[True] if os.path.isfile(_file) else [] for _file in path_files], []))
|
2023-12-19 23:58:57 +01:00
|
|
|
|
|
|
|
|
return result
|
2024-04-01 19:54:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def resource_path(relative_path):
|
|
|
|
|
try:
|
|
|
|
|
base_path = sys._MEIPASS
|
|
|
|
|
except Exception:
|
|
|
|
|
base_path = os.path.abspath(".")
|
|
|
|
|
|
|
|
|
|
return os.path.join(base_path, relative_path)
|
2024-11-12 22:15:11 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def url_to_filename(url: str) -> str:
|
|
|
|
|
"""Return basename corresponding to url.
|
|
|
|
|
>>> print(url_to_filename('http://example.com/path/to/file%C3%80?opt=1'))
|
|
|
|
|
fileÀ
|
|
|
|
|
>>> print(url_to_filename('http://example.com/slash%2fname')) # '/' in name
|
|
|
|
|
Taken from https://gist.github.com/zed/c2168b9c52b032b5fb7d
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
...
|
|
|
|
|
ValueError
|
|
|
|
|
"""
|
|
|
|
|
urlpath: str = urlsplit(url).path
|
|
|
|
|
basename: str = posixpath.basename(unquote(urlpath))
|
|
|
|
|
|
|
|
|
|
if os.path.basename(basename) != basename or unquote(posixpath.basename(urlpath)) != basename:
|
|
|
|
|
raise ValueError # reject '%2f' or 'dir%5Cbasename.ext' on Windows
|
|
|
|
|
|
|
|
|
|
return basename
|
2025-02-28 16:57:18 +01:00
|
|
|
|
|
|
|
|
|
2025-03-24 07:12:16 +01:00
|
|
|
def path_split_parts_suffix(p: pathlib.Path) -> [[str, str]]:
|
|
|
|
|
"""Splits the path to file in parts and also splits the suffix from the file stem.
|
2025-02-28 16:57:18 +01:00
|
|
|
|
2025-03-24 07:12:16 +01:00
|
|
|
:param p: Path to file which should be split in parts.
|
|
|
|
|
:type p: pathlib.Path
|
|
|
|
|
:return: List of tuples (Stem, Suffix) of each path part.
|
|
|
|
|
"""
|
|
|
|
|
result: [[str, str]] = [[part, ""] for part in p.parts]
|
|
|
|
|
result[-1][0] = p.stem
|
|
|
|
|
result[-1][-1] = p.suffix
|
|
|
|
|
|
|
|
|
|
return result
|