Add opt-in sample_videos cleanup rule for loose sample video files

This commit is contained in:
Justin Oro
2026-09-06 17:47:05 -07:00
parent 8ceaa9cfdb
commit 2f73463bbe
2 changed files with 83 additions and 66 deletions
+48 -61
View File
@@ -2,7 +2,7 @@
#
# Configures what cleanup (-c) moves to a local trash folder. Nothing here
# is deleted permanently -- matched items are moved to a .trash folder next
# to the script, and every move can be undone. All three sections below are
# to the script, and every move can be undone. All sections below are
# optional and independent; enable only the ones you want.
#
# ============================================================================
@@ -23,21 +23,22 @@
# moved to trash, along with everything inside it. There is no
# confirmation beyond the normal cleanup run prompts, so double check
# this list before running for real.
#
# Uncomment and edit the list below to enable:
#
# folders:
# - Screens
# - Featurettes
# - Extras
# - Sample
# - Samples
# - Behind the Scenes
# - Deleted Scenes
# - Trailers
# - Interviews
# - Bonus
#
folders:
- Screens
- Featurettes
- Extras
- Sample
- Samples
- Soundtrack
- Behind the Scenes
- Deleted Scenes
- Trailers
- Interviews
- Bonus
- "* torrent"
- "* torrents"
# ============================================================================
# files
# ============================================================================
@@ -63,20 +64,39 @@
# the "subtitles:" section below is the friendlier, language-agnostic
# way to protect or target any language, including your own.
# - Video file extensions (mp4, mkv, avi, etc.) are never deleted by
# cleanup, even if a pattern here would otherwise match them.
# cleanup, even if a pattern here would otherwise match them. Use
# "sample_videos:" below for the one exception to that.
files:
- "*.txt"
- "*.nfo"
- "*.jpg"
- "*.jpeg"
- "*.png"
- "*.smi"
- "*.ico"
- "*.url"
- "*.sfv"
- "*.part"
# ============================================================================
# sample_videos
# ============================================================================
#
# Uncomment and edit the list below to enable:
#
# files:
# - "*.txt"
# - "*.nfo"
# - "*.jpg"
# - "*.jpeg"
# - "*.png"
# - "*.ico"
# - "*.url"
# - "*.sfv"
# Whether to move loose sample video files to trash during cleanup.
#
# - Video file extensions (mp4, mkv, avi, etc.) are otherwise never touched
# by cleanup, even under "files:" above -- this is the one exception.
# - Only matches a video file with the whole word "sample" in its name
# (e.g. "Sample.mkv", "Movie-Sample.avi", "Movie.sample.mp4"), so it
# won't catch a real video that just happens to contain "sample" as part
# of a longer word.
# - This only catches loose sample files sitting next to the real video.
# A sample shipped inside its own "Sample" folder is already handled by
# adding "Sample" to the "folders:" list above.
sample_videos: true
# ============================================================================
# subtitles
# ============================================================================
@@ -119,39 +139,6 @@
# word frequency for Latin-script languages. This is a best-effort
# heuristic for .srt content, not a guarantee, and always errs toward
# leaving a file alone if the result is unclear.
#
# Uncomment and edit to enable (replace "Spanish" with your own primary
# language, or whichever language you want to keep):
#
# subtitles:
# - "!Spanish"
folders:
- Screens
- Featurettes
- Extras
- Sample
- Samples
- Soundtrack
- Behind the Scenes
- Deleted Scenes
- Trailers
- Interviews
- Bonus
- "* torrent"
- "* torrents"
files:
- "*.txt"
- "*.nfo"
- "*.jpg"
- "*.jpeg"
- "*.png"
- "*.smi"
- "*.ico"
- "*.url"
- "*.sfv"
- "*.part"
subtitles:
- "!English"
+35 -5
View File
@@ -57,6 +57,8 @@ VIDEO_EXTENSIONS = {
"mp4", "mkv", "avi", "mov", "wmv", "m4v", "mpg", "mpeg", "flv", "ts", "m2ts", "webm",
}
SAMPLE_VIDEO_PATTERN = re.compile(r'(?<![A-Za-z0-9])sample(?![A-Za-z0-9])', re.IGNORECASE)
SUBTITLE_EXTENSIONS = {"srt", "sub"}
SUBTITLE_FOLDER_NAMES = {"subs", "subtitles"}
@@ -2853,7 +2855,7 @@ def save_scan_cache(cache):
CACHE_FILE.write_text(json.dumps(cache, indent=2))
def compute_cleanup_rules_signature(delete_folder_names, delete_file_patterns, keep_languages, delete_languages):
def compute_cleanup_rules_signature(delete_folder_names, delete_file_patterns, keep_languages, delete_languages, sample_videos_enabled=False):
hasher = hashlib.sha256()
for name in sorted(delete_folder_names):
hasher.update(f"f:{name.lower()}\n".encode("utf-8"))
@@ -2863,6 +2865,7 @@ def compute_cleanup_rules_signature(delete_folder_names, delete_file_patterns, k
hasher.update(f"k:{lang}\n".encode("utf-8"))
for lang in sorted(delete_languages or []):
hasher.update(f"d:{lang}\n".encode("utf-8"))
hasher.update(f"s:{bool(sample_videos_enabled)}\n".encode("utf-8"))
return hasher.hexdigest()
@@ -2893,6 +2896,23 @@ def load_delete_folder_names():
return [name.lower() for name in parse_simple_yaml_list(DELETE_FILE, "folders")]
def parse_simple_yaml_bool(path, key):
if not path.exists():
return False
for raw_line in path.read_text().splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith(f"{key}:"):
value = line.split(":", 1)[1].strip().strip('"').strip("'").lower()
return value in ("true", "yes", "1", "on")
return False
def load_delete_sample_videos_flag():
return parse_simple_yaml_bool(DELETE_FILE, "sample_videos")
TRAILING_WRAPPER_CHARS = ")]}'\""
@@ -3111,7 +3131,7 @@ def matches_file_name(name, patterns, path=None):
return True
def find_cleanup_targets(share, delete_folder_names, delete_file_patterns, keep_languages=None, delete_languages=None, unchanged_folder_names=None):
def find_cleanup_targets(share, delete_folder_names, delete_file_patterns, keep_languages=None, delete_languages=None, unchanged_folder_names=None, sample_videos_enabled=False):
targets = []
keep_languages = keep_languages or set()
delete_languages = delete_languages or set()
@@ -3146,6 +3166,10 @@ def find_cleanup_targets(share, delete_folder_names, delete_file_patterns, keep_
elif entry.is_file():
ext = entry.suffix.lower().lstrip(".")
if ext in VIDEO_EXTENSIONS:
if sample_videos_enabled and SAMPLE_VIDEO_PATTERN.search(entry.stem):
vprint(f" Match (sample video): {entry}")
targets.append(("file", entry))
continue
vprint(f" Skipping (protected video file): {entry}")
continue
if matches_file_name(entry.name, delete_file_patterns, entry):
@@ -3224,19 +3248,24 @@ def run_cleanup(args, log):
delete_folder_names = load_delete_folder_names()
delete_file_patterns = load_delete_file_patterns()
keep_languages, delete_languages = load_delete_subtitle_rules()
sample_videos_enabled = load_delete_sample_videos_flag()
vprint(f"Loaded {len(delete_folder_names)} folder pattern(s) from {DELETE_FILE.name}: {delete_folder_names}")
vprint(f"Loaded {len(delete_file_patterns)} file pattern(s) from {DELETE_FILE.name}: {delete_file_patterns}")
vprint(f"Loaded subtitle rules: keep={keep_languages or 'none'}, delete={delete_languages or 'none'}")
vprint(f"Sample video cleanup enabled: {sample_videos_enabled}")
has_duplicates_folder = (Path(share) / DUPLICATES_FOLDER_NAME).is_dir()
if not delete_folder_names and not delete_file_patterns and not keep_languages and not delete_languages and not has_duplicates_folder:
if (
not delete_folder_names and not delete_file_patterns and not keep_languages
and not delete_languages and not has_duplicates_folder and not sample_videos_enabled
):
print("No cleanup rules configured.")
print(f"Edit {DELETE_FILE.name} to enable cleanup.")
return
rules_signature = compute_cleanup_rules_signature(
delete_folder_names, delete_file_patterns, keep_languages, delete_languages
delete_folder_names, delete_file_patterns, keep_languages, delete_languages, sample_videos_enabled
)
cache = load_scan_cache()
share_key = str(Path(share).resolve())
@@ -3268,7 +3297,8 @@ def run_cleanup(args, log):
print()
targets = find_cleanup_targets(
share, delete_folder_names, delete_file_patterns, keep_languages, delete_languages, unchanged_folder_names
share, delete_folder_names, delete_file_patterns, keep_languages, delete_languages,
unchanged_folder_names, sample_videos_enabled
)
vprint(f"Scan complete. {len(targets)} item(s) matched.")