refactor(serverlibrary): move suffixes to library
move audio/video suffixes to serverlibrary remove from audiobooks_library, ffmpeg_handling
This commit is contained in:
1 parent
18e1fc9325
commit
ad796c174e
5 files changed
+103
-51
No files matched your search
@@ -17,7 +17,6 @@ logging.setLoggerClass(CustomLogger)
|
|||||||
logger: CustomLogger = logging.getLogger(__name__) # type: ignore[assignment]
|
logger: CustomLogger = logging.getLogger(__name__) # type: ignore[assignment]
|
||||||
|
|
||||||
metadata_fields = ["artist", "album", "composer", "comment", "genre", "grouping", "album artist", "description"]
|
metadata_fields = ["artist", "album", "composer", "comment", "genre", "grouping", "album artist", "description"]
|
||||||
audio_extensions = {"*.m4b", "*.m4a", "*.mp3", "*.flac", "*.ogg", "*.opus"}
|
|
||||||
|
|
||||||
def ffmpeg_escape(path: Path) -> str:
|
def ffmpeg_escape(path: Path) -> str:
|
||||||
return str(path.resolve()).replace("'", r"'\''")
|
return str(path.resolve()).replace("'", r"'\''")
|
||||||
@@ -192,12 +191,12 @@ class Audiobook:
|
|||||||
|
|
||||||
def init_book(self) -> None:
|
def init_book(self) -> None:
|
||||||
self.files = [
|
self.files = [
|
||||||
file for pattern in audio_extensions for file in self.bookPath.glob(pattern)
|
file for pattern in AUDIO_SUFFIXES for file in self.bookPath.glob(pattern)
|
||||||
]
|
]
|
||||||
self.firstFile = self.get_first_file()
|
self.firstFile = self.get_first_file()
|
||||||
self.size = sum(
|
self.size = sum(
|
||||||
file.stat().st_size
|
file.stat().st_size
|
||||||
for pattern in audio_extensions
|
for pattern in AUDIO_SUFFIXES
|
||||||
for file in self.bookPath.glob(pattern)
|
for file in self.bookPath.glob(pattern)
|
||||||
)
|
)
|
||||||
self.fileConunt = len(self.files)
|
self.fileConunt = len(self.files)
|
||||||
@@ -815,7 +814,7 @@ class AudiobookLibrary:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
media_files = []
|
media_files = []
|
||||||
for ext in audio_extensions:
|
for ext in AUDIO_SUFFIXES:
|
||||||
media_files.extend(list(self.libraryPath.rglob(ext)))
|
media_files.extend(list(self.libraryPath.rglob(ext)))
|
||||||
dirs = {file.parent for file in media_files}
|
dirs = {file.parent for file in media_files}
|
||||||
for directory in dirs:
|
for directory in dirs:
|
||||||
|
|||||||
@@ -6,15 +6,6 @@ import subprocess
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
AUDIO_EXTENSIONS = {
|
|
||||||
".mp3",
|
|
||||||
".m4b",
|
|
||||||
".m4a",
|
|
||||||
}
|
|
||||||
|
|
||||||
RED = "\033[31m"
|
|
||||||
GREEN = "\033[32m"
|
|
||||||
RESET = "\033[0m"
|
|
||||||
USE_KID3 = True
|
USE_KID3 = True
|
||||||
def normalize(text: str) -> str:
|
def normalize(text: str) -> str:
|
||||||
return re.sub(r"[^a-z0-9]", "", text.lower())
|
return re.sub(r"[^a-z0-9]", "", text.lower())
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ from serverlibrary import *
|
|||||||
logging.setLoggerClass(CustomLogger)
|
logging.setLoggerClass(CustomLogger)
|
||||||
logger: CustomLogger = logging.getLogger(__name__) # type: ignore[assignment]
|
logger: CustomLogger = logging.getLogger(__name__) # type: ignore[assignment]
|
||||||
|
|
||||||
VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".mov", ".webm"}
|
|
||||||
AUDIO_EXTS = {".mp3", ".m4a", ".m4b", ".opus"}
|
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# OUTPUT ENFORCEMENT (clean + centralized)
|
# OUTPUT ENFORCEMENT (clean + centralized)
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from .core import (
|
from .core import (
|
||||||
TRACE,
|
|
||||||
LOG_LEVELS,
|
LOG_LEVELS,
|
||||||
|
TRACE,
|
||||||
CustomLogger,
|
CustomLogger,
|
||||||
setup_logging,
|
AUDIO_SUFFIXES,
|
||||||
blue,
|
blue,
|
||||||
combined_score,
|
combined_score,
|
||||||
get_audio_tracks,
|
get_audio_tracks,
|
||||||
@@ -15,18 +15,20 @@ from .core import (
|
|||||||
score_match_fuzzy,
|
score_match_fuzzy,
|
||||||
score_match_jaccard,
|
score_match_jaccard,
|
||||||
score_match_recall_jaccard,
|
score_match_recall_jaccard,
|
||||||
|
setup_logging,
|
||||||
tmdb_get,
|
tmdb_get,
|
||||||
tvdb_get,
|
tvdb_get,
|
||||||
|
VIDEO_SUFFIXES,
|
||||||
word_similarity,
|
word_similarity,
|
||||||
words,
|
words,
|
||||||
yellow,
|
yellow,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"TRACE",
|
|
||||||
"LOG_LEVELS",
|
"LOG_LEVELS",
|
||||||
|
"TRACE",
|
||||||
"CustomLogger",
|
"CustomLogger",
|
||||||
"setup_logging",
|
"AUDIO_SUFFIXES",
|
||||||
"blue",
|
"blue",
|
||||||
"combined_score",
|
"combined_score",
|
||||||
"get_audio_tracks",
|
"get_audio_tracks",
|
||||||
@@ -39,10 +41,11 @@ __all__ = [
|
|||||||
"score_match_fuzzy",
|
"score_match_fuzzy",
|
||||||
"score_match_jaccard",
|
"score_match_jaccard",
|
||||||
"score_match_recall_jaccard",
|
"score_match_recall_jaccard",
|
||||||
|
"setup_logging",
|
||||||
"tmdb_get",
|
"tmdb_get",
|
||||||
"tvdb_get",
|
"tvdb_get",
|
||||||
|
"VIDEO_SUFFIXES",
|
||||||
"word_similarity",
|
"word_similarity",
|
||||||
"words",
|
"words",
|
||||||
"yellow",
|
"yellow",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ reset = "\033[0m"
|
|||||||
class CustomLogger(logging.Logger):
|
class CustomLogger(logging.Logger):
|
||||||
def trace(self, msg: str, *args, **kwargs) -> None:
|
def trace(self, msg: str, *args, **kwargs) -> None:
|
||||||
if self.isEnabledFor(TRACE):
|
if self.isEnabledFor(TRACE):
|
||||||
|
kwargs.setdefault("stacklevel", 2)
|
||||||
self._log(TRACE, msg, args, **kwargs)
|
self._log(TRACE, msg, args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@@ -38,12 +39,13 @@ LOG_LEVELS = {
|
|||||||
"CRITICAL": logging.CRITICAL,
|
"CRITICAL": logging.CRITICAL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(log_level: str = "INFO", logfile: str | None = None) -> None:
|
def setup_logging(log_level: str = "INFO", logfile: str | None = None) -> None:
|
||||||
logging.addLevelName(TRACE, "TRACE")
|
logging.addLevelName(TRACE, "TRACE")
|
||||||
|
|
||||||
file_formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
|
file_formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
|
||||||
console_formatter = colorlog.ColoredFormatter(
|
console_formatter = colorlog.ColoredFormatter(
|
||||||
"%(log_color)s%(asctime)s %(levelname)-8s%(reset)s %(name)s %(message)s",
|
"%(log_color)s%(asctime)s %(levelname)-8s%(reset)s %(funcName)s %(message)s",
|
||||||
log_colors={
|
log_colors={
|
||||||
"TRACE": "cyan",
|
"TRACE": "cyan",
|
||||||
"DEBUG": "blue",
|
"DEBUG": "blue",
|
||||||
@@ -78,9 +80,19 @@ def setup_logging(log_level: str = "INFO", logfile: str | None = None) -> None:
|
|||||||
root_logger.info(f"Set logger to {log_level}, Logfile: {logfile_data}")
|
root_logger.info(f"Set logger to {log_level}, Logfile: {logfile_data}")
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Media Files
|
||||||
|
# -----------------------------
|
||||||
|
|
||||||
|
AUDIO_SUFFIXES = {"*.m4b", "*.m4a", "*.mp3", "*.flac", "*.ogg", "*.opus"}
|
||||||
|
VIDEO_SUFFIXES = {".mp4", ".mkv", ".avi", ".mov", ".webm"}
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
|
|
||||||
|
|
||||||
def safe_delete(path: Path):
|
def safe_delete(path: Path):
|
||||||
if path.is_dir():
|
if path.is_dir():
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
@@ -89,52 +101,84 @@ def safe_delete(path: Path):
|
|||||||
|
|
||||||
|
|
||||||
def safe_rename(
|
def safe_rename(
|
||||||
src: Path,
|
src_filepath: Path,
|
||||||
dst: Path,
|
dst_filename: Path,
|
||||||
dry: bool = False,
|
dry_run: bool = False,
|
||||||
confirm: bool = False,
|
confirm: bool = False,
|
||||||
silent=False,
|
silent=False,
|
||||||
overwrite=False,
|
overwrite=False,
|
||||||
|
logger=None,
|
||||||
) -> tuple[str, bool]:
|
) -> tuple[str, bool]:
|
||||||
"""
|
"""
|
||||||
Safely rename a file or directory without overwriting existing files.
|
Safely rename or move a file or directory to a new destination.
|
||||||
Automatically appends _1, _2, ... if needed.
|
|
||||||
|
If the destination already exists, a unique name is automatically
|
||||||
|
generated by appending a numeric suffix (_1, _2, ...) unless
|
||||||
|
overwrite is enabled. Supports dry-run mode, optional user
|
||||||
|
confirmation, and silent operation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src_filepath: Source file or directory to rename.
|
||||||
|
dst_filename: Target path and filename.
|
||||||
|
dry_run: If True, only report the action without performing it.
|
||||||
|
confirm: If True, prompt for confirmation before renaming.
|
||||||
|
silent: If True, suppress informational output.
|
||||||
|
overwrite: If True, replace an existing destination.
|
||||||
|
logger: Logger handler
|
||||||
|
Returns:
|
||||||
|
A tuple containing:
|
||||||
|
- The final destination path as a string.
|
||||||
|
- True if a rename was performed successfully, otherwise False.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If the source path does not exist.
|
||||||
|
OSError: If the rename operation fails.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
src = Path(src)
|
src_filepath = Path(src_filepath).expanduser().resolve()
|
||||||
dst = Path(dst)
|
dst_filename = Path(dst_filename).expanduser()
|
||||||
|
if dst_filename.parent == Path("."):
|
||||||
|
dst_filename = src_filepath.parent / dst_filename
|
||||||
|
dst_filename = dst_filename.resolve()
|
||||||
|
|
||||||
|
if not src_filepath.exists():
|
||||||
|
raise FileNotFoundError(f"File {src_filepath} not found")
|
||||||
# nothing to do
|
# nothing to do
|
||||||
if src.resolve() == dst.resolve():
|
if src_filepath == dst_filename:
|
||||||
return "", False
|
return "", False
|
||||||
|
|
||||||
directory = dst.parent
|
# directory = dst_filename.parent
|
||||||
base = dst.stem
|
# base = dst_filename.stem
|
||||||
ext = dst.suffix
|
# ext = dst_filename.suffix
|
||||||
|
|
||||||
final_dst = dst
|
# final_dst = dst_filename
|
||||||
counter = 1
|
# counter = 1
|
||||||
|
|
||||||
# find free name
|
# # find free name
|
||||||
while final_dst.exists():
|
# while final_dst.exists():
|
||||||
final_dst = directory / f"{base}_{counter}{ext}"
|
# final_dst = directory / f"{base}_{counter}{ext}"
|
||||||
counter += 1
|
# counter += 1
|
||||||
ret = f"{src} → {dst}"
|
ret = f"{src_filepath} → {dst_filename}"
|
||||||
if not silent:
|
if not silent:
|
||||||
print(ret)
|
if logger is not None:
|
||||||
|
logger.info(ret)
|
||||||
if dry:
|
else:
|
||||||
|
print(ret)
|
||||||
|
if dry_run:
|
||||||
return ret, True
|
return ret, True
|
||||||
|
|
||||||
if confirm:
|
if confirm:
|
||||||
input("Press Enter to confirm...")
|
input("Press Enter to confirm...")
|
||||||
|
|
||||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
dst_filename.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if dst.exists() and not overwrite:
|
if dst_filename.exists() and not overwrite:
|
||||||
print("File already exists...")
|
if logger is not None:
|
||||||
|
logger.warning("File already exists...")
|
||||||
|
else:
|
||||||
|
print(ret)
|
||||||
return "", False
|
return "", False
|
||||||
src.rename(dst)
|
src_filepath.rename(dst_filename)
|
||||||
return ret, True
|
return ret, True
|
||||||
|
|
||||||
|
|
||||||
@@ -177,10 +221,19 @@ def get_subtitle_tracks(source):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not tracks:
|
if not tracks:
|
||||||
tracks.append({"number": 0, "description": "", "lang": "", "language": "", "primary": False})
|
tracks.append(
|
||||||
|
{
|
||||||
|
"number": 0,
|
||||||
|
"description": "",
|
||||||
|
"lang": "",
|
||||||
|
"language": "",
|
||||||
|
"primary": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
|
||||||
def get_audio_tracks(
|
def get_audio_tracks(
|
||||||
source: str, title: int | None = None, handbrake: bool = False
|
source: str, title: int | None = None, handbrake: bool = False
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
@@ -286,10 +339,19 @@ def get_audio_tracks(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not tracks:
|
if not tracks:
|
||||||
tracks.append({"number": 0, "description": "","lang":"","language":"", "primary": False})
|
tracks.append(
|
||||||
|
{
|
||||||
|
"number": 0,
|
||||||
|
"description": "",
|
||||||
|
"lang": "",
|
||||||
|
"language": "",
|
||||||
|
"primary": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
|
||||||
def words(text):
|
def words(text):
|
||||||
return set(re.findall(r"[^\W_]+", text.lower()))
|
return set(re.findall(r"[^\W_]+", text.lower()))
|
||||||
|
|
||||||
@@ -360,7 +422,7 @@ def tmdb_get(endpoint, params=None):
|
|||||||
BASE_URL = "https://api.themoviedb.org/3"
|
BASE_URL = "https://api.themoviedb.org/3"
|
||||||
|
|
||||||
url = BASE_URL + endpoint
|
url = BASE_URL + endpoint
|
||||||
print(f"Set of Tmdb query: {url}")
|
# print_or_log(f"Set of Tmdb query: {url}")
|
||||||
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
url,
|
url,
|
||||||
@@ -378,7 +440,7 @@ def tvdb_get(endpoint, params=None):
|
|||||||
"""Perform a GET request against TVDB."""
|
"""Perform a GET request against TVDB."""
|
||||||
BASE_URL = "https://api4.thetvdb.com/v4"
|
BASE_URL = "https://api4.thetvdb.com/v4"
|
||||||
url = BASE_URL + endpoint
|
url = BASE_URL + endpoint
|
||||||
print(f"Set of TVDB query: {url}")
|
# print_or_log(f"Set of TVDB query: {url}")
|
||||||
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
url,
|
url,
|
||||||
|
|||||||
Reference in new issue
Block a user