Homelab Academy / Modules
Guided practice + break/fix + lab gate.
Module: Sonarr & Radarr
Activity type: Lab (Practice)
Lab risk: low
Objective: Explain the difference between a movie folder format and a movie file format in Radarr.
All files created or modified by this lab remain under /opt/lab-classroom/class48/. The lab does not connect to Radarr or rename a production media file.
A synthetic movie library contains inconsistent folder names, weak file names, sidecar files, duplicate-title movies from different years, and one edition. You will build an inventory and calculate deterministic destination paths from an authoritative fixture manifest.
1
Create the isolated fixture library
Run the following command. It creates small text fixtures with media-like extensions; it does not create or copy real video content.
python3 - <<'PY'
from pathlib import Path
import json
root = Path('/opt/lab-classroom/class48')
assert str(root) == '/opt/lab-classroom/class48'
library = root / 'library'
reports = root / 'reports'
backups = root / 'backups'
for path in (library, reports, backups):
path.mkdir(parents=True, exist_ok=True)
fixtures = [
{
'source': 'Arrival (2016)/Arrival.2016.mkv',
'title': 'Arrival', 'year': 2016, 'tmdb_id': 329865,
'quality': 'Bluray-1080p', 'edition': ''
},
{
'source': 'Dune 2021/Dune.2021.1080p.mkv',
'title': 'Dune', 'year': 2021, 'tmdb_id': 438631,
'quality': 'WEBDL-1080p', 'edition': ''
},
{
'source': 'The Thing/thing_final.mkv',
'title': 'The Thing', 'year': 1982, 'tmdb_id': 1091,
'quality': 'Bluray-1080p', 'edition': ''
},
{
'source': 'Blade Runner 1982/BR-final-cut.mkv',
'title': 'Blade Runner', 'year': 1982, 'tmdb_id': 78,
'quality': 'Bluray-1080p', 'edition': "Final Cut"
},
{
'source': 'Crash 1996/Crash.mkv',
'title': 'Crash', 'year': 1996, 'tmdb_id': 884,
'quality': 'DVD', 'edition': ''
},
{
'source': 'Crash 2004/Crash.mp4',
'title': 'Crash', 'year': 2004, 'tmdb_id': 1640,
'quality': 'WEBDL-1080p', 'edition': ''
}
]
for item in fixtures:
target = library / item['source']
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text('synthetic class 48 fixture\n', encoding='utf-8')
(library / 'Arrival (2016)' / 'Arrival.2016.en.srt').write_text('1\n00:00:00,000 --> 00:00:01,000\nSynthetic subtitle\n', encoding='utf-8')
(library / 'Dune 2021' / 'poster.jpg').write_text('synthetic artwork fixture\n', encoding='utf-8')
(root / 'manifest.json').write_text(json.dumps(fixtures, indent=2), encoding='utf-8')
print(root)
print(f'Fixture records: {len(fixtures)}')
PY
2
Inventory the synthetic library
Review every fixture path before proposing a change.
find /opt/lab-classroom/class48/library -type f -printf '%P\n' | sort
3
Create a dry-run naming audit
Create an audit program that derives proposed paths from manifest metadata, checks source existence, validates video extensions, examines Unicode normalization, and detects destination collisions.
cat > /opt/lab-classroom/class48/audit_naming.py <<'PY'
from pathlib import Path
import csv
import json
import re
import unicodedata
ROOT = Path('/opt/lab-classroom/class48')
if str(ROOT) != '/opt/lab-classroom/class48':
raise SystemExit('Unexpected lab root')
LIBRARY = ROOT / 'library'
REPORT = ROOT / 'reports' / 'naming-audit.csv'
SUMMARY = ROOT / 'reports' / 'summary.txt'
VIDEO_EXTENSIONS = {'.mkv', '.mp4', '.m4v'}
def clean_component(value):
value = unicodedata.normalize('NFC', value)
value = re.sub(r'[\x00-\x1f]', '', value)
value = value.replace('/', ' - ')
return value.strip().rstrip('.')
records = json.loads((ROOT / 'manifest.json').read_text(encoding='utf-8'))
rows = []
proposed_paths = {}
for record in records:
source_relative = Path(record['source'])
source = LIBRARY / source_relative
title = clean_component(record['title'])
year = int(record['year'])
tmdb_id = int(record['tmdb_id'])
quality = clean_component(record['quality'])
edition = clean_component(record.get('edition', ''))
folder = f'{title} ({year}) [tmdb-{tmdb_id}]'
edition_text = f' - {edition}' if edition else ''
filename = f'{title} ({year}){edition_text} - {quality}{source.suffix.lower()}'
proposed = Path(folder) / filename
warnings = []
if not source.exists():
warnings.append('SOURCE_MISSING')
if source.suffix.lower() not in VIDEO_EXTENSIONS:
warnings.append('UNEXPECTED_VIDEO_EXTENSION')
if str(source_relative) != unicodedata.normalize('NFC', str(source_relative)):
warnings.append('SOURCE_NOT_NFC')
key = unicodedata.normalize('NFC', str(proposed)).casefold()
proposed_paths.setdefault(key, []).append(str(source_relative))
rows.append({
'source': str(source_relative),
'proposed': str(proposed),
'status': 'WARN' if warnings else 'OK',
'notes': ';'.join(warnings)
})
collisions = {key: value for key, value in proposed_paths.items() if len(value) > 1}
for row in rows:
key = unicodedata.normalize('NFC', row['proposed']).casefold()
if key in collisions:
row['status'] = 'BLOCK'
row['notes'] = ';'.join(filter(None, [row['notes'], 'DESTINATION_COLLISION']))
REPORT.parent.mkdir(parents=True, exist_ok=True)
with REPORT.open('w', newline='', encoding='utf-8') as handle:
writer = csv.DictWriter(handle, fieldnames=['source', 'proposed', 'status', 'notes'])
writer.writeheader()
writer.writerows(rows)
summary_lines = [
f'Records: {len(rows)}',
f'OK: {sum(row["status"] == "OK" for row in rows)}',
f'WARN: {sum(row["status"] == "WARN" for row in rows)}',
f'BLOCK: {sum(row["status"] == "BLOCK" for row in rows)}',
f'Collisions: {len(collisions)}',
'No media paths were renamed.'
]
SUMMARY.write_text('\n'.join(summary_lines) + '\n', encoding='utf-8')
print('\n'.join(summary_lines))
PY
python3 /opt/lab-classroom/class48/audit_naming.py
4
Review the complete path mapping
Inspect every source and proposed destination. Treat any BLOCK row as a stop condition for a real migration.
python3 - <<'PY'
from pathlib import Path
import csv
report = Path('/opt/lab-classroom/class48/reports/naming-audit.csv')
with report.open(encoding='utf-8', newline='') as handle:
for row in csv.DictReader(handle):
print(f"{row['status']:5} | {row['source']} -> {row['proposed']} | {row['notes']}")
PY
5
Identify unmanaged sidecars
Compare the complete filesystem inventory with the manifest's primary movie files. Sidecars are not automatically bad, but a cleanup plan must account for them.
python3 - <<'PY'
from pathlib import Path
import json
root = Path('/opt/lab-classroom/class48')
library = root / 'library'
managed = {item['source'] for item in json.loads((root / 'manifest.json').read_text(encoding='utf-8'))}
all_files = {str(path.relative_to(library)) for path in library.rglob('*') if path.is_file()}
for path in sorted(all_files - managed):
print(f'SIDECAR | {path}')
PY
6
Translate the audit into a Radarr change plan
In a real Radarr instance, compare the policy with the live naming preview. Confirm token support, inspect unmapped files, correct metadata, select one low-impact movie as a pilot, and use Radarr's supported organize or rename workflow. Do not apply production changes as part of this lab.
test -f /opt/lab-classroom/class48/reports/naming-audit.csv && echo PASS and confirm that PASS is printed.cat /opt/lab-classroom/class48/reports/summary.txt and confirm that it ends with No media paths were renamed.grep -c '^' /opt/lab-classroom/class48/reports/naming-audit.csv and confirm that the result is 7: one header and six data rows.grep -F 'Blade Runner (1982) - Final Cut - Bluray-1080p.mkv' /opt/lab-classroom/class48/reports/naming-audit.csv and confirm that a matching row is returned.grep -F '[tmdb-884]' /opt/lab-classroom/class48/reports/naming-audit.csv and grep -F '[tmdb-1640]' /opt/lab-classroom/class48/reports/naming-audit.csv; confirm that both Crash records have distinct identifiers.find /opt/lab-classroom/class48/library -type f -printf '%P\n' | sort and confirm that original fixture names remain unchanged.| Symptom | Likely cause | Fix |
|---|---|---|
| The setup command reports a permission error. | The current account cannot create or write within /opt/lab-classroom/class48/. | Have the lab administrator provision /opt/lab-classroom/class48/ for the student account, then rerun the setup. Do not redirect the exercise to a production media root. |
| The audit reports SOURCE_MISSING. | A fixture path was renamed, the manifest was edited, or setup did not finish. | Compare manifest.json with the library inventory and rerun the fixture setup to restore the expected synthetic source paths. |
| The report contains DESTINATION_COLLISION. | Two records normalize to the same case-insensitive proposed path, or their identity fields were duplicated. | Stop the migration plan. Verify metadata identifiers, years, editions, and template fields. Do not resolve the condition by allowing one file to replace another. |
| Radarr's naming preview does not recognize a token from an external guide. | The guide targets another Radarr release, the token was entered incorrectly, or that field is unavailable in the current naming context. | Use the token selector and preview presented by the installed Radarr version. Remove unsupported fields and test the revised format before saving. |
| A movie is renamed cleanly but now represents the wrong title. | The media file was associated with an incorrect Radarr movie record before naming was applied. | Correct the movie match and metadata association first. Reinspect the path preview before organizing the file again. |
| Subtitles no longer load after a production rename. | The subtitle base name no longer matches the renamed video, or the subtitle application retained the old path. | Review sidecar handling before further renames, restore the recorded path mapping if necessary, and rename or reimport subtitles using a controlled workflow. |
| A media server shows both old and new library entries. | Its database has not reconciled the path change, or both an old root and a new root are being scanned. | Confirm that only the intended library roots are configured, initiate the media server's supported library scan, and inspect unavailable entries before removing records. |
| Names that look identical are treated as different paths. | The strings use different Unicode normalization forms or visually similar characters. | Use the audit's normalized comparison, inspect exact metadata values, and test the proposed names on the actual destination filesystem before a production migration. |
Run Radarr with a dedicated service identity rather than an interactive administrator identity.
Grant the service only the access required for configured download and media paths.
Do not expose Radarr directly to an untrusted network; place remote access behind an authenticated and maintained access layer.
Protect API keys because they permit automation clients to query or change Radarr.
Limit write access to naming reports and migration maps because they disclose library structure.
Treat imported subtitle and metadata files as untrusted content even when they are not executable media.
Keep backups of the Radarr application data and database separate from the movie library.
Export or record the current naming settings before changing them.
Review unmapped files, duplicate movie records, editions, and metadata mismatches before bulk organization.
Use a small pilot containing a noncritical movie and its sidecars.
Pause competing import or post-processing jobs during an approved production rename window.
Preserve a source-to-destination mapping and verify downstream applications before expanding the change.
Library paths can reveal viewing interests and household information. Restrict access to audit reports, screenshots, logs, and support bundles.
The audit itself does not rename fixture media, so no media rollback is required.
To preserve an earlier report before rerunning the audit, move it within the lab workspace using mv /opt/lab-classroom/class48/reports/naming-audit.csv /opt/lab-classroom/class48/backups/naming-audit-before-rerun.csv.
Rerun the fixture setup if a synthetic source file was accidentally changed; all resulting writes remain under /opt/lab-classroom/class48/.
Stop after the pilot if any title match, edition, subtitle, link, permission, or media-server verification fails.
Use the recorded source-to-destination mapping to reverse only the affected paths through an approved application workflow.
Restore the previous Radarr naming formats from the recorded configuration.
Verify Radarr's movie path, the physical path, sidecars, playback, and media-server indexing after reversal.
Restoring the old naming template alone does not automatically reverse names that were already applied.