feat: definido projeto com uma sequência e múltiplos arquivos de m
- definido projeto com uma sequência e múltiplos arquivos de mídia - criada persistência de projeto com uma sequência e seleções de faixas e clipes - criada guia Projeto para selecionar sequência, faixas e clipes do Premiere - adicionada gravação das seleções da guia Projeto no SQLite Resumo: - 10 arquivos alterados - 4 novos - 6 modificados - 0 removidos 6 files changed, 71 insertions(+), 2 deletions(-) Arquivos: - .jhonny/analises.db - CONTEXT.md - code/cep-plugin/index.html - code/cep-plugin/main.js - code/engine/persistencia/__init__.py - code/engine/persistencia/esquema.py - code/cep-plugin/abas/ - code/engine/persistencia/repositorio_de_projetos.py - code/engine/registrar_projeto_de_edicao.py - code/engine/testes/test_repositorio_de_projetos.py
This commit is contained in:
@@ -25,6 +25,7 @@ from .repositorio_de_planos import (
|
||||
RepositorioDePlanos,
|
||||
plano_de_dict,
|
||||
)
|
||||
from .repositorio_de_projetos import ErroDeProjeto, ProjetoDeEdicao, RepositorioDeProjetos
|
||||
from .repositorio_de_edicoes import EdicaoDeVideo, ErroDeEdicao, RepositorioDeEdicoes, edicao_de_dict
|
||||
from .repositorio_de_analises_sqlite import RepositorioDeAnalisesSQLite
|
||||
from .repositorio_de_retakes_sqlite import RepositorioDeRetakesSQLite
|
||||
@@ -47,6 +48,9 @@ __all__ = [
|
||||
"PlanoDeEdicao",
|
||||
"RepositorioDeEnunciados",
|
||||
"RepositorioDePlanos",
|
||||
"RepositorioDeProjetos",
|
||||
"ProjetoDeEdicao",
|
||||
"ErroDeProjeto",
|
||||
"RepositorioDeEdicoes",
|
||||
"ResultadoDeBusca",
|
||||
"abrir_banco",
|
||||
|
||||
@@ -285,6 +285,32 @@ CREATE TABLE IF NOT EXISTS aplicacoes_do_plano (
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_aplicacoes_plano ON aplicacoes_do_plano(plano_id);
|
||||
|
||||
-- Um projeto representa exatamente uma sequência, mas pode selecionar vários
|
||||
-- arquivos, faixas e clipes pertencentes a ela.
|
||||
CREATE TABLE IF NOT EXISTS projetos_de_edicao (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
video_id TEXT NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
|
||||
nome TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'aberto',
|
||||
criado_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
||||
atualizado_em TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_projetos_video ON projetos_de_edicao(video_id, atualizado_em);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS faixas_do_projeto (
|
||||
projeto_id INTEGER NOT NULL REFERENCES projetos_de_edicao(id) ON DELETE CASCADE,
|
||||
faixa_id TEXT NOT NULL,
|
||||
selecionada INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (projeto_id, faixa_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clipes_do_projeto (
|
||||
projeto_id INTEGER NOT NULL REFERENCES projetos_de_edicao(id) ON DELETE CASCADE,
|
||||
clipe_id TEXT NOT NULL,
|
||||
selecionado INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (projeto_id, clipe_id)
|
||||
);
|
||||
|
||||
-- Configuração editorial escolhida no painel, antes de existir um plano da IA.
|
||||
-- A análise continua normalizada nas tabelas do Scanner; esta tabela guarda
|
||||
-- somente a decisão do usuário e suas opções para a edição.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Persistência dos projetos de edição e das seleções da guia Projeto."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class ErroDeProjeto(ValueError):
|
||||
"""Projeto inválido para persistência."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProjetoDeEdicao:
|
||||
"""Projeto associado a exatamente uma sequência do Premiere."""
|
||||
|
||||
nome: str
|
||||
video_id: str
|
||||
faixas: tuple[str, ...] = ()
|
||||
clipes: tuple[str, ...] = ()
|
||||
status: str = "aberto"
|
||||
|
||||
|
||||
class RepositorioDeProjetos:
|
||||
"""Grava e recupera projetos sem duplicar dados da timeline."""
|
||||
|
||||
def __init__(self, conexao: sqlite3.Connection) -> None:
|
||||
"""Inicializa o repositório com uma conexão SQLite preparada."""
|
||||
self.conexao = conexao
|
||||
|
||||
def registrar(self, projeto: ProjetoDeEdicao) -> int:
|
||||
"""Cria um projeto e grava suas faixas e clipes selecionados."""
|
||||
if not projeto.nome.strip():
|
||||
raise ErroDeProjeto("O projeto precisa ter um nome.")
|
||||
if not projeto.video_id.strip():
|
||||
raise ErroDeProjeto("O projeto precisa ter uma sequência.")
|
||||
with self.conexao:
|
||||
cursor = self.conexao.execute(
|
||||
"""INSERT INTO projetos_de_edicao (video_id, nome, status)
|
||||
VALUES (?, ?, ?)""",
|
||||
(projeto.video_id, projeto.nome, projeto.status),
|
||||
)
|
||||
projeto_id = int(cursor.lastrowid)
|
||||
self.conexao.executemany(
|
||||
"INSERT INTO faixas_do_projeto (projeto_id, faixa_id) VALUES (?, ?)",
|
||||
[(projeto_id, faixa_id) for faixa_id in projeto.faixas],
|
||||
)
|
||||
self.conexao.executemany(
|
||||
"INSERT INTO clipes_do_projeto (projeto_id, clipe_id) VALUES (?, ?)",
|
||||
[(projeto_id, clipe_id) for clipe_id in projeto.clipes],
|
||||
)
|
||||
return projeto_id
|
||||
|
||||
def carregar(self, projeto_id: int) -> dict[str, object] | None:
|
||||
"""Carrega um projeto com sua sequência, faixas e clipes selecionados."""
|
||||
linha = self.conexao.execute(
|
||||
"SELECT * FROM projetos_de_edicao WHERE id = ?", (projeto_id,),
|
||||
).fetchone()
|
||||
if linha is None:
|
||||
return None
|
||||
return {
|
||||
"id": linha["id"], "nome": linha["nome"], "video_id": linha["video_id"],
|
||||
"status": linha["status"], "faixas": [
|
||||
r["faixa_id"] for r in self.conexao.execute(
|
||||
"""SELECT faixa_id FROM faixas_do_projeto
|
||||
WHERE projeto_id = ? AND selecionada = 1 ORDER BY rowid""",
|
||||
(projeto_id,),
|
||||
)
|
||||
], "clipes": [
|
||||
r["clipe_id"] for r in self.conexao.execute(
|
||||
"""SELECT clipe_id FROM clipes_do_projeto
|
||||
WHERE projeto_id = ? AND selecionado = 1 ORDER BY rowid""",
|
||||
(projeto_id,),
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["ErroDeProjeto", "ProjetoDeEdicao", "RepositorioDeProjetos"]
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Entrada de linha de comando para salvar um projeto de edição no SQLite."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
CAMINHO_DO_CODIGO = Path(__file__).resolve().parent.parent
|
||||
if str(CAMINHO_DO_CODIGO) not in sys.path:
|
||||
sys.path.insert(0, str(CAMINHO_DO_CODIGO))
|
||||
|
||||
from engine.persistencia.conexao import abrir_banco
|
||||
from engine.persistencia.repositorio_de_projetos import ProjetoDeEdicao, RepositorioDeProjetos
|
||||
|
||||
|
||||
def registrar(caminho_do_pedido: Path, caminho_do_banco: Path) -> int:
|
||||
"""Lê a seleção da guia Projeto e grava seu projeto no banco."""
|
||||
dados = json.loads(caminho_do_pedido.read_text(encoding="utf-8"))
|
||||
conexao = abrir_banco(caminho_do_banco)
|
||||
try:
|
||||
projeto = ProjetoDeEdicao(
|
||||
nome=str(dados["nome"]), video_id=str(dados["video_id"]),
|
||||
faixas=tuple(str(valor) for valor in dados.get("faixas", [])),
|
||||
clipes=tuple(str(valor) for valor in dados.get("clipes", [])),
|
||||
)
|
||||
return RepositorioDeProjetos(conexao).registrar(projeto)
|
||||
finally:
|
||||
conexao.close()
|
||||
|
||||
|
||||
def principal() -> None:
|
||||
"""Executa o registro pela linha de comando."""
|
||||
argumentos = argparse.ArgumentParser()
|
||||
argumentos.add_argument("pedido", type=Path)
|
||||
argumentos.add_argument("--banco", type=Path, default=Path(".jhonny/analises.db"))
|
||||
opcoes = argumentos.parse_args()
|
||||
try:
|
||||
identificador = registrar(opcoes.pedido, opcoes.banco)
|
||||
print(json.dumps({"ok": True, "projeto_id": identificador}, ensure_ascii=False))
|
||||
except (OSError, KeyError, ValueError) as erro:
|
||||
print(json.dumps({"ok": False, "erro": str(erro)}, ensure_ascii=False))
|
||||
raise SystemExit(1) from erro
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
principal()
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Testes da persistência de projetos com uma sequência e várias seleções."""
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from engine.persistencia import ProjetoDeEdicao, RepositorioDeProjetos, abrir_banco
|
||||
|
||||
|
||||
class TesteRepositorioDeProjetos(unittest.TestCase):
|
||||
"""Garante a regra de uma sequência por projeto."""
|
||||
|
||||
def test_registra_um_projeto_com_varias_faixas_e_clipes(self):
|
||||
with tempfile.TemporaryDirectory() as pasta:
|
||||
conexao = abrir_banco(Path(pasta) / "analises.db")
|
||||
conexao.execute("INSERT INTO videos (id, nome) VALUES ('video-1', 'Sequência')")
|
||||
repositorio = RepositorioDeProjetos(conexao)
|
||||
projeto_id = repositorio.registrar(ProjetoDeEdicao(
|
||||
nome="Projeto depoimento", video_id="video-1",
|
||||
faixas=("video_0", "audio_0"), clipes=("clipe-v", "clipe-a"),
|
||||
))
|
||||
projeto = repositorio.carregar(projeto_id)
|
||||
conexao.close()
|
||||
|
||||
self.assertEqual(projeto["video_id"], "video-1")
|
||||
self.assertEqual(projeto["faixas"], ["video_0", "audio_0"])
|
||||
self.assertEqual(projeto["clipes"], ["clipe-v", "clipe-a"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user