reparatie diverse bestanden
This commit is contained in:
@@ -43,8 +43,8 @@ Agents mogen NOOIT:
|
|||||||
- code buiten deze repository aanpassen
|
- code buiten deze repository aanpassen
|
||||||
- andere Podman containers aanpassen
|
- andere Podman containers aanpassen
|
||||||
- systeemconfiguratie wijzigen
|
- systeemconfiguratie wijzigen
|
||||||
- Agents mogen geen wijzigingen maken die het contract in contracts/API_GOLDEN.md breken.
|
- wijzigingen maken die het contract in contracts/API_GOLDEN.md breken
|
||||||
- Agents mogen geen refactors of verbeteringen uitvoeren die niet expliciet gevraagd zijn.
|
- refactors of verbeteringen uitvoeren die niet expliciet gevraagd zijn
|
||||||
|
|
||||||
Agents mogen alleen werken **binnen deze repository**.
|
Agents mogen alleen werken **binnen deze repository**.
|
||||||
|
|
||||||
@@ -56,14 +56,14 @@ Agents mogen alleen werken **binnen deze repository**.
|
|||||||
De volgende bestanden zijn kritisch en mogen alleen aangepast worden na expliciete bevestiging van de gebruiker:
|
De volgende bestanden zijn kritisch en mogen alleen aangepast worden na expliciete bevestiging van de gebruiker:
|
||||||
Deze bestanden bevatten infrastructuur en API-contracten.
|
Deze bestanden bevatten infrastructuur en API-contracten.
|
||||||
|
|
||||||
app/services/tvdb_auth_service.py
|
- app/services/tvdb_auth_service.py
|
||||||
container/Containerfile
|
- container/Containerfile
|
||||||
requirements.txt
|
- requirements.txt
|
||||||
contracts/API_GOLDEN.md
|
- contracts/API_GOLDEN.md
|
||||||
AGENTS.md
|
- AGENTS.md
|
||||||
CHANGE_POLICY.md
|
- CHANGE_POLICY.md
|
||||||
SAFE_FILES.md
|
- SAFE_FILES.md
|
||||||
docs/ARCHITECTURE.md
|
- docs/ARCHITECTURE.md
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -118,6 +118,13 @@ Dit script valideert:
|
|||||||
|
|
||||||
Als een regressietest faalt moet de wijziging worden teruggedraaid.
|
Als een regressietest faalt moet de wijziging worden teruggedraaid.
|
||||||
|
|
||||||
|
Test-URL policy:
|
||||||
|
|
||||||
|
- Op de host moet standaard getest worden via `http://127.0.0.1:8085`
|
||||||
|
- In een sandbox of containerized AI-omgeving moet getest worden via `http://host.containers.internal:8085`
|
||||||
|
- Testscripts moeten automatisch één van beide kunnen detecteren
|
||||||
|
- Agents mogen niet aannemen dat `127.0.0.1:8085` vanuit de sandbox naar de host verwijst
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Feature Test Policy
|
# Feature Test Policy
|
||||||
@@ -145,9 +152,7 @@ Regressietests mogen **geen nieuw TVDB-token forceren**.
|
|||||||
|
|
||||||
De volgende tests moeten altijd slagen:
|
De volgende tests moeten altijd slagen:
|
||||||
|
|
||||||
curl 127.0.0.1:8085/api/health
|
./regression_tests.sh
|
||||||
curl 127.0.0.1:8085/api/tvdb/auth-status
|
|
||||||
curl "127.0.0.1:8085/api/tvdb/search?q=elsbeth"
|
|
||||||
|
|
||||||
Als een van deze tests faalt moet de wijziging worden teruggedraaid.
|
Als een van deze tests faalt moet de wijziging worden teruggedraaid.
|
||||||
|
|
||||||
|
|||||||
+120
@@ -55,3 +55,123 @@ def search_series(q: str = Query(..., min_length=1)):
|
|||||||
return {"items": normalized}
|
return {"items": normalized}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def _series_display_name(name: str | None, year: int | str | None) -> str | None:
|
||||||
|
if name and year:
|
||||||
|
return f"{name} ({year})"
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_episodes(payload: dict) -> list[dict]:
|
||||||
|
data = payload.get("data")
|
||||||
|
if isinstance(data, list):
|
||||||
|
return data
|
||||||
|
if isinstance(data, dict):
|
||||||
|
episodes = data.get("episodes")
|
||||||
|
if isinstance(episodes, list):
|
||||||
|
return episodes
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_aired_episodes(client: TvdbClient, series_id: int) -> list[dict]:
|
||||||
|
attempts = [
|
||||||
|
(f"/series/{series_id}/episodes/default", {"seasonType": "aired"}),
|
||||||
|
(f"/series/{series_id}/episodes/official", None),
|
||||||
|
]
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for path, params in attempts:
|
||||||
|
try:
|
||||||
|
payload = client.get(path, params=params)
|
||||||
|
items = _extract_episodes(payload)
|
||||||
|
if items:
|
||||||
|
return items
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append(f"{path}: {exc}")
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
"Unable to load aired episodes from TVDB; tried known episode endpoints"
|
||||||
|
if not errors
|
||||||
|
else f"Unable to load aired episodes from TVDB ({'; '.join(errors)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/series/{series_id}/episodes")
|
||||||
|
def get_series_episodes(
|
||||||
|
series_id: int,
|
||||||
|
order_type: str = Query("aired", min_length=1),
|
||||||
|
):
|
||||||
|
if order_type.lower() != "aired":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Unsupported order_type. Only 'aired' is supported in this MVP.",
|
||||||
|
)
|
||||||
|
|
||||||
|
client = TvdbClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
series_payload = client.get(f"/series/{series_id}")
|
||||||
|
series_data = series_payload.get("data", {}) if isinstance(series_payload, dict) else {}
|
||||||
|
series_name = (
|
||||||
|
series_data.get("name")
|
||||||
|
or series_data.get("seriesName")
|
||||||
|
or series_data.get("slug")
|
||||||
|
)
|
||||||
|
series_year = series_data.get("year")
|
||||||
|
|
||||||
|
episodes = _fetch_aired_episodes(client, series_id)
|
||||||
|
normalized = []
|
||||||
|
|
||||||
|
for item in episodes:
|
||||||
|
season_number = (
|
||||||
|
item.get("seasonNumber")
|
||||||
|
or item.get("airedSeason")
|
||||||
|
or item.get("season")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
episode_number = (
|
||||||
|
item.get("number")
|
||||||
|
or item.get("airedEpisodeNumber")
|
||||||
|
or item.get("episodeNumber")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
title = item.get("name") or item.get("episodeName") or ""
|
||||||
|
aired_on = item.get("aired") or item.get("firstAired")
|
||||||
|
|
||||||
|
label = None
|
||||||
|
try:
|
||||||
|
season_num = int(season_number)
|
||||||
|
episode_num = int(episode_number)
|
||||||
|
label = f"S{season_num:02}E{episode_num:02} - {title}"
|
||||||
|
if aired_on:
|
||||||
|
label = f"{label} - {aired_on}"
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
normalized.append(
|
||||||
|
{
|
||||||
|
"id": item.get("id"),
|
||||||
|
"season_number": season_number,
|
||||||
|
"episode_number": episode_number,
|
||||||
|
"title": title,
|
||||||
|
"aired": aired_on,
|
||||||
|
"label": label,
|
||||||
|
"raw": item,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"series": {
|
||||||
|
"id": series_data.get("id") or series_id,
|
||||||
|
"name": series_name,
|
||||||
|
"year": series_year,
|
||||||
|
"display_name": _series_display_name(series_name, series_year),
|
||||||
|
},
|
||||||
|
"order_type": "aired",
|
||||||
|
"items": normalized,
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|||||||
+23
-9
@@ -1,12 +1,26 @@
|
|||||||
Ik heb een paar opmerkingen
|
FROM python:3.12-slim
|
||||||
|
|
||||||
AGENTS.md
|
WORKDIR /app
|
||||||
Regression testst
|
|
||||||
curl -X POST 127.0.0.1:8085/api/tvdb/login
|
|
||||||
zorgt deze curl er niet continue voor dat er een nieuw token wordt aangevraagd en dat thetvdb mij dan gaat blokkeren?
|
|
||||||
|
|
||||||
ARCHITECTURE.md
|
# Install minimal system dependencies
|
||||||
Ik wil de volumes voor de videobestanden graag als volgt. Dat komt overeen met de host en maakt het herkenbaarder
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
/Volumes/8TB/Shared_Folders/TV_Shows → /Volumes/8TB/Shared_Folders/TV_Shows
|
# Copy requirements
|
||||||
/Volumes/8TB_RAID1/Shared_Folders/Library/TV_Shows → /Volumes/8TB_RAID1/Shared_Folders/Library/TV_Shows
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy app
|
||||||
|
COPY app /app/app
|
||||||
|
|
||||||
|
# Create runtime dirs
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
BASE_URL="${BASE_URL:-http://127.0.0.1:8085}"
|
if [ -z "${BASE_URL:-}" ]; then
|
||||||
|
if curl --silent --fail http://127.0.0.1:8085/api/health >/dev/null 2>&1; then
|
||||||
|
BASE_URL="http://127.0.0.1:8085"
|
||||||
|
elif curl --silent --fail http://host.containers.internal:8085/api/health >/dev/null 2>&1; then
|
||||||
|
BASE_URL="http://host.containers.internal:8085"
|
||||||
|
else
|
||||||
|
echo "ERROR: could not determine BASE_URL. Tried 127.0.0.1 and host.containers.internal." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
TMP_DIR="$(mktemp -d)"
|
TMP_DIR="$(mktemp -d)"
|
||||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
|||||||
Executable
+98
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ -z "${BASE_URL:-}" ]; then
|
||||||
|
if curl --silent --fail http://127.0.0.1:8085/api/health >/dev/null 2>&1; then
|
||||||
|
BASE_URL="http://127.0.0.1:8085"
|
||||||
|
elif curl --silent --fail http://host.containers.internal:8085/api/health >/dev/null 2>&1; then
|
||||||
|
BASE_URL="http://host.containers.internal:8085"
|
||||||
|
else
|
||||||
|
echo "ERROR: could not determine BASE_URL. Tried 127.0.0.1 and host.containers.internal." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
SERIES_ID="${SERIES_ID:-430543}"
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
echo "== Feature test 1: episodes endpoint returns series + items =="
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
"${BASE_URL}/api/tvdb/series/${SERIES_ID}/episodes?order_type=aired" \
|
||||||
|
-o "${TMP_DIR}/episodes.json"
|
||||||
|
|
||||||
|
cat "${TMP_DIR}/episodes.json"
|
||||||
|
|
||||||
|
python3 - "${TMP_DIR}/episodes.json" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
path = Path(sys.argv[1])
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
assert isinstance(data, dict), "episodes response must be an object"
|
||||||
|
assert "series" in data and isinstance(data["series"], dict), "episodes response missing series object"
|
||||||
|
assert data.get("order_type") == "aired", "order_type must be aired"
|
||||||
|
assert "items" in data and isinstance(data["items"], list), "episodes response missing items list"
|
||||||
|
|
||||||
|
series = data["series"]
|
||||||
|
for key in ["id", "name", "year", "display_name"]:
|
||||||
|
assert key in series, f"series missing key: {key}"
|
||||||
|
|
||||||
|
print("episodes endpoint JSON validation passed")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Feature test 2: unsupported order_type is rejected =="
|
||||||
|
curl --silent --show-error \
|
||||||
|
-o "${TMP_DIR}/episodes_bad_order.json" \
|
||||||
|
-w "%{http_code}" \
|
||||||
|
"${BASE_URL}/api/tvdb/series/${SERIES_ID}/episodes?order_type=absolute" \
|
||||||
|
> "${TMP_DIR}/episodes_bad_order.status"
|
||||||
|
|
||||||
|
cat "${TMP_DIR}/episodes_bad_order.json"
|
||||||
|
|
||||||
|
python3 - "${TMP_DIR}/episodes_bad_order.status" "${TMP_DIR}/episodes_bad_order.json" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
status = Path(sys.argv[1]).read_text(encoding="utf-8").strip()
|
||||||
|
data = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
assert status == "400", f"expected HTTP 400, got {status}"
|
||||||
|
assert isinstance(data, dict), "error response must be an object"
|
||||||
|
assert "detail" in data, "error response missing detail"
|
||||||
|
|
||||||
|
print("unsupported order_type validation passed")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Feature test 3: search output keeps year/display_name contract =="
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
"${BASE_URL}/api/tvdb/search?q=elsbeth" \
|
||||||
|
-o "${TMP_DIR}/search.json"
|
||||||
|
|
||||||
|
cat "${TMP_DIR}/search.json"
|
||||||
|
|
||||||
|
python3 - "${TMP_DIR}/search.json" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
path = Path(sys.argv[1])
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
assert isinstance(data, dict), "search response must be an object"
|
||||||
|
assert "items" in data and isinstance(data["items"], list), "search response missing items list"
|
||||||
|
assert len(data["items"]) > 0, "search items list must not be empty"
|
||||||
|
|
||||||
|
first = data["items"][0]
|
||||||
|
for key in ["id", "name", "year", "display_name"]:
|
||||||
|
assert key in first, f"search item missing key: {key}"
|
||||||
|
|
||||||
|
print("search contract validation passed")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "All episodes feature tests passed."
|
||||||
+10
-1
@@ -1,7 +1,16 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
BASE_URL="${BASE_URL:-http://127.0.0.1:8085}"
|
if [ -z "${BASE_URL:-}" ]; then
|
||||||
|
if curl --silent --fail http://127.0.0.1:8085/api/health >/dev/null 2>&1; then
|
||||||
|
BASE_URL="http://127.0.0.1:8085"
|
||||||
|
elif curl --silent --fail http://host.containers.internal:8085/api/health >/dev/null 2>&1; then
|
||||||
|
BASE_URL="http://host.containers.internal:8085"
|
||||||
|
else
|
||||||
|
echo "ERROR: could not determine BASE_URL. Tried 127.0.0.1 and host.containers.internal." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
TMP_DIR="$(mktemp -d)"
|
TMP_DIR="$(mktemp -d)"
|
||||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,639 @@
|
|||||||
|
# Rename-MVP — Definitieve Codex instructie en technisch ontwerp
|
||||||
|
|
||||||
|
## Doel
|
||||||
|
|
||||||
|
Bouw verder aan een minimalistische webapplicatie als uitgeklede webversie van **Rename My TV Series**.
|
||||||
|
|
||||||
|
De applicatie moet:
|
||||||
|
|
||||||
|
- draaien in een **rootless Podman container**
|
||||||
|
- lokale mediabestanden kunnen hernoemen binnen expliciet toegestane videomappen
|
||||||
|
- metadata ophalen via **TheTVDB v4 API**
|
||||||
|
- een **4-panel interface** hebben
|
||||||
|
- altijd eerst een **preview** tonen voordat bestanden echt worden hernoemd
|
||||||
|
- veilig omgaan met paden, tokens en bestandsoperaties
|
||||||
|
- de **serienaam inclusief jaartal** gebruiken in de UI en in de bestandsnaam
|
||||||
|
|
||||||
|
Dit project heeft al een **werkende baseline**:
|
||||||
|
- container build werkt
|
||||||
|
- FastAPI health endpoint werkt
|
||||||
|
- TVDB login werkt
|
||||||
|
- JWT parsing werkt
|
||||||
|
- token persistence werkt
|
||||||
|
- auth-status endpoint werkt
|
||||||
|
- TVDB search werkt
|
||||||
|
- `token_source` werkt
|
||||||
|
|
||||||
|
Belangrijk: dit project is nu **geen greenfield ontwerp meer**, maar een **bestaand werkend project** dat gecontroleerd verder moet worden uitgebouwd.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Technische context
|
||||||
|
|
||||||
|
## Hostomgeving
|
||||||
|
|
||||||
|
- Host OS: Debian Trixie
|
||||||
|
- Podman version: 5.4.2
|
||||||
|
- Project root: `/home/kodi/.config/rename-mvp`
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
- Rootless Podman
|
||||||
|
- FastAPI backend
|
||||||
|
- SQLite voor app-state en logging
|
||||||
|
- JSON voor TVDB auth state
|
||||||
|
- TheTVDB API v4
|
||||||
|
- Lokale tests via `127.0.0.1:8085`
|
||||||
|
|
||||||
|
## Bestaande paden
|
||||||
|
|
||||||
|
### Auth state JSON
|
||||||
|
- Host: `/home/kodi/.config/rename-mvp/data/tvdb_auth.json`
|
||||||
|
- Container: `/app/data/tvdb_auth.json`
|
||||||
|
|
||||||
|
### Werkende testscripts
|
||||||
|
- `/home/kodi/.config/rename-mvp/regression_tests.sh`
|
||||||
|
- `/home/kodi/.config/rename-mvp/feature_test_template.sh`
|
||||||
|
|
||||||
|
### Governance-bestanden
|
||||||
|
- `/home/kodi/.config/rename-mvp/AGENTS.md`
|
||||||
|
- `/home/kodi/.config/rename-mvp/CHANGE_POLICY.md`
|
||||||
|
- `/home/kodi/.config/rename-mvp/SAFE_FILES.md`
|
||||||
|
- `/home/kodi/.config/rename-mvp/docs/ARCHITECTURE.md`
|
||||||
|
- `/home/kodi/.config/rename-mvp/contracts/API_GOLDEN.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Niet-onderhandelbare regels
|
||||||
|
|
||||||
|
## 2.1 Geen contractbreuk
|
||||||
|
|
||||||
|
Je mag geen bestaande API endpoints of bestaande response-structuren breken.
|
||||||
|
|
||||||
|
Je mag:
|
||||||
|
- nieuwe velden toevoegen
|
||||||
|
- nieuwe endpoints toevoegen
|
||||||
|
|
||||||
|
Je mag niet:
|
||||||
|
- bestaande veldnamen wijzigen
|
||||||
|
- bestaande velden verwijderen
|
||||||
|
- response-structuren wijzigen
|
||||||
|
- bestaande endpoints hernoemen
|
||||||
|
- bestaande endpoints verwijderen
|
||||||
|
|
||||||
|
`contracts/API_GOLDEN.md` is leidend.
|
||||||
|
|
||||||
|
## 2.2 Geen speculative improvements
|
||||||
|
|
||||||
|
Voer **geen refactors, herstructureringen of “verbeteringen” uit die niet expliciet nodig zijn voor de gevraagde wijziging**.
|
||||||
|
|
||||||
|
Als een wijziging niet strikt nodig is om het gevraagde doel te bereiken, doe die wijziging niet.
|
||||||
|
|
||||||
|
## 2.3 Safe files
|
||||||
|
|
||||||
|
De volgende bestanden zijn beschermd en mogen alleen gewijzigd worden als dat expliciet noodzakelijk is en als je het duidelijk benoemt:
|
||||||
|
|
||||||
|
- `app/services/tvdb_auth_service.py`
|
||||||
|
- `container/Containerfile`
|
||||||
|
- `requirements.txt`
|
||||||
|
- `contracts/API_GOLDEN.md`
|
||||||
|
- `AGENTS.md`
|
||||||
|
- `CHANGE_POLICY.md`
|
||||||
|
- `SAFE_FILES.md`
|
||||||
|
- `docs/ARCHITECTURE.md`
|
||||||
|
|
||||||
|
## 2.4 Scopebeperking
|
||||||
|
|
||||||
|
Werk alleen binnen deze repository:
|
||||||
|
|
||||||
|
Pad op de host: `/home/kodi/.config/rename-mvp`
|
||||||
|
Pad in deze codex omgeving: `/workspace/rename-mvp`
|
||||||
|
|
||||||
|
|
||||||
|
Je mag niets aanpassen buiten deze projectscope.
|
||||||
|
|
||||||
|
## 2.5 Escalated execution
|
||||||
|
|
||||||
|
Escalated execution is toegestaan binnen deze projectscope voor:
|
||||||
|
|
||||||
|
- `podman build`
|
||||||
|
- `podman run`
|
||||||
|
- curl tests
|
||||||
|
- lokale build- en testacties
|
||||||
|
- bestanden aanmaken of aanpassen binnen dit project
|
||||||
|
- dependency install binnen dit project
|
||||||
|
|
||||||
|
Niet toegestaan:
|
||||||
|
|
||||||
|
- systeembrede wijzigingen
|
||||||
|
- firewallwijzigingen
|
||||||
|
- system upgrades
|
||||||
|
- andere repositories
|
||||||
|
- andere stacks of containers aanpassen
|
||||||
|
- destructieve cleanup acties buiten deze projectscope
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Bestaande baseline die behouden moet blijven
|
||||||
|
|
||||||
|
Deze functionaliteit werkt al en mag niet stuk:
|
||||||
|
|
||||||
|
## 3.1 Health endpoint
|
||||||
|
`GET /api/health`
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
```json
|
||||||
|
{"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.2 TVDB login endpoint
|
||||||
|
`POST /api/tvdb/login`
|
||||||
|
|
||||||
|
Doet een expliciete forced login en vraagt een nieuw token aan.
|
||||||
|
|
||||||
|
Deze endpoint is **alleen voor debugging en handmatige auth-tests**, niet voor regressietests.
|
||||||
|
|
||||||
|
## 3.3 TVDB auth-status endpoint
|
||||||
|
`GET /api/tvdb/auth-status`
|
||||||
|
|
||||||
|
Deze endpoint moet de actuele auth-status tonen inclusief:
|
||||||
|
- configured
|
||||||
|
- has_token
|
||||||
|
- issued_at
|
||||||
|
- expires_at
|
||||||
|
- expires_at_unix
|
||||||
|
- renew_after
|
||||||
|
- renew_after_unix
|
||||||
|
- is_expired
|
||||||
|
- is_renewal_due
|
||||||
|
- last_login_attempt_at
|
||||||
|
- last_login_success_at
|
||||||
|
- last_login_status
|
||||||
|
- last_login_error
|
||||||
|
- jwt_payload
|
||||||
|
- token_source
|
||||||
|
|
||||||
|
## 3.4 TVDB search endpoint
|
||||||
|
`GET /api/tvdb/search?q=elsbeth`
|
||||||
|
|
||||||
|
Deze endpoint werkt al en moet series teruggeven met ten minste:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- year
|
||||||
|
- display_name
|
||||||
|
|
||||||
|
De UI en bestandsnaamopbouw moeten het **jaartal van de serie** gebruiken.
|
||||||
|
|
||||||
|
## 3.5 Token lifecycle
|
||||||
|
De TVDB bearer token is een JWT.
|
||||||
|
Gebruik:
|
||||||
|
- `exp` als primaire bron voor expiry
|
||||||
|
- `iat` indien aanwezig
|
||||||
|
- renew alleen wanneer nodig
|
||||||
|
- niet onnodig opnieuw `/login` doen
|
||||||
|
|
||||||
|
De token mag **niet continu vernieuwd worden**.
|
||||||
|
|
||||||
|
## 3.6 token_source
|
||||||
|
De auth-state gebruikt:
|
||||||
|
- `none`
|
||||||
|
- `login`
|
||||||
|
- `cached`
|
||||||
|
- `renewed`
|
||||||
|
|
||||||
|
Dit gedrag moet behouden blijven.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Functioneel doel van de applicatie
|
||||||
|
|
||||||
|
De uiteindelijke webapp moet een minimalistische 4-panel interface hebben.
|
||||||
|
|
||||||
|
## Paneel 1 — TVDB Search
|
||||||
|
Functie:
|
||||||
|
- zoekterm invoeren
|
||||||
|
- zoeken via backend naar TheTVDB
|
||||||
|
- resultaten tonen
|
||||||
|
- gekozen serie details tonen
|
||||||
|
|
||||||
|
Zoekresultaten moeten minimaal tonen:
|
||||||
|
- serienaam
|
||||||
|
- jaar
|
||||||
|
- first aired indien beschikbaar
|
||||||
|
- network indien beschikbaar
|
||||||
|
- poster indien beschikbaar
|
||||||
|
|
||||||
|
De series moeten in de UI worden weergegeven als bijvoorbeeld:
|
||||||
|
|
||||||
|
- `Elsbeth (2024)`
|
||||||
|
- `The Office (2005)`
|
||||||
|
|
||||||
|
## Paneel 2 — Episodes
|
||||||
|
Functie:
|
||||||
|
- afleveringen van gekozen serie tonen
|
||||||
|
- minimaal `Aired Order` ondersteunen
|
||||||
|
- afleveringen per seizoen tonen
|
||||||
|
- gebruiker kan willekeurige afleveringen selecteren
|
||||||
|
|
||||||
|
Afleveringen tonen bijvoorbeeld:
|
||||||
|
- `S02E03 - Devil's Night - 2025-03-13`
|
||||||
|
|
||||||
|
## Paneel 3 — Selected Episodes
|
||||||
|
Functie:
|
||||||
|
- lijst van gekozen afleveringen voor de huidige sessie
|
||||||
|
|
||||||
|
Functionaliteit:
|
||||||
|
- toevoegen
|
||||||
|
- verwijderen
|
||||||
|
- clear
|
||||||
|
- reorder
|
||||||
|
- move up / move down
|
||||||
|
|
||||||
|
## Paneel 4 — Selected Files
|
||||||
|
Functie:
|
||||||
|
- lijst van gekozen lokale videobestanden
|
||||||
|
|
||||||
|
Functionaliteit:
|
||||||
|
- Add Files
|
||||||
|
- Add Directory
|
||||||
|
- Sort
|
||||||
|
- Remove
|
||||||
|
- Clear
|
||||||
|
- Move Up / Move Down
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
1. user zoekt serie
|
||||||
|
2. user kiest serie
|
||||||
|
3. backend haalt afleveringen op
|
||||||
|
4. user voegt afleveringen toe aan selected episodes
|
||||||
|
5. user voegt bestanden toe aan selected files
|
||||||
|
6. backend maakt een 1-op-1 mapping op basis van volgorde
|
||||||
|
7. backend maakt preview
|
||||||
|
8. user bevestigt
|
||||||
|
9. backend voert rename uit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Bestandsnaamopbouw
|
||||||
|
|
||||||
|
De standaard bestandsnaam moet zijn:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{series} ({year}) - S{season:02}E{episode:02} - {title}{ext}
|
||||||
|
```
|
||||||
|
|
||||||
|
Voorbeeld:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Elsbeth (2024) - S02E03 - Devil's Night.mkv
|
||||||
|
```
|
||||||
|
|
||||||
|
Belangrijk:
|
||||||
|
- het **serienaam-jaartal** is verplicht onderdeel van de naam
|
||||||
|
- het jaartal komt uit TheTVDB serie metadata
|
||||||
|
- de UI moet dit jaartal ook tonen
|
||||||
|
|
||||||
|
De eerste MVP hoeft **airdate niet per se in de filename** te zetten zolang het afgesproken template hierboven intact blijft.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. TVDB authenticatie en tokenbeheer
|
||||||
|
|
||||||
|
## 6.1 Authflow
|
||||||
|
Gebruik de officiële TVDB v4 flow:
|
||||||
|
- `POST /login`
|
||||||
|
- body bevat `apikey`
|
||||||
|
- `pin` alleen meesturen als aanwezig
|
||||||
|
- response bevat bearer token
|
||||||
|
|
||||||
|
## 6.2 JWT verwerking
|
||||||
|
De token is een JWT.
|
||||||
|
|
||||||
|
Verplicht:
|
||||||
|
- decodeer payload
|
||||||
|
- lees `exp`
|
||||||
|
- lees `iat` indien aanwezig
|
||||||
|
- gebruik `exp` als primaire expiry-bron
|
||||||
|
|
||||||
|
## 6.3 Opslag
|
||||||
|
TVDB auth state wordt persistent opgeslagen in:
|
||||||
|
|
||||||
|
`/app/data/tvdb_auth.json`
|
||||||
|
|
||||||
|
Hostpad:
|
||||||
|
|
||||||
|
`/home/kodi/.config/rename-mvp/data/tvdb_auth.json`
|
||||||
|
|
||||||
|
In JSON mogen staan:
|
||||||
|
- token
|
||||||
|
- issued_at
|
||||||
|
- expires_at
|
||||||
|
- expires_at_unix
|
||||||
|
- renew_after
|
||||||
|
- renew_after_unix
|
||||||
|
- last_login_attempt_at
|
||||||
|
- last_login_success_at
|
||||||
|
- last_login_status
|
||||||
|
- last_login_error
|
||||||
|
- jwt_payload
|
||||||
|
- token_source
|
||||||
|
|
||||||
|
Niet in JSON:
|
||||||
|
- `TVDB_API_KEY`
|
||||||
|
- `TVDB_PIN`
|
||||||
|
|
||||||
|
Die moeten uit environment variables komen.
|
||||||
|
|
||||||
|
## 6.4 Renew-logica
|
||||||
|
Renew alleen wanneer:
|
||||||
|
- `now >= renew_after`
|
||||||
|
- of TVDB `401` teruggeeft
|
||||||
|
|
||||||
|
Niet continu nieuwe tokens opvragen.
|
||||||
|
|
||||||
|
`POST /api/tvdb/login` is geen regressietest en geen normale flow; het is een debug endpoint.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Toegestane videopaden
|
||||||
|
|
||||||
|
De container moet dezelfde videopaden gebruiken als de host.
|
||||||
|
|
||||||
|
## Allowed media roots
|
||||||
|
|
||||||
|
- `/Volumes/8TB/Shared_Folders/TV_Shows`
|
||||||
|
- `/Volumes/8TB_RAID1/Shared_Folders/Library/TV_Shows`
|
||||||
|
|
||||||
|
## Volume mappings
|
||||||
|
|
||||||
|
- `/Volumes/8TB/Shared_Folders/TV_Shows -> /Volumes/8TB/Shared_Folders/TV_Shows`
|
||||||
|
- `/Volumes/8TB_RAID1/Shared_Folders/Library/TV_Shows -> /Volumes/8TB_RAID1/Shared_Folders/Library/TV_Shows`
|
||||||
|
- `/home/kodi/.config/rename-mvp/data -> /app/data`
|
||||||
|
|
||||||
|
De app mag alleen lezen/schrijven binnen een van de toegestane media roots.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Securitymodel
|
||||||
|
|
||||||
|
## Bestandspaden
|
||||||
|
Nooit:
|
||||||
|
- absolute clientpaden blind vertrouwen
|
||||||
|
- `..` toestaan
|
||||||
|
- buiten allowed media roots resolven
|
||||||
|
|
||||||
|
Altijd:
|
||||||
|
- path validation doen
|
||||||
|
- binnen allowlist van media roots blijven
|
||||||
|
- alleen toegestane extensies accepteren
|
||||||
|
- preview afdwingen vóór rename
|
||||||
|
|
||||||
|
## Allowed extensions MVP
|
||||||
|
- `.mkv`
|
||||||
|
- `.mp4`
|
||||||
|
- `.avi`
|
||||||
|
- `.m4v`
|
||||||
|
- `.srt`
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
Nooit loggen:
|
||||||
|
- API keys
|
||||||
|
- bearer tokens
|
||||||
|
- env secrets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. Container en runtime
|
||||||
|
|
||||||
|
## Containerfile
|
||||||
|
De bestaande werkende runtime moet behouden blijven.
|
||||||
|
|
||||||
|
## Rootless Podman
|
||||||
|
De applicatie draait rootless.
|
||||||
|
|
||||||
|
## Lokale test-URL
|
||||||
|
Gebruik voor runtime-tests de BASE_URL die bereikbaar is vanuit de huidige omgeving:
|
||||||
|
|
||||||
|
- op de host meestal: http://127.0.0.1:8085
|
||||||
|
- in sandbox/container-omgevingen meestal: http://host.containers.internal:8085
|
||||||
|
|
||||||
|
Gebruik niet automatisch localhost.
|
||||||
|
|
||||||
|
De testscripts moeten BASE_URL automatisch detecteren of expliciet meegegeven krijgen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. Verwachte repositorystructuur
|
||||||
|
|
||||||
|
Houd deze structuur aan. Geen onnodige nieuwe mappen toevoegen.
|
||||||
|
|
||||||
|
```text
|
||||||
|
/home/kodi/.config/rename-mvp
|
||||||
|
├── AGENTS.md
|
||||||
|
├── CHANGE_POLICY.md
|
||||||
|
├── SAFE_FILES.md
|
||||||
|
├── regression_tests.sh
|
||||||
|
├── feature_test_template.sh
|
||||||
|
├── requirements.txt
|
||||||
|
├── .env
|
||||||
|
├── .env.example
|
||||||
|
├── README.md
|
||||||
|
├── app/
|
||||||
|
├── container/
|
||||||
|
├── data/
|
||||||
|
├── contracts/
|
||||||
|
│ └── API_GOLDEN.md
|
||||||
|
└── docs/
|
||||||
|
└── ARCHITECTURE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 11. API-contract dat behouden moet blijven
|
||||||
|
|
||||||
|
## 11.1 Health
|
||||||
|
`GET /api/health`
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11.2 TVDB Login
|
||||||
|
`POST /api/tvdb/login`
|
||||||
|
|
||||||
|
Response bevat ten minste:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"issued_at": "...",
|
||||||
|
"expires_at": "...",
|
||||||
|
"renew_after": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11.3 TVDB Auth Status
|
||||||
|
`GET /api/tvdb/auth-status`
|
||||||
|
|
||||||
|
Response bevat ten minste:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"configured": true,
|
||||||
|
"has_token": true,
|
||||||
|
"expires_at": "...",
|
||||||
|
"renew_after": "...",
|
||||||
|
"token_source": "cached"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Meer velden zijn toegestaan, minder niet.
|
||||||
|
|
||||||
|
## 11.4 TVDB Search
|
||||||
|
`GET /api/tvdb/search?q=query`
|
||||||
|
|
||||||
|
Response bevat ten minste:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "...",
|
||||||
|
"name": "...",
|
||||||
|
"year": "...",
|
||||||
|
"display_name": "..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 12. Testbeleid
|
||||||
|
|
||||||
|
## 12.1 Verplichte regressietests na elke wijziging
|
||||||
|
Na **iedere wijziging** moet dit script worden uitgevoerd:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./regression_tests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Dit script is verplicht.
|
||||||
|
|
||||||
|
Het valideert de bestaande baseline en mag geen nieuw TVDB-token forceren.
|
||||||
|
|
||||||
|
## 12.2 Verplichte feature tests
|
||||||
|
Na iedere wijziging moet je daarnaast **drie tests uitvoeren voor de nieuwe of gewijzigde functionaliteit**.
|
||||||
|
|
||||||
|
Gebruik hiervoor als basis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./feature_test_template.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Deze template moet per feature worden aangepast zodat de nieuwe functionaliteit echt en mechanisch getest wordt.
|
||||||
|
|
||||||
|
## 12.3 Geen forced login in regressie
|
||||||
|
Verboden als standaard regressietest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 127.0.0.1:8085/api/tvdb/login
|
||||||
|
```
|
||||||
|
|
||||||
|
Deze endpoint is alleen voor debugging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 13. Ontwikkelworkflow voor Codex
|
||||||
|
|
||||||
|
Bij iedere opdracht moet je dit proces volgen:
|
||||||
|
|
||||||
|
1. lees eerst de bestaande governance-bestanden:
|
||||||
|
- `AGENTS.md`
|
||||||
|
- `CHANGE_POLICY.md`
|
||||||
|
- `SAFE_FILES.md`
|
||||||
|
- `docs/ARCHITECTURE.md`
|
||||||
|
- `contracts/API_GOLDEN.md`
|
||||||
|
|
||||||
|
2. vat kort samen wat je gaat wijzigen
|
||||||
|
|
||||||
|
3. benoem welke bestanden je wilt aanpassen
|
||||||
|
|
||||||
|
4. voer alleen de strikt noodzakelijke wijziging uit
|
||||||
|
|
||||||
|
5. draai verplichte regressietests:
|
||||||
|
- `./regression_tests.sh`
|
||||||
|
|
||||||
|
6. draai drie feature tests voor de nieuwe functionaliteit
|
||||||
|
|
||||||
|
7. rapporteer:
|
||||||
|
- welke bestanden zijn aangepast
|
||||||
|
- welke regressietests zijn uitgevoerd
|
||||||
|
- welke feature tests zijn uitgevoerd
|
||||||
|
- welke risico’s of beperkingen er nog zijn
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 14. Eerste concrete opdracht aan Codex
|
||||||
|
|
||||||
|
Begin niet meteen met een brede refactor.
|
||||||
|
|
||||||
|
Begin met een gecontroleerde volgende stap.
|
||||||
|
|
||||||
|
## Eerste aanbevolen fase
|
||||||
|
Bouw de volgende functionaliteit uit:
|
||||||
|
|
||||||
|
### Doel
|
||||||
|
- endpoint om afleveringen van een gekozen TVDB serie op te halen
|
||||||
|
- minimaal ondersteuning voor `aired order`
|
||||||
|
- seriegegevens inclusief `year` behouden
|
||||||
|
- output geschikt maken voor paneel 2 van de UI
|
||||||
|
|
||||||
|
### Verwachte richting
|
||||||
|
Bijvoorbeeld iets als:
|
||||||
|
|
||||||
|
`GET /api/tvdb/series/{series_id}/episodes?order_type=aired`
|
||||||
|
|
||||||
|
Maar:
|
||||||
|
- breek geen bestaande endpoints
|
||||||
|
- wijzig geen bestaande auth-logica
|
||||||
|
- wijzig geen container setup
|
||||||
|
- gebruik bestaande TVDB token lifecycle
|
||||||
|
- voeg tests toe
|
||||||
|
|
||||||
|
### Voor deze wijziging verplicht
|
||||||
|
1. regressietests blijven slagen
|
||||||
|
2. drie feature tests toevoegen/uitvoeren voor het nieuwe episodes endpoint
|
||||||
|
3. output JSON valideren
|
||||||
|
4. zoekresultaten en year-handling niet breken
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 15. Wat je uitdrukkelijk niet moet doen
|
||||||
|
|
||||||
|
- geen speculative refactors
|
||||||
|
- geen auth-systeem herschrijven
|
||||||
|
- geen token lifecycle vereenvoudigen
|
||||||
|
- geen nieuwe frameworkkeuzes introduceren
|
||||||
|
- geen projectstructuur wijzigen
|
||||||
|
- geen governance-bestanden wijzigen tenzij expliciet nodig en gemeld
|
||||||
|
- geen forced login gebruiken als regressietest
|
||||||
|
- geen container- of pathmodel veranderen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 16. Samenvatting voor Codex
|
||||||
|
|
||||||
|
Dit project heeft al een werkende baseline.
|
||||||
|
|
||||||
|
Jouw taak is niet om het project opnieuw te ontwerpen, maar om het gecontroleerd uit te bouwen.
|
||||||
|
|
||||||
|
Belangrijkste regels:
|
||||||
|
- behoud werkende baseline
|
||||||
|
- breek geen API contract
|
||||||
|
- gebruik JWT `exp` als expiry-bron
|
||||||
|
- renew token alleen wanneer nodig
|
||||||
|
- gebruik seriejaar in UI en filename
|
||||||
|
- werk alleen binnen allowed media roots
|
||||||
|
- draai altijd `./regression_tests.sh`
|
||||||
|
- draai altijd drie feature tests voor nieuwe functionaliteit
|
||||||
|
- geen speculative improvements
|
||||||
|
- escalated execution is toegestaan binnen alleen deze projectscope
|
||||||
Reference in New Issue
Block a user