diff --git a/creation.html b/creation.html index 343ee390..a5282dc2 100755 --- a/creation.html +++ b/creation.html @@ -804,6 +804,76 @@ }); } }); + + document.addEventListener("DOMContentLoaded", () => { + // --- LÓGICA DE UPLOAD DE SAMPLE AVULSO --- + const uploadSampleBtn = document.getElementById("upload-sample-btn"); + const sampleInput = document.getElementById("sample-file-input"); + + if (uploadSampleBtn && sampleInput) { + + // 1. Botão clica no input invisível + uploadSampleBtn.addEventListener("click", () => { + sampleInput.click(); + }); + + // 2. Quando o arquivo é selecionado + sampleInput.addEventListener("change", async () => { + if (sampleInput.files.length === 0) return; + + const file = sampleInput.files[0]; + + // Pergunta a categoria para organizar no servidor (opcional) + // O backend usa isso para criar pastas: samples/drums, samples/vocals, etc. + const category = prompt( + "Em qual categoria este sample se encaixa? (Ex: drums, effects, vocals)", + "imported" + ); + + if (category === null) { + // Usuário cancelou + sampleInput.value = ""; + return; + } + + // Prepara o formulário + const formData = new FormData(); + formData.append("sample_file", file); // Deve bater com 'sample_file' no Python + formData.append("subfolder", category); // Deve bater com 'subfolder' no Python + + // Feedback Visual (ícone girando ou ficando transparente) + const originalIcon = uploadSampleBtn.className; + uploadSampleBtn.className = "fa-solid fa-spinner fa-spin"; // Ícone de loading + uploadSampleBtn.style.pointerEvents = "none"; + + try { + const response = await fetch("https://alice.ufsj.edu.br:33002/api/upload/sample", { + method: "POST", + body: formData + }); + + const result = await response.json(); + + if (response.ok) { + alert("Sucesso! " + result.message); + // Opcional: Recarregar a lista de samples lateral se você tiver uma função para isso + // reloadBrowser(); + } else { + alert("Erro ao enviar: " + (result.error || "Desconhecido")); + } + + } catch (error) { + console.error("Erro no upload:", error); + alert("Erro de conexão com o servidor."); + } finally { + // Restaura o botão e limpa o input + uploadSampleBtn.className = originalIcon; + uploadSampleBtn.style.pointerEvents = "auto"; + sampleInput.value = ""; + } + }); + } + }); diff --git a/scripts/handler/upload_server.py b/scripts/handler/upload_server.py index 026d5d2d..fd1513dc 100755 --- a/scripts/handler/upload_server.py +++ b/scripts/handler/upload_server.py @@ -220,17 +220,16 @@ def update_xml_paths_exact(mmp_filename, replacements): @app.route("/api/download/", methods=["GET"]) def download_project_package(project_name): """ - Gera um ZIP contendo o arquivo .mmp e apenas os samples importados necessários. - Estrutura do ZIP: - - projeto.mmp - - src_mmpSearch/ - - samples/ - - imported/ - - sample1.wav - - sample2.wav + Gera um ZIP onde: + 1. O .mmp é modificado NA VOLÁTIL (sem salvar no disco) para ter caminhos curtos. + 2. A estrutura do ZIP fica limpa: + - projeto.mmp + - imported/ + - sample1.wav + - sample2.wav """ - # Garante que estamos pegando o .mmp (mesmo que peçam sem extensão) - if not project_name.endswith('.mmp'): + # Garante extensão .mmp + if not project_name.lower().endswith('.mmp'): project_name += '.mmp' clean_name = secure_filename(project_name) @@ -239,56 +238,65 @@ def download_project_package(project_name): if not os.path.exists(mmp_path): return jsonify({"error": "Projeto não encontrado"}), 404 - # Prepara o buffer do ZIP na memória (não salva no disco para economizar I/O) + # Prepara o buffer do ZIP na memória memory_file = io.BytesIO() try: + # 1. Parseia o XML original do disco + tree = ET.parse(mmp_path) + root = tree.getroot() + + # Conjunto para rastrear quais arquivos físicos precisamos colocar no ZIP + samples_to_pack = set() + + # 2. Modifica o XML na MEMÓRIA (não afeta o arquivo original no servidor) + for audio_node in root.findall(".//audiofileprocessor"): + src = audio_node.get('src', '') + + # Verifica se é um sample importado do nosso sistema + if src.startswith(XML_IMPORTED_PATH_PREFIX): + # Pega apenas o nome do arquivo (ex: 'kick_deep.wav') + filename = os.path.basename(src) + + # Define o NOVO caminho curto relativo para dentro do ZIP + # Antes: src_mmpSearch/samples/imported/kick_deep.wav + # Agora: imported/kick_deep.wav + short_path = f"imported/{filename}" + + # Atualiza o XML na memória + audio_node.set('src', short_path) + + # Adiciona à lista de arquivos para copiar + samples_to_pack.add(filename) + + # 3. Cria o ZIP with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zf: - # 1. Adiciona o arquivo .mmp na RAIZ do ZIP - # arcname é o nome que o arquivo terá dentro do zip - zf.write(mmp_path, arcname=clean_name) + # A) Escreve o .mmp MODIFICADO no ZIP + # Convertemos a árvore XML modificada para string binária + xml_str = ET.tostring(root, encoding='utf-8', method='xml') + zf.writestr(clean_name, xml_str) - # 2. Lê o XML para descobrir quais samples incluir - tree = ET.parse(mmp_path) - root = tree.getroot() - - # Conjunto para evitar adicionar o mesmo sample 2x - samples_to_pack = set() - - for audio_node in root.findall(".//audiofileprocessor"): - src = audio_node.get('src', '') - - # Verifica se é um sample importado (nosso padrão) - # O src no XML já deve estar como: src_mmpSearch/samples/imported/nome.wav - if src.startswith(XML_IMPORTED_PATH_PREFIX): - # Extrai apenas o nome do arquivo (ex: kick.wav) - sample_filename = os.path.basename(src) - samples_to_pack.add(sample_filename) - - # 3. Adiciona os samples encontrados ao ZIP mantendo a estrutura de pasta + # B) Adiciona os samples físicos na pasta 'imported/' do ZIP for sample_name in samples_to_pack: - # Caminho físico no servidor (onde o arquivo realmente está) physical_path = os.path.join(XML_IMPORTED_PATH_PREFIX, sample_name) - # Caminho DENTRO do ZIP (para o LMMS ler relativo ao mmp) - # Deve ser: src_mmpSearch/samples/imported/sample.wav - zip_internal_path = f"{XML_IMPORTED_PATH_PREFIX}/{sample_name}" + # Caminho curto dentro do ZIP + zip_entry_name = f"imported/{sample_name}" if os.path.exists(physical_path): - zf.write(physical_path, arcname=zip_internal_path) - print(f"[ZIP] Adicionado: {sample_name}") + zf.write(physical_path, arcname=zip_entry_name) + print(f"[ZIP CLEAN] Adicionado: {zip_entry_name}") else: - print(f"[ZIP] AVISO: Sample listado no XML mas não encontrado no disco: {sample_name}") + print(f"[ZIP CLEAN] AVISO: Arquivo faltante no disco: {sample_name}") - # Finaliza o ponteiro do arquivo + # Finaliza e envia memory_file.seek(0) - return send_file( memory_file, mimetype='application/zip', as_attachment=True, - download_name=f"{os.path.splitext(clean_name)[0]}_pack.zip" + download_name=f"{os.path.splitext(clean_name)[0]}.zip" ) except Exception as e: