diff --git a/.jhonny/analises.db b/.jhonny/analises.db
index 172c4fc..813750a 100644
Binary files a/.jhonny/analises.db and b/.jhonny/analises.db differ
diff --git a/code/cep-plugin/main.js b/code/cep-plugin/main.js
index 781687c..5515fc1 100755
--- a/code/cep-plugin/main.js
+++ b/code/cep-plugin/main.js
@@ -2888,6 +2888,42 @@ function trilhasAoAbrir() {
var pasta = document.getElementById("trilhasPasta");
if (pasta && configuracao.pasta) pasta.value = configuracao.pasta;
trilhasAtualizarEstado();
+ if (pasta && pasta.value.trim() && fs.existsSync(pasta.value.trim())) trilhasAtualizarCatalogo(pasta.value.trim());
+}
+
+function trilhasAtualizarCatalogo(pasta) {
+ var argumentos = [TRILHAS_ENGINE_SCRIPT, pasta, "--banco", TRILHAS_BANCO_PATH, "--listar"];
+ runStreamed(SCANNER_PYTHON_BIN, argumentos, function (linha) {
+ if (linha.indexOf("@@PROGRESS ") !== 0) return;
+ try { trilhasAtualizarEvento(JSON.parse(linha.slice(11))); } catch (_) {}
+ }, function () {}, { cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", stdio: ["ignore", "pipe", "pipe"] });
+}
+
+function trilhasListarArquivos(pasta) {
+ var extensoes = { ".wav": true, ".mp3": true, ".flac": true, ".m4a": true, ".aiff": true, ".ogg": true };
+ var arquivos = [];
+ function visitar(diretorio) {
+ fs.readdirSync(diretorio).forEach(function (nome) {
+ var caminho = path.join(diretorio, nome);
+ var estatisticas = fs.statSync(caminho);
+ if (estatisticas.isDirectory()) visitar(caminho);
+ else if (estatisticas.isFile() && extensoes[path.extname(nome).toLowerCase()]) arquivos.push(caminho);
+ });
+ }
+ visitar(pasta);
+ return arquivos.sort();
+}
+
+function trilhasPrepararLista(pasta) {
+ try {
+ var arquivos = trilhasListarArquivos(pasta);
+ trilhasRenderizarLista(arquivos.map(function (arquivo) { return path.relative(pasta, arquivo) || path.basename(arquivo); }));
+ document.getElementById("trilhasPastaStatus").textContent = arquivos.length
+ ? arquivos.length + " arquivo(s) de áudio encontrado(s). Pronto para análise."
+ : "Nenhum arquivo de áudio compatível foi encontrado nesta pasta.";
+ } catch (erro) {
+ document.getElementById("trilhasPastaStatus").textContent = "Não foi possível ler a pasta selecionada.";
+ }
}
function trilhasSelecionarPasta() {
@@ -2900,6 +2936,7 @@ function trilhasSelecionarPasta() {
document.getElementById("trilhasPasta").value = escolhida;
trilhasSalvarConfiguracao(escolhida);
trilhasAtualizarEstado();
+ trilhasPrepararLista(escolhida);
} catch (erro) { showToast("err", erro.message); }
}
@@ -2910,18 +2947,23 @@ function trilhasAtualizarEstado() {
var valida = pasta.value.trim() && fs.existsSync(pasta.value.trim()) && fs.statSync(pasta.value.trim()).isDirectory();
botao.disabled = !valida || taskState.active;
document.getElementById("trilhasPastaStatus").textContent = valida ? "Pasta pronta para análise." : "Selecione uma pasta existente.";
+ if (valida && !taskState.active && !document.querySelector(".trilha-item")) trilhasPrepararLista(pasta.value.trim());
}
function trilhasRenderizarLista(nomes) {
var lista = document.getElementById("trilhasLista");
lista.innerHTML = "";
- (nomes || []).forEach(function (nome, indice) {
+ (nomes || []).forEach(function (registro, indice) {
+ var nome = typeof registro === "string" ? registro : registro.nome;
+ var status = typeof registro === "string" ? "aguardando" : (registro.status || "aguardando");
var item = document.createElement("div");
item.className = "trilha-item";
item.id = "trilha-item-" + indice;
- item.setAttribute("data-state", "aguardando");
+ item.setAttribute("data-state", status === "concluida" ? "concluida" : status === "erro" ? "erro" : "aguardando");
item.innerHTML = '
';
item.querySelector("strong").textContent = nome;
+ item.querySelector(".trilha-info > span").textContent = status === "concluida" ? "Analisada" : status === "erro" ? "Erro na análise anterior" : "Aguardando análise";
+ if (status === "concluida") item.querySelector(".progress-fill").style.width = "100%";
lista.appendChild(item);
});
document.getElementById("trilhasContagem").textContent = (nomes || []).length + " trilha(s)";
@@ -2929,7 +2971,8 @@ function trilhasRenderizarLista(nomes) {
}
function trilhasAtualizarEvento(evento) {
- if (evento.evento === "lote_iniciado") { trilhasRenderizarLista(evento.nomes || []); return; }
+ if (evento.evento === "catalogo_atualizado") { trilhasRenderizarLista(evento.trilhas || []); return; }
+ if (evento.evento === "lote_iniciado") { trilhasRenderizarLista(evento.trilhas || evento.nomes || []); return; }
if (!evento.nome) return;
var itens = Array.prototype.slice.call(document.querySelectorAll(".trilha-item"));
var item = itens.filter(function (elemento) { return elemento.querySelector("strong").textContent === evento.nome; })[0];
diff --git a/code/engine/analisar_trilhas.py b/code/engine/analisar_trilhas.py
index 45d4a70..3c3d9e5 100644
--- a/code/engine/analisar_trilhas.py
+++ b/code/engine/analisar_trilhas.py
@@ -23,8 +23,8 @@ def emitir(evento: str, **dados: object) -> None:
print("@@PROGRESS " + json.dumps({"evento": evento, **dados}, ensure_ascii=False), flush=True)
-def executar(pasta: Path, banco: Path, diretorio_dos_modelos: Path | None) -> int:
- """Analisa todos os arquivos musicais encontrados na pasta e subpastas."""
+def executar(pasta: Path, banco: Path, diretorio_dos_modelos: Path | None, apenas_listar: bool = False) -> int:
+ """Atualiza o catálogo e, opcionalmente, analisa as trilhas pendentes."""
arquivos = tuple(sorted(
arquivo for arquivo in pasta.rglob("*")
if arquivo.is_file() and arquivo.suffix.lower() in EXTENSOES_DE_AUDIO
@@ -33,14 +33,27 @@ def executar(pasta: Path, banco: Path, diretorio_dos_modelos: Path | None) -> in
try:
repositorio = RepositorioDeTrilhasSQLite(conexao)
trilhas = repositorio.registrar_pasta_e_arquivos(pasta, arquivos)
+ if apenas_listar:
+ emitir("catalogo_atualizado", trilhas=[
+ {"nome": trilha.nome_do_arquivo, "status": trilha.status}
+ for trilha in trilhas
+ ])
+ return 0
classificador = (
ClassificadorDeMusicaEssentia(diretorio_dos_modelos)
if diretorio_dos_modelos and diretorio_dos_modelos.is_dir()
else None
)
analisador = AnalisadorDeMusicaInstrumentalEssentia(classificador=classificador)
- emitir("lote_iniciado", total=len(trilhas), pasta=str(pasta), nomes=[trilha.nome_do_arquivo for trilha in trilhas])
- for ordem, trilha in enumerate(trilhas, start=1):
+ pendentes = tuple(trilha for trilha in trilhas if trilha.status != "concluida")
+ emitir(
+ "lote_iniciado",
+ total=len(pendentes),
+ pasta=str(pasta),
+ nomes=[trilha.nome_do_arquivo for trilha in pendentes],
+ trilhas=[{"nome": trilha.nome_do_arquivo, "status": trilha.status} for trilha in trilhas],
+ )
+ for ordem, trilha in enumerate(pendentes, start=1):
repositorio.iniciar_analise(trilha.identificador)
emitir("trilha_iniciada", nome=trilha.nome_do_arquivo, indice=ordem, total=len(trilhas), percentual=(ordem - 1) * 100 / max(1, len(trilhas)))
try:
@@ -62,9 +75,10 @@ def principal() -> None:
argumentos.add_argument("pasta", type=Path)
argumentos.add_argument("--banco", type=Path, default=Path(".jhonny/analises.db"))
argumentos.add_argument("--modelos", type=Path, default=None)
+ argumentos.add_argument("--listar", action="store_true")
opcoes = argumentos.parse_args()
try:
- raise SystemExit(executar(opcoes.pasta, opcoes.banco, opcoes.modelos))
+ raise SystemExit(executar(opcoes.pasta, opcoes.banco, opcoes.modelos, opcoes.listar))
except (OSError, RuntimeError, ValueError) as erro:
emitir("lote_com_erro", erro=str(erro))
raise SystemExit(1) from erro
diff --git a/code/engine/persistencia/repositorio_de_trilhas.py b/code/engine/persistencia/repositorio_de_trilhas.py
index a81519c..dd1455f 100644
--- a/code/engine/persistencia/repositorio_de_trilhas.py
+++ b/code/engine/persistencia/repositorio_de_trilhas.py
@@ -52,7 +52,12 @@ class RepositorioDeTrilhasSQLite:
"""INSERT INTO trilhas_musicais(pasta_id, caminho, nome_do_arquivo, tamanho_em_bytes)
VALUES (?, ?, ?, ?)
ON CONFLICT(caminho) DO UPDATE SET nome_do_arquivo = excluded.nome_do_arquivo,
- tamanho_em_bytes = excluded.tamanho_em_bytes, atualizada_em = strftime('%Y-%m-%dT%H:%M:%fZ','now')""",
+ status = CASE WHEN trilhas_musicais.tamanho_em_bytes != excluded.tamanho_em_bytes
+ THEN 'aguardando' ELSE trilhas_musicais.status END,
+ hash_do_conteudo = CASE WHEN trilhas_musicais.tamanho_em_bytes != excluded.tamanho_em_bytes
+ THEN NULL ELSE trilhas_musicais.hash_do_conteudo END,
+ tamanho_em_bytes = excluded.tamanho_em_bytes,
+ atualizada_em = strftime('%Y-%m-%dT%H:%M:%fZ','now')""",
(pasta_id, str(arquivo), arquivo.name, arquivo.stat().st_size),
)
identificador = linha.lastrowid or self.conexao.execute(
diff --git a/code/engine/testes/test_repositorio_de_trilhas.py b/code/engine/testes/test_repositorio_de_trilhas.py
index 1294db1..cb9a211 100644
--- a/code/engine/testes/test_repositorio_de_trilhas.py
+++ b/code/engine/testes/test_repositorio_de_trilhas.py
@@ -58,6 +58,23 @@ class TesteRepositorioDeTrilhasSQLite(unittest.TestCase):
self.assertEqual(primeira[0].identificador, segunda[0].identificador)
self.assertEqual(quantidade, 1)
+ def test_arquivo_alterado_volta_para_aguardando(self):
+ with tempfile.TemporaryDirectory() as diretorio:
+ pasta = Path(diretorio) / "trilhas"
+ pasta.mkdir()
+ arquivo = pasta / "tema.wav"
+ arquivo.write_bytes(b"audio")
+ repositorio = RepositorioDeTrilhasSQLite(Path(diretorio) / "trilhas.db")
+ trilha = repositorio.registrar_pasta_e_arquivos(pasta, (arquivo,))[0]
+ repositorio.iniciar_analise(trilha.identificador)
+ repositorio.registrar_analise(trilha, ResultadoDaAnaliseMusical(caminho_do_arquivo=arquivo))
+
+ arquivo.write_bytes(b"audio alterado")
+ atualizada = repositorio.registrar_pasta_e_arquivos(pasta, (arquivo,))[0]
+ repositorio.conexao.close()
+
+ self.assertEqual(atualizada.status, "aguardando")
+
if __name__ == "__main__":
unittest.main()