"""Persistência SQLite da estrutura da timeline (faixas e clipes). É a "coluna vertebral" que liga os `clipe_id`/`video_id` usados em transcrição, evidências visuais e retakes de volta à timeline real — para que um agente de edição possa navegar tudo por consultas SQL, sem precisar reabrir os arquivos de origem ou o projeto de edição. """ from __future__ import annotations import json import sqlite3 from pathlib import Path from ..dominio.entidades.modelos import Clipe, Faixa, IntervaloDeTempo, Timeline from .conexao import abrir_banco class RepositorioDeTimelineSQLite: """Grava e recarrega uma `Timeline` (faixas e clipes) por vídeo.""" def __init__(self, banco: str | Path | sqlite3.Connection = ".jhonny/analises.db") -> None: self.conexao = banco if isinstance(banco, sqlite3.Connection) else abrir_banco(banco) def registrar_timeline(self, timeline: Timeline) -> None: """Grava a timeline de forma idempotente (substitui faixas/clipes anteriores).""" video_id = timeline.identificador with self.conexao: self.conexao.execute( """INSERT INTO videos (id, nome, duracao, taxa_de_quadros, largura, altura) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET nome = excluded.nome, duracao = excluded.duracao, taxa_de_quadros = excluded.taxa_de_quadros, largura = excluded.largura, altura = excluded.altura""", (video_id, timeline.nome, timeline.duracao, timeline.taxa_de_quadros, timeline.largura, timeline.altura), ) # CASCADE em clipes/faixas cuida da limpeza ao apagar as faixas do vídeo. self.conexao.execute("DELETE FROM faixas WHERE video_id = ?", (video_id,)) for faixa in timeline.faixas: self.conexao.execute( "INSERT INTO faixas (id, video_id, nome, tipo, indice) VALUES (?, ?, ?, ?, ?)", (faixa.identificador, video_id, faixa.nome, faixa.tipo, faixa.indice), ) for clipe in faixa.clipes: origem = clipe.intervalo_na_origem self.conexao.execute( """INSERT INTO clipes (id, video_id, faixa_id, nome, inicio_na_timeline, fim_na_timeline, inicio_na_origem, fim_na_origem, arquivo, offline, metadados) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", (clipe.identificador, video_id, faixa.identificador, clipe.nome, clipe.intervalo_na_timeline.inicio, clipe.intervalo_na_timeline.fim, origem.inicio if origem else None, origem.fim if origem else None, clipe.arquivo, int(clipe.offline), json.dumps(clipe.metadados, ensure_ascii=False)), ) def carregar_timeline(self, video_id: str) -> Timeline | None: """Reconstrói a `Timeline` (faixas e clipes) a partir do banco.""" video = self.conexao.execute( "SELECT * FROM videos WHERE id = ?", (video_id,), ).fetchone() if video is None: return None faixas: list[Faixa] = [] for linha_faixa in self.conexao.execute( "SELECT * FROM faixas WHERE video_id = ? ORDER BY indice", (video_id,), ).fetchall(): clipes = [ Clipe( identificador=item["id"], nome=item["nome"], intervalo_na_timeline=IntervaloDeTempo( item["inicio_na_timeline"], item["fim_na_timeline"]), intervalo_na_origem=( IntervaloDeTempo(item["inicio_na_origem"], item["fim_na_origem"]) if item["inicio_na_origem"] is not None else None ), arquivo=item["arquivo"], identificador_da_faixa=item["faixa_id"], offline=bool(item["offline"]), metadados=json.loads(item["metadados"] or "{}"), ) for item in self.conexao.execute( "SELECT * FROM clipes WHERE video_id = ? AND faixa_id = ? " "ORDER BY inicio_na_timeline", (video_id, linha_faixa["id"]), ).fetchall() ] faixas.append(Faixa( identificador=linha_faixa["id"], nome=linha_faixa["nome"], tipo=linha_faixa["tipo"], indice=linha_faixa["indice"], clipes=clipes, )) return Timeline( identificador=video["id"], nome=video["nome"], duracao=video["duracao"], taxa_de_quadros=video["taxa_de_quadros"], largura=video["largura"], altura=video["altura"], faixas=faixas, )