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 = '
Aguardando
'; 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()