PROBLEM: Ko je clip range premaknjen zelo daleč od LLM-jevega prvotnega
center-ja, klikni+Enter za zelen IN postavi rdeč OUT (in vice versa) —
ker se odločitev IN vs OUT temelji na 'initialCenter' (fiksen iz LLM
analize), ki ne predstavlja več trenutne uporabnikove pozicije.
Primer (Die Granaten — A Frau is a Frau):
- initialIn=0:50, initialOut=1:30 → initialCenter=1:10
- User razširi clip: trimStart=0:06, trimEnd=2:33
- User klikne pri 2:02, želi postaviti IN bližje začetku
- Stara koda: 2:02 > 1:10 → postavi OUT (NAROBE, ker je playhead daleč
desno od dejanskega center-ja trim range-a)
FIX: Odločitev IN vs OUT po BLIŽINI playhead-a do trim handle-a:
- Bližje trimStart → IN marker
- Bližje trimEnd → OUT marker
Plus: clamp markerjev znotraj trim range-a (vsaj 0.5s razlika med IN/OUT)
Plus: počisti staro initialCenter logiko v onPointerMove (handle drag)
WHY: Doslej basic auth z deljenim AUTH_USER/AUTH_PASS — tvegan za delitev
z drugimi (urednika ipd.). Z Google Sign-In dobi vsak uporabnik svoj
account, dostop pa nadzira whitelist v env.
NEW BACKEND:
- GET /login — login stran z Google Sign-In gumbom (GSI popup)
- POST /auth/google/callback — verify Google ID token + set session cookie
- GET /auth/me — vrne email + auth method (za frontend header)
- GET /logout — pobriše cookie + redirect /login
- POST /logout
AUTH FLOW:
1. Browser GET / → check_auth() → ni session → redirect /login
2. /login → Google Sign-In popup (preko gsi/client.js)
3. User izbere Google account → JS pošlje credential na /auth/google/callback
4. Server verifies token (oauth2.googleapis.com/tokeninfo)
5. Email mora biti v ALLOWED_EMAILS env
6. Set HttpOnly+Secure session cookie (HMAC-SHA256, 30 dni)
7. Redirect /
SECURITY:
- Session token: base64url(email|expiry|HMAC). Server ne ranji ne hrani.
- HMAC z SESSION_SECRET (auto-derived iz AUTH_USER+AUTH_PASS če ni nastavljen)
- Cookie HttpOnly + Secure + SameSite=lax
- Token verify: aud check, iss check, exp check, email_verified check
BACKWARD COMPAT:
- HTTPBasic auth še vedno deluje (cron, scripte, API klici)
- check_auth() probaj prvo cookie, potem basic
- Brez GOOGLE_CLIENT_ID env: vse še vedno dela na basic auth
ENV VARS (treba dodati v Coolify):
- GOOGLE_CLIENT_ID=938379241163-pvb328plec2207rbtufic8u5fgb6mkn9.apps.googleusercontent.com
- ALLOWED_EMAILS=sebastjan.artic@gmail.com,ales.cadez@gmail.com
- SESSION_SECRET=<random 64-char hex> (optional — defaultni se izračuna)
FRONTEND:
- Header: 👤 email + ↪ Odjava gumb (samo ko je prijavljen)
- DOMContentLoaded fetcha /auth/me, prikaže email
PROBLEM: '✕' gumb je do zdaj brisal samo lokalne fajle + S3, ampak
NE Nextcloud upload (rel je ostal na folxspeed/REELS/{TV}/) in NE
dedup DB zapis (zato se enaka pesem ni mogla več upload-ati).
NEW BACKEND:
- _nextcloud_delete(filename, target_subdir) helper preko WebDAV DELETE
(404 šteje kot success — če že ne obstaja, OK)
- delete_job() razširjen:
1. Nextcloud delete (če nextcloud_status='uploaded' ali ima nextcloud_url)
2. Dedup DB remove (processed_videos zapis za tisto TV postajo)
3. Lokal + S3 delete vseh workfile-ov (kot prej)
4. Glob za yt-dlp artifacte ({job_id}_yt*) — info.json, .part, .f137.mp4
5. Job metadata
- Response: {deleted, nextcloud_delete: 'ok'|'not_found'|'fail: msg', nextcloud_filename}
NEW FRONTEND:
- buildJobEl() doda data-nc-status atribut na kartico
- deleteJob() bere dataset.ncStatus za Nextcloud info
- Confirm dialog detail razložen seznam KAJ se zbriše + opozorilo če Nextcloud
- Če Nextcloud delete ni uspel po API klicu, alert
PROBLEM: STT (Soniox/Scribe) včasih popolnoma narobe slišije besedilo —
npr. 'BILA JE LJUBEZEN PRVA' postane 'BILAL JO ME ZANPRLA' (TRIO ŠUBIC).
Edit modal je do zdaj zahteval popoln recut z novim clip range.
NEW: '🔁 Ponovi transkript' gumb v Edit modalu z dropdown za provider:
- Scribe (ElevenLabs) — default, najboljši za nemščino + slovenščino
- Soniox — slovenski default v auto routing
- Whisper local (faster-whisper)
Backend:
POST /api/jobs/{id}/retranscribe { provider, auto_upload?, lang? }
- Briše stari MP4 + analysis.json + .srt + .ass (lokal + S3)
- Re-queue job z whisper_provider override
- Ohrani isti clip range — analyze.py si naredi svežo analizo
- retranscribe_count števec za sledenje poskusov
UX: confirm dialog → 1.5s status → auto-close modal → watchJob za live progress.
Brez auto_upload v Nextcloud — preveri rezultat preden re-uploadaš.
PROBLEM: Songs.txt na MB Windows playerjih je v CP1250 (slovenski/CEE),
NE Windows-1252 (Western European). iconv -f WINDOWS-1252 je 'Č' (0xC8)
napačno interpretiral kot 'È', zaradi česar je 811 zapisov v Qnet bazi
imelo 'È' namesto 'Č' (npr. 'POSKOÈNI', 'ÈAS ZA ZABAVO', 'STORŽIÈ').
Posledica: ko je qnet_match povezal job na napačno labeliran zapis,
je 'parsed_title' polnil z mojibake iz Qnet baze (15 jobov).
FIX: WINDOWS-1252 → WINDOWS-1250.
Razlike v CP1250 vs CP1252 (slovanske črke):
Č↔È, č↔è, Ć↔Æ, ć↔æ, Đ↔Ð, đ↔ð, Ń↔Ñ, Ł↔£, ł↔³, Ś↔Œ, ś↔œ, ź↔Ÿ
Ž, š, ž — ostanejo (isti byte v obeh)
BACKFILL (ločen skript, že apliciran):
- Qnet lookup: 2746 polj v 20860 zapisih popravljenih
- Qnet songs.json: 2856 polj
- 15 jobov: parsed_artist/title popravljen na pravilen UTF-8
PROBLEMS:
1. parse_artist_title je uporabljal Path(s).stem za stripping ext-a,
kar je pri YT title 'ANS.NAVEZA - SREČA OPOTEČA' vrnilo 'ANS' (Path
smatra '.NAVEZA - SREČA OPOTEČA' kot extension). Posledica: parser
failed → ACR fallback → 'Folx' kot artist na 15+ jobih.
2. tv_station je imel dva imena: 'FOLX SLOVENIJA' (frontend default)
in 'FOLX SLO' (qnet match output) — UI tabi niso seštevali pravilno.
FIXES:
- parse_artist_title: ext stripping samo za znane ekstenzije
(.mp4, .mp3, .m4a, .webm, .mkv, .avi, .mov, .wav, .flac, .aac,
.opus, .ogg, .wmv, .mxf), NE za naključne pike v YT title.
- Vsi defaultni 'FOLX SLOVENIJA' → 'FOLX SLO' v Pydantic modelih +
templates + filter tabi.
- Nextcloud mapping STATION_TO_NEXTCLOUD_FOLDER ostane nespremenjen
(FOLX SLO → mapa 'FOLX SLOVENIJA', kjer pač zaplane).
BACKFILL (že apliciran prek scripte):
- 15 jobov z parsed_artist='Folx' popravljenih na pravi izvajalec
iz youtube_title (ANS.NAVEZA, ANS.BITENC, ANS. ROKA ŽLINDRE, ipd.).
- 86 jobov tv_station 'FOLX SLOVENIJA' → 'FOLX SLO'.
Prej: max-width 1100px, 1fr/1fr grid → na velikih ekranih je bil
levi 'Nov reel' panel zelo otesnjen, desni list pa premajhen.
Zdaj:
- main max-width 1600px (več prostora na velikih ekranih)
- levi panel fiksno 440px (vsebina diha + tabi v eni vrstici)
- desni jobs list zavzema vso preostalo širino (1fr)
- Responsive: <1100px → levi 380px, <900px → stack 1 stolpec
- Tabi: Vse / FOLX SLOVENIJA / FOLX DE / ONE DE / ZWEI MUSIC / ADRIA / (brez postaje)
- Števec na vsakem tabu = število NEPOTRJENIH jobov (!hidden_after_upload) za to postajo
- Klik na tab = filter samo tisti station, samo nepotrjeni
- 'Vse' = vsi nepotrjeni (kot do zdaj)
- '(brez postaje)' tab se skrije, če ni jobov brez postaje
- Persist v localStorage (reels_jobs_station_filter)
- Iskanje IGNORIRA station filter (vrne tudi naložene + vse postaje)
- Empty state sporočilo prilagojeno glede na izbran filter
- main.py: 4 helper funcs (_persist_to_s3, _ensure_local, _delete_from_s3,
_ffmpeg_then_persist) - no-op fallback when S3 creds missing
- save_job(): mirror metadata JSON to S3
- process_job(): mirror YT download + render output + analysis/srt/ass to S3
- upload_video(): mirror direct uploads to S3
- _precache_edit_assets(): Popen->threaded with S3 sync after ffmpeg
- read endpoints (download, preview, source_video, waveform, preview_clip,
get_transcript, recut_job): _ensure_local() fallback fetch from S3
- delete_job(): cascade delete to S3 (mirror unlink)
- cleanup.py: NEW module, deletes local files >48h that exist in S3.
Verified by S3 head_object + size match. NOT YET ACTIVATED in cron.
Backward compat: lokalna mapa ostane primary. Brez env vars S3_* vsi
helperji vrnejo False (no-op). Production behavior identičen, dokler
ne dobi S3 creds.