Compare commits
11 Commits
daf0f55499
...
6bdb9b5932
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bdb9b5932 | |||
| a34f9cf08d | |||
| f910e4fa31 | |||
| 717750a36b | |||
| 19af3c8cc2 | |||
| e55251c532 | |||
| 21c0c54b83 | |||
| 1eaf1817e6 | |||
| 29b26c0e9f | |||
| 2fcd9168b2 | |||
| 40008906ba |
48
AGENTS.md
Normal file
48
AGENTS.md
Normal 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.
|
||||||
463
docs/database-schema-gap-analysis.md
Normal file
463
docs/database-schema-gap-analysis.md
Normal 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` på `movie_history.history_id`,
|
||||||
|
- primärnyckel med `AUTOINCREMENT` på `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 0–10,
|
||||||
|
- 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 0–10, 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.
|
||||||
@ -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
|
||||||
|
|||||||
@ -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.
|
|
||||||
101
docs/project-split-analysis-summary.md
Normal file
101
docs/project-split-analysis-summary.md
Normal 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.
|
||||||
575
docs/project-split-analysis.md
Normal file
575
docs/project-split-analysis.md
Normal 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 1–3 innan sökvägar eller körkod flyttas. Dessa
|
||||||
|
steg etablerar ägarskap och gör facitdatabasen reproducerbar.
|
||||||
|
|
||||||
|
Utför steg 4–5 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 6–8 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.
|
||||||
164
docs/reference-database-read-contract.md
Normal file
164
docs/reference-database-read-contract.md
Normal 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.
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||||
|
|||||||
@ -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)
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user