processes/ -> playbooks/ (git mv preserves history; updated cross-refs in ROADMAP, README, subtitles playbook + scripts). playbooks/import-media/README.md v1.0 — 7-step import workflow: stage on onyx -> rsync to nullstone -> chmod -> verify scan -> Items/Counts bump -> optional subtitle pass -> run-log Cross-references docs/05/07/08, ADMIN-GUIDE, README. Mirrors the existing subtitles playbook structure (CHANGELOG + runs/_template). CHANGELOG v1.0 lists known gaps (bin/cleanup-import.sh and bin/normalize.py still doc-only, ROADMAP M6). First run logged: playbooks/import-media/runs/lilo-stitch-2002.md. Lilo & Stitch (2002) imported to /home/user/media/movies/, item c2f4aff133c1b9631500fadf293b0b2f, TMDb 11544, MovieCount 3 -> 4. LibraryMonitor didn't auto-fire — needed manual /Library/Refresh; playbook updated to make this an unconditional step. Source: 1080p BluRay HEVC 10-bit / EAC3 5.1 / 2x PGS embedded subs. Per quality bar (README.md:41) — passes.
229 lines
7.8 KiB
Markdown
229 lines
7.8 KiB
Markdown
# Import Media Playbook
|
||
|
||
> Repeatable workflow for adding a new movie or TV show to ARRFLIX.
|
||
> Mirror the format of `playbooks/subtitles/README.md` (CHANGELOG-driven, runs/ folder for per-import logs).
|
||
|
||
Version: **v1.0** (2026-05-10)
|
||
|
||
---
|
||
|
||
## TL;DR — five-step import
|
||
|
||
1. **Stage** on onyx — rename to canonical form, cleanup junk
|
||
2. **rsync** to nullstone `/home/user/media/<library>/<Title> (<Year>)/`
|
||
3. **Permissions** — `chown user:user`, files `644`, dirs `755`
|
||
4. **Verify scan** — `LibraryMonitor` auto-refreshes ~1–3 s after copy; `Items/Counts` should bump
|
||
5. **Subtitles** (if needed) — follow `playbooks/subtitles/`
|
||
|
||
If anything goes wrong, **don't delete the source download** until the new content is confirmed playing in the app (`ADMIN-GUIDE.md:74`).
|
||
|
||
---
|
||
|
||
## Pre-flight checklist
|
||
|
||
Before importing:
|
||
|
||
- [ ] Source file is canonical quality (4K source-permitting, AI-upscale otherwise — no 480p filler, no junk encodes per `README.md:41`)
|
||
- [ ] Title and year confirmed (TMDb / IMDb)
|
||
- [ ] No copy already in library — check via `curl -s -H "X-Emby-Token: $TOK" 'https://arrflix.s8n.ru/Items?Recursive=true&IncludeItemTypes=Movie&searchTerm=<title>'`
|
||
- [ ] Container running healthy: `ssh user@192.168.0.100 'docker ps --filter name=jellyfin --format "{{.Names}} {{.Status}}"'`
|
||
|
||
---
|
||
|
||
## Step 1 — Stage on onyx
|
||
|
||
Source land in `/home/admin/Downloads/<release-name>/`. Stage to a clean dir before pushing.
|
||
|
||
### Movie
|
||
|
||
```bash
|
||
SRC="/home/admin/Downloads/<Original Release Name>/<file>.mkv"
|
||
DEST_DIR="/home/admin/staging-jelly/<Title> (<Year>)"
|
||
mkdir -p "$DEST_DIR"
|
||
cp "$SRC" "$DEST_DIR/<Title> (<Year>).mkv"
|
||
ls -la "$DEST_DIR"
|
||
```
|
||
|
||
### TV (single season)
|
||
|
||
```bash
|
||
SRC="/home/admin/Downloads/<Series Release>"
|
||
DEST_DIR="/home/admin/staging-jelly/<Series> (<Year>)/Season <NN>"
|
||
mkdir -p "$DEST_DIR"
|
||
# rename per docs/08 — <Series> (<Year>) - SNNEMM - <Title>.mkv
|
||
for ep in "$SRC"/*.mkv; do
|
||
cp "$ep" "$DEST_DIR/<Series> (<Year>) - S<NN>E<MM> - <Episode Title>.mkv"
|
||
done
|
||
```
|
||
|
||
### Filename rules (canonical, see `docs/05` + `docs/08`)
|
||
|
||
- Year in `()` mandatory, even when unique
|
||
- Forbidden chars in path: `< > : " / \ | ? *`
|
||
- Strip group tags: `[YIFY]`, `[RARBG]`, `[FS99 Joy]`, `-FQM`, `-AMIABLE`, etc.
|
||
- Strip resolution/codec/source/audio tokens: `1080p`, `2160p`, `BluRay`, `WEB-DL`, `x265`, `HEVC`, `10bit`, `AAC`, `DTS`, `EAC3`, `5.1`, `H264`
|
||
- Strip language tokens from video file (`.eng`, `.pl`, etc. — those go on subtitle sidecars only)
|
||
- Lowercase extension
|
||
- Movies: `<Title> (<Year>).<ext>`
|
||
- TV: `<Series> (<Year>) - S<NN>E<MM> - <Episode Title>.<ext>`, episodes inside `Season <NN>/`
|
||
|
||
### Cleanup junk
|
||
|
||
If source dir has extra files (NFO, JPG, sample, torrent, RARBG.txt, etc.) — staging copy should include ONLY the media file. NFOs that pin TMDb/IMDb ID are exception (see `docs/07`).
|
||
|
||
`bin/cleanup-import.sh` and `bin/normalize.py` are documented in `docs/07` and `docs/08` but not yet extracted as runnable scripts (ROADMAP M6). For now, manual cleanup per the rules above.
|
||
|
||
---
|
||
|
||
## Step 2 — rsync to nullstone
|
||
|
||
### Movie
|
||
|
||
```bash
|
||
rsync -a --info=progress2 --no-owner --no-group \
|
||
"/home/admin/staging-jelly/<Title> (<Year>)" \
|
||
user@192.168.0.100:/home/user/media/movies/
|
||
```
|
||
|
||
### TV (one season)
|
||
|
||
```bash
|
||
rsync -a --info=progress2 --no-owner --no-group \
|
||
"/home/admin/staging-jelly/<Series> (<Year>)/Season <NN>" \
|
||
'user@192.168.0.100:/home/user/media/tv/<Series> (<Year>)/'
|
||
```
|
||
|
||
`scp -r` works too but `rsync -a` preserves perms-by-mode + resumes on interrupt.
|
||
|
||
---
|
||
|
||
## Step 3 — Permissions on nullstone
|
||
|
||
Files must be `user:user` 644 (per live audit, `stat -c '%U:%G %a'`):
|
||
|
||
```bash
|
||
ssh user@192.168.0.100 '
|
||
cd "/home/user/media/movies/<Title> (<Year>)"
|
||
find . -type d -exec chmod 755 {} \;
|
||
find . -type f -exec chmod 644 {} \;
|
||
ls -la
|
||
'
|
||
```
|
||
|
||
`rsync` over ssh as `user@` already lands files as `user:user`. The chmod pass is for safety (`mv` from another path may differ).
|
||
|
||
---
|
||
|
||
## Step 4 — Verify scan picked up
|
||
|
||
Jellyfin's `LibraryMonitor` watches the bind mount and auto-refreshes within ~1–3 s of file landing. Confirm in logs:
|
||
|
||
```bash
|
||
ssh user@192.168.0.100 'docker logs jellyfin --tail 20 | grep -E "LibraryMonitor.*<Title>|<Title>.*refreshed"'
|
||
```
|
||
|
||
Expect: `LibraryMonitor: <Title> ... will be refreshed.`
|
||
|
||
If LibraryMonitor doesn't fire (rare), force a manual refresh:
|
||
|
||
```bash
|
||
TOK=<your-admin-token>
|
||
ssh user@192.168.0.100 "docker exec jellyfin curl -s -X POST -H 'X-Emby-Token: $TOK' http://127.0.0.1:8096/Library/Refresh"
|
||
```
|
||
|
||
---
|
||
|
||
## Step 5 — Verify count bump + metadata + playback
|
||
|
||
```bash
|
||
TOK=<token>
|
||
ssh user@192.168.0.100 "docker exec jellyfin curl -s -H 'X-Emby-Token: $TOK' http://127.0.0.1:8096/Items/Counts" | python3 -m json.tool
|
||
```
|
||
|
||
`MovieCount` (or `EpisodeCount`) should be `+1` (or `+N`).
|
||
|
||
Find the new item ID:
|
||
|
||
```bash
|
||
ssh user@192.168.0.100 "docker exec jellyfin curl -s -H 'X-Emby-Token: $TOK' 'http://127.0.0.1:8096/Items?Recursive=true&IncludeItemTypes=Movie&searchTerm=<Title>'" | python3 -c "import sys,json;[print(i['Id'],i['Name']) for i in json.load(sys.stdin)['Items']]"
|
||
```
|
||
|
||
Probe codec/streams (sanity check no transcode surprise):
|
||
|
||
```bash
|
||
ssh user@192.168.0.100 'docker exec jellyfin /usr/lib/jellyfin-ffmpeg/ffprobe -hide_banner "/media/movies/<Title> (<Year>)/<Title> (<Year>).mkv" 2>&1 | grep -E "Stream|Duration"'
|
||
```
|
||
|
||
Open in browser → confirm artwork + title + duration look right.
|
||
|
||
If TMDb match is wrong (rare for canonical titles), force the right one by adding `[tmdbid-NNNNN]` to the folder name and re-scanning. See `docs/05:54-56`.
|
||
|
||
---
|
||
|
||
## Step 6 — Subtitles (optional, see playbook)
|
||
|
||
If the user wants English subs and source doesn't have them embedded:
|
||
- Follow `playbooks/subtitles/README.md`
|
||
- Drop `.eng.srt` next to the mkv
|
||
- After dropping subs: `POST /Items/{id}/Refresh?MetadataRefreshMode=ValidationOnly&Recursive=true`
|
||
|
||
---
|
||
|
||
## Step 7 — Document the run
|
||
|
||
Copy `runs/_template.md` to `runs/<title-slug>.md` and record:
|
||
- Source provenance (laptop path, release name, hash if you bothered)
|
||
- Target nullstone path
|
||
- Item ID
|
||
- Counts before/after
|
||
- Codec/stream summary
|
||
- Subtitle status
|
||
|
||
Commit + push: see `testing/DEPLOY.md` for git workflow.
|
||
|
||
---
|
||
|
||
## Verification checklist (from `docs/05:1082-1099`)
|
||
|
||
- [ ] Folder matches `<Title> (<Year>)` (movies) or `<Series> (<Year>)/Season <NN>` (TV)
|
||
- [ ] Filename matches canonical pattern (no group tags, no codec/resolution tokens)
|
||
- [ ] Year present, four digits, in parentheses
|
||
- [ ] No forbidden chars `< > : " / \ | ? *`
|
||
- [ ] Folder + filename match exactly (basename equals folder name for movies)
|
||
- [ ] One folder per item — no shared folders
|
||
- [ ] NFO present iff TMDb override needed (`[tmdbid-NNNN]` token)
|
||
- [ ] Subtitle sidecars correctly named (`.eng.srt`, not `.en.srt` or stripped)
|
||
- [ ] `/Library/Refresh` returned 204 / scheduled task ran (or LibraryMonitor auto-fired)
|
||
- [ ] `ProviderIds` populated on the new item (TMDb match successful)
|
||
- [ ] Image artwork populated
|
||
|
||
---
|
||
|
||
## Rollback / abort
|
||
|
||
If a partial copy fails or the wrong item gets matched:
|
||
|
||
```bash
|
||
# Remove the item folder
|
||
ssh user@192.168.0.100 'rm -rf "/home/user/media/movies/<Title> (<Year>)"'
|
||
# Force scan to drop it from index
|
||
ssh user@192.168.0.100 "docker exec jellyfin curl -s -X POST -H 'X-Emby-Token: $TOK' http://127.0.0.1:8096/Library/Refresh"
|
||
```
|
||
|
||
Source download on laptop is untouched — restage and retry per `docs/07:55-58`.
|
||
|
||
---
|
||
|
||
## Cross-references
|
||
|
||
| Topic | File |
|
||
|-------|------|
|
||
| Brand quality bar | `README.md:41` |
|
||
| Daily ops media flow | `ADMIN-GUIDE.md:35–46` |
|
||
| Folder/filename rules | `docs/05`, `docs/08` |
|
||
| Pre-import cleanup logic | `docs/07` |
|
||
| Subtitle sidecar form | `docs/03` + `playbooks/subtitles/` |
|
||
| TMDb override token | `docs/05:54-56` |
|
||
| Verification checklist (canonical) | `docs/05:1082-1099` |
|
||
| LibraryMonitor proof | live `docker logs jellyfin | grep LibraryMonitor` |
|
||
| Existing run examples | `runs/` |
|