From e98e0794458136ae6740963e8c163b0e8693c7f3 Mon Sep 17 00:00:00 2001 From: Morgan Blackthorne Date: Tue, 19 May 2026 06:40:46 -0700 Subject: [PATCH] Port list_renamed_vids.py into plexadm as list renames subcommand Add `plexadm list renames [--script] [filter]` which iterates all Plex videos and reports those whose filename does not match their title. Output is either a human-readable diff or shell mv commands (--script). Excludes Message/Post/PPV entries and titles containing '?', matching the original script's behaviour. Adds --base-dir to override the path prefix stripped from file locations (default: /data/NSFW Scenes/). Remove reference/legacy-python/list_renamed_vids.py now that the functionality is covered by the CLI. Update README with usage examples. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 24 ++++++ plexadm/cli.py | 57 ++++++++++++ reference/legacy-python/list_renamed_vids.py | 91 -------------------- 3 files changed, 81 insertions(+), 91 deletions(-) delete mode 100755 reference/legacy-python/list_renamed_vids.py diff --git a/README.md b/README.md index 8c61305..e7cd169 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,30 @@ plexadm list special uncollected plexadm list special multipart ``` +### Renames + +List videos whose filename does not match their Plex title: + +```bash +plexadm list renames +plexadm list renames "TUSHY" +``` + +Output shell `mv` commands instead of a human-readable diff: + +```bash +plexadm list renames --script +plexadm list renames --script "TUSHY" +``` + +Override the base directory prefix stripped from file paths (default: `/data/NSFW Scenes/`): + +```bash +plexadm list renames --base-dir "/other/path/" +``` + +Message, Post, PPV, and titles containing `?` are excluded automatically. + ## Collection Commands These commands add or remove collection membership immediately. diff --git a/plexadm/cli.py b/plexadm/cli.py index 8bc2910..247c78d 100644 --- a/plexadm/cli.py +++ b/plexadm/cli.py @@ -497,6 +497,56 @@ def rename_collections(args: argparse.Namespace) -> int: return 0 +SCENE_BASE_DIR = "/data/NSFW Scenes/" + + +def list_renames(args: argparse.Namespace) -> int: + ctx = build_context(args) + filter_text = args.filter_text + + for video in ctx.all_videos(): + locations = getattr(video, "locations", []) or [] + if not locations: + continue + + if filter_text: + haystack = [video.title] + locations + if not any(filter_text in entry for entry in haystack): + continue + + filename = Path(locations[0]).name + match_found = any(video.title in location for location in locations) + + if ( + " - Message " in filename + or " - Post " in filename + or "PPV" in filename + or " PPV " in locations[0] + or "?" in video.title + ): + match_found = True + + has_location_mismatch = any(video.title not in location for location in locations) + needs_review = not args.script and len(locations) > 1 and has_location_mismatch + + if not match_found or needs_review: + old_location = locations[0].replace(args.base_dir, "") + new_fname = f"{video.title}.mp4" + first_writer = new_fname.split(" - ", 1)[0].split(",")[0] + if args.script: + print(f'mv "{old_location}" "{first_writer}/{new_fname}"') + else: + if len(locations) > 1: + print(f"WARNING: {video.title} has multiple locations!") + for location in locations: + print(f" {location}") + print("") + else: + print(f"{old_location} -> {first_writer}/{new_fname}") + + return 0 + + def find_missing_file(args: argparse.Namespace) -> int: ctx = build_context(args) target = Path(args.path) @@ -649,6 +699,13 @@ def build_parser() -> argparse.ArgumentParser: studio_writers = list_sub.add_parser("studio-writers") studio_writers.add_argument("studio") set_func(studio_writers, list_studio_writers) + renames = list_sub.add_parser("renames") + renames.add_argument( + "filter_text", nargs="?", help="Only include videos where title or file path contains this text." + ) + renames.add_argument("--script", action="store_true", help="Output mv commands instead of human-readable diff.") + renames.add_argument("--base-dir", default=SCENE_BASE_DIR, help="Base directory prefix to strip from file paths.") + set_func(renames, list_renames) special = list_sub.add_parser("special") special.add_argument( "kind", diff --git a/reference/legacy-python/list_renamed_vids.py b/reference/legacy-python/list_renamed_vids.py deleted file mode 100755 index eff422d..0000000 --- a/reference/legacy-python/list_renamed_vids.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -# -# import modules -# -import argparse -import configparser -import os -from plexapi.server import PlexServer -# -# set default variables -# -config = configparser.ConfigParser() -config.read(os.getenv('HOME')+'/.plexconfig.ini') -plexHost = config['default']['plexHost'] -plexPort = config['default']['plexPort'] -plexSection = config['default']['plexSection'] -plexToken = config['default']['plexToken'] -plexSectionName = config['default']['plexSectionName'] -baseurl = f"http://{plexHost}:{plexPort}" -# -# Connect to server -# -plex = PlexServer(baseurl, plexToken) -# -# Select section -# -plexSection = plex.library.section(plexSectionName) - -parser = argparse.ArgumentParser( - description=( - "List videos that would be renamed. Use 'output' for old/new names or " - "'script' for mv commands." - ) -) -parser.add_argument("mode", choices=["output", "script"]) -parser.add_argument( - "filter_text", - nargs="?", - help="Only include videos where the title or any file location contains this text.", -) -args = parser.parse_args() - -filter_text = args.filter_text - -for video in plexSection.all(): - if filter_text: - haystack = [video.title] + video.locations - if not any(filter_text in entry for entry in haystack): - continue - - matchFound = False - filename = os.path.basename(video.locations[0]) - # if len(video.locations) > 1: - # print(f"{video.title} has multiple locations!") - # for location in video.locations: - # print(location) - # print('') - # else: - for location in video.locations: - if video.title in location: - matchFound = True - - if " - Message " in filename or " - Post " in filename or "PPV" in filename: - # Exclude Message/Post/PPV files from results - matchFound = True - elif " PPV " in video.locations[0]: - # Leave these alone for rsync purposes - matchFound = True - elif '?' in video.title: - # Can't use this in a filename - matchFound = True - - has_location_mismatch = any(video.title not in location for location in video.locations) - needs_review = args.mode == "output" and len(video.locations) > 1 and has_location_mismatch - - if matchFound is False or needs_review: - oldLocation = video.locations[0].replace('/data/NSFW Scenes/', '') - newFname = f"{video.title}.mp4" - writerNames = newFname.split(' - ', 1)[0] - writersList = writerNames.split(',') - firstWriter = writersList[0] - if args.mode == "script": - print(f"mv \"{oldLocation}\" \"{firstWriter}/{newFname}\"") - else: - if len(video.locations) > 1: - print(f"WARNING: {video.title} has multiple locations!") - for location in video.locations: - print(f" {location}") - print("") - else: - print(f"{oldLocation} -> {firstWriter}/{newFname}")