docs(runbook): add arr-stack downloads cleanup investigation and scripts
~16T freed on aya01 (92% → 57% mergerfs pool). Documents root cause (no hardlinks across mergerfs due to cross-device mounts), cleanup passes via Sonarr/Radarr API verification, and pending decisions (Bleach remux, 111 skipped Sonarr entries).
This commit is contained in:
259
docs/runbooks/arr-cleanup/cleanup-orphans.py
Normal file
259
docs/runbooks/arr-cleanup/cleanup-orphans.py
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Delete download entries from /media/downloads/sonarr that are NOT in Sonarr,
|
||||
logging every action (size, path, timestamp, outcome) to cleanup.log.
|
||||
|
||||
Runs in two passes:
|
||||
1. Tries hard to match each orphan against Sonarr (title + romaji + partial).
|
||||
Anything that matches is skipped — only true non-matches are deleted.
|
||||
2. For each confirmed non-match, checks whether a directory with that show
|
||||
name exists in /media/series (belt-and-suspenders). If it does, skips.
|
||||
3. Deletes remaining entries and logs every outcome.
|
||||
|
||||
Usage:
|
||||
python3 cleanup-orphans.py --dry-run # show what would be deleted
|
||||
python3 cleanup-orphans.py --yes # delete without confirmation
|
||||
"""
|
||||
|
||||
import urllib.request
|
||||
import json
|
||||
import subprocess
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
|
||||
SONARR_URL = "http://localhost:8989/api/v3"
|
||||
SSH_HOST = "aya01"
|
||||
DL_ROOT = "/media/downloads/sonarr"
|
||||
SERIES_ROOT = "/media/series"
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
LOG_FILE = os.path.join(script_dir, "cleanup.log")
|
||||
|
||||
with open(os.path.join(script_dir, '..', 'sonarr.api.env')) as f:
|
||||
SONARR_KEY = f.read().strip()
|
||||
|
||||
|
||||
def api_get(url):
|
||||
with urllib.request.urlopen(url, timeout=30) as r:
|
||||
return json.load(r)
|
||||
|
||||
|
||||
def norm(s):
|
||||
return re.sub(r'[^a-z0-9]', '', s.lower())
|
||||
|
||||
|
||||
def ssh_run(cmd):
|
||||
r = subprocess.run(['ssh', SSH_HOST, cmd], capture_output=True, text=True)
|
||||
return r.stdout.strip()
|
||||
|
||||
|
||||
def ssh_exists(path):
|
||||
return ssh_run(f'[ -e {json.dumps(path)} ] && echo yes || echo no') == 'yes'
|
||||
|
||||
|
||||
def ssh_size(path):
|
||||
"""Return size in bytes, or 0 if path doesn't exist."""
|
||||
out = ssh_run(f'du -sb {json.dumps(path)} 2>/dev/null | cut -f1')
|
||||
try:
|
||||
return int(out)
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def ssh_delete(path):
|
||||
r = subprocess.run(['ssh', SSH_HOST, f'rm -rf {json.dumps(path)}'],
|
||||
capture_output=True, text=True)
|
||||
return r.returncode == 0, r.stderr.strip()
|
||||
|
||||
|
||||
def log(line):
|
||||
ts = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
entry = f"[{ts}] {line}"
|
||||
print(entry)
|
||||
with open(LOG_FILE, 'a') as f:
|
||||
f.write(entry + '\n')
|
||||
|
||||
|
||||
def extract_title(name):
|
||||
"""Strip season/episode/quality tags to recover a bare show title."""
|
||||
name = re.sub(r'\.(mkv|mp4|ts|avi)$', '', name, flags=re.IGNORECASE)
|
||||
name = re.sub(r'^\[.*?\]\s*', '', name) # [Group] prefix
|
||||
name = re.sub(r'\s*\[.*?\]\s*', ' ', name) # inline [tags]
|
||||
name = re.sub(r'[\.\s_\-]?[Ss]\d{1,2}[Ee]\d{1,2}.*$', '', name)
|
||||
name = re.sub(r'[\.\s_\-]?[Ss]\d{1,2}[\.\s_\-].*$', '', name)
|
||||
name = re.sub(r'[\.\s_\-]?[Ss]\d{2}$', '', name)
|
||||
name = re.sub(r'[\.\s_\-]?(19|20)\d{2}.*$', '', name)
|
||||
name = re.sub(r'[\.\s_\-]?\d{3,4}p.*$', '', name) # 1080p etc
|
||||
name = re.sub(r'[\.\-_]+', ' ', name).strip()
|
||||
return name
|
||||
|
||||
|
||||
def build_sonarr_index(series):
|
||||
idx = {}
|
||||
for s in series:
|
||||
for title_variant in [s['title'], s.get('titleSlug', ''), s.get('sortTitle', '')]:
|
||||
if title_variant:
|
||||
idx[norm(title_variant)] = s
|
||||
# Also index alternate titles if present
|
||||
for alt in s.get('alternateTitles', []):
|
||||
t = alt.get('title', '')
|
||||
if t:
|
||||
idx[norm(t)] = s
|
||||
return idx
|
||||
|
||||
|
||||
def find_in_sonarr(dl_name, idx):
|
||||
title = extract_title(dl_name)
|
||||
tn = norm(title)
|
||||
if tn in idx:
|
||||
return idx[tn], title
|
||||
# Partial: dl title starts with series title (or vice versa), min 6 chars
|
||||
for k, rec in idx.items():
|
||||
if k and len(k) >= 6 and len(tn) >= 6:
|
||||
if tn.startswith(k) or k.startswith(tn):
|
||||
return rec, title
|
||||
return None, title
|
||||
|
||||
|
||||
def confirm(prompt):
|
||||
answer = input(f"{prompt} [y/N] ").strip().lower()
|
||||
return answer == 'y'
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
parser.add_argument('--yes', '-y', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.dry_run:
|
||||
print("DRY-RUN — nothing will be deleted\n")
|
||||
|
||||
log("=" * 60)
|
||||
log(f"cleanup-orphans.py started (dry_run={args.dry_run})")
|
||||
|
||||
print("Fetching Sonarr series (including alternate titles)...")
|
||||
series = api_get(f"{SONARR_URL}/series?apikey={SONARR_KEY}")
|
||||
print(f" {len(series)} series")
|
||||
idx = build_sonarr_index(series)
|
||||
|
||||
# Collect series dirs on disk for secondary check
|
||||
# Strip years, imdb tags, and punctuation so "Bleach (2004) {imdb-...}" matches "Bleach"
|
||||
print("Fetching /media/series directory listing...")
|
||||
series_on_disk_raw = ssh_run(f'ls {json.dumps(SERIES_ROOT)}/').splitlines()
|
||||
def norm_dir(d):
|
||||
d = re.sub(r'\{.*?\}', '', d) # remove {imdb-...}
|
||||
d = re.sub(r'\(?\d{4}\)?', '', d) # remove years
|
||||
d = re.sub(r'[^a-z0-9]', '', d.lower())
|
||||
return d
|
||||
series_on_disk_norm = {norm_dir(d) for d in series_on_disk_raw if d.strip()}
|
||||
|
||||
print("Fetching download listing...")
|
||||
dl_entries = ssh_run(f'ls {json.dumps(DL_ROOT)}/').splitlines()
|
||||
dl_entries = [e.strip() for e in dl_entries if e.strip()]
|
||||
print(f" {len(dl_entries)} entries in {DL_ROOT}")
|
||||
|
||||
# --- First pass: match against Sonarr ---
|
||||
not_in_sonarr = []
|
||||
in_sonarr = []
|
||||
|
||||
for dl in dl_entries:
|
||||
rec, extracted_title = find_in_sonarr(dl, idx)
|
||||
if rec:
|
||||
in_sonarr.append((dl, rec['title']))
|
||||
else:
|
||||
not_in_sonarr.append((dl, extracted_title))
|
||||
|
||||
print(f"\n Matched to Sonarr: {len(in_sonarr)}")
|
||||
print(f" NOT in Sonarr: {len(not_in_sonarr)}")
|
||||
|
||||
# --- Second pass: check if series dir exists on disk anyway ---
|
||||
skip_has_series_dir = []
|
||||
to_delete = []
|
||||
|
||||
for dl, title in not_in_sonarr:
|
||||
title_n = norm(title)
|
||||
# Check if any series dir on disk has a similar name
|
||||
has_dir = any(
|
||||
d and len(d) >= 6 and (title_n.startswith(d) or d.startswith(title_n))
|
||||
for d in series_on_disk_norm
|
||||
)
|
||||
# Also check the full download path exists
|
||||
dl_path = f"{DL_ROOT}/{dl}"
|
||||
if has_dir:
|
||||
skip_has_series_dir.append((dl, title, dl_path))
|
||||
else:
|
||||
to_delete.append((dl, title, dl_path))
|
||||
|
||||
if skip_has_series_dir:
|
||||
print(f"\n SKIPPED (series dir found on disk, needs manual review): {len(skip_has_series_dir)}")
|
||||
for dl, title, _ in skip_has_series_dir:
|
||||
print(f" {title:40s} ← {dl[:60]}")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"TO DELETE ({len(to_delete)} entries — not in Sonarr, no series dir on disk)")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Get sizes in parallel
|
||||
print("\nMeasuring sizes...")
|
||||
size_cmd = ' && '.join(
|
||||
f'du -sb {json.dumps(f"{DL_ROOT}/{dl}")} 2>/dev/null | cut -f1'
|
||||
for dl, _, _ in to_delete
|
||||
)
|
||||
if to_delete:
|
||||
size_out = ssh_run(f'bash -c {json.dumps(size_cmd)}').splitlines()
|
||||
else:
|
||||
size_out = []
|
||||
|
||||
sizes = {}
|
||||
for i, (dl, title, path) in enumerate(to_delete):
|
||||
try:
|
||||
sizes[dl] = int(size_out[i]) if i < len(size_out) else 0
|
||||
except (ValueError, IndexError):
|
||||
sizes[dl] = 0
|
||||
|
||||
total_bytes = sum(sizes.values())
|
||||
for dl, title, path in sorted(to_delete, key=lambda x: x[1]):
|
||||
sz = sizes.get(dl, 0)
|
||||
print(f" {sz/1e9:6.1f}G {title:40s} ← {dl[:60]}")
|
||||
|
||||
print(f"\n Total: {total_bytes/1e9:.1f}G across {len(to_delete)} entries")
|
||||
|
||||
if not to_delete:
|
||||
log("Nothing to delete.")
|
||||
return
|
||||
|
||||
if not args.dry_run and not args.yes:
|
||||
if not confirm(f"\nDelete {len(to_delete)} entries?"):
|
||||
log("Aborted by user.")
|
||||
return
|
||||
|
||||
# --- Delete with logging ---
|
||||
deleted_count = 0
|
||||
deleted_bytes = 0
|
||||
failed_count = 0
|
||||
|
||||
for dl, title, path in sorted(to_delete, key=lambda x: x[1]):
|
||||
sz = sizes.get(dl, 0)
|
||||
if args.dry_run:
|
||||
log(f"DRY-RUN | {sz/1e9:.2f}G | {title} | {path}")
|
||||
deleted_count += 1
|
||||
deleted_bytes += sz
|
||||
else:
|
||||
ok, err = ssh_delete(path)
|
||||
if ok:
|
||||
log(f"DELETED | {sz/1e9:.2f}G | {title} | {path}")
|
||||
deleted_count += 1
|
||||
deleted_bytes += sz
|
||||
else:
|
||||
log(f"FAILED | {sz/1e9:.2f}G | {title} | {path} | {err}")
|
||||
failed_count += 1
|
||||
|
||||
log(f"DONE | deleted={deleted_count} | freed={deleted_bytes/1e9:.1f}G | failed={failed_count}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
160
docs/runbooks/arr-cleanup/cleanup.log
Normal file
160
docs/runbooks/arr-cleanup/cleanup.log
Normal file
@@ -0,0 +1,160 @@
|
||||
[2026-04-22T21:18:32Z] ============================================================
|
||||
[2026-04-22T21:18:32Z] cleanup-orphans.py started (dry_run=True)
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 14.62G | BLEACH Thousand Year Blood War | /media/downloads/sonarr/BLEACH.Thousand-Year.Blood.War.S01.JAPANESE.1080p.DSNP.WEBRip.AAC2.0.x264-NTb[rartv]
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1971.45G | Bleach USBD Remux TL | /media/downloads/sonarr/Bleach USBD Remux TL
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 0.52G | Gachiakuta 09 | /media/downloads/sonarr/[KiyoshiiSubs] Gachiakuta - 09 [1080p][H.265 - 10Bit].mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.44G | Gachiakuta 19 ( | /media/downloads/sonarr/[SubsPlease] Gachiakuta - 19 (1080p) [019A6A50].mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 24.39G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S01.1080p.MAX.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 36.73G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S02.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 37.52G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S03.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 36.83G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S04.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 37.77G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S05.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 36.07G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S06.1080p.HMAX.WEB-DL.DD.5.1.H.264-GNOME
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 29.48G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S07.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 28.71G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S08.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 4.58G | Grimgar Of Fantasy And Ash ( | /media/downloads/sonarr/Grimgar Of Fantasy And Ash (2016) S01 1080p BluRay 10bit EAC3 2 0 x265-iVy
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 01 (1080p) [4CA94F81]
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 05 (1080p) [A0556FA8].mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 06 (1080p) [982D7547].mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 07 (1080p) [247CFB44].mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.52G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 10 (1080p) [ABE1B90A]
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.52G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 13 (1080p) [230618C3].mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 0.52G | Hikikomari Kyuuketsuki no Monmon 07 ( | /media/downloads/sonarr/[SubsPlease] Hikikomari Kyuuketsuki no Monmon - 07 (1080p) [B07BA1C7]
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 10.49G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 8.97G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 4.23G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S02.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 4.97G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S02.1080p.NF.WEB-DL.DDP5.1.Atmos.x264-Telly
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 5.99G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S03.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 5.26G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S03.1080p.NF.WEBRip.DDP5.1.Atmos.x264-SMURF
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 4.44G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S04.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 0.88G | SANDA | /media/downloads/sonarr/SANDA.S01E02.1080p.WEB.H264-SENSEI
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 0.70G | Senpai Is An Otokonoko | /media/downloads/sonarr/Senpai.Is.An.Otokonoko.S01E05.720p.WEB.H264-SKYANiME
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E01.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E02.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E03.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E04.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E07.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E08.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E10.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E12.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 31.17G | Sex Education | /media/downloads/sonarr/Sex.Education.S01.1080p.NF.WEB.DDP5.1.x264-DEFLATE
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 41.53G | Sex Education | /media/downloads/sonarr/Sex.Education.S02.1080p.NF.WEB.DDP5.1.x264-NTb
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 15.56G | Sex Education | /media/downloads/sonarr/Sex.Education.S03.1080p.NF.WEB-DL.DDP5.1.H.264-FLUX
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 21.49G | Sex Education | /media/downloads/sonarr/Sex.Education.S04.1080p.NF.WEB-DL.DDP5.1.H.264-Archie
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.49G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E02.THE.HERO.OF.MY.DREAMS.1080p.CR.WEB-DL.AAC2.0.H.264.DUAL-VARYG.mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.49G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E03.THE.MAN.WHO.STANDS.AT.THE.TOP.1080p.CR.WEB-DL.AAC2.0.H.264.DUAL-VARYG.mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.48G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E04.CLASH.1080p.CR.WEB-DL.AAC2.0.H.264.DUAL-VARYG.mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 0.34G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E07.A.Fight.He.Cant.Lose.1080p.B-Global.WEB-DL.JPN.AAC2.0.H.264.MSubs-ToonsHub.mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 0.26G | Wind Breaker | /media/downloads/sonarr/Wind Breaker - S01E12 - 1080p WEB HEVC -NanDesuKa (B-Global).mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.46G | Wind Breaker 01 ( | /media/downloads/sonarr/[SubsPlease] Wind Breaker - 01 (1080p) [5D5071F6].mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.46G | Wind Breaker 05 ( | /media/downloads/sonarr/[SubsPlease] Wind Breaker - 05 (1080p) [B6649F46].mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.46G | Wind Breaker 06 ( | /media/downloads/sonarr/[SubsPlease] Wind Breaker - 06 (1080p) [1C13E5BC].mkv
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 0.74G | Wistoria Wand And Sword | /media/downloads/sonarr/Wistoria.Wand.And.Sword.S01E01.720p.WEB.H264-SKYANiME
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.45G | Wistoria Wand and Sword | /media/downloads/sonarr/Wistoria.Wand.and.Sword.S01E02.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 1.44G | Wistoria Wand and Sword | /media/downloads/sonarr/Wistoria.Wand.and.Sword.S01E03.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:18:55Z] DRY-RUN | 0.00G | www UIndex org Severance | /media/downloads/sonarr/www.UIndex.org - Severance S02E10 Cold Harbor 1080p ATVP WEB-DL DDP5 1 Atmos H 264-Kitsune
|
||||
[2026-04-22T21:18:55Z] DONE | deleted=53 | freed=2449.6G | failed=0
|
||||
[2026-04-22T21:23:05Z] ============================================================
|
||||
[2026-04-22T21:23:05Z] cleanup-orphans.py started (dry_run=True)
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 24.39G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S01.1080p.MAX.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 36.73G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S02.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 37.52G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S03.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 36.83G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S04.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 37.77G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S05.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 36.07G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S06.1080p.HMAX.WEB-DL.DD.5.1.H.264-GNOME
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 29.48G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S07.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 28.71G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S08.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 4.58G | Grimgar Of Fantasy And Ash ( | /media/downloads/sonarr/Grimgar Of Fantasy And Ash (2016) S01 1080p BluRay 10bit EAC3 2 0 x265-iVy
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 01 (1080p) [4CA94F81]
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 05 (1080p) [A0556FA8].mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 06 (1080p) [982D7547].mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 07 (1080p) [247CFB44].mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.52G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 10 (1080p) [ABE1B90A]
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.52G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 13 (1080p) [230618C3].mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 0.52G | Hikikomari Kyuuketsuki no Monmon 07 ( | /media/downloads/sonarr/[SubsPlease] Hikikomari Kyuuketsuki no Monmon - 07 (1080p) [B07BA1C7]
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 10.49G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 8.97G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 4.23G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S02.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 4.97G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S02.1080p.NF.WEB-DL.DDP5.1.Atmos.x264-Telly
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 5.99G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S03.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 5.26G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S03.1080p.NF.WEBRip.DDP5.1.Atmos.x264-SMURF
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 4.44G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S04.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 0.88G | SANDA | /media/downloads/sonarr/SANDA.S01E02.1080p.WEB.H264-SENSEI
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 0.70G | Senpai Is An Otokonoko | /media/downloads/sonarr/Senpai.Is.An.Otokonoko.S01E05.720p.WEB.H264-SKYANiME
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E01.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E02.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E03.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E04.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E07.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E08.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E10.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E12.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 31.17G | Sex Education | /media/downloads/sonarr/Sex.Education.S01.1080p.NF.WEB.DDP5.1.x264-DEFLATE
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 41.53G | Sex Education | /media/downloads/sonarr/Sex.Education.S02.1080p.NF.WEB.DDP5.1.x264-NTb
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 15.56G | Sex Education | /media/downloads/sonarr/Sex.Education.S03.1080p.NF.WEB-DL.DDP5.1.H.264-FLUX
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 21.49G | Sex Education | /media/downloads/sonarr/Sex.Education.S04.1080p.NF.WEB-DL.DDP5.1.H.264-Archie
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.49G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E02.THE.HERO.OF.MY.DREAMS.1080p.CR.WEB-DL.AAC2.0.H.264.DUAL-VARYG.mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.49G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E03.THE.MAN.WHO.STANDS.AT.THE.TOP.1080p.CR.WEB-DL.AAC2.0.H.264.DUAL-VARYG.mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.48G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E04.CLASH.1080p.CR.WEB-DL.AAC2.0.H.264.DUAL-VARYG.mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 0.34G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E07.A.Fight.He.Cant.Lose.1080p.B-Global.WEB-DL.JPN.AAC2.0.H.264.MSubs-ToonsHub.mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 0.26G | Wind Breaker | /media/downloads/sonarr/Wind Breaker - S01E12 - 1080p WEB HEVC -NanDesuKa (B-Global).mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.46G | Wind Breaker 01 ( | /media/downloads/sonarr/[SubsPlease] Wind Breaker - 01 (1080p) [5D5071F6].mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.46G | Wind Breaker 05 ( | /media/downloads/sonarr/[SubsPlease] Wind Breaker - 05 (1080p) [B6649F46].mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.46G | Wind Breaker 06 ( | /media/downloads/sonarr/[SubsPlease] Wind Breaker - 06 (1080p) [1C13E5BC].mkv
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 0.74G | Wistoria Wand And Sword | /media/downloads/sonarr/Wistoria.Wand.And.Sword.S01E01.720p.WEB.H264-SKYANiME
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.45G | Wistoria Wand and Sword | /media/downloads/sonarr/Wistoria.Wand.and.Sword.S01E02.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 1.44G | Wistoria Wand and Sword | /media/downloads/sonarr/Wistoria.Wand.and.Sword.S01E03.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:23:28Z] DRY-RUN | 0.00G | www UIndex org Severance | /media/downloads/sonarr/www.UIndex.org - Severance S02E10 Cold Harbor 1080p ATVP WEB-DL DDP5 1 Atmos H 264-Kitsune
|
||||
[2026-04-22T21:23:28Z] DONE | deleted=49 | freed=461.6G | failed=0
|
||||
[2026-04-22T21:32:57Z] ============================================================
|
||||
[2026-04-22T21:32:57Z] cleanup-orphans.py started (dry_run=False)
|
||||
[2026-04-22T21:33:31Z] DELETED | 24.39G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S01.1080p.MAX.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:34:04Z] DELETED | 36.73G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S02.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:34:39Z] DELETED | 37.52G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S03.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:35:05Z] DELETED | 36.83G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S04.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:35:33Z] DELETED | 37.77G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S05.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:35:51Z] DELETED | 36.07G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S06.1080p.HMAX.WEB-DL.DD.5.1.H.264-GNOME
|
||||
[2026-04-22T21:36:01Z] DELETED | 29.48G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S07.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:36:09Z] DELETED | 28.71G | Game of Thrones | /media/downloads/sonarr/Game.of.Thrones.S08.NORDiC.1080p.HMAX.WEB-DL.DDP5.1.Atmos.H.264-DKV
|
||||
[2026-04-22T21:36:10Z] DELETED | 4.58G | Grimgar Of Fantasy And Ash ( | /media/downloads/sonarr/Grimgar Of Fantasy And Ash (2016) S01 1080p BluRay 10bit EAC3 2 0 x265-iVy
|
||||
[2026-04-22T21:36:11Z] DELETED | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 01 (1080p) [4CA94F81]
|
||||
[2026-04-22T21:36:11Z] DELETED | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 05 (1080p) [A0556FA8].mkv
|
||||
[2026-04-22T21:36:11Z] DELETED | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 06 (1080p) [982D7547].mkv
|
||||
[2026-04-22T21:36:12Z] DELETED | 1.53G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 07 (1080p) [247CFB44].mkv
|
||||
[2026-04-22T21:36:13Z] DELETED | 1.52G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 10 (1080p) [ABE1B90A]
|
||||
[2026-04-22T21:36:13Z] DELETED | 1.52G | Hibike! Euphonium | /media/downloads/sonarr/[SubsPlease] Hibike! Euphonium S3 - 13 (1080p) [230618C3].mkv
|
||||
[2026-04-22T21:36:13Z] DELETED | 0.52G | Hikikomari Kyuuketsuki no Monmon 07 ( | /media/downloads/sonarr/[SubsPlease] Hikikomari Kyuuketsuki no Monmon - 07 (1080p) [B07BA1C7]
|
||||
[2026-04-22T21:36:15Z] DELETED | 10.49G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:36:16Z] DELETED | 8.97G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG
|
||||
[2026-04-22T21:36:16Z] DELETED | 4.23G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S02.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:36:17Z] DELETED | 4.97G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S02.1080p.NF.WEB-DL.DDP5.1.Atmos.x264-Telly
|
||||
[2026-04-22T21:36:17Z] DELETED | 5.99G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S03.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:36:18Z] DELETED | 5.26G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S03.1080p.NF.WEBRip.DDP5.1.Atmos.x264-SMURF
|
||||
[2026-04-22T21:36:22Z] DELETED | 4.44G | Love Death and Robots | /media/downloads/sonarr/Love.Death.and.Robots.S04.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX
|
||||
[2026-04-22T21:36:22Z] DELETED | 0.88G | SANDA | /media/downloads/sonarr/SANDA.S01E02.1080p.WEB.H264-SENSEI
|
||||
[2026-04-22T21:36:22Z] DELETED | 0.70G | Senpai Is An Otokonoko | /media/downloads/sonarr/Senpai.Is.An.Otokonoko.S01E05.720p.WEB.H264-SKYANiME
|
||||
[2026-04-22T21:36:22Z] DELETED | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E01.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:36:23Z] DELETED | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E02.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:36:23Z] DELETED | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E03.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:36:23Z] DELETED | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E04.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:36:23Z] DELETED | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E07.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:36:24Z] DELETED | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E08.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:36:24Z] DELETED | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E10.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:36:24Z] DELETED | 1.39G | Senpai is an Otokonoko | /media/downloads/sonarr/Senpai.is.an.Otokonoko.S01E12.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:36:25Z] DELETED | 31.17G | Sex Education | /media/downloads/sonarr/Sex.Education.S01.1080p.NF.WEB.DDP5.1.x264-DEFLATE
|
||||
[2026-04-22T21:36:26Z] DELETED | 41.53G | Sex Education | /media/downloads/sonarr/Sex.Education.S02.1080p.NF.WEB.DDP5.1.x264-NTb
|
||||
[2026-04-22T21:36:26Z] DELETED | 15.56G | Sex Education | /media/downloads/sonarr/Sex.Education.S03.1080p.NF.WEB-DL.DDP5.1.H.264-FLUX
|
||||
[2026-04-22T21:36:27Z] DELETED | 21.49G | Sex Education | /media/downloads/sonarr/Sex.Education.S04.1080p.NF.WEB-DL.DDP5.1.H.264-Archie
|
||||
[2026-04-22T21:36:27Z] DELETED | 1.49G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E02.THE.HERO.OF.MY.DREAMS.1080p.CR.WEB-DL.AAC2.0.H.264.DUAL-VARYG.mkv
|
||||
[2026-04-22T21:36:28Z] DELETED | 1.49G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E03.THE.MAN.WHO.STANDS.AT.THE.TOP.1080p.CR.WEB-DL.AAC2.0.H.264.DUAL-VARYG.mkv
|
||||
[2026-04-22T21:36:28Z] DELETED | 1.48G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E04.CLASH.1080p.CR.WEB-DL.AAC2.0.H.264.DUAL-VARYG.mkv
|
||||
[2026-04-22T21:36:28Z] DELETED | 0.34G | WIND BREAKER | /media/downloads/sonarr/WIND.BREAKER.S01E07.A.Fight.He.Cant.Lose.1080p.B-Global.WEB-DL.JPN.AAC2.0.H.264.MSubs-ToonsHub.mkv
|
||||
[2026-04-22T21:36:29Z] DELETED | 0.26G | Wind Breaker | /media/downloads/sonarr/Wind Breaker - S01E12 - 1080p WEB HEVC -NanDesuKa (B-Global).mkv
|
||||
[2026-04-22T21:36:29Z] DELETED | 1.46G | Wind Breaker 01 ( | /media/downloads/sonarr/[SubsPlease] Wind Breaker - 01 (1080p) [5D5071F6].mkv
|
||||
[2026-04-22T21:36:29Z] DELETED | 1.46G | Wind Breaker 05 ( | /media/downloads/sonarr/[SubsPlease] Wind Breaker - 05 (1080p) [B6649F46].mkv
|
||||
[2026-04-22T21:36:30Z] DELETED | 1.46G | Wind Breaker 06 ( | /media/downloads/sonarr/[SubsPlease] Wind Breaker - 06 (1080p) [1C13E5BC].mkv
|
||||
[2026-04-22T21:36:30Z] DELETED | 0.74G | Wistoria Wand And Sword | /media/downloads/sonarr/Wistoria.Wand.And.Sword.S01E01.720p.WEB.H264-SKYANiME
|
||||
[2026-04-22T21:36:30Z] DELETED | 1.45G | Wistoria Wand and Sword | /media/downloads/sonarr/Wistoria.Wand.and.Sword.S01E02.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:36:31Z] DELETED | 1.44G | Wistoria Wand and Sword | /media/downloads/sonarr/Wistoria.Wand.and.Sword.S01E03.1080p.WEB.H264-KAWAII
|
||||
[2026-04-22T21:36:31Z] DELETED | 0.00G | www UIndex org Severance | /media/downloads/sonarr/www.UIndex.org - Severance S02E10 Cold Harbor 1080p ATVP WEB-DL DDP5 1 Atmos H 264-Kitsune
|
||||
[2026-04-22T21:36:31Z] DONE | deleted=49 | freed=461.6G | failed=0
|
||||
132
docs/runbooks/arr-cleanup/cleanup.py
Normal file
132
docs/runbooks/arr-cleanup/cleanup.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Delete confirmed-safe download entries from /media/downloads/sonarr and /media/downloads/radarr.
|
||||
|
||||
Reads /tmp/arr_verified.json produced by verify.py.
|
||||
Only deletes entries where status == 'safe' (API-confirmed imported + disk path verified).
|
||||
Orphans and path_missing entries are never touched.
|
||||
|
||||
Usage:
|
||||
python3 cleanup.py --dry-run # print what would be deleted
|
||||
python3 cleanup.py --arr sonarr # delete only sonarr downloads
|
||||
python3 cleanup.py --arr radarr # delete only radarr downloads
|
||||
python3 cleanup.py # delete both (prompts for confirmation)
|
||||
|
||||
# Target a single series/movie by title substring:
|
||||
python3 cleanup.py --arr sonarr --title "American Dragon"
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
SSH_HOST = "aya01"
|
||||
SONARR_DL_ROOT = "/media/downloads/sonarr"
|
||||
RADARR_DL_ROOT = "/media/downloads/radarr"
|
||||
VERIFIED_JSON = "/tmp/arr_verified.json"
|
||||
|
||||
|
||||
def ssh_delete(path, dry_run):
|
||||
"""Delete path on remote host. Returns True on success."""
|
||||
if dry_run:
|
||||
print(f" [DRY-RUN] would delete: {path}")
|
||||
return True
|
||||
result = subprocess.run(
|
||||
['ssh', SSH_HOST, f'rm -rf {json.dumps(path)}'],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" ERROR deleting {path}: {result.stderr.strip()}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def ssh_exists(path):
|
||||
r = subprocess.run(['ssh', SSH_HOST, f'[ -e {json.dumps(path)} ] && echo yes || echo no'],
|
||||
capture_output=True, text=True)
|
||||
return r.stdout.strip() == 'yes'
|
||||
|
||||
|
||||
def confirm(prompt):
|
||||
answer = input(f"{prompt} [y/N] ").strip().lower()
|
||||
return answer == 'y'
|
||||
|
||||
|
||||
def process(entries, dl_root, label, dry_run, title_filter, yes=False):
|
||||
safe = [m for m in entries if m['status'] == 'safe']
|
||||
if title_filter:
|
||||
safe = [m for m in safe if title_filter.lower() in m['title'].lower()]
|
||||
|
||||
if not safe:
|
||||
print(f"No safe entries to delete for {label}.")
|
||||
return 0, 0
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"{label} — {len(safe)} entries to delete")
|
||||
print(f"{'='*60}")
|
||||
|
||||
for m in safe:
|
||||
pct = m.get('percentOfEpisodes', '')
|
||||
pct_str = f" [{pct:.0f}%]" if isinstance(pct, float) else ''
|
||||
files = m.get('episodeFileCount', '')
|
||||
total = m.get('totalEpisodeCount', '')
|
||||
count_str = f" ({files}/{total} eps)" if files != '' else f" (hasFile=True)"
|
||||
print(f" {m['title']}{pct_str}{count_str}")
|
||||
print(f" ← {m['dl']}")
|
||||
print(f" → {m['check_path']}")
|
||||
|
||||
if not dry_run and not yes:
|
||||
if not confirm(f"\nDelete {len(safe)} {label} download entries?"):
|
||||
print("Skipped.")
|
||||
return 0, 0
|
||||
|
||||
deleted, failed = 0, 0
|
||||
for m in safe:
|
||||
dl_path = f"{dl_root}/{m['dl']}"
|
||||
# Double-check the series/movie still exists on disk before deleting the download
|
||||
if not dry_run and not ssh_exists(m['check_path']):
|
||||
print(f" SKIP {m['title']}: media path no longer on disk ({m['check_path']})")
|
||||
failed += 1
|
||||
continue
|
||||
ok = ssh_delete(dl_path, dry_run)
|
||||
if ok:
|
||||
deleted += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
print(f"\n{label}: {deleted} deleted, {failed} failed/skipped")
|
||||
return deleted, failed
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--dry-run', action='store_true', help='Print actions without deleting')
|
||||
parser.add_argument('--yes', '-y', action='store_true', help='Skip confirmation prompt')
|
||||
parser.add_argument('--arr', choices=['sonarr', 'radarr', 'both'], default='both')
|
||||
parser.add_argument('--title', default='', help='Only process entries matching this title substring')
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(VERIFIED_JSON) as f:
|
||||
data = json.load(f)
|
||||
|
||||
if args.dry_run:
|
||||
print("DRY-RUN mode — nothing will be deleted\n")
|
||||
|
||||
total_deleted, total_failed = 0, 0
|
||||
|
||||
if args.arr in ('radarr', 'both'):
|
||||
d, f = process(data['radarr_matched'], RADARR_DL_ROOT, 'Radarr', args.dry_run, args.title, args.yes)
|
||||
total_deleted += d
|
||||
total_failed += f
|
||||
|
||||
if args.arr in ('sonarr', 'both'):
|
||||
d, f = process(data['sonarr_matched'], SONARR_DL_ROOT, 'Sonarr', args.dry_run, args.title, args.yes)
|
||||
total_deleted += d
|
||||
total_failed += f
|
||||
|
||||
print(f"\nTotal: {total_deleted} deleted, {total_failed} failed/skipped")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
146
docs/runbooks/arr-cleanup/findings.md
Normal file
146
docs/runbooks/arr-cleanup/findings.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# arr-stack Downloads Cleanup — Investigation Findings
|
||||
|
||||
## Storage Layout (aya01)
|
||||
|
||||
| Device | FS | Size | Used | Mount |
|
||||
|--------|----|------|------|-------|
|
||||
| `/dev/sdc3` | btrfs | 1.9T | 177G (10%) | `/` (system) |
|
||||
| `/dev/sda1` | btrfs `proxmox` | 2.8T | 1.3T (48%) | `/opt` |
|
||||
| `/dev/sdd1` | ext4 | 17T | 15T (92%) | `/mnt/hdd0` |
|
||||
| `/dev/sde1` | ext4 | 17T | 15T (92%) | `/mnt/hdd2` |
|
||||
| `/dev/sdf1` | ext4 | 17T | 15T (92%) | `/mnt/hdd1` |
|
||||
| `mergerfs` | fuse | 49T | 43T (92%) | `/media` |
|
||||
|
||||
`/media` is a mergerfs union of hdd0 + hdd1 + hdd2. All three HDDs were at ~92% capacity before cleanup.
|
||||
|
||||
**After cleanup (2026-04-23):**
|
||||
|
||||
| Device | Used | Avail | Use% |
|
||||
|--------|------|-------|------|
|
||||
| `/dev/sdd1` (hdd0) | 9.4T | 6.2T | 61% |
|
||||
| `/dev/sdf1` (hdd1) | 9.3T | 6.3T | 60% |
|
||||
| `/dev/sde1` (hdd2) | 7.8T | 7.8T | 51% |
|
||||
| `mergerfs /media` | 27T | 21T | 57% |
|
||||
|
||||
**~16T freed total** (92% → 57% on the mergerfs pool).
|
||||
|
||||
## /media Breakdown (before cleanup)
|
||||
|
||||
| Directory | Size |
|
||||
|-----------|------|
|
||||
| `downloads` | **22T** |
|
||||
| `series` | 16T |
|
||||
| `movies` | 5T |
|
||||
|
||||
## Root Cause: No Hardlinks → All Imports Are Copies
|
||||
|
||||
Zero hardlinked files exist anywhere across all three HDDs. Confirmed by inspecting the Kubernetes manifests in `argocd-homelab/services/arr-stack/` and by inode comparison of 1365 download/media file pairs (0 shared inodes found).
|
||||
|
||||
**All three services mount the mergerfs `/media/` path via NFS:**
|
||||
|
||||
```
|
||||
sonarr: NFS 192.168.20.12:/media/downloads → /downloads
|
||||
NFS 192.168.20.12:/media/series → /tv
|
||||
radarr: NFS 192.168.20.12:/media/downloads → /downloads
|
||||
NFS 192.168.20.12:/media/movies → /movies
|
||||
qbit: NFS 192.168.20.12:/media/downloads → /downloads
|
||||
```
|
||||
|
||||
mergerfs does not support hardlinks across underlying filesystems. When qBit downloads to `/media/downloads/sonarr/` (lands on e.g. hdd1) and Sonarr imports to `/media/series/` (lands on e.g. hdd0), the hardlink attempt crosses a physical disk boundary → falls back to copy. Every import doubles the data.
|
||||
|
||||
## Cleanup Performed (2026-04-23)
|
||||
|
||||
Three passes using the scripts in this directory:
|
||||
|
||||
### Pass 1 — Orphans (not in Sonarr at all)
|
||||
Script: `cleanup-orphans.py`
|
||||
|
||||
Deleted 49 entries totalling **461.6G** — downloads with no matching Sonarr series and no series directory on disk. Includes Game of Thrones (all 8 seasons), Sex Education (all 4 seasons), Love Death & Robots (multiple duplicate copies), and various anime episode files.
|
||||
|
||||
111 entries were SKIPPED (series dir found on disk, needs manual review) — includes Bleach, House, Lucifer, You, Detective Conan episodes, What If, etc. See cleanup.log for full list.
|
||||
|
||||
### Pass 2 — Confirmed-imported Sonarr downloads
|
||||
Script: `cleanup.py --arr sonarr`
|
||||
|
||||
Deleted **1106 entries**, 0 failed. These were downloads where Sonarr confirmed `episodeFileCount > 0` AND the series directory was verified to exist on disk at the time of `verify.py` run.
|
||||
|
||||
### Pass 3 — Confirmed-imported Radarr downloads
|
||||
Script: `cleanup.py --arr radarr`
|
||||
|
||||
Deleted **259 entries**, 0 failed. These were downloads where Radarr confirmed `hasFile=True` AND the file/directory path was verified to exist on disk.
|
||||
|
||||
### Totals
|
||||
| Pass | Entries | Space |
|
||||
|------|---------|-------|
|
||||
| Orphans (cleanup-orphans.py) | 49 | ~461G |
|
||||
| Sonarr imports (cleanup.py) | 1106 | ~12T (estimated) |
|
||||
| Radarr imports (cleanup.py) | 259 | ~4T (estimated) |
|
||||
| **Total** | **1414** | **~16T freed** |
|
||||
|
||||
All deletions logged to `cleanup.log` with UTC timestamp, size, title, path, outcome.
|
||||
|
||||
## Verification Results (via API + disk path check)
|
||||
|
||||
API keys stored in `../sonarr.api.env` and `../radarr.api.env`.
|
||||
Access via `kubectl -n arr-stack port-forward svc/sonarr 8989:8989` and `svc/radarr 7878:7878`.
|
||||
|
||||
Container path mappings:
|
||||
- Sonarr `/tv/` → `/media/series/`
|
||||
- Radarr `/movies/` → `/media/movies/`
|
||||
|
||||
| | Safe to delete | Orphans (not in arr) | Keep |
|
||||
|---|---|---|---|
|
||||
| **Radarr** (289 items, ~5.2T) | **265** | 25 | 0 |
|
||||
| **Sonarr** (1439 items, ~17T) | **1106** | 333 | 0 |
|
||||
|
||||
"Safe to delete" = API confirms `hasFile=True` (Radarr) or `episodeFileCount > 0` (Sonarr), AND the reported file/directory path was verified to exist on disk via SSH.
|
||||
|
||||
### Radarr Orphans (25) — not matched in Radarr, not deleted
|
||||
- Constantine (2005)
|
||||
- Cowboy Bebop: Knockin' on Heaven's Door (2001)
|
||||
- Les Misérables (2012)
|
||||
- Pokémon Detective Pikachu (2019)
|
||||
- Code Geass: Fukkatsu no Lelouch (2019)
|
||||
- Eiga Go-Toubun no Hanayome (2022)
|
||||
- Gisaengchung / Parasite (Korean title — matching failure)
|
||||
- Dune: Part One (2021) — matching failure, is in Radarr
|
||||
- Harry Potter (older/duplicate copies — matching failure)
|
||||
- Porco Rosso / Kurenai no buta — matching failure
|
||||
- Castle in the Sky / Laputa — matching failure
|
||||
- Steins;Gate: The Movie — matching failure
|
||||
- Project Silence / Talchul — matching failure
|
||||
- Digimon: Frontier & Savers films
|
||||
- One Piece films (several)
|
||||
- Paripi Koumei movie
|
||||
- Fantastic Four (2025) extra copies (3)
|
||||
- JJK DCP trailer file
|
||||
|
||||
### 6 Radarr "path mismatch" entries (all confirmed safe, deleted)
|
||||
Flagged due to path comparison artifacts, manually verified on disk:
|
||||
- Star Wars Episode IV/V/VI/IX — each is a separate Radarr entry; all directories exist
|
||||
- WALL·E — `·` middle-dot character caused comparison failure; file exists
|
||||
|
||||
## Pending Decisions
|
||||
|
||||
### Bleach USBD Remux TL (1.8T)
|
||||
`/media/downloads/sonarr/Bleach USBD Remux TL` — full lossless Bluray remux S00–S16 (-ZR- group).
|
||||
Currently in SKIPPED (series dir `/media/series/Bleach (2004) {imdb-tt0434665}/` exists, 310G imported).
|
||||
Most seasons were imported from x265 Bluray packs (-iVy group) rather than from this remux.
|
||||
S11 has no imported content at all. S13, S14 partially imported.
|
||||
Decision: keep (for quality imports once disk freed) or delete (free 1.8T, accept x265 quality).
|
||||
See memory file for full per-season breakdown.
|
||||
|
||||
### SKIPPED downloads (111 Sonarr entries)
|
||||
Downloads where the series directory exists on disk but the series is not currently in Sonarr.
|
||||
Likely removed series (House, Lucifer, You, Black Clover, etc.) or ongoing shows with stale episodes.
|
||||
These need manual review — series may have been intentionally removed from Sonarr.
|
||||
|
||||
## Fix (not applied — future reference)
|
||||
Mount per-HDD NFS paths instead of the mergerfs path, so downloads and media share the same physical filesystem and hardlinks work:
|
||||
```yaml
|
||||
# sonarr/radarr/qtun deployments — change NFS path from:
|
||||
path: /media/downloads → path: /mnt/hdd0/downloads
|
||||
path: /media/series → path: /mnt/hdd0/series
|
||||
path: /media/movies → path: /mnt/hdd0/movies
|
||||
```
|
||||
Jellyfin/Plex continue reading from `/media/` (mergerfs). New imports hardlink within hdd0.
|
||||
246
docs/runbooks/arr-cleanup/verify.py
Normal file
246
docs/runbooks/arr-cleanup/verify.py
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cross-reference /media/downloads/sonarr and /media/downloads/radarr against
|
||||
the Sonarr/Radarr APIs, then verify reported file paths actually exist on disk.
|
||||
|
||||
Requirements:
|
||||
- kubectl port-forwards active:
|
||||
kubectl -n arr-stack port-forward svc/sonarr 8989:8989
|
||||
kubectl -n arr-stack port-forward svc/radarr 7878:7878
|
||||
- SSH access to aya01
|
||||
- API keys in ../sonarr.api.env and ../radarr.api.env
|
||||
|
||||
Output:
|
||||
/tmp/arr_verified.json — full structured results for use by cleanup.py
|
||||
"""
|
||||
|
||||
import urllib.request
|
||||
import json
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
|
||||
SONARR_URL = "http://localhost:8989/api/v3"
|
||||
RADARR_URL = "http://localhost:7878/api/v3"
|
||||
SSH_HOST = "aya01"
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def load_key(filename):
|
||||
path = os.path.join(script_dir, '..', filename)
|
||||
return open(path).read().strip()
|
||||
|
||||
SONARR_KEY = load_key('sonarr.api.env')
|
||||
RADARR_KEY = load_key('radarr.api.env')
|
||||
|
||||
|
||||
def api_get(url):
|
||||
with urllib.request.urlopen(url, timeout=30) as r:
|
||||
return json.load(r)
|
||||
|
||||
|
||||
def norm(s):
|
||||
return re.sub(r'[^a-z0-9]', '', s.lower())
|
||||
|
||||
|
||||
def extract_title(name, is_movie):
|
||||
"""Strip release tags from a download name to recover a bare title."""
|
||||
name = re.sub(r'\.(mkv|mp4|avi|m4v)$', '', name, flags=re.IGNORECASE)
|
||||
name = re.sub(r'\[.*?\]', '', name)
|
||||
if is_movie:
|
||||
name = re.sub(r'[\.\s_\-]?(19|20)\d{2}.*$', '', name)
|
||||
else:
|
||||
name = re.sub(r'[\.\s_\-]?[Ss]\d{1,2}([Ee]\d{1,2})?.*$', '', name)
|
||||
return re.sub(r'[\.\-_]+', ' ', name).strip()
|
||||
|
||||
|
||||
def build_index(records, key_fn):
|
||||
idx = {}
|
||||
for rec in records:
|
||||
for k in key_fn(rec):
|
||||
if k:
|
||||
idx[k] = rec
|
||||
return idx
|
||||
|
||||
|
||||
def find_match(dl_name, idx, is_movie):
|
||||
title = extract_title(dl_name, is_movie)
|
||||
tn = norm(title)
|
||||
if tn in idx:
|
||||
return idx[tn]
|
||||
for k, rec in idx.items():
|
||||
if k and len(k) > 5 and (tn.startswith(k) or k.startswith(tn)):
|
||||
return rec
|
||||
return None
|
||||
|
||||
|
||||
def ssh_check_paths(paths):
|
||||
"""Return (existing, missing) sets for the given list of paths."""
|
||||
if not paths:
|
||||
return set(), set()
|
||||
cmds = '\n'.join(
|
||||
f'[ -e {json.dumps(p)} ] && echo "EXISTS:{p}" || echo "MISSING:{p}"'
|
||||
for p in paths
|
||||
)
|
||||
r = subprocess.run(['ssh', SSH_HOST, 'bash', '-s'],
|
||||
input=cmds, capture_output=True, text=True)
|
||||
existing, missing = set(), set()
|
||||
for line in r.stdout.splitlines():
|
||||
if line.startswith('EXISTS:'):
|
||||
existing.add(line[7:])
|
||||
elif line.startswith('MISSING:'):
|
||||
missing.add(line[8:])
|
||||
return existing, missing
|
||||
|
||||
|
||||
def main():
|
||||
print("Fetching Radarr movies...")
|
||||
radarr_movies = api_get(f"{RADARR_URL}/movie?apikey={RADARR_KEY}")
|
||||
print(f" {len(radarr_movies)} movies")
|
||||
|
||||
print("Fetching Sonarr series...")
|
||||
sonarr_series = api_get(f"{SONARR_URL}/series?apikey={SONARR_KEY}")
|
||||
print(f" {len(sonarr_series)} series")
|
||||
|
||||
# Radarr index
|
||||
def radarr_keys(m):
|
||||
return [norm(m['title']), norm(f"{m['title']}{m.get('year','')}")]
|
||||
|
||||
radarr_idx = build_index(radarr_movies, radarr_keys)
|
||||
|
||||
# Enrich radarr records with disk path
|
||||
for m in radarr_movies:
|
||||
mf = m.get('movieFile')
|
||||
m['_file_path'] = (
|
||||
mf['path'].replace('/movies/', '/media/movies/', 1) if mf and mf.get('path') else None
|
||||
)
|
||||
m['_dir_path'] = m.get('path', '').replace('/movies/', '/media/movies/', 1)
|
||||
|
||||
# Sonarr index
|
||||
def sonarr_keys(s):
|
||||
return [norm(s['title'])]
|
||||
|
||||
sonarr_idx = build_index(sonarr_series, sonarr_keys)
|
||||
|
||||
for s in sonarr_series:
|
||||
s['_dir_path'] = s.get('path', '').replace('/tv/', '/media/series/', 1)
|
||||
|
||||
# Download listings
|
||||
print(f"\nFetching download listings from {SSH_HOST}...")
|
||||
r = subprocess.run(
|
||||
['ssh', SSH_HOST, 'ls /media/downloads/sonarr/ && echo "===RADARR===" && ls /media/downloads/radarr/'],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
parts = r.stdout.split('===RADARR===\n')
|
||||
sonarr_dls = [l.strip() for l in parts[0].splitlines() if l.strip()]
|
||||
radarr_dls = [l.strip() for l in parts[1].splitlines() if l.strip()]
|
||||
print(f" Sonarr downloads: {len(sonarr_dls)}")
|
||||
print(f" Radarr downloads: {len(radarr_dls)}")
|
||||
|
||||
# Match and collect paths
|
||||
radarr_matched, radarr_orphans = [], []
|
||||
for dl in radarr_dls:
|
||||
rec = find_match(dl, radarr_idx, is_movie=True)
|
||||
if rec is None:
|
||||
radarr_orphans.append(dl)
|
||||
else:
|
||||
check_path = rec['_file_path'] or rec['_dir_path']
|
||||
radarr_matched.append({
|
||||
'dl': dl,
|
||||
'title': rec['title'],
|
||||
'year': rec.get('year'),
|
||||
'hasFile': rec.get('hasFile', False),
|
||||
'monitored': rec.get('monitored'),
|
||||
'check_path': check_path,
|
||||
})
|
||||
|
||||
sonarr_matched, sonarr_orphans = [], []
|
||||
for dl in sonarr_dls:
|
||||
rec = find_match(dl, sonarr_idx, is_movie=False)
|
||||
if rec is None:
|
||||
sonarr_orphans.append(dl)
|
||||
else:
|
||||
stats = rec.get('statistics', {})
|
||||
sonarr_matched.append({
|
||||
'dl': dl,
|
||||
'title': rec['title'],
|
||||
'episodeFileCount': stats.get('episodeFileCount', 0),
|
||||
'totalEpisodeCount': stats.get('totalEpisodeCount', 0),
|
||||
'percentOfEpisodes': stats.get('percentOfEpisodes', 0),
|
||||
'monitored': rec.get('monitored'),
|
||||
'status': rec.get('status'),
|
||||
'check_path': rec['_dir_path'],
|
||||
})
|
||||
|
||||
# Batch disk verification
|
||||
all_paths = list(set(
|
||||
[m['check_path'] for m in radarr_matched if m['check_path']] +
|
||||
[m['check_path'] for m in sonarr_matched if m['check_path']]
|
||||
))
|
||||
print(f"\nVerifying {len(all_paths)} paths on disk...")
|
||||
existing, missing = ssh_check_paths(all_paths)
|
||||
print(f" {len(existing)} exist, {len(missing)} missing")
|
||||
|
||||
# Classify
|
||||
def classify_radarr(m):
|
||||
if not m['hasFile'] or not m['check_path']:
|
||||
return 'not_imported'
|
||||
if m['check_path'] in existing:
|
||||
return 'safe'
|
||||
return 'path_missing'
|
||||
|
||||
def classify_sonarr(m):
|
||||
if m['episodeFileCount'] == 0 or not m['check_path']:
|
||||
return 'not_imported'
|
||||
if m['check_path'] in existing:
|
||||
return 'safe'
|
||||
return 'path_missing'
|
||||
|
||||
for m in radarr_matched:
|
||||
m['status'] = classify_radarr(m)
|
||||
for m in sonarr_matched:
|
||||
m['status'] = classify_sonarr(m)
|
||||
|
||||
result = {
|
||||
'radarr_matched': radarr_matched,
|
||||
'radarr_orphans': radarr_orphans,
|
||||
'sonarr_matched': sonarr_matched,
|
||||
'sonarr_orphans': sonarr_orphans,
|
||||
'existing_paths': list(existing),
|
||||
'missing_paths': list(missing),
|
||||
}
|
||||
|
||||
out_path = '/tmp/arr_verified.json'
|
||||
with open(out_path, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
print(f"\nResults written to {out_path}")
|
||||
|
||||
# Summary
|
||||
r_safe = [m for m in radarr_matched if m['status'] == 'safe']
|
||||
r_miss = [m for m in radarr_matched if m['status'] == 'path_missing']
|
||||
r_noimp = [m for m in radarr_matched if m['status'] == 'not_imported']
|
||||
s_safe = [m for m in sonarr_matched if m['status'] == 'safe']
|
||||
s_miss = [m for m in sonarr_matched if m['status'] == 'path_missing']
|
||||
s_noimp = [m for m in sonarr_matched if m['status'] == 'not_imported']
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("SUMMARY")
|
||||
print("="*60)
|
||||
print(f"Radarr: {len(r_safe)} safe | {len(r_miss)} path missing | {len(r_noimp)} not imported | {len(radarr_orphans)} orphans")
|
||||
print(f"Sonarr: {len(s_safe)} safe | {len(s_miss)} path missing | {len(s_noimp)} not imported | {len(sonarr_orphans)} orphans")
|
||||
|
||||
if r_miss:
|
||||
print("\nRadarr path_missing (review manually):")
|
||||
for m in r_miss:
|
||||
print(f" {m['title']} → {m['check_path']}")
|
||||
print(f" DL: {m['dl']}")
|
||||
if s_miss:
|
||||
print("\nSonarr path_missing (review manually):")
|
||||
for m in s_miss:
|
||||
print(f" {m['title']} → {m['check_path']}")
|
||||
print(f" DL: {m['dl']}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user