From 36417bd119680be7e814d958f2027d3416da778f Mon Sep 17 00:00:00 2001 From: Justin Oro Date: Mon, 24 Aug 2026 10:50:54 -0700 Subject: [PATCH] fix undo log persistence, non-English titles, season-folder absolute numbering, and rename race handling --- delete.yaml | 27 ++++---- plex-library-tool.py | 142 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 126 insertions(+), 43 deletions(-) diff --git a/delete.yaml b/delete.yaml index 796ac2a..c7b2cb6 100644 --- a/delete.yaml +++ b/delete.yaml @@ -126,19 +126,20 @@ # subtitles: # - "!Spanish" - folders: - - Screens - - Featurettes - - Extras - - Sample - - Samples - - Behind the Scenes - - Deleted Scenes - - Trailers - - Interviews - - Bonus - - "* torrent" - - "* torrents" +folders: + - Screens + - Featurettes + - Extras + - Sample + - Samples + - Soundtrack + - Behind the Scenes + - Deleted Scenes + - Trailers + - Interviews + - Bonus + - "* torrent" + - "* torrents" files: - "*.txt" diff --git a/plex-library-tool.py b/plex-library-tool.py index 3b238cc..f48e3d5 100755 --- a/plex-library-tool.py +++ b/plex-library-tool.py @@ -64,23 +64,38 @@ LEGACY_SEE_PATTERN = re.compile(r'(? {canonical_name}") continue - entry.rename(dest) + ok, err = safe_rename(entry, dest) + if not ok: + print(f"Skipping ({err}): {entry.name} -> {canonical_name}") + continue log.record(entry, dest) print(f"Renamed folder: {entry.name} -> {canonical_name}") @@ -1473,21 +1509,32 @@ def organize_season_folders(folder, log): if dest.exists() and not same_existing_path(dest, sub): print(f"Skipping (target already exists): {sub.name} -> {new_name}") continue - sub.rename(dest) + ok, err = safe_rename(sub, dest) + if not ok: + print(f"Skipping ({err}): {sub.name} -> {new_name}") + continue log.record(sub, dest) print(f"Renamed folder: {sub.name} -> {new_name}") renamed += 1 return renamed -def resolve_season_folder_file(item, season): +def resolve_season_folder_file(item, season, api_key=None, tmdb_id=None): se = parse_season_episode(item.name) if se: return se[0], se[1], se[2] - return parse_season_only(item.name), parse_episode_only(item.name), 'E' + + file_season = parse_season_only(item.name) + episode = parse_episode_only(item.name) + if episode is None: + resolved = resolve_absolute_episode(api_key, tmdb_id, item.name) + if resolved is not None: + return resolved[0], resolved[1], 'E' + + return file_season, episode, 'E' -def preview_season_folder_files(folder, final_name): +def preview_season_folder_files(folder, final_name, api_key=None, tmdb_id=None): for sub in list_subfolders(folder): season = parse_season_folder_name(sub.name) if season is None: @@ -1497,7 +1544,7 @@ def preview_season_folder_files(folder, final_name): for item in list_video_files(sub): ext = item.suffix.lower().lstrip(".") - file_season, episode, marker = resolve_season_folder_file(item, season) + file_season, episode, marker = resolve_season_folder_file(item, season, api_key, tmdb_id) target_season = file_season if file_season is not None else season subtitles = gather_video_subtitles(item, True) @@ -1525,7 +1572,7 @@ def preview_season_folder_files(folder, final_name): preview_consolidated_subtitles(subtitles, item) -def rename_season_folder_files(folder, final_name, log): +def rename_season_folder_files(folder, final_name, log, api_key=None, tmdb_id=None): renamed = 0 skipped = 0 @@ -1538,7 +1585,7 @@ def rename_season_folder_files(folder, final_name, log): for item in list_video_files(sub): ext = item.suffix.lower().lstrip(".") - file_season, episode, marker = resolve_season_folder_file(item, season) + file_season, episode, marker = resolve_season_folder_file(item, season, api_key, tmdb_id) target_season = file_season if file_season is not None else season target_season_folder = season_folder_name(target_season) target_dir = folder / target_season_folder if target_season != season else sub @@ -1564,7 +1611,11 @@ def rename_season_folder_files(folder, final_name, log): if target_dir != sub: target_dir.mkdir(exist_ok=True) - item.rename(dest) + ok, err = safe_rename(item, dest) + if not ok: + print(f"Skipping ({err}): {item.name}") + skipped += 1 + continue log.record(item, dest) if target_dir != sub: print(f"Moved: {item.name} -> {target_season_folder}/{new_name}") @@ -1618,7 +1669,11 @@ def merge_duplicate_show_folder(source_folder, target_folder, raw_name, final_na continue item_target_dir.mkdir(exist_ok=True) - item.rename(dest) + ok, err = safe_rename(item, dest) + if not ok: + print(f"Skipping ({err}): {item.name}") + skipped += 1 + continue log.record(item, dest) print(f"Moved: {item.name} -> {target_folder.name}/{item_season_folder}/{new_name}") moved += 1 @@ -1658,7 +1713,11 @@ def merge_duplicate_show_folder(source_folder, target_folder, raw_name, final_na continue item_target_dir.mkdir(exist_ok=True) - item.rename(dest) + ok, err = safe_rename(item, dest) + if not ok: + print(f"Skipping ({err}): {item.name}") + skipped += 1 + continue log.record(item, dest) print(f"Moved: {item.name} -> {target_folder.name}/{item_season_folder}/{new_name}") moved += 1 @@ -1803,7 +1862,11 @@ def rename_video_files(folder, media_type, final_name, log, api_key, tmdb_id=Non print(f"Skipping (target already exists): {item.name}") skipped += 1 continue - item.rename(dest) + ok, err = safe_rename(item, dest) + if not ok: + print(f"Skipping ({err}): {item.name}") + skipped += 1 + continue log.record(item, dest) print(f"Renamed file: {item.name} -> {new_name}") renamed += 1 @@ -1844,7 +1907,11 @@ def rename_video_files(folder, media_type, final_name, log, api_key, tmdb_id=Non continue season_dir.mkdir(exist_ok=True) - item.rename(dest) + ok, err = safe_rename(item, dest) + if not ok: + print(f"Skipping ({err}): {item.name}") + skipped += 1 + continue log.record(item, dest) print(f"Moved: {item.name} -> {target_season_folder}/{new_name}") renamed += 1 @@ -2217,6 +2284,7 @@ def remove_empty_folders(share, confirm_all, dry_run=False): def run_cleanup(args, log): path_arg = args.cleanup if isinstance(args.cleanup, str) else None share = resolve_share(path_arg) + log.set_label(f"{Path(share).name}-cleanup") delete_folder_names = load_delete_folder_names() delete_file_patterns = load_delete_file_patterns() @@ -2391,7 +2459,11 @@ def process_loose_movie_files(share, api_key, log, test_mode, test_limit, args): continue target_folder.mkdir(exist_ok=True) - item.rename(dest) + ok, err = safe_rename(item, dest) + if not ok: + print(f"Skipping ({err}): {item.name}") + folders_skipped += 1 + continue log.record(item, dest) print(f"Moved: {item.name} -> {folder_name}/{new_name}") folders_renamed += 1 @@ -2468,7 +2540,11 @@ def process_loose_tv_files(share, api_key, log, test_mode, test_limit, args): continue (target_folder / target_season_folder_name).mkdir(parents=True, exist_ok=True) - item.rename(dest) + ok, err = safe_rename(item, dest) + if not ok: + print(f"Skipping ({err}): {item.name}") + files_skipped += 1 + continue log.record(item, dest) print(f"Moved: {item.name} -> {folder_name}/{target_season_folder_name}/{new_name}") folders_renamed += 1 @@ -2482,6 +2558,7 @@ def process_loose_tv_files(share, api_key, log, test_mode, test_limit, args): def run_scan(args, log): path_arg = args.rename if isinstance(args.rename, str) else None share = resolve_share(path_arg) + log.set_label(Path(share).name) cache = load_scan_cache() share_key = str(Path(share).resolve()) @@ -2583,7 +2660,7 @@ def run_scan(args, log): if media_type == "tv": simulated_tv_folder_names.add(folder_name) preview_season_folders(folder) - preview_season_folder_files(folder, final_name) + preview_season_folder_files(folder, final_name, api_key, match_id) preview_video_files(folder, media_type, final_name, api_key, match_id) examples_shown += 1 continue @@ -2614,7 +2691,11 @@ def run_scan(args, log): folders_skipped += 1 continue - folder.rename(new_folder) + ok, err = safe_rename(folder, new_folder) + if not ok: + print(f"Skipping ({err}): {raw_name} -> {folder_name}") + folders_skipped += 1 + continue log.record(folder, new_folder) print(f"Renamed folder: {raw_name} -> {folder_name}") folders_renamed += 1 @@ -2624,7 +2705,7 @@ def run_scan(args, log): if media_type == "tv": organize_season_folders(folder, log) - sub_renamed, sub_skipped = rename_season_folder_files(folder, final_name, log) + sub_renamed, sub_skipped = rename_season_folder_files(folder, final_name, log, api_key, match_id) files_renamed += sub_renamed files_skipped += sub_skipped @@ -2844,6 +2925,7 @@ def run_backup(args): def run_manual_rename(current_name, new_name, log): + log.set_label("manual") src = Path(current_name) dst = Path(new_name)