Compare commits

..

14 Commits

13 changed files with 1591 additions and 2542 deletions

48
AGENTS.md Normal file
View File

@ -0,0 +1,48 @@
# Repository Guidelines
## Project Structure & Module Organization
This repository maintains a local SQLite index of WCX publications. Production code lives in `scripts/`: `wcx_sync.py` scrapes site metadata, `import_site.py` imports it into SQLite, and `update_wcx.sh` coordinates synchronization, import, and OCR. OCR is split across `process_pending_ocr.py`, `check_ocr.py`, `ocr.sh`, and `parse_ocr.py`.
`scripts/schema.sql` defines the database model. Generated databases belong in `database/`, while generated scraper output belongs in `import/`; both are ignored by Git. Historical source data is stored in `migration/`. Database design notes live in `docs/`. Filename matching and duration diagnostics live in the external `/storage/disk1/WCX-collection` repository.
## Architectural boundaries
This repository is the reference-data project. Filename matching and duration diagnostics have moved to `/storage/disk1/WCX-collection/scripts/match_filenames.py` and `/storage/disk1/WCX-collection/scripts/diagnose_duration_match.py`; this repository no longer contains or runs that functionality. The reference-data project owns the database schema, metadata, OCR, imports, history, and aliases, and it is the sole writer to the reference database.
WCX-collection is an external read-only consumer of the reference database and may open it only in strict read-only mode using SQLite `mode=ro`. Local file status, paths, matching decisions, and collection status must never be stored in the reference database. Any future file-management registry or database must be owned separately and may refer to `movie.id` as the stable external ID. Do not introduce a shared Python library or API without a concrete need.
## Build, Test, and Development Commands
There is no build step or third-party Python package installation; scripts use Python 3's standard library.
- `scripts/update_wcx.sh --skip-ocr` synchronizes and imports site data without requiring a Vision API key.
- `scripts/update_wcx.sh` runs the complete update, including pending OCR work.
- `scripts/migrate_csv.py --dry-run` validates legacy CSV data without modifying SQLite.
- `python3 -m py_compile scripts/*.py` performs a quick syntax check.
Use a copied test database for commands that can write data. Never recreate or overwrite `database/wcx.db` casually because it contains manually maintained metadata.
## Coding Style & Naming Conventions
Use four-space indentation and `snake_case` for Python functions and variables; use `PascalCase` for dataclasses and other classes. Prefer type hints, `pathlib.Path`, explicit error handling, and small focused functions. Shell scripts should use Bash with `set -Eeuo pipefail`, uppercase configuration constants, quoted expansions, and clear failure messages. No formatter or linter is currently configured, so follow the surrounding style.
## Testing Guidelines
The project has no formal test framework or coverage threshold. Validate changes with syntax checks, dry runs, and disposable database copies. Name future Python tests `test_*.py` and keep fixtures isolated from production data. OCR tests require `GOOGLE_VISION_API_KEY`; do not expose the key in logs or commits.
## Working style
- Work incrementally and conservatively.
- Preserve existing behavior unless the requested change explicitly alters it.
- Inspect the relevant files before proposing changes.
- Present a short implementation plan before larger changes.
- Do not modify unrelated files.
- Prefer complete, coherent changes over broad refactoring.
- Run relevant tests or validation commands after changes.
- Show the resulting git diff and summarize what changed.
- Never commit or push unless explicitly requested.
## Commit & Pull Request Guidelines
Recent commits use short, descriptive subjects such as `Add movie history for site updates` and `Organize sync script and clean migration data`. Keep each commit focused and use an imperative, specific subject. Pull requests should explain the data flow affected, list validation commands, call out schema or migration implications, and include representative terminal output when matching or OCR behavior changes. Do not commit generated JSON, SQLite files, credentials, or Python caches.

View File

@ -0,0 +1,463 @@
# Gap-analys för reproducerbart facitdatabasschema
## 1. Omfattning och metod
Analysen är avgränsad till om facitdatabasens struktur kan återskapas från
versionshanterade filer. Följande har inspekterats:
- `docs/project-split-analysis.md` och
`docs/project-split-analysis-summary.md`,
- `scripts/schema.sql`,
- databasberoendena i `import_site.py`, `migrate_csv.py`, `check_ocr.py`,
`process_pending_ocr.py` samt de externa read-only-konsumenterna
`/storage/disk1/WCX-collection/scripts/match_filenames.py` och
`/storage/disk1/WCX-collection/scripts/diagnose_duration_match.py`,
- `sqlite_schema`, `PRAGMA table_xinfo`, `PRAGMA foreign_key_list`,
`PRAGMA index_list` och `PRAGMA index_xinfo` i
`/storage/disk1/WCX/database/wcx.db`, öppnad med `sqlite3 -readonly`, och
- resultatet av att läsa `scripts/schema.sql` i en separat SQLite-databas i
minnet.
**Verifierat faktum:** Produktionsdatabasen öppnades endast med
`sqlite3 -readonly`. Ingen kontroll eller verifiering skrev till den.
**Verifierat faktum:** `PRAGMA integrity_check` gav `ok` och
`PRAGMA foreign_key_check` gav inga rader. Både `PRAGMA user_version` och
`PRAGMA application_id` är `0`.
**Antagande:** Schemat som finns i den inspekterade `wcx.db` är den avsedda
produktionsmodellen. Analysen kan verifiera faktisk struktur och kodberoenden,
men inte om varje historiskt schemaingrepp var avsiktligt.
## 2. Fullständigt faktiskt produktionsschema
### 2.1 Tabeller
**Verifierat faktum:** Följande användardefinierade tabeller finns i
produktionen. SQL-definitionerna nedan är hämtade från `sqlite_schema`. Den
kompakta placeringen av OCR-kolumnerna på samma rad som `modified_at` är bara
hur SQLite lagrade den stegvis ändrade tabellens SQL-text; den ändrar inte
kolumnernas semantik.
```sql
CREATE TABLE movie (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
aka TEXT,
nationality TEXT,
age INTEGER,
shoot_location TEXT,
shoot_date TEXT,
duration_seconds INTEGER,
description TEXT,
rating REAL,
web_url TEXT,
thumbnail TEXT,
published TEXT,
updated TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
ocr_status TEXT NOT NULL DEFAULT 'pending',
ocr_raw_text TEXT,
ocr_error TEXT,
ocr_processed_at TEXT,
CHECK (age IS NULL OR age >= 0),
CHECK (rating IS NULL OR rating BETWEEN 0 AND 10),
CHECK (duration_seconds IS NULL OR duration_seconds >= 0)
);
CREATE TABLE movie_history (
history_id INTEGER PRIMARY KEY AUTOINCREMENT,
movie_id TEXT NOT NULL,
name TEXT NOT NULL,
aka TEXT,
nationality TEXT,
age INTEGER,
shoot_location TEXT,
shoot_date TEXT,
duration_seconds INTEGER,
description TEXT,
rating REAL,
web_url TEXT,
thumbnail TEXT,
published TEXT,
updated TEXT,
created_at TEXT,
modified_at TEXT,
ocr_status TEXT,
ocr_raw_text TEXT,
ocr_error TEXT,
ocr_processed_at TEXT,
archived_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
change_source TEXT NOT NULL DEFAULT 'site_import',
change_summary TEXT NOT NULL,
FOREIGN KEY (movie_id) REFERENCES movie(id)
);
CREATE TABLE movie_name_alias (
alias_id INTEGER PRIMARY KEY AUTOINCREMENT,
movie_id TEXT NOT NULL,
alias TEXT NOT NULL,
normalized_alias TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'manual',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (movie_id) REFERENCES movie(id),
UNIQUE (movie_id, normalized_alias)
);
```
**Verifierat faktum:** SQLite har dessutom skapat den interna tabellen
`sqlite_sequence(name, seq)` eftersom `movie_history.history_id` och
`movie_name_alias.alias_id` använder `AUTOINCREMENT`. Den ska inte deklareras
manuellt i projektets schema.
### 2.2 Index
**Verifierat faktum:** Produktionen innehåller följande explicita index:
```sql
CREATE INDEX idx_movie_history_movie_id
ON movie_history(movie_id);
CREATE INDEX idx_movie_history_archived_at
ON movie_history(archived_at);
CREATE INDEX idx_movie_name_alias_movie_id
ON movie_name_alias(movie_id);
CREATE INDEX idx_movie_name_alias_normalized
ON movie_name_alias(normalized_alias);
```
**Verifierat faktum:** SQLite har också skapat två unika automatiska index:
| Index | Ursprung | Nyckelkolumner | Partiellt |
| --- | --- | --- | --- |
| `sqlite_autoindex_movie_1` | `PRIMARY KEY` | `movie.id` | nej |
| `sqlite_autoindex_movie_name_alias_1` | `UNIQUE` | `movie_id`, `normalized_alias` | nej |
Alla sex index använder stigande ordning och `BINARY`-kollation. De fyra
explicita indexen är icke-unika och icke-partiella.
### 2.3 Constraints
**Verifierat faktum:** Produktionsschemat deklarerar:
- primärnyckel på `movie.id`,
- primärnyckel med `AUTOINCREMENT``movie_history.history_id`,
- primärnyckel med `AUTOINCREMENT``movie_name_alias.alias_id`,
- `NOT NULL` enligt SQL-definitionerna ovan,
- standardvärdena `CURRENT_TIMESTAMP`, `'pending'`, `'site_import'` och
`'manual'` enligt SQL-definitionerna ovan,
- `CHECK` för icke-negativ `age` och `duration_seconds` samt `rating` i
intervallet 010,
- främmande nyckel från `movie_history.movie_id` till `movie.id`, med
`NO ACTION` för både update och delete,
- främmande nyckel från `movie_name_alias.movie_id` till `movie.id`, med
`NO ACTION` för både update och delete, och
- unikhet för kombinationen
`movie_name_alias(movie_id, normalized_alias)`.
**Verifierat faktum:** Det finns ingen `CHECK`-constraint som begränsar
`ocr_status` till de statusvärden som koden använder (`pending`, `completed`,
`manual_review` och `failed`).
**Verifierat faktum:** Främmande nycklar är del av schemat, men SQLite kräver
att `PRAGMA foreign_keys = ON` aktiveras per anslutning för enforcement.
`import_site.py` aktiverar detta. De övriga inspekterade Python-skripten gör
det inte. Detta är ett runtime-beteende, inte en strukturell skillnad mellan
produktionsschemat och Git-schemat.
### 2.4 Triggers och views
**Verifierat faktum:** Produktionsdatabasen innehåller inga triggers och inga
views. Historisering görs uttryckligen av `import_site.py`, inte av en trigger.
## 3. Schema som kan återskapas från Git i dag
**Verifierat faktum:** När `scripts/schema.sql` läses i en tom SQLite-databas
skapas `movie`, `movie_history`, `movie_name_alias`, den interna
`sqlite_sequence`, samma fyra explicita index och samma två automatiska index
som i produktionen. Inga triggers eller views skapas.
Det återskapade Git-schemats `movie` är:
```sql
CREATE TABLE IF NOT EXISTS movie (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
aka TEXT,
nationality TEXT,
age INTEGER,
shoot_location TEXT,
shoot_date TEXT,
duration_seconds INTEGER,
description TEXT,
rating REAL,
web_url TEXT,
thumbnail TEXT,
published TEXT,
updated TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (age IS NULL OR age >= 0),
CHECK (rating IS NULL OR rating BETWEEN 0 AND 10),
CHECK (duration_seconds IS NULL OR duration_seconds >= 0)
);
```
Git-schemats `movie_history` och `movie_name_alias` är semantiskt identiska
med definitionerna i avsnitt 2.1. De omfattar redan samtliga fyra
OCR-kolumner i `movie_history`. Git skapar även exakt de fyra explicita index
som visas i avsnitt 2.2.
**Verifierat faktum:** `IF NOT EXISTS` i Git-filen men inte i den lagrade
produktionsdefinitionen är ingen strukturell skillnad i den skapade databasen.
**Verifierat faktum:** Git innehåller ingen versionsmarkör eller
migrationshistorik. En nyskapad databas får `user_version = 0`, precis som den
inspekterade produktionsdatabasen.
## 4. Exakt jämförelse
### 4.1 Skillnader
**Verifierat faktum:** Den fullständiga strukturella skillnaden är fyra
kolumner som finns i produktionens `movie` men saknas i Git-schemats `movie`:
| Position i produktion | Kolumn | Typ | `NOT NULL` | Standardvärde | Saknas från Git |
| ---: | --- | --- | --- | --- | --- |
| 16 | `ocr_status` | `TEXT` | ja | `'pending'` | ja |
| 17 | `ocr_raw_text` | `TEXT` | nej | inget | ja |
| 18 | `ocr_error` | `TEXT` | nej | inget | ja |
| 19 | `ocr_processed_at` | `TEXT` | nej | inget | ja |
Konsekvenserna är konkreta:
- nya `movie`-rader från `import_site.py` förlitar sig på standardvärdet
`'pending'`,
- OCR-kön kan inte läsa `ocr_status` eller `ocr_error`,
- OCR-resultat kan inte lagras, och
- `import_site.py` kan inte läsa en komplett rad för arkivering till
`movie_history`.
### 4.2 Element som inte skiljer sig
**Verifierat faktum:** Följande matchar exakt semantiskt mellan produktion och
en tom databas skapad från Git:
- tabellerna `movie_history` och `movie_name_alias`, inklusive samtliga
kolumner, ordning, typer, nullbarhet och standardvärden,
- alla `movie`-kolumner som föregår OCR-kolumnerna,
- samtliga tre `CHECK`-constraints på `movie`,
- båda främmande nycklarna och deras `NO ACTION`-beteende,
- unikhetskravet på `(movie_id, normalized_alias)`,
- samtliga fyra explicita index,
- båda automatiska unika indexen,
- avsaknaden av triggers, och
- avsaknaden av views.
**Verifierat faktum:** Inga tabeller, index, constraints, triggers eller views
finns endast i Git-schemat. Den interna tabellen `sqlite_sequence` uppstår i
båda fallen och är inte ett gap.
## 5. Skriptberoenden per schemaelement
### 5.1 `movie`
| Skript | Lästa eller skrivna element | Förutsättning |
| --- | --- | --- |
| `import_site.py` | Alla 20 produktionskolumner läses för befintliga poster; webbägda fält och `modified_at` uppdateras; nya poster infogas. | Kräver de fyra OCR-kolumnerna för `SELECT` och historisering. Nya poster förlitar sig på `ocr_status DEFAULT 'pending'`. Förlitar sig även på `movie_history`, dess OCR-kolumner och obligatoriska `change_summary`. Aktiverar främmande nycklar. |
| `migrate_csv.py` | Läser `id`; infogar eller uppdaterar alla metadatafält samt `ocr_status`; uppdaterar `modified_at`. | Kräver `ocr_status`. Anger status explicit och förlitar sig därför inte på dess default i sina egna inserts. Skriptet aktiverar inte främmande nycklar. |
| `check_ocr.py` | Läser `name`, `thumbnail`; skriver `nationality`, `shoot_location`, `shoot_date`, samtliga fyra OCR-kolumner och `modified_at`. | Kräver alla fyra OCR-kolumnerna. Använder statusvärdena `completed`, `manual_review` och `failed`, men schemat validerar inte statusdomänen. |
| `process_pending_ocr.py` | Läser `id`, `name`, `published`, `ocr_status`, `ocr_error`. | Kräver `ocr_status` och `ocr_error`; förutsätter att nya obehandlade poster får status `pending`. |
| `/storage/disk1/WCX-collection/scripts/match_filenames.py` | Läser `id`, `name`, `duration_seconds`. | Extern read-only-konsument; fungerar mot både produktions- och Git-schemat. |
| `/storage/disk1/WCX-collection/scripts/diagnose_duration_match.py` | Läser `id`, `name`, `duration_seconds`. | Externt manuellt diagnosverktyg som öppnar facitdatabasen read-only. |
### 5.2 `movie_history`
| Skript | Beroende |
| --- | --- |
| `import_site.py` | Infogar den föregående kompletta `movie`-raden, inklusive samtliga OCR-fält, samt `change_source` och `change_summary`. `archived_at` lämnas till standardvärdet. |
| `/storage/disk1/WCX-collection/scripts/match_filenames.py` | Läser `movie_id`, `duration_seconds` och `archived_at`. |
| `/storage/disk1/WCX-collection/scripts/diagnose_duration_match.py` | Läser `movie_id` och `duration_seconds`. |
**Verifierat faktum:** Inget skript förutsätter en historiseringstrigger;
`import_site.py` gör historik-insert och film-update i samma anslutningskontext.
### 5.3 `movie_name_alias`
| Skript | Beroende |
| --- | --- |
| `/storage/disk1/WCX-collection/scripts/match_filenames.py` | Läser `movie_id`, `alias`, `normalized_alias` och `source`. |
**Verifierat faktum:** Inget inspekterat skript skriver alias. Constraints och
index på aliastabellen finns ändå likadant i produktion och Git.
## 6. Minsta säkra åtgärd
**Rekommendation:** Gör en enda avgränsad schemaändring: lägg till de fyra
verifierade produktionskolumnerna i `movie` i `scripts/schema.sql`, direkt
efter `modified_at`, med exakt följande definitioner:
```sql
ocr_status TEXT NOT NULL DEFAULT 'pending',
ocr_raw_text TEXT,
ocr_error TEXT,
ocr_processed_at TEXT,
```
Detta är den minsta åtgärden eftersom alla andra tabeller, index och
constraints redan reproduceras. Lägg inte samtidigt till triggers, views,
nya index, status-`CHECK`, ändrad främmande-nyckelpolicy eller andra
modellförbättringar; sådana ändringar skulle gå utöver att återskapa dagens
produktion.
**Rekommendation:** Behandla den korrigerade `schema.sql` som ett komplett
grundschema för helt nya databaser. Verifiera det först mot en ny temporär
databas. Kör inte den kompletta filen som en påstådd reparationsmigration mot
produktion: `CREATE TABLE IF NOT EXISTS` kompletterar inte kolumner i en redan
existerande tabell.
**Antagande:** Produktionsdatabasen behöver ingen strukturändring för detta
gap, eftersom den redan har exakt de fyra föreslagna kolumnerna. Den behöver
endast adopteras av en framtida migrationsmodell efter separat kontroll.
## 7. Enkel migrationsmodell för SQLite
**Rekommendation:** Inför en liten, versionshanterad modell med två delar:
1. `schema.sql` är aktuell baseline för nya, tomma databaser.
2. En katalog med ordnade migrationer, exempelvis
`migrations/0001_add_movie_ocr_columns.sql`, beskriver övergångar för äldre
databaser.
Använd en liten Python-runner baserad på standardbibliotekets `sqlite3` och en
tabell som exempelvis:
```sql
CREATE TABLE schema_migration (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
Runnern bör:
- öppna en explicit vald databas och aldrig ha produktionssökvägen som dold
teststandard,
- aktivera `PRAGMA foreign_keys = ON`,
- kontrollera befintliga kolumner med `PRAGMA table_xinfo` före migration,
- köra varje ej registrerad migration atomärt i en transaktion,
- registrera versionen först när hela migrationen lyckats,
- avbryta om databasen har en okänd eller partiell struktur, och
- köra `PRAGMA foreign_key_check` efteråt.
**Rekommendation:** Eftersom SQLite saknar portabelt
`ALTER TABLE ... ADD COLUMN IF NOT EXISTS` ska migration `0001` inte bara
exekveras blint. Runnern ska skilja mellan tre tillstånd:
- inga OCR-kolumner finns: lägg till alla fyra och registrera migrationen,
- alla fyra finns med exakt rätt typ, nullbarhet och default: registrera
migrationen som adopterad utan att ändra tabellen, och
- endast några finns eller definitionerna avviker: avbryt och kräv manuell
analys.
Exempel på de faktiska övergångssatserna för ett äldre Git-skapat schema är:
```sql
ALTER TABLE movie
ADD COLUMN ocr_status TEXT NOT NULL DEFAULT 'pending';
ALTER TABLE movie
ADD COLUMN ocr_raw_text TEXT;
ALTER TABLE movie
ADD COLUMN ocr_error TEXT;
ALTER TABLE movie
ADD COLUMN ocr_processed_at TEXT;
```
**Rekommendation:** Använd antingen `schema_migration` eller konsekvent
`PRAGMA user_version` som versionskälla, inte två oberoende sanningar. Tabellen
är enklare att granska eftersom den även lagrar migrationsnamn och tidpunkt.
Nuvarande `user_version = 0` visar att ingen versionsmodell används i dag.
## 8. Verifieringsplan utan produktionsskrivningar
Alla steg nedan ska använda en ny katalog skapad med `mktemp -d` och en ny
databasfil där. Ingen testparameter får peka på
`/storage/disk1/WCX/database/wcx.db`.
1. **Skydda referensen.** Registrera produktionsdatabasens filstorlek, mtime
och kryptografiska hash före testet. Använd därefter endast
`sqlite3 -readonly` om referensmetadata behöver läsas igen.
2. **Skapa baseline.** Skapa `${TEMP_DIR}/wcx.db` från den korrigerade
`schema.sql`.
3. **Jämför struktur maskinellt.** Jämför tabeller, kolumner, ordning, typer,
nullbarhet, defaults, primärnyckelflaggor, främmande nycklar, explicita och
automatiska index, indexkolumner, triggers och views mot den dokumenterade
produktionsmodellen. Ignorera endast harmlösa skillnader i SQL-format och
`IF NOT EXISTS`.
4. **Kontrollera constraints.** Verifiera att ogiltig negativ `age` och
`duration_seconds`, rating utanför 010, dubblett av
`(movie_id, normalized_alias)` samt främmande nyckelbrott med
`PRAGMA foreign_keys = ON` nekas i temporärdatabasen.
5. **Kontrollera defaults.** Infoga en minimal `movie(id, name)` och verifiera
att `created_at` och `modified_at` sätts samt att `ocr_status = 'pending'`
och övriga OCR-fält är `NULL`.
6. **Testa import.** Kör `import_site.py` med explicit `--input` till en liten
temporär JSON-fil och explicit `--database` till temporärdatabasen. Kör en
andra import med nyare uppdateringsdatum och verifiera att
`movie_history` får hela den föregående raden inklusive OCR-fälten.
7. **Testa CSV-operationer.** Kör först `migrate_csv.py --dry-run`. Om apply
behöver provas, använd endast explicit `--database` till temporärdatabasen
och en isolerad test-CSV.
8. **Testa OCR:s databasberoenden utan API-anrop.** Anropa de rena
databasfunktionerna i `check_ocr.py` mot temporärdatabasen för statusfallen
failed, manual review och completed. Verifiera alla skrivna metadata- och
OCR-kolumner. Testa `process_pending_ocr.py`-frågorna separat så att
`pending` kan väljas och `ocr_error` läsas. Gör inget Vision API-anrop.
9. **Testa konsumentfrågorna.** Kör matchningskodens databasladdning och
längddiagnostik mot temporärdatabasen efter att film, alias och historik har
lagts in.
10. **Testa migrationsvägen separat.** Skapa ytterligare en ny temporär
databas från dagens ofullständiga Git-schema, kör migration `0001`, och
upprepa strukturjämförelsen. Testa även adoption mot en ny databas som
redan har alla fyra korrekta kolumner samt avbrott mot ett medvetet
partiellt schema.
11. **Kör integritetskontroller.** Kräv `PRAGMA integrity_check = 'ok'` och
tomt resultat från `PRAGMA foreign_key_check` i varje temporärdatabas.
12. **Bekräfta produktionsskyddet.** Kontrollera att produktionens storlek,
mtime och hash är identiska med värdena från steg 1. Radera därefter endast
den explicit skapade temporärkatalogen.
**Rekommendation:** Godkänn reproducerbarheten först när både baseline-vägen
och migrationsvägen ger samma semantiska schema som avsnitt 2 och samtliga
databasberoende skript klarar sina isolerade operationer mot temporärdata.
## 9. Slutsats
**Verifierat faktum:** Facitdatabasen är nästan, men inte helt,
reproducerbar från Git. Gapet består exakt av fyra OCR-kolumner i `movie`.
Inga tabeller, index, constraints, triggers eller views saknas utöver dessa
kolumndefinitioner, och inga sådana objekt skiljer sig i övrigt.
**Rekommendation:** Komplettera baselineschemat med de fyra exakta
produktionsdefinitionerna och inför en liten, transaktionell migrationsrunner
som kan migrera äldre Git-skapade databaser och adoptera en redan korrekt
produktion utan att ändra den. All utveckling och verifiering ska ske mot nya
temporära databaser.

View File

@ -1,3 +1,20 @@
# Historisk implementationsprompt för filnamnsmatchning
## Status efter projektuppdelningen
Detta dokument är den ursprungliga implementationsprompten från tiden före
projektuppdelningen och bevaras endast som historiskt källmaterial. Gamla
sökvägar och framtidsformuleringar nedan beskriver dåvarande repositorystruktur
och är inte aktuella instruktioner.
Den implementerade filnamnsmatchningen finns nu i
`/storage/disk1/WCX-collection/scripts/match_filenames.py`, och dess framtida
tester hör hemma under `/storage/disk1/WCX-collection/tests/`.
WCX-collection är en separat extern konsument som öppnar facitdatabasen strikt
read-only med SQLite `mode=ro`; facitprojektet är fortsatt ensam skrivare.
## Ursprunglig prompt
Jag vill implementera en första, skrivskyddad version av funktionaliteten som beskrivs i: Jag vill implementera en första, skrivskyddad version av funktionaliteten som beskrivs i:
```text ```text

View File

@ -1,892 +0,0 @@
# Kravspecifikation: Matchning av lokala videofiler mot WCX-databasen
## 1. Syfte
Funktionen ska identifiera vilken post i WCX-databasen en lokal videofil sannolikt motsvarar.
Databasen innehåller filmens kanoniska namn enligt siten. Namnet består alltid av ett eller flera personnamn och inga andra beskrivande ord.
Lokala filnamn är däremot ofta inkonsekventa och kan innehålla:
* sammanfogade namn
* understreck eller andra separatorer
* upprepade namn
* felstavningar
* extra beskrivande ord
* tekniska eller innehållsrelaterade suffix
* flera personnamn
* inkonsekvent användning av stora och små bokstäver
Exempel:
```text
Alyn_Borav_AlynBorav.mp4
AngeliqueLapiedra_ScarlettLapiedra.mp4
BrendaBoop_Interwiev_Fisting.mp4
Busty_Slovakian_Ilona_On_Bed.mp4
Eden_Ivy_EdenIvy_DP.mp4
Version_AngelikaFyres_DP.mp4
```
Dessa ska kunna matchas mot databasnamn som:
```text
Alyn Borav
Angelique Lapiedra + Scarlett Lapiedra
Brenda Boop
Ilona
Eden Ivy
Angelika Fyres
```
Funktionen ska prioritera korrekthet framför täckning. En fil som inte kan identifieras med tillräcklig säkerhet ska lämnas för manuell inspektion.
---
## 2. Grundprinciper
Matchningen ska vara konservativ.
Funktionen ska inte försöka automatcha varje fil. Den ska klassificera resultatet i tre huvudkategorier:
```text
matched
ambiguous
unmatched
```
Betydelse:
* `matched`: en kandidat är tillräckligt stark och tydligt bättre än övriga kandidater
* `ambiguous`: en eller flera rimliga kandidater finns, men säkerheten är inte tillräcklig
* `unmatched`: ingen rimlig kandidat kunde identifieras
En felaktig automatisk match är allvarligare än att en fil lämnas omatchad.
---
## 3. Datakällor
Funktionen ska använda följande datakällor.
### 3.1 Aktuella filmposter
Från tabellen `movie`:
```text
id
name
duration_seconds
```
Övriga fält kan senare användas, men krävs inte för den första versionen.
### 3.2 Historiska filmposter
Från tabellen `movie_history`:
```text
movie_id
name
duration_seconds
archived_at
change_summary
```
Historiska längder kan användas för att identifiera att en lokal fil motsvarar en äldre version av samma film.
### 3.3 Lokala videofiler
För varje fil ska minst följande information läsas:
```text
path
filename
file extension
actual duration
```
Den faktiska längden hämtas med `ffprobe`.
---
## 4. Namnmodell
Databasfältet `movie.name` innehåller alltid ett eller flera personnamn.
Exempel med en person:
```text
Alyn Borav
Ilona
Eden Ivy
```
Exempel med flera personer:
```text
Angelique Lapiedra + Scarlett Lapiedra
Susanna Melo + Cherry Sweet
```
Plustecknet ska betraktas som separator mellan personer.
Varje databaspost ska därför kunna representeras som:
```text
movie name
list of person names
normalized full name
normalized person names
```
Exempel:
```text
Movie name:
Angelique Lapiedra + Scarlett Lapiedra
Persons:
- Angelique Lapiedra
- Scarlett Lapiedra
Normalized persons:
- angeliquelapiedra
- scarlettlapiedra
Normalized full name:
angeliquelapiedrascarlettlapiedra
```
---
## 5. Normalisering av namn
Filnamn och databasnamn ska normaliseras innan jämförelse.
Normaliseringen ska minst:
1. ta bort filändelsen
2. konvertera till gemener
3. ta bort mellanslag
4. ta bort understreck
5. ta bort bindestreck
6. ta bort plustecken
7. ta bort punkter och annan interpunktion
8. normalisera diakritiska tecken
9. behålla bokstäver och siffror
Exempel:
```text
Alyn_Borav_AlynBorav.mp4
→ alynboravalynborav
```
```text
Angelique Lapiedra + Scarlett Lapiedra
→ angeliquelapiedrascarlettlapiedra
```
```text
CatherineBoss.mp4
→ catherineboss
```
```text
Catherine Boss
→ catherineboss
```
---
## 6. Matchning av filnamn
### 6.1 Exakt normaliserad delsträng
Om ett normaliserat databasnamn förekommer exakt i det normaliserade filnamnet ska detta räknas som en stark signal.
Exempel:
```text
Fil:
Eden_Ivy_EdenIvy_DP.mp4
Normaliserad fil:
edenivyedenivydp
Databasnamn:
Eden Ivy
Normaliserat namn:
edenivy
```
`edenivy` förekommer två gånger och ska ge en stark matchning.
### 6.2 Upprepade namn
Upprepade personnamn i filnamnet ska inte tolkas som flera personer.
Exempel:
```text
Vanessa_Rodriguez_VanessaRodriguez.mp4
```
ska fortfarande motsvara personen:
```text
Vanessa Rodriguez
```
Upprepning kan däremot stärka matchningen.
### 6.3 Extra ord i filnamnet
Extra ord behöver inte tas bort för att exakt delsträngsmatchning ska fungera.
Exempel:
```text
BrendaBoop_Interwiev_Fisting.mp4
```
kan matchas mot:
```text
Brenda Boop
```
eftersom det normaliserade namnet förekommer intakt i filnamnet.
Exempel på vanliga extra ord:
```text
DP
Group
Interview
Fisting
OralSex
TruFun67
Version
OnBed
PissingInMouth
```
Dessa ska initialt behandlas som okänd extrainformation och inte som del av databasnamnet.
### 6.4 Korta namn
Korta namn som exempelvis:
```text
Ilona
Mia
Eva
Ana
```
kräver större försiktighet eftersom de kan förekomma som delar av andra ord.
För korta namn ska tokenbaserad matchning väga tyngre än ren delsträngsmatchning.
### 6.5 Flera personer
Om en databaspost innehåller flera personer ska samtliga namn kunna sökas oberoende av ordning.
Exempel:
```text
Angelique Lapiedra + Scarlett Lapiedra
```
ska kunna matcha både:
```text
AngeliqueLapiedra_ScarlettLapiedra.mp4
```
och:
```text
ScarlettLapiedra_AngeliqueLapiedra.mp4
```
Matchningen ska därför inte kräva att personerna förekommer i samma ordning som i databasen.
### 6.6 Längsta och mest specifika kandidat
Om flera databasnamn matchar ska den mest specifika kandidaten prioriteras.
Exempel:
```text
Scarlett Knight
Scarlett Knight + Anya Shidlerova
```
Fil:
```text
ScarlettKnight_AnyaShidlerova.mp4
```
Posten med båda personerna ska rankas högre.
### 6.7 Exakt personmängd
När flera kända personer identifieras i filnamnet ska databaskandidater med exakt samma personmängd prioriteras.
Exempel:
```text
Filnamn:
Susanna_Melo_Cherry_Sweet.mp4
Identifierade personer:
- Susanna Melo
- Cherry Sweet
```
Följande kandidat ska rankas högre:
```text
Susanna Melo + Cherry Sweet
```
än:
```text
Susanna Melo
```
---
## 7. Stavfel och fuzzy matching
Filnamn kan innehålla stavfel.
Exempel:
```text
SusannaMello
Susamma_Melo
```
kan sannolikt motsvara:
```text
Susanna Melo
```
Fuzzy matching får användas för att skapa kandidater, men ska vara konservativt.
I den första versionen gäller:
* fuzzy matching får inte ensam leda till automatisk match
* fuzzy-resultat ska normalt klassificeras som `ambiguous`
* kandidat och orsak ska visas för manuell bedömning
Exempel:
```text
Filename:
SusannaMello.mp4
Candidate:
Susanna Melo
Status:
ambiguous
Reason:
fuzzy name match
```
Verifierade stavningsvarianter kan senare sparas som alias.
Ett alias ska endast registreras efter manuell bekräftelse.
---
## 8. Längd som sekundär signal
Videons faktiska längd ska användas för att stärka eller försvaga redan rimliga namnkandidater.
Längden ska inte ensam kunna identifiera en film.
Exempel:
```text
File:
Alyn_Borav_AlynBorav.mp4
File duration:
43:00
```
Kandidater:
```text
Alyn Borav
Site duration: 43:05
Difference: 5 seconds
```
```text
Alyn Borav + Cherry Sweet
Site duration: 1:54:00
Difference: 71 minutes
```
Den första kandidaten ska då få betydligt högre poäng.
### 8.1 Absolut och relativ skillnad
Både absolut skillnad och procentuell skillnad ska beräknas.
```text
absolute difference = abs(file duration - site duration)
relative difference = absolute difference / site duration
```
### 8.2 Initial poängmodell för längd
En första approximation kan vara:
```text
≤ 15 sekunder eller ≤ 1 % mycket stark signal
≤ 60 sekunder eller ≤ 2 % stark signal
≤ 3 minuter eller ≤ 5 % rimlig signal
≤ 10 minuter eller ≤ 10 % svag signal
> 10 minuter och > 10 % negativ signal
```
Exakt poängsättning ska kunna justeras efter testning mot verkliga filer.
### 8.3 Längd får inte rädda en dålig namnmatch
Exempel:
```text
unknown_video.mp4
```
ska inte automatchas till en film bara för att längden råkar vara nästan identisk.
Längd används endast för att diskriminera mellan kandidater som redan har en rimlig namnmatchning.
### 8.4 Klippta och förlängda versioner
En större längdskillnad ska inte automatiskt innebära att kandidaten är fel.
Filen kan vara:
```text
truncated
extended
edited
missing intro
missing ending
```
En stark namnmatchning kombinerad med tydlig längdavvikelse ska normalt ge:
```text
ambiguous
```
eller en särskild versionsklassificering, inte automatiskt `unmatched`.
---
## 9. Historiska längder
Matchningen ska kunna jämföra filens längd mot både aktuell och historisk sitelängd.
Exempel:
```text
Current site duration: 63:05
Historical duration: 43:05
Local file duration: 43:00
```
Möjlig slutsats:
```text
Filmen matchar sannolikt rätt databaspost men motsvarar en äldre version.
```
Möjliga versionsklassificeringar:
```text
current_version_match
historical_version_match
possible_truncated_version
possible_extended_version
duration_mismatch
unknown_version
```
Historisk längd ska vara en stödjande signal och inte ensam avgöra filmidentiteten.
---
## 10. Kandidatpoäng
Varje databaspost ska kunna få en sammanlagd poäng baserad på flera signaler.
Exempel på positiva signaler:
```text
exakt normaliserat fullständigt namn
samtliga personnamn hittade
exakt personmängd
namnet förekommer flera gånger
längden ligger mycket nära aktuell sitelängd
längden ligger mycket nära historisk sitelängd
stor marginal till kandidat nummer två
```
Exempel på negativa signaler:
```text
bara en del av namnet hittades
fuzzy matching krävdes
filnamnet innehåller ytterligare kända personer
databasposten förväntar flera personer men endast en hittas
stor skillnad i längd
kort och osäker namnträff
flera kandidater har nästan samma poäng
```
Poängmodellen ska vara förklarbar. Resultatet ska visa vilka signaler som påverkade poängen.
---
## 11. Säkerhetskrav för automatisk matchning
En fil får klassificeras som `matched` endast om:
1. bästa kandidaten uppnår ett fastställt minimikrav
2. kandidaten bygger på en tillräckligt stark namnmatchning
3. kandidaten ligger tydligt före näst bästa kandidat
4. inga motstridiga personsignaler finns
5. eventuell fuzzy matching inte är den enda avgörande signalen
Exempel på säker match:
```text
Candidate 1: 170
Candidate 2: 45
Margin: 125
```
Exempel på osäker match:
```text
Candidate 1: 92
Candidate 2: 87
Margin: 5
```
Det senare ska klassificeras som:
```text
ambiguous
```
även om förstaplatsen har relativt hög poäng.
---
## 12. Manuell inspektion
Filer som klassificeras som `ambiguous` eller `unmatched` ska kunna granskas manuellt.
För `ambiguous` ska rapporten visa de bästa kandidaterna.
Exempel:
```text
Filename:
SusannaMello_Cherry.mp4
Status:
ambiguous
Candidate 1:
Susanna Melo + Cherry Sweet
Score: 91
Candidate 2:
Susanna Melo
Score: 86
Reasons:
- fuzzy match for Susanna Melo
- partial match for Cherry Sweet
- candidates have similar scores
```
Manuell granskning ska kunna resultera i:
```text
confirmed match
rejected candidates
verified alias
ignored file
```
Manuella beslut ska kunna sparas senare, men första versionen behöver endast rapportera kandidater.
---
## 13. Första versionens omfattning
Den första versionen ska vara helt skrivskyddad.
Den får:
* läsa filer från en katalog
* läsa poster från SQLite-databasen
* läsa historiska längder
* köra `ffprobe`
* normalisera filnamn
* rangordna kandidater
* skriva rapporter
Den får inte:
* byta namn på filer
* flytta filer
* radera filer
* ändra databasen
* automatiskt registrera alias
* permanent koppla filer till filmer
---
## 14. Förväntat kommando
Exempel:
```bash
scripts/match_filenames.py /path/to/video/files
```
Alternativt stöd för en textfil med filnamn:
```bash
scripts/match_filenames.py \
--file-list filenames.txt
```
Möjliga framtida argument:
```text
--database
--recursive
--limit
--output-directory
--min-score
--min-margin
--debug
--no-ffprobe
```
---
## 15. Rapportformat
Resultatet bör delas upp i minst tre rapporter:
```text
matched.csv
ambiguous.csv
unmatched.csv
```
### 15.1 matched.csv
Föreslagna kolumner:
```text
path
filename
file_duration_seconds
movie_id
movie_name
site_duration_seconds
matched_duration_seconds
version_classification
score
score_margin
match_reason
```
### 15.2 ambiguous.csv
Föreslagna kolumner:
```text
path
filename
file_duration_seconds
candidate_1_id
candidate_1_name
candidate_1_score
candidate_2_id
candidate_2_name
candidate_2_score
reason
```
### 15.3 unmatched.csv
Föreslagna kolumner:
```text
path
filename
file_duration_seconds
best_candidate
best_score
reason
```
---
## 16. Konsolutskrift
En läsbar konsolrapport ska också kunna visas.
Exempel:
```text
Alyn_Borav_AlynBorav.mp4
File duration: 43:00
1. Alyn Borav
Name score: 120
Duration score: 50
Total: 170
Duration diff: 5 sec
2. Alyn Borav + Cherry Sweet
Name score: 65
Duration score: -30
Total: 35
Duration diff: 71 min
Result: matched
Version: current_version_match
```
Exempel på osäker fil:
```text
SusannaMello_Cherry.mp4
File duration: 42:58
1. Susanna Melo + Cherry Sweet
Total: 91
2. Susanna Melo
Total: 86
Result: ambiguous
Reason: fuzzy match and insufficient score margin
```
---
## 17. Prestanda
Filnamnsmatchningen ska kunna genomföras utan att läsa videofilernas innehåll.
`ffprobe` ska endast läsa metadata och ska inte omkoda eller spela upp videon.
Första versionen behöver inte använda:
* fullständiga filhashar
* perceptuella videofingeravtryck
* bildextraktion
* maskininlärning
* OCR från videobilder
Sådana funktioner kan utvärderas senare för svåra fall.
---
## 18. Framtida utökningar
Möjliga senare förbättringar:
### 18.1 Alias
Verifierade stavningsvarianter kan sparas:
```text
susannamello → Susanna Melo
susammamelo → Susanna Melo
```
Alias ska endast skapas efter manuell bekräftelse.
### 18.2 Filtabell
Lokala filer kan senare lagras i en tabell, exempelvis:
```text
media_file
```
En film ska kunna ha flera lokala filer eftersom olika omkodningar och versioner kan förekomma.
### 18.3 Manuella beslut
Manuellt bekräftade matchningar ska inte behöva lösas igen.
### 18.4 Visuella fingeravtryck
För svåra fall kan ett perceptuellt videofingeravtryck skapas genom att:
* extrahera bilder med intervall
* skala ned bilderna
* beräkna perceptuella hashvärden
* jämföra sekvenser av hashvärden
Detta ligger utanför den första versionen.
---
## 19. Acceptanskriterier för första versionen
Den första versionen är godkänd när den kan:
1. läsa alla namn och längder från databasen
2. dela flerpersonsnamn på `+`
3. normalisera databasnamn och filnamn
4. hitta exakta normaliserade namn i smutsiga filnamn
5. hantera upprepade namn
6. hantera namn utan separatorer
7. prioritera den mest specifika flerpersonsposten
8. läsa faktisk videolängd med `ffprobe`
9. jämföra mot aktuell sitelängd
10. jämföra mot historiska sitelängder
11. rangordna kandidater
12. klassificera filer som `matched`, `ambiguous` eller `unmatched`
13. lämna osäkra filer för manuell inspektion
14. skriva separata rapporter
15. inte ändra filer eller databas
Det viktigaste kvalitetsmåttet är:
```text
precision bland automatiskt matchade filer
```
Målet är nära noll felaktiga automatiska matchningar, även om det innebär att en betydande andel filer lämnas för manuell granskning.

View File

@ -0,0 +1,101 @@
# Sammanfattning: uppdelning av WCX-projektet
## Databasschemarisken
Den viktigaste risken före en projektuppdelning är att det versionshanterade
databasschemat inte återskapar den databas som produktionen faktiskt använder.
`database/wcx.db` innehåller OCR-kolumnerna `ocr_status`, `ocr_raw_text`,
`ocr_error` och `ocr_processed_at` i tabellen `movie`, men kolumnerna saknas i
`scripts/schema.sql`. Samtidigt förutsätter OCR-skripten och dokumentationen att
de finns.
En ny databas skapad enbart från Git blir därför inte kompatibel med hela
facitflödet. Efter en uppdelning kan detta se ut som ett fel i den flyttade
koden, trots att grundorsaken är ett ofullständigt schema. Facitschemat måste
därför göras komplett och reproducerbart innan filer eller drift flyttas.
Ändringen ska utvecklas och verifieras mot en ny, disposable databas och inte
tillämpas blint på `database/wcx.db`.
Filhanteringen har dessutom ett direkt kontrakt mot tabellerna `movie`,
`movie_name_alias` och `movie_history`. Inkompatibla ändringar i dessa tabeller
kan bryta det andra projektet. Detta läskontrakt behöver dokumenteras och
versionshanteras eller åtminstone omfattas av en tydlig ändringsprocess.
## Föreslagen målarkitektur
Repositoryt delas i två självständiga Git-projekt:
1. **Facitprojektet** äger schema, migrationsdata, webbsynkronisering, import,
OCR och schemalagd uppdatering. Det är ensam ägare av och enda skrivare till
tabellerna `movie`, `movie_history` och `movie_name_alias`.
2. **Filhanteringsprojektet** äger filinventering, `ffprobe`-integration,
matchningskod, tester och matchningsdokumentation. Det läser facitdatabasen
genom en konfigurerbar SQLite-sökväg och ansluter strikt skrivskyddat med
`mode=ro`. Filsystemsrättigheter bör också neka skrivning där det är
praktiskt möjligt.
Gränsen mellan projekten bör inledningsvis vara ett litet, dokumenterat
SQLite-läskontrakt. Ett API eller gemensamt Python-bibliotek behövs inte för den
nuvarande lokala användningen. Om filhanteringen senare behöver lagra lokala
filsökvägar, manuella beslut eller bearbetningsstatus ska den få en egen
databas. Den får referera till ett stabilt `movie.id`, men ska aldrig skriva
lokalt tillstånd i facitdatabasen.
Runtime-data ska hållas tydligt åtskild från Git-spårad kod. Den genererade
`import/wcx_site_index.json` bör behandlas enligt ett uttryckligt beslut om
runtime-data och normalt inte följa med som källfil till ett nytt repository.
## Beslut före implementation
Följande behöver avgöras innan den fysiska uppdelningen:
- Ska ett korrigerat komplett grundschema vara tillräckligt, eller behövs även
en versionshanterad migrationsmekanism för framtida schemaändringar?
- Var ska facitdatabasen ligga: i facitprojektets runtime-katalog eller i en
separat, stabil datakatalog?
- Ska projekten köras med samma operativsystemkonto, eller ska
filhanteringsprocessen få ett separat konto utan skrivrättighet till facit?
- Hur versionssätts läskontraktet, och hur samordnas inkompatibla
schemaändringar mellan projekten?
- Är `movie.id` tillräckligt stabilt för framtida externa referenser från en
separat filhanteringsdatabas?
- Ska `import/wcx_site_index.json` tas bort ur Git-indexet och endast genereras
vid körning?
- Var finns de externa systemd-enheterna, och vilka sökvägar, miljövariabler,
rättigheter och låsfiler måste ändras?
- Vilka namn och installationsplatser ska de två nya Git-projekten ha?
- Om produktionsdatabasen ska flyttas: vilken slutlig runtime-plats,
backupmetod och rättighetsmodell ska användas?
Direkt SQLite-läsning antas ge tillräcklig samtidighet och tillgänglighet. Om
det antagandet inte gäller behöver arkitekturen omprövas innan implementation.
## Rekommenderad migreringsordning
1. **Fastställ ansvar och läskontrakt.** Dokumentera att facitprojektet är ensam
skrivare samt exakt vilka tabeller och kolumner filhanteringen får läsa.
2. **Gör schemat reproducerbart.** Komplettera schemahanteringen och verifiera
en helt ny disposable databas, inklusive import- och OCR-operationer. Lämna
produktionsdatabasen orörd.
3. **Bestäm hanteringen av genererade filer.** Rätta Git-index och
dokumentation om import-JSON ska vara ren runtime-data.
4. **Gör sökvägar flyttbara i nuvarande repository.** Behåll explicita
kommandoradsargument, gör databassökvägar konfigurerbara och använd relativa
skriptsökvägar där det passar. Kontrollera även verklig systemd-konfiguration.
5. **Tvinga fram read-only.** Byt filhanteringens anslutningar till SQLite URI
med `mode=ro` och verifiera både normal läsning och att skrivning nekas.
Använd inte `immutable=1` för en databas som uppdateras samtidigt.
6. **Separera dokumentation och filstruktur logiskt.** Gör respektive projekt
självständigt begripligt och verifiera att gamla absoluta sökvägar är borta.
7. **Skapa de två Git-projekten.** Flytta först när schema, konfiguration och
read-only-gräns är verifierade. Testa facitimport och filmatchning separat
och behåll det gamla repositoryt tills båda fungerar.
8. **Flytta drift och systemd sist.** Kör facit manuellt från den nya platsen
före schemalagd produktionsdrift. Använd en säker SQLite-backupmetod om
databasen måste flyttas medan systemet är i bruk.
9. **Inför en separat filhanteringsdatabas endast vid konkret behov.** Detta är
en senare funktionsändring, inte en förutsättning för projektuppdelningen.
Schemaändring, read-only-förstärkning, filflytt och produktionsdrift bör göras
som separata, granskningsbara förändringar så att eventuella fel går att
isolera.

View File

@ -0,0 +1,575 @@
# Analys och migreringsplan för uppdelning av WCX-projektet
## Status efter uppdelningen
Detta är en historisk analys som togs fram före projektuppdelningen.
`match_filenames.py` och dokumentationen för filnamnsmatchning har nu flyttats
till det separata repositoryt `/storage/disk1/WCX-collection`. WCX-collection
läser facitdatabasen strikt read-only, medan facitprojektet fortsatt är ensam
skrivare till databasen. Dokumentets rekommendationer, nulägesbeskrivningar och
framtidsformuleringar ska därför läsas som historisk bakgrund, inte som en
aktuell arbetsplan.
## 1. Syfte och avgränsning
Detta dokument beskriver hur det nuvarande WCX-repositoryt kan delas upp i
två separata Git-projekt:
1. ett facitprojekt som underhåller och äger filmmetadata i SQLite, och
2. ett filhanteringsprojekt som inventerar och matchar lokala videofiler.
Den grundläggande ägargränsen är att endast facitprojektet får skriva till
facitdatabasen. Filhanteringsprojektet får läsa den direkt, men anslutningen
ska vara strikt skrivskyddad. Filhanteringen kan senare få en egen databas för
lokala filer och matchningsbeslut.
Ett API eller ett gemensamt Python-bibliotek ingår inte i den föreslagna
lösningen. Ett sådant lager bör införas först om konkreta framtida behov visar
att direkt, skrivskyddad SQLite-läsning inte räcker.
Märkningarna i dokumentet betyder:
- **Verifierat faktum:** observerat i nuvarande repository eller databas.
- **Antagande:** rimlig utgångspunkt som behöver bekräftas.
- **Rekommendation:** föreslagen riktning, inte ett redan fattat beslut.
- **Ej fattat beslut:** en fråga som måste avgöras före eller under migreringen.
## 2. Nulägesanalys
### 2.1 Repositorystruktur
**Verifierat faktum:** Repositoryt innehåller följande huvudsakliga delar:
```text
WCX/
├── README.md
├── database/
│ ├── wcx.db
│ ├── wcx-test.db
│ ├── wcx-before-csv-migration.db
│ ├── update_wcx.lock
│ └── last_scheduled_update
├── import/
│ └── wcx_site_index.json
├── migration/
│ └── persons.utf8bom.csv
├── docs/
│ ├── filename-matching-requirements.md
│ └── filename-matching-requirements-prompt.md
└── scripts/
├── schema.sql
├── wcx_sync.py
├── import_site.py
├── migrate_csv.py
├── update_wcx.sh
├── scheduled_update_wcx.sh
├── process_pending_ocr.py
├── check_ocr.py
├── ocr.sh
├── parse_ocr.py
├── match_filenames.py
└── test_duration_matching.py
```
**Verifierat faktum:** Produktionsdatabasen och databasens test- och
säkerhetskopior finns lokalt under `database/` och matchar repositoryts
ignore-regler för `database/*.db`.
**Verifierat faktum:** `import/wcx_site_index.json` är spårad av Git trots att
README anger att `import/*.json` är genererade filer som inte ska
versionshanteras. En ignore-regel slutar inte spåra en fil som redan finns i
Git-indexet.
**Verifierat faktum:** README beskriver huvudsakligen uppbyggnad och underhåll
av facitdatabasen. Kraven för filnamnsmatchning ligger separat under `docs/`.
### 2.2 Identifierade ansvarsområden
Facitunderhåll omfattar:
- hämtning av webbplatsens metadata,
- import och historisering av webbplatsdata,
- initialt schema och framtida schemaförändringar,
- återställning eller komplettering från historisk CSV,
- OCR och validering av OCR-resultat,
- manuellt eller automatiskt underhållen filmmetadata,
- schemalagd körning och exklusiv rätt att skriva facitdatabasen.
Filhantering omfattar:
- inventering av lokala videofiler,
- läsning av filmetadata med `ffprobe`,
- normalisering och matchning av filnamn,
- jämförelse med aktuella namn, alias och historiska längder,
- rapportering av säkra, tvetydiga och uteblivna matchningar,
- senare eventuell lagring av lokal filstatus i en separat databas.
**Verifierat faktum:** Nuvarande implementation har redan en naturlig gräns:
facitskripten skriver SQLite-data, medan matchningsskripten endast utför
`SELECT` och inte importerar kod från facitskripten.
## 3. Klassificering av skript och data
### 3.1 Facitunderhåll
**Verifierat faktum:** Följande filer hör till facitunderhåll:
| Fil | Ansvar |
| --- | --- |
| `scripts/schema.sql` | Definierar tabeller och index. |
| `scripts/wcx_sync.py` | Hämtar WCX-metadata och skriver ett JSON-index. |
| `scripts/import_site.py` | Infogar och uppdaterar `movie` samt arkiverar tidigare värden i `movie_history`. |
| `scripts/migrate_csv.py` | Validerar historisk CSV och kan infoga eller uppdatera `movie`. |
| `scripts/process_pending_ocr.py` | Läser väntande OCR-poster och startar behandling. |
| `scripts/check_ocr.py` | Läser en filmpost och skriver OCR-resultat och status till `movie`. |
| `scripts/ocr.sh` | Anropar Google Vision för en bild. |
| `scripts/parse_ocr.py` | Tolkar OCR-text; använder inte databasen direkt. |
| `scripts/update_wcx.sh` | Orkestrerar synkronisering, import och OCR. |
| `scripts/scheduled_update_wcx.sh` | Begränsar och startar schemalagda facituppdateringar. |
| `migration/persons.utf8bom.csv` | Historisk, versionshanterad metadatakälla. |
**Rekommendation:** Dessa filer bör tillsammans med en renodlad facit-README
flyttas till facitprojektet.
### 3.2 Filhantering
**Verifierat faktum:** Följande filer hör till filhantering:
| Fil | Ansvar |
| --- | --- |
| `scripts/match_filenames.py` | Läser videofiler och facitdata, kör `ffprobe` och klassificerar matchningar. |
| `scripts/test_duration_matching.py` | Diagnostiserar en fils längd mot aktuell och historisk facitlängd. |
| `docs/filename-matching-requirements.md` | Kravspecifikation för matchning. |
| `docs/filename-matching-requirements-prompt.md` | Historiskt arbets- och kravunderlag för matchningen. |
**Rekommendation:** `match_filenames.py`, dess tester och den relevanta
dokumentationen bör flyttas tillsammans till filhanteringsprojektet.
**Ej fattat beslut:** Det behöver avgöras om
`filename-matching-requirements-prompt.md` ska följa med som historiskt
arbetsmaterial, arkiveras eller utelämnas ur det nya projektet.
## 4. Nuvarande beroenden
### 4.1 Facitflöde
**Verifierat faktum:** Den schemalagda körkedjan är:
```text
scheduled_update_wcx.sh
→ update_wcx.sh
→ wcx_sync.py
→ import_site.py
→ process_pending_ocr.py
→ check_ocr.py
→ ocr.sh
→ parse_ocr.py
```
Webbplatsens data flödar enligt följande:
```text
WCX-webbplats
→ wcx_sync.py
→ import/wcx_site_index.json
→ import_site.py
→ database/wcx.db
```
`import_site.py`, `migrate_csv.py` och `check_ocr.py` har avsiktliga
databasskrivningar. `process_pending_ocr.py` läser databasen direkt och låter
`check_ocr.py` utföra skrivningarna.
### 4.2 Filhanteringens facitberoende
**Verifierat faktum:** `match_filenames.py` läser följande minimala
databaskontrakt:
```text
movie:
id
name
duration_seconds
movie_name_alias:
movie_id
alias
normalized_alias
source
movie_history:
movie_id
duration_seconds
archived_at
```
`test_duration_matching.py` läser `movie.id`, `movie.duration_seconds` och
historiska längder ur `movie_history`.
**Verifierat faktum:** Matchningen har inga gemensamma Python-importer med
facitunderhållet. Kopplingen består av SQLite-databasens placering och schema.
**Verifierat faktum:** `match_filenames.py` och
`test_duration_matching.py` använder vanlig `sqlite3.connect(path)`. Deras SQL
är för närvarande endast läsande, men SQLite-anslutningen tvingar inte fram
read-only-läge.
**Rekommendation:** Filhanteringsprojektet ska öppna databasen med SQLite URI
och `mode=ro`. Filsystemsrättigheter bör om möjligt också neka den process som
kör filhanteringen skrivrättighet till facitdatabasen.
### 4.3 Hårdkodade sökvägar
**Verifierat faktum:** Följande produktionssökvägar är hårdkodade:
- `check_ocr.py`: databas, `ocr.sh` och `parse_ocr.py` under
`/storage/disk1/WCX`.
- `process_pending_ocr.py`: databas och `check_ocr.py` under
`/storage/disk1/WCX`.
- `import_site.py`: import-JSON och databas under `/storage/disk1/WCX`.
- `migrate_csv.py`: migrations-CSV och databas under `/storage/disk1/WCX`.
- `update_wcx.sh` och `scheduled_update_wcx.sh`:
`ROOT_DIR=/storage/disk1/WCX`.
- `match_filenames.py` och `test_duration_matching.py`:
`/storage/disk1/WCX/database/wcx.db`.
**Verifierat faktum:** `wcx_sync.py` har
`/storage/disk1/X/wcx_index.json` som eget standardutdata, medan
`update_wcx.sh` skickar den avsedda sökvägen
`/storage/disk1/WCX/import/wcx_site_index.json` explicit.
**Verifierat faktum:** README och matchningsdokumenten innehåller också flera
absoluta sökvägar till nuvarande repository.
## 5. Identifierat schemaproblem
**Verifierat faktum:** Den faktiska `database/wcx.db` innehåller tabellerna
`movie`, `movie_history` och `movie_name_alias`.
**Verifierat faktum:** Den faktiska tabellen `movie` innehåller dessutom
följande OCR-kolumner:
```text
ocr_status
ocr_raw_text
ocr_error
ocr_processed_at
```
**Verifierat faktum:** Dessa fyra kolumner saknas i nuvarande
`scripts/schema.sql`, trots att facitskripten och README förutsätter att de
finns.
Konsekvensen är att en ny databas som skapas enbart från det
versionshanterade schemat inte är kompatibel med hela facitflödet. Vid en
projektuppdelning kan detta orsaka svårdiagnostiserade fel: den flyttade koden
kan verka felaktig trots att den verkliga orsaken är att Git inte innehåller
ett komplett, reproducerbart schema.
**Rekommendation:** Gör facitdatabasens schema reproducerbart innan den fysiska
projektuppdelningen. Ändringen ska utvecklas och verifieras mot en ny,
disposable databas och får inte tillämpas blint på `database/wcx.db`.
**Ej fattat beslut:** Det behöver avgöras om nuvarande databas kan beskrivas av
ett korrigerat komplett grundschema, eller om projektet även behöver en
versionshanterad migrationsmekanism för framtida schemaändringar.
## 6. Risker och öppna frågor
### 6.1 Risker
1. **Oavsiktliga databasskrivningar från filhanteringen.** Nuvarande kod är
läsande men anslutningen är inte tekniskt read-only.
2. **Schemaändringar bryter konsumenten.** Filhanteringen beror direkt på tre
facittabeller och ett begränsat antal kolumner.
3. **Ofullständigt schema ger icke reproducerbara installationer.** OCR-flödet
kan inte byggas upp korrekt från nuvarande `schema.sql`.
4. **Hårdkodade sökvägar bryts vid flytt.** Både Python-, shell- och externa
systemd-konfigurationer kan peka på den gamla platsen.
5. **Felaktig kopiering av en aktiv SQLite-databas.** Om WAL används kan en
kopia av endast `.db` bli inkonsekvent. `immutable=1` är inte lämpligt för
en databas som facitprojektet fortsätter uppdatera.
6. **Otydligt dataägarskap.** Alias och historiska längder används av
filhanteringen men måste fortsatt ägas av facitprojektet.
7. **Genererad JSON är spårad.** Den kan oavsiktligt följa med som källfil till
ett nytt repository.
8. **Tester beror på lokal produktionslik data.** Det saknas ett isolerat,
reproducerbart testunderlag för hela gränsen mellan projekten.
9. **Driftkonfiguration ligger utanför Git.** README hänvisar till systemd-
enheter som inte finns i repositoryt och kan innehålla fler absoluta
sökvägar.
### 6.2 Öppna frågor
- **Ej fattat beslut:** Var ska facitdatabasen ligga efter uppdelningen: under
facitprojektets runtime-katalog eller i en separat stabil datakatalog?
- **Ej fattat beslut:** Ska båda projekten köras av samma operativsystemkonto,
eller ska filhanteringen få ett konto som saknar skrivrättighet till facit?
- **Ej fattat beslut:** Hur ska databasens läskontrakt versionssättas och hur
ska inkompatibla schemaändringar kommuniceras mellan projekten?
- **Ej fattat beslut:** Ska `import/wcx_site_index.json` tas bort ur Git-indexet
och endast genereras vid körning?
- **Ej fattat beslut:** Var finns systemd-enheterna, och vilka sökvägar måste
uppdateras när facitprojektet flyttas?
- **Antagande:** `movie.id` är en stabil identitet som en framtida separat
filhanteringsdatabas kan referera till.
- **Antagande:** Direkt SQLite-läsning ger tillräcklig samtidighet och
tillgänglighet för den aktuella lokala användningen.
## 7. Rekommenderad målarkitektur
### 7.1 Facitprojekt
**Rekommendation:** Facitprojektet ska innehålla schema, migrationsdata,
webbsynkronisering, import, OCR och schemalagd uppdatering. Det ska vara ensam
ägare av följande data:
```text
movie
movie_history
movie_name_alias
```
Det ska vara den enda komponent som har rätt att skriva facitdatabasen.
Runtime-data bör hållas tydligt åtskild från Git-spårad källkod, även om den
fysiskt ligger nära projektet.
### 7.2 Filhanteringsprojekt
**Rekommendation:** Filhanteringsprojektet ska innehålla matchningskod,
`ffprobe`-integration, tester och matchningsdokumentation. Databasens sökväg
ska vara konfigurerbar och anslutningen strikt read-only.
Filhanteringen ska inte skapa alias, historik eller matchningsposter i
facitdatabasen. Om beständigt tillstånd senare behövs ska en separat databas
ägas av filhanteringsprojektet. Den kan referera till facitets stabila
`movie.id`, men får inte använda främmande nycklar som kräver skrivning i eller
tät koppling till facitfilen.
### 7.3 Gräns mellan projekten
**Rekommendation:** Den initiala gränsen ska vara ett litet dokumenterat
SQLite-läskontrakt, inte ett API och inte ett gemensamt Python-bibliotek.
```text
facitprojekt ── skriver ──> facit.sqlite
└── strikt read-only ──> filhanteringsprojekt
filhanteringsprojekt ── kan senare skriva ──> egen filhanteringsdatabas
```
Facitprojektet ska kunna ändra intern implementation fritt så länge det
dokumenterade läskontraktet förblir kompatibelt, eller ändringen samordnas med
filhanteringsprojektet.
## 8. Stegvis migreringsplan
Varje steg nedan anger om det enbart gäller dokumentation eller struktur, om
det kan påverka körbart beteende samt hur det bör verifieras.
### Steg 1: Dokumentera ansvar och läskontrakt
**Rekommendation:** Fastställ skriftligt att facitprojektet äger samtliga
facittabeller och är ensam skrivare. Dokumentera de tabeller och kolumner som
filhanteringen får läsa.
- Typ: ren dokumentationsändring.
- Körbart beteende: ingen påverkan.
- Verifiering:
- jämför kontraktet med alla `SELECT` i matchningsskripten,
- kontrollera att inga skrivande SQL-satser finns i filhanteringsdelen,
- granska och godkänn den avsedda ägargränsen.
### Steg 2: Gör facitschemat reproducerbart
**Rekommendation:** Uppdatera facitets schemahantering så att en ny databas får
alla kolumner som facitflödet behöver, inklusive OCR-kolumnerna.
- Typ: kod-/schemaändring.
- Körbart beteende: kan påverkas; nya databaser och återställningsflöden
förändras.
- Verifiering:
- skapa en ny disposable databas från versionshanterade schemafiler,
- jämför tabeller, kolumner, index, begränsningar och standardvärden med den
avsedda modellen,
- kör syntaxkontroll av Python-skripten,
- kör import mot en liten test-JSON,
- kör OCR-flödets databasoperationer utan att använda produktionsdatabasen,
- kontrollera att `database/wcx.db` är oförändrad.
### Steg 3: Rätta dokumenterad hantering av genererade filer
**Rekommendation:** Bestäm om `import/wcx_site_index.json` är runtime-data. Om
så är fallet, sluta spåra filen utan att radera den lokala runtime-kopian och
anpassa dokumentationen.
- Typ: Git-struktur och dokumentation.
- Körbart beteende: normalt ingen påverkan, men en ny checkout kommer inte
längre innehålla en färdig importfil.
- Verifiering:
- kontrollera `.gitignore` och Git-index,
- kör synkskriptet mot en tillfällig utdatafil,
- verifiera att uppdateringsflödet skapar importfilen innan import,
- verifiera att inga genererade data lagts till i Git.
### Steg 4: Gör sökvägar flyttbara i nuvarande repository
**Rekommendation:** Behåll explicita kommandoradsargument för databas och
indata. Beräkna interna skriptsökvägar relativt skriptens plats där det är
lämpligt. Lägg till konfigurerbar databas till diagnostik som saknar det.
Behåll om möjligt nuvarande standardvärden under övergången.
- Typ: kod- och konfigurationsstruktur.
- Körbart beteende: kan påverkas, särskilt standardkörningar och schemalagd
drift.
- Verifiering:
- kör `python3 -m py_compile scripts/*.py`,
- kör varje relevant kommando med explicita sökvägar mot disposable data,
- kör `scripts/update_wcx.sh --skip-ocr` mot en säker testuppsättning eller
testa dess delkommandon separat,
- verifiera både nuvarande standardplats och en alternativ projektplats,
- inspektera systemd-enheternas verkliga kommandon och sökvägar.
### Steg 5: Tvinga fram read-only i filhanteringen
**Rekommendation:** Ändra filhanteringens SQLite-anslutningar till URI-baserat
`mode=ro`. Använd inte `immutable=1` för en databas som kan uppdateras medan
den läses.
- Typ: kodändring och säkerhetsförstärkning.
- Körbart beteende: kan påverkas; fel sökväg och otillräckliga läsrättigheter
ska nu ge tydliga fel i stället för att en ny databas skapas eller öppnas
skrivbart.
- Verifiering:
- kör matchning och längddiagnostik mot en disposable facitdatabas,
- kontrollera databasfilens hash och mtime före och efter,
- verifiera att läsning fungerar när filen saknar skrivrättighet för
processen,
- lägg in ett isolerat negativt test som visar att en `INSERT` via samma
anslutningssätt nekas,
- kontrollera att samtidig normal facituppdatering och läsning hanteras av
SQLite utan att använda osäkra filkopior.
### Steg 6: Separera dokumentation och filstruktur logiskt
**Rekommendation:** Renodla facitets README och skapa en självständig README
för filhanteringen. Ordna filerna enligt den planerade projektgränsen, men
behåll dem tillfälligt i samma Git-repository om det förenklar verifieringen.
- Typ: i första hand dokumentation och struktur.
- Körbart beteende: ingen påverkan om endast dokument flyttas; möjliga
sökvägseffekter om skript flyttas.
- Verifiering:
- följ installations- och körinstruktionerna från en ren testkatalog,
- sök efter gamla absoluta projektsökvägar,
- kontrollera att shellskript hittar sina delskript efter en eventuell flytt,
- kontrollera att facit- och filhanteringsdokumentation är självständigt
begripliga.
### Steg 7: Skapa två Git-projekt
**Rekommendation:** Flytta facit- respektive filhanteringsfiler till separata
repositoryn först när schema, konfiguration och read-only-gräns är verifierade.
Behåll det gamla repositoryt tills båda nya projekten har körts framgångsrikt.
Föreslagen facitomfattning:
```text
README.md
scripts/schema.sql
scripts/wcx_sync.py
scripts/import_site.py
scripts/migrate_csv.py
scripts/process_pending_ocr.py
scripts/check_ocr.py
scripts/ocr.sh
scripts/parse_ocr.py
scripts/update_wcx.sh
scripts/scheduled_update_wcx.sh
migration/
```
Föreslagen filhanteringsomfattning:
```text
README.md
scripts/match_filenames.py
scripts/test_duration_matching.py
docs/filename-matching-requirements.md
```
- Typ: Git- och projektstruktur.
- Körbart beteende: kan påverkas genom nya sökvägar, installation och drift.
- Verifiering:
- kontrollera `git status` och ignore-regler i båda projekten,
- kör syntaxkontroll och respektive projekts diagnostik,
- kör facitimport mot en disposable databas,
- kör filmatchning mot samma databas i read-only-läge,
- verifiera att filhanteringen fungerar utan facitprojektets källkod i
`PYTHONPATH` eller samma katalog,
- kontrollera att inga databaser, API-nycklar eller genererade JSON-filer har
följt med i Git.
### Steg 8: Flytta drift och systemd till facitprojektet
**Rekommendation:** Uppdatera externa systemd-enheter och runtime-sökvägar
först efter en lyckad manuell facitkörning från den nya platsen. Databasen ska
inte kopieras medan den skrivs; använd en säker SQLite-backupmetod om en fysisk
flytt behövs.
- Typ: extern konfiguration och driftsättning.
- Körbart beteende: direkt påverkan på schemalagd produktion.
- Verifiering:
- inspektera enhetsfiler och miljövariabler,
- kör facituppdateringen manuellt med `--skip-ocr` mot avsedd databas,
- kör därefter en kontrollerad OCR-körning om API-nyckel finns,
- starta tjänsten manuellt och kontrollera logg, låsfil och tidsstämpel,
- verifiera att endast en uppdatering kan köras samtidigt,
- kör filhantering som separat läsare efter uppdateringen.
### Steg 9: Inför separat filhanteringsdatabas först vid konkret behov
**Rekommendation:** Skapa inte en andra databas som del av den första
uppdelningen. Inför den först när lokala filsökvägar, manuella beslut eller
bearbetningsstatus behöver beständig lagring.
- Typ: framtida funktions- och schemaändring i filhanteringsprojektet.
- Körbart beteende: påverkar filhantering men inte facitdatabasen.
- Verifiering:
- testa den nya databasen isolerat,
- verifiera att facitanslutningen fortfarande är `mode=ro`,
- kontrollera att endast stabilt `movie.id` lagras som extern referens,
- verifiera att inga skrivningar riktas till den anslutna facitdatabasen.
## 9. Rekommenderad ordning och stoppunkter
**Rekommendation:** Utför steg 13 innan sökvägar eller körkod flyttas. Dessa
steg etablerar ägarskap och gör facitdatabasen reproducerbar.
Utför steg 45 i det befintliga repositoryt och verifiera oförändrat beteende
mot disposable data. Dessa är de viktigaste tekniska förutsättningarna för en
säker uppdelning.
Utför steg 68 som separata, granskningsbara förändringar. Lägg inte
schemaändring, read-only-förstärkning, filflytt och produktionsdrift i samma
ändring, eftersom fel då blir svårare att isolera.
**Ej fattat beslut:** Exakta namn och installationsplatser för de två nya
Git-projekten är inte bestämda.
**Ej fattat beslut:** Ingen fysisk flytt av produktionsdatabasen bör planeras
förrän dess slutliga runtime-plats, backupmetod och filrättigheter är beslutade.
## 10. Sammanfattande rekommendation
Den befintliga koden har redan en användbar ansvarslinje: facitverktygen
producerar och underhåller SQLite-data, medan filhanteringen konsumerar ett
litet urval av denna data. Uppdelningen behöver därför inte börja med ett API
eller koddelning.
Den säkra vägen är att först göra facitschemat komplett och reproducerbart,
dokumentera det lilla läskontraktet, göra sökvägar konfigurerbara och tvinga
filhanteringens anslutning till read-only. Därefter kan filerna delas mellan
två Git-projekt med betydligt lägre risk. En separat filhanteringsdatabas bör
vänta tills beständigt lokalt tillstånd faktiskt behövs.

View File

@ -0,0 +1,164 @@
# Producer contract for read access to the WCX reference database
## Purpose and ownership
This document defines the SQLite read contract that this reference-data
project provides to the external file-management repository
`/storage/disk1/WCX-collection`. The contract identifies the database objects
that WCX-collection depends on and the compatibility boundary that this
producer must preserve.
This reference-data project owns the database schema and all reference data,
including canonical metadata, history, and aliases. It is the sole writer to
the reference database. WCX-collection is an external read-only consumer and
must open the database through a SQLite URI with `mode=ro` and `uri=True`.
Local file paths, local processing state, matching decisions, and collection
state are outside this contract and must not be written to the reference
database.
## Connection contract
The consumer in `/storage/disk1/WCX-collection` resolves the configured
database path, converts it to a file URI, appends `?mode=ro`, and opens it as
follows:
```python
database_uri = f"{database_file.resolve().as_uri()}?mode=ro"
with sqlite3.connect(database_uri, uri=True) as connection:
connection.row_factory = sqlite3.Row
```
The database file must already exist. The consumer must not fall back to a
normal writable SQLite connection.
## Tables and columns read
The current contract contains exactly three tables and ten selected columns:
| Table | Columns read |
| --- | --- |
| `movie` | `id`, `name`, `duration_seconds` |
| `movie_name_alias` | `movie_id`, `alias`, `normalized_alias`, `source` |
| `movie_history` | `movie_id`, `duration_seconds`, `archived_at` |
No other table or column is read by WCX-collection's current database loader.
### Column meanings for matching
| Column | Meaning in the file matcher |
| --- | --- |
| `movie.id` | Stable external identity for a movie. It links the current movie to aliases and historical durations and is the identifier that file-management data may reference externally. |
| `movie.name` | Current canonical display name and the primary name used to generate filename candidates. Empty names are excluded. |
| `movie.duration_seconds` | Current known duration. It supplies the current duration version used to support or distinguish name matches. |
| `movie_name_alias.movie_id` | Associates an alias with `movie.id`. |
| `movie_name_alias.alias` | Human-readable alternative name used to generate additional filename candidates. Empty aliases are excluded. |
| `movie_name_alias.normalized_alias` | Stored normalized form used directly for name comparison and duplicate suppression. |
| `movie_name_alias.source` | Describes the alias source and is retained in match results so the reported match can identify how the alias was obtained. |
| `movie_history.movie_id` | Associates a historical duration with `movie.id`. |
| `movie_history.duration_seconds` | Earlier known duration. Positive values are used as historical duration versions so an older local release can still support a match. |
| `movie_history.archived_at` | Identifies when the historical version was archived and is retained with the historical duration for diagnostics and reporting. |
`movie.id` is the stable external identity in this contract. `movie.name` is
not an external identity: the site importer can update the current name and
archives the previous movie state in `movie_history`. File-management records
must therefore refer to `movie.id`, never to a name as an identifier.
## SQL queries
The current WCX-collection implementation executes exactly these three
queries.
### Current movies
```sql
SELECT
id,
name,
duration_seconds
FROM movie
WHERE name IS NOT NULL
AND TRIM(name) <> ''
ORDER BY name, id
```
### Aliases
```sql
SELECT
movie_id,
alias,
normalized_alias,
source
FROM movie_name_alias
WHERE alias IS NOT NULL
AND TRIM(alias) <> ''
ORDER BY movie_id, alias
```
### Historical durations
```sql
SELECT
movie_id,
duration_seconds,
archived_at
FROM movie_history
WHERE duration_seconds IS NOT NULL
AND duration_seconds > 0
ORDER BY movie_id, archived_at
```
These are read-only `SELECT` statements. The matcher does not issue database
writes or invoke another database-writing component.
## Backward compatibility
The following schema or semantic changes would be backward-incompatible for
the current file-management code:
- removing or renaming any of the three contracted tables,
- removing or renaming any selected column,
- changing a selected value so it can no longer be converted as currently
expected (`id`, names, alias source, and archive time to text; durations to
positive integer seconds),
- making `movie.id` unstable or reusing an ID for another movie,
- breaking the relationships from `movie_name_alias.movie_id` or
`movie_history.movie_id` to `movie.id`,
- changing `duration_seconds` to another unit or meaning,
- changing `normalized_alias` so it no longer represents the normalized alias
expected by filename comparison,
- changing the meaning of `source` such that it can no longer identify the
alias provenance shown in match output, or
- preventing the three queries from running through a SQLite `mode=ro`
connection.
Adding unrelated tables or columns is compatible. Adding rows, updating a
movie name while retaining the same `movie.id`, adding aliases, and appending
historical durations are also compatible with the current query contract.
## Process for contract changes
As producer and schema owner, the reference-data project is responsible for
preserving this contract. Before it makes an incompatible change:
1. Identify the affected table, column, value semantics, or relationship and
document the proposed replacement.
2. Coordinate with WCX-collection and update the consumer to support the new
contract, preferably with a transition period in which it can read both
forms.
3. Create a disposable database containing representative current names,
aliases, current durations, and historical durations under the proposed
schema.
4. Verify all three queries and representative filename matching against that
database through `mode=ro`.
5. Verify that a write through the same connection is rejected.
6. Release or deploy the compatible file-management version before removing
the old contract from the reference database.
7. Remove the old contract only after both projects explicitly agree that no
active consumer depends on it.
The reference-data project must not deploy an incompatible schema or semantic
change until a compatible WCX-collection version is available. It must not
silently break or reinterpret the documented read contract.

141
docs/scheduled-update.md Normal file
View File

@ -0,0 +1,141 @@
# Schemalagd WCX-uppdatering
## Syfte
Systemd-jobbet uppdaterar regelbundet WCX-projektets lokala sajtindex och
facitdatabas samt behandlar väntande OCR-poster. Jobbet körs som användaren
`urban`.
Systemd startar först:
- `/storage/disk1/WCX/scripts/scheduled_update_wcx.sh`
Wrappern avgör om en uppdatering ska göras och anropar därefter:
- `/storage/disk1/WCX/scripts/update_wcx.sh`
`update_wcx.sh` hämtar sajtindexet, importerar metadata till SQLite och kör
OCR-flödet.
## Installerade systemd-filer
Konfigurationen består av:
- service: `/etc/systemd/system/wcx-update.service`
- timer: `/etc/systemd/system/wcx-update.timer`
- miljöfil: `/etc/wcx/update-wcx.env`
Servicen körs som `urban`. Miljöfilen innehåller
`GOOGLE_VISION_API_KEY` och ska ägas av `root:root` med rättigheten
`0600`. Den faktiska API-nyckeln får aldrig läggas i Git, skrivas in i detta
dokument eller visas i terminalutdata och loggar.
Exempel på miljöfilens format, utan verkligt nyckelvärde:
```text
GOOGLE_VISION_API_KEY=<hemligt värde>
```
## Schema och körningsskydd
Timern kontrollerar varje natt klockan 03:30 om jobbet ska startas:
```ini
OnCalendar=*-*-* 03:30:00
Persistent=true
```
`scheduled_update_wcx.sh` kontrollerar tidsstämpeln i
`database/last_scheduled_update`. Den fullständiga uppdateringen körs endast
om minst 47 timmar har gått sedan den senaste lyckade schemalagda körningen.
Tidsstämpeln uppdateras först efter att `update_wcx.sh` har lyckats.
`Persistent=true` innebär att systemd försöker ta igen en missad
kalenderkörning efter att datorn har varit avstängd. Wrapperns 47-timmarskontroll
avgör då om själva uppdateringen behöver köras.
`update_wcx.sh` tar ett exklusivt, icke-blockerande `flock`-lås via
`database/update_wcx.lock`. En ny körning avbryts om en annan uppdatering
redan pågår.
## Kontroll och validering
Kontrollera service och timer:
```bash
sudo systemctl status wcx-update.service
sudo systemctl status wcx-update.timer
sudo systemctl list-timers --all wcx-update.timer
```
Validera unit-filerna efter en konfigurationsändring:
```bash
sudo systemd-analyze verify \
/etc/systemd/system/wcx-update.service \
/etc/systemd/system/wcx-update.timer
```
Starta en manuell körning genom systemd:
```bash
sudo systemctl start wcx-update.service
```
Den manuella servicekörningen går genom `scheduled_update_wcx.sh`. Om mindre
än 47 timmar har gått sedan senaste lyckade schemalagda körning avslutas den
utan att starta en ny fullständig uppdatering.
Aktivera och starta timern:
```bash
sudo systemctl enable --now wcx-update.timer
```
Stoppa och inaktivera timern:
```bash
sudo systemctl disable --now wcx-update.timer
```
Att stoppa timern stoppar inte automatiskt en servicekörning som redan pågår.
Kontrollera därför servicens status separat innan en pågående körning stoppas.
## Loggar
Scriptens standardutdata och felutdata samlas av systemd-journalen. Jobbet
skriver ingen separat loggfil.
Visa hela serviceloggen:
```bash
sudo journalctl -u wcx-update.service
```
Följ en pågående körning:
```bash
sudo journalctl -u wcx-update.service -f
```
Visa loggar från den aktuella uppstarten:
```bash
sudo journalctl -b -u wcx-update.service
```
## Filer som jobbet skriver
En fullständig uppdatering kan skriva eller ersätta:
- `/storage/disk1/WCX/import/wcx_site_index.json.tmp`
- `/storage/disk1/WCX/import/wcx_site_index.json`
- `/storage/disk1/WCX/database/wcx.db`
- SQLite-journal- eller WAL-filer bredvid databasen
- `/storage/disk1/WCX/database/update_wcx.lock`
- `/storage/disk1/WCX/database/last_scheduled_update`
Sajtindexet skrivs först till en temporär fil och ersätter sedan den ordinarie
JSON-filen. Import- och OCR-stegen uppdaterar facitdatabasen. Lockfilen används
för att förhindra överlappande körningar, och tidsstämpelfilen används av
wrappern för 47-timmarskontrollen.

View File

@ -0,0 +1,17 @@
PRAGMA foreign_keys = ON;
INSERT INTO movie_name_alias (
movie_id,
alias,
normalized_alias,
source
)
SELECT
id,
'Kittina Ivory',
'kittinaivory',
'manual'
FROM movie
WHERE id = 'kittina-clairette_7801'
AND name = 'Kittina Clairette'
ON CONFLICT (movie_id, normalized_alias) DO NOTHING;

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,11 @@ CREATE TABLE IF NOT EXISTS movie (
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
ocr_status TEXT NOT NULL DEFAULT 'pending',
ocr_raw_text TEXT,
ocr_error TEXT,
ocr_processed_at TEXT,
CHECK (age IS NULL OR age >= 0), CHECK (age IS NULL OR age >= 0),
CHECK (rating IS NULL OR rating BETWEEN 0 AND 10), CHECK (rating IS NULL OR rating BETWEEN 0 AND 10),
CHECK (duration_seconds IS NULL OR duration_seconds >= 0) CHECK (duration_seconds IS NULL OR duration_seconds >= 0)

View File

@ -1,270 +0,0 @@
#!/usr/bin/env python3
"""
Small local test for matching a file duration against current and historical
movie durations.
This script does not modify the database or any files.
"""
import sqlite3
import subprocess
import sys
from pathlib import Path
DATABASE_FILE = Path(
"/storage/disk1/WCX/database/wcx.db"
)
def read_file_duration(file_path: Path) -> int:
result = subprocess.run(
[
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
str(file_path),
],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(
result.stderr.strip()
or f"ffprobe failed for {file_path}"
)
try:
return round(float(result.stdout.strip()))
except ValueError as error:
raise RuntimeError(
f"Invalid ffprobe duration: {result.stdout!r}"
) from error
def format_duration(seconds: int | None) -> str:
if seconds is None:
return "unknown"
hours, remainder = divmod(seconds, 3600)
minutes, seconds = divmod(remainder, 60)
if hours:
return f"{hours}:{minutes:02d}:{seconds:02d}"
return f"{minutes}:{seconds:02d}"
def load_movie_durations(
connection: sqlite3.Connection,
movie_name: str,
) -> tuple[str, int | None, list[int]]:
current = connection.execute(
"""
SELECT id, duration_seconds
FROM movie
WHERE name = ?
""",
(movie_name,),
).fetchone()
if current is None:
raise ValueError(
f"Movie not found: {movie_name}"
)
movie_id = str(current[0])
current_duration = current[1]
historical_rows = connection.execute(
"""
SELECT DISTINCT duration_seconds
FROM movie_history
WHERE movie_id = ?
AND duration_seconds IS NOT NULL
ORDER BY duration_seconds
""",
(movie_id,),
).fetchall()
historical_durations = [
int(row[0])
for row in historical_rows
if row[0] != current_duration
]
return (
movie_id,
current_duration,
historical_durations,
)
def classify_duration(
file_duration: int,
current_duration: int | None,
historical_durations: list[int],
) -> tuple[str, int | None, int | None]:
"""
Return:
classification
closest matching duration
absolute difference in seconds
"""
candidates: list[tuple[str, int]] = []
if current_duration is not None:
candidates.append(
("current_version_match", current_duration)
)
for duration in historical_durations:
candidates.append(
("historical_version_match", duration)
)
if not candidates:
return "unknown_version", None, None
classification, closest_duration = min(
candidates,
key=lambda item: abs(file_duration - item[1]),
)
difference = abs(
file_duration - closest_duration
)
relative_difference = (
difference / closest_duration
if closest_duration > 0
else 1.0
)
close_match = (
difference <= 180
or relative_difference <= 0.05
)
if close_match:
return (
classification,
closest_duration,
difference,
)
if file_duration < closest_duration:
return (
"possible_truncated_version",
closest_duration,
difference,
)
if file_duration > closest_duration:
return (
"possible_extended_version",
closest_duration,
difference,
)
return (
"duration_mismatch",
closest_duration,
difference,
)
def main() -> None:
if len(sys.argv) != 3:
raise SystemExit(
"Usage: test_duration_matching.py "
"<video-file> <movie-name>"
)
file_path = Path(sys.argv[1])
movie_name = sys.argv[2]
if not file_path.is_file():
raise FileNotFoundError(
f"Video file not found: {file_path}"
)
file_duration = read_file_duration(
file_path
)
with sqlite3.connect(DATABASE_FILE) as connection:
(
movie_id,
current_duration,
historical_durations,
) = load_movie_durations(
connection,
movie_name,
)
(
classification,
matched_duration,
difference,
) = classify_duration(
file_duration=file_duration,
current_duration=current_duration,
historical_durations=historical_durations,
)
print(f"Movie ID: {movie_id}")
print(f"Movie name: {movie_name}")
print(
f"File duration: "
f"{format_duration(file_duration)} "
f"({file_duration} seconds)"
)
print(
f"Current duration: "
f"{format_duration(current_duration)}"
)
if historical_durations:
print("Historical durations:")
for duration in historical_durations:
print(
f" {format_duration(duration)} "
f"({duration} seconds)"
)
else:
print("Historical durations: none")
print(
f"Closest duration: "
f"{format_duration(matched_duration)}"
)
print(
f"Difference: "
f"{format_duration(difference)}"
)
print(
f"Classification: "
f"{classification}"
)
if __name__ == "__main__":
try:
main()
except (
FileNotFoundError,
RuntimeError,
ValueError,
sqlite3.Error,
) as error:
print(f"Error: {error}", file=sys.stderr)
sys.exit(1)

View File

@ -9,12 +9,12 @@ import urllib.error
import urllib.request import urllib.request
from html.parser import HTMLParser from html.parser import HTMLParser
from pathlib import Path from pathlib import Path
from urllib.parse import urljoin, urlparse from urllib.parse import parse_qs, urljoin, urlparse
BASE_URL = "https://www.woodmancastingx.com" BASE_URL = "https://www.woodmancastingx.com"
LIST_PATH = "/casting-xxx/" LIST_PATH = "/casting-xxx/"
DEFAULT_OUT = "/storage/disk1/X/wcx_index.json" DEFAULT_OUT = "/storage/disk1/X/wcx_index.json"
DEFAULT_PAGES = 48 MAX_PAGES = 200
DEFAULT_SLEEP_MS = 300 DEFAULT_SLEEP_MS = 300
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) wcx_sync.py/1.0" USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) wcx_sync.py/1.0"
@ -217,6 +217,41 @@ def parse_list_page(data):
return items return items
def parse_last_page(data):
parser = TreeParser()
parser.feed(data)
base = urlparse(BASE_URL)
page_numbers = []
for node in walk(parser.root):
if node.name != "a":
continue
href = (node.attrs.get("href") or "").strip()
if not href:
continue
target = urlparse(urljoin(BASE_URL, href))
if target.netloc != base.netloc or target.path != LIST_PATH:
continue
for value in parse_qs(target.query).get("page", []):
if value.isdigit() and int(value) >= 1:
page_numbers.append(int(value))
if not page_numbers:
raise RuntimeError("Could not detect a numeric last page from page 1 pagination")
last_page = max(page_numbers)
if last_page > MAX_PAGES:
raise RuntimeError(
f"Detected last page {last_page}, exceeding safety limit {MAX_PAGES}"
)
return last_page
class DetailDateParser(HTMLParser): class DetailDateParser(HTMLParser):
def __init__(self): def __init__(self):
super().__init__(convert_charrefs=True) super().__init__(convert_charrefs=True)
@ -344,7 +379,7 @@ def write_json_one_object_per_line(path, items):
def main(): def main():
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("--out", default=DEFAULT_OUT) ap.add_argument("--out", default=DEFAULT_OUT)
ap.add_argument("--pages", "--end-page", dest="pages", type=int, default=DEFAULT_PAGES) ap.add_argument("--pages", "--end-page", dest="pages", type=int)
ap.add_argument("--sleep-ms", type=int, default=DEFAULT_SLEEP_MS) ap.add_argument("--sleep-ms", type=int, default=DEFAULT_SLEEP_MS)
ap.add_argument("--timeout", type=int, default=20) ap.add_argument("--timeout", type=int, default=20)
ap.add_argument("--retries", type=int, default=3) ap.add_argument("--retries", type=int, default=3)
@ -354,29 +389,41 @@ def main():
out_path = Path(args.out) out_path = Path(args.out)
print(f"Start. pages=1..{args.pages}, out={out_path}, dry_run={args.dry_run}") requested_pages = args.pages if args.pages is not None else "auto"
print(f"Start. pages={requested_pages}, out={out_path}, dry_run={args.dry_run}")
if args.pages is not None and not 1 <= args.pages <= MAX_PAGES:
ap.error(f"--pages must be between 1 and {MAX_PAGES}")
list_items = [] list_items = []
pages_fetched = 0 pages_fetched = 0
for page in range(1, args.pages + 1): first_url = f"{BASE_URL}{LIST_PATH}?page=1"
url = f"{BASE_URL}{LIST_PATH}?page={page}" print(f"Fetching list page 1: {first_url}")
print(f"Fetching list page {page}: {url}") first_data = fetch_url(first_url, args.timeout, args.retries, args.debug)
try: last_page = args.pages if args.pages is not None else parse_last_page(first_data)
print(f"Using last list page: {last_page}")
for page in range(1, last_page + 1):
url = f"{BASE_URL}{LIST_PATH}?page={page}"
if page == 1:
data = first_data
else:
print(f"Fetching list page {page}: {url}")
data = fetch_url(url, args.timeout, args.retries, args.debug) data = fetch_url(url, args.timeout, args.retries, args.debug)
except RuntimeError as e:
print(f"WARNING: {e}", file=sys.stderr)
continue
page_items = parse_list_page(data) page_items = parse_list_page(data)
if not page_items:
raise RuntimeError(f"List page {page} returned zero results: {url}")
list_items.extend(page_items) list_items.extend(page_items)
pages_fetched += 1 pages_fetched += 1
if args.debug: if args.debug:
print(f"DEBUG: page {page} items={len(page_items)}, accumulated={len(list_items)}") print(f"DEBUG: page {page} items={len(page_items)}, accumulated={len(list_items)}")
if page < args.pages and args.sleep_ms > 0: if page < last_page and args.sleep_ms > 0:
time.sleep(args.sleep_ms / 1000) time.sleep(args.sleep_ms / 1000)
list_items = unique_by_title(list_items) list_items = unique_by_title(list_items)
@ -418,7 +465,7 @@ def main():
merged = merge_items(old_items, enriched_items) merged = merge_items(old_items, enriched_items)
merged = sort_items(merged) merged = sort_items(merged)
print(f"Pages fetched: {pages_fetched} / {args.pages}") print(f"Pages fetched: {pages_fetched} / {last_page}")
print(f"new_items: {len(enriched_items)} | old_items: {len(old_items)} | merged: {len(merged)}") print(f"new_items: {len(enriched_items)} | old_items: {len(old_items)} | merged: {len(merged)}")
if args.dry_run: if args.dry_run: