inserindo server flask de upload e watchdog para verificar alterações em samples
This commit is contained in:
1 parent
c35f3233b7
commit
8c391412e1
6 files changed
+434
-161
No files matched your search
@@ -707,3 +707,62 @@ body.sidebar-hidden .global-toolbar {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- ESTILOS PARA O MENU ARQUIVO --- */
|
||||||
|
|
||||||
|
.file-menu-container {
|
||||||
|
position: relative; /* Essencial para o posicionamento do dropdown */
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
background-color: var(--background-light);
|
||||||
|
color: var(--text-light);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover {
|
||||||
|
background-color: var(--background-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-menu-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
background-color: var(--background-lighter);
|
||||||
|
border: 1px solid var(--border-color-dark);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
min-width: 200px;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-menu-dropdown.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-menu-dropdown a {
|
||||||
|
color: var(--text-light);
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-menu-dropdown a:hover {
|
||||||
|
background-color: var(--accent-blue);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
+56
-32
@@ -19,15 +19,16 @@ import {
|
|||||||
loadAndRenderSampleBrowser,
|
loadAndRenderSampleBrowser,
|
||||||
showOpenProjectModal,
|
showOpenProjectModal,
|
||||||
closeOpenProjectModal,
|
closeOpenProjectModal,
|
||||||
|
handleSampleUpload, // Importa handleSampleUpload, embora não seja mais usado diretamente
|
||||||
} from "./ui.js";
|
} from "./ui.js";
|
||||||
import { adjustValue, enforceNumericInput } from "./utils.js";
|
import { adjustValue, enforceNumericInput } from "./utils.js";
|
||||||
import { DEFAULT_PAN, DEFAULT_VOLUME } from "./config.js";
|
import { DEFAULT_PAN, DEFAULT_VOLUME } from "./config.js";
|
||||||
|
|
||||||
// --- INICIALIZAÇÃO E EVENTOS FINAIS ---
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const newProjectBtn = document.getElementById("new-project-btn");
|
const newProjectBtn = document.getElementById("new-project-btn");
|
||||||
const openMmpBtn = document.getElementById("open-mmp-btn");
|
const openMmpBtn = document.getElementById("open-mmp-btn");
|
||||||
const saveMmpBtn = document.getElementById("save-mmp-btn");
|
const saveMmpBtn = document.getElementById("save-mmp-btn");
|
||||||
|
const uploadSampleBtn = document.getElementById("upload-sample-btn");
|
||||||
const addInstrumentBtn = document.getElementById("add-instrument-btn");
|
const addInstrumentBtn = document.getElementById("add-instrument-btn");
|
||||||
const removeInstrumentBtn = document.getElementById("remove-instrument-btn");
|
const removeInstrumentBtn = document.getElementById("remove-instrument-btn");
|
||||||
const playBtn = document.getElementById("play-btn");
|
const playBtn = document.getElementById("play-btn");
|
||||||
@@ -35,12 +36,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const rewindBtn = document.getElementById("rewind-btn");
|
const rewindBtn = document.getElementById("rewind-btn");
|
||||||
const metronomeBtn = document.getElementById("metronome-btn");
|
const metronomeBtn = document.getElementById("metronome-btn");
|
||||||
const mmpFileInput = document.getElementById("mmp-file-input");
|
const mmpFileInput = document.getElementById("mmp-file-input");
|
||||||
|
const sampleFileInput = document.getElementById("sample-file-input");
|
||||||
const openProjectModal = document.getElementById("open-project-modal");
|
const openProjectModal = document.getElementById("open-project-modal");
|
||||||
const openModalCloseBtn = document.getElementById("open-modal-close-btn");
|
const openModalCloseBtn = document.getElementById("open-modal-close-btn");
|
||||||
const loadFromComputerBtn = document.getElementById("load-from-computer-btn");
|
const loadFromComputerBtn = document.getElementById("load-from-computer-btn");
|
||||||
const sidebarToggle = document.getElementById("sidebar-toggle");
|
const sidebarToggle = document.getElementById("sidebar-toggle");
|
||||||
const addBarBtn = document.getElementById("add-bar-btn");
|
const addBarBtn = document.getElementById("add-bar-btn");
|
||||||
|
|
||||||
const masterVolumeKnob = document.getElementById("master-volume-knob");
|
const masterVolumeKnob = document.getElementById("master-volume-knob");
|
||||||
const masterPanKnob = document.getElementById("master-pan-knob");
|
const masterPanKnob = document.getElementById("master-pan-knob");
|
||||||
|
|
||||||
@@ -52,56 +53,53 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return;
|
return;
|
||||||
Object.assign(appState, {
|
Object.assign(appState, {
|
||||||
tracks: [],
|
tracks: [],
|
||||||
|
activeTrackId: null,
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
playbackIntervalId: null,
|
playbackIntervalId: null,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
metronomeEnabled: false,
|
metronomeEnabled: false,
|
||||||
originalXmlDoc: null,
|
originalXmlDoc: null,
|
||||||
|
currentBeatBasslineName: 'Novo Projeto',
|
||||||
masterVolume: DEFAULT_VOLUME,
|
masterVolume: DEFAULT_VOLUME,
|
||||||
masterPan: DEFAULT_PAN
|
masterPan: DEFAULT_PAN
|
||||||
});
|
});
|
||||||
// Reseta os inputs para o padrão
|
|
||||||
document.getElementById('bpm-input').value = 140;
|
document.getElementById('bpm-input').value = 140;
|
||||||
document.getElementById('bars-input').value = 1;
|
document.getElementById('bars-input').value = 1;
|
||||||
document.getElementById('compasso-a-input').value = 4;
|
document.getElementById('compasso-a-input').value = 4;
|
||||||
document.getElementById('compasso-b-input').value = 4;
|
document.getElementById('compasso-b-input').value = 4;
|
||||||
|
const titleElement = document.getElementById('beat-bassline-title');
|
||||||
|
if(titleElement) titleElement.textContent = 'Novo Projeto';
|
||||||
renderApp();
|
renderApp();
|
||||||
setupMasterKnobs(); // Re-inicializa os knobs master
|
setupMasterKnobs();
|
||||||
});
|
});
|
||||||
|
|
||||||
addBarBtn.addEventListener("click", () => {
|
addBarBtn.addEventListener("click", () => {
|
||||||
const barsInput = document.getElementById("bars-input");
|
const barsInput = document.getElementById("bars-input");
|
||||||
if (barsInput) {
|
if (barsInput) adjustValue(barsInput, 1);
|
||||||
adjustValue(barsInput, 1);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupMasterKnobs() {
|
function setupMasterKnobs() {
|
||||||
function updateMasterKnobVisual(knobElement, controlType) {
|
function updateMasterKnobVisual(knobElement, controlType) {
|
||||||
const indicator = knobElement.querySelector(".knob-indicator");
|
const indicator = knobElement.querySelector(".knob-indicator");
|
||||||
if (!indicator) return;
|
if (!indicator) return;
|
||||||
|
|
||||||
const minAngle = -135;
|
const minAngle = -135;
|
||||||
const maxAngle = 135;
|
const maxAngle = 135;
|
||||||
let percentage = 0.5;
|
let percentage = 0.5;
|
||||||
let title = "";
|
let title = "";
|
||||||
|
|
||||||
if (controlType === "volume") {
|
if (controlType === "volume") {
|
||||||
const value = appState.masterVolume;
|
const value = appState.masterVolume;
|
||||||
percentage = value / 1.5;
|
percentage = value / 1.5;
|
||||||
title = `Volume Master: ${Math.round(value * 100)}%`;
|
title = `Volume Master: ${Math.round(value * 100)}%`;
|
||||||
} else { // pan
|
} else {
|
||||||
const value = appState.masterPan;
|
const value = appState.masterPan;
|
||||||
percentage = (value + 1) / 2;
|
percentage = (value + 1) / 2;
|
||||||
const panDisplay = Math.round(value * 100);
|
const panDisplay = Math.round(value * 100);
|
||||||
title = `Pan Master: ${ panDisplay === 0 ? "Centro" : panDisplay < 0 ? `${-panDisplay} L` : `${panDisplay} R` }`;
|
title = `Pan Master: ${ panDisplay === 0 ? "Centro" : panDisplay < 0 ? `${-panDisplay} L` : `${panDisplay} R` }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const angle = minAngle + percentage * (maxAngle - minAngle);
|
const angle = minAngle + percentage * (maxAngle - minAngle);
|
||||||
indicator.style.transform = `translateX(-50%) rotate(${angle}deg)`;
|
indicator.style.transform = `translateX(-50%) rotate(${angle}deg)`;
|
||||||
knobElement.title = title;
|
knobElement.title = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addMasterKnobInteraction(knobElement, controlType) {
|
function addMasterKnobInteraction(knobElement, controlType) {
|
||||||
knobElement.addEventListener("wheel", (e) => {
|
knobElement.addEventListener("wheel", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -109,56 +107,45 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const direction = e.deltaY < 0 ? 1 : -1;
|
const direction = e.deltaY < 0 ? 1 : -1;
|
||||||
if (controlType === "volume") {
|
if (controlType === "volume") {
|
||||||
const newValue = appState.masterVolume + direction * step;
|
const newValue = appState.masterVolume + direction * step;
|
||||||
const clampedValue = Math.max(0, Math.min(1.5, newValue));
|
appState.masterVolume = Math.max(0, Math.min(1.5, newValue));
|
||||||
appState.masterVolume = clampedValue;
|
updateMasterVolume(appState.masterVolume);
|
||||||
updateMasterVolume(clampedValue);
|
|
||||||
} else {
|
} else {
|
||||||
const newValue = appState.masterPan + direction * step;
|
const newValue = appState.masterPan + direction * step;
|
||||||
const clampedValue = Math.max(-1, Math.min(1, newValue));
|
appState.masterPan = Math.max(-1, Math.min(1, newValue));
|
||||||
appState.masterPan = clampedValue;
|
updateMasterPan(appState.masterPan);
|
||||||
updateMasterPan(clampedValue);
|
|
||||||
}
|
}
|
||||||
updateMasterKnobVisual(knobElement, controlType);
|
updateMasterKnobVisual(knobElement, controlType);
|
||||||
});
|
});
|
||||||
|
|
||||||
knobElement.addEventListener("mousedown", (e) => {
|
knobElement.addEventListener("mousedown", (e) => {
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const startY = e.clientY;
|
const startY = e.clientY;
|
||||||
const startValue = controlType === "volume" ? appState.masterVolume : appState.masterPan;
|
const startValue = controlType === "volume" ? appState.masterVolume : appState.masterPan;
|
||||||
document.body.classList.add("knob-dragging");
|
document.body.classList.add("knob-dragging");
|
||||||
|
|
||||||
function onMouseMove(moveEvent) {
|
function onMouseMove(moveEvent) {
|
||||||
const deltaY = startY - moveEvent.clientY;
|
const deltaY = startY - moveEvent.clientY;
|
||||||
const sensitivity = controlType === "volume" ? 150 : 200;
|
const sensitivity = controlType === "volume" ? 150 : 200;
|
||||||
const newValue = startValue + deltaY / sensitivity;
|
const newValue = startValue + deltaY / sensitivity;
|
||||||
if (controlType === "volume") {
|
if (controlType === "volume") {
|
||||||
const clampedValue = Math.max(0, Math.min(1.5, newValue));
|
appState.masterVolume = Math.max(0, Math.min(1.5, newValue));
|
||||||
appState.masterVolume = clampedValue;
|
updateMasterVolume(appState.masterVolume);
|
||||||
updateMasterVolume(clampedValue);
|
|
||||||
} else {
|
} else {
|
||||||
const clampedValue = Math.max(-1, Math.min(1, newValue));
|
appState.masterPan = Math.max(-1, Math.min(1, newValue));
|
||||||
appState.masterPan = clampedValue;
|
updateMasterPan(appState.masterPan);
|
||||||
updateMasterPan(clampedValue);
|
|
||||||
}
|
}
|
||||||
updateMasterKnobVisual(knobElement, controlType);
|
updateMasterKnobVisual(knobElement, controlType);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseUp() {
|
function onMouseUp() {
|
||||||
document.body.classList.remove("knob-dragging");
|
document.body.classList.remove("knob-dragging");
|
||||||
document.removeEventListener("mousemove", onMouseMove);
|
document.removeEventListener("mousemove", onMouseMove);
|
||||||
document.removeEventListener("mouseup", onMouseUp);
|
document.removeEventListener("mouseup", onMouseUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("mousemove", onMouseMove);
|
document.addEventListener("mousemove", onMouseMove);
|
||||||
document.addEventListener("mouseup", onMouseUp);
|
document.addEventListener("mouseup", onMouseUp);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addMasterKnobInteraction(masterVolumeKnob, "volume");
|
addMasterKnobInteraction(masterVolumeKnob, "volume");
|
||||||
updateMasterKnobVisual(masterVolumeKnob, "volume");
|
updateMasterKnobVisual(masterVolumeKnob, "volume");
|
||||||
|
|
||||||
addMasterKnobInteraction(masterPanKnob, "pan");
|
addMasterKnobInteraction(masterPanKnob, "pan");
|
||||||
updateMasterKnobVisual(masterPanKnob, "pan");
|
updateMasterKnobVisual(masterPanKnob, "pan");
|
||||||
}
|
}
|
||||||
@@ -172,6 +159,44 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
closeOpenProjectModal();
|
closeOpenProjectModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
uploadSampleBtn.addEventListener("click", () => sampleFileInput.click());
|
||||||
|
|
||||||
|
// --- INÍCIO DA CORREÇÃO ---
|
||||||
|
// Lógica de upload de sample para o servidor Flask
|
||||||
|
sampleFileInput.addEventListener("change", async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("sampleFile", file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ATENÇÃO: Verifique se a URL e a porta estão corretas para o seu servidor Flask
|
||||||
|
const response = await fetch('http://localhost:5000/upload-sample', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert("Sample enviado com sucesso!");
|
||||||
|
// Recarrega a lista de samples para exibir o novo arquivo
|
||||||
|
await loadAndRenderSampleBrowser();
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || "Erro desconhecido no servidor.");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao enviar o sample:", error);
|
||||||
|
alert(`Falha no upload: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.target.value = null; // Limpa o input para permitir o mesmo arquivo de novo
|
||||||
|
});
|
||||||
|
// --- FIM DA CORREÇÃO ---
|
||||||
|
|
||||||
saveMmpBtn.addEventListener("click", generateMmpFile);
|
saveMmpBtn.addEventListener("click", generateMmpFile);
|
||||||
addInstrumentBtn.addEventListener("click", addTrackToState);
|
addInstrumentBtn.addEventListener("click", addTrackToState);
|
||||||
removeInstrumentBtn.addEventListener("click", removeLastTrackFromState);
|
removeInstrumentBtn.addEventListener("click", removeLastTrackFromState);
|
||||||
@@ -223,7 +248,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Inicia a aplicação
|
|
||||||
loadAndRenderSampleBrowser();
|
loadAndRenderSampleBrowser();
|
||||||
renderApp();
|
renderApp();
|
||||||
setupMasterKnobs();
|
setupMasterKnobs();
|
||||||
|
|||||||
+64
-16
@@ -6,15 +6,16 @@ import {
|
|||||||
updateTrackVolume,
|
updateTrackVolume,
|
||||||
updateTrackPan,
|
updateTrackPan,
|
||||||
} from "./state.js";
|
} from "./state.js";
|
||||||
import { playSample } from "./audio.js";
|
import { playSample, stopPlayback } from "./audio.js";
|
||||||
import { getTotalSteps } from "./utils.js";
|
import { getTotalSteps } from "./utils.js";
|
||||||
import { loadProjectFromServer } from "./file.js";
|
import { loadProjectFromServer } from "./file.js";
|
||||||
|
|
||||||
let samplePathMap = {};
|
let samplePathMap = {};
|
||||||
const globalPatternSelector = document.getElementById('global-pattern-selector');
|
const globalPatternSelector = document.getElementById('global-pattern-selector');
|
||||||
|
|
||||||
|
if (globalPatternSelector) {
|
||||||
globalPatternSelector.addEventListener('change', () => {
|
globalPatternSelector.addEventListener('change', () => {
|
||||||
// Atualiza o índice GLOBAL
|
// A linha stopPlayback() foi REMOVIDA daqui, permitindo a troca em tempo real.
|
||||||
appState.activePatternIndex = parseInt(globalPatternSelector.value, 10);
|
appState.activePatternIndex = parseInt(globalPatternSelector.value, 10);
|
||||||
|
|
||||||
const firstTrack = appState.tracks[0];
|
const firstTrack = appState.tracks[0];
|
||||||
@@ -28,11 +29,12 @@ globalPatternSelector.addEventListener('change', () => {
|
|||||||
}
|
}
|
||||||
redrawSequencer();
|
redrawSequencer();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function updateGlobalPatternSelector() {
|
export function updateGlobalPatternSelector() {
|
||||||
|
if (!globalPatternSelector) return;
|
||||||
const referenceTrack = appState.tracks[0];
|
const referenceTrack = appState.tracks[0];
|
||||||
globalPatternSelector.innerHTML = '';
|
globalPatternSelector.innerHTML = '';
|
||||||
|
|
||||||
if (referenceTrack && referenceTrack.patterns.length > 0) {
|
if (referenceTrack && referenceTrack.patterns.length > 0) {
|
||||||
referenceTrack.patterns.forEach((pattern, index) => {
|
referenceTrack.patterns.forEach((pattern, index) => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
@@ -40,7 +42,6 @@ export function updateGlobalPatternSelector() {
|
|||||||
option.textContent = pattern.name;
|
option.textContent = pattern.name;
|
||||||
globalPatternSelector.appendChild(option);
|
globalPatternSelector.appendChild(option);
|
||||||
});
|
});
|
||||||
// Usa o índice GLOBAL
|
|
||||||
globalPatternSelector.selectedIndex = appState.activePatternIndex;
|
globalPatternSelector.selectedIndex = appState.activePatternIndex;
|
||||||
globalPatternSelector.disabled = false;
|
globalPatternSelector.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -51,6 +52,42 @@ export function updateGlobalPatternSelector() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleSampleUpload(file) {
|
||||||
|
const validExtensions = ['.wav', '.flac', '.ogg', '.mp3'];
|
||||||
|
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
if (!validExtensions.includes(fileExtension)) {
|
||||||
|
alert("Formato de arquivo inválido. Por favor, envie .wav, .flac, .ogg, ou .mp3.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const dataURL = e.target.result;
|
||||||
|
const browserContent = document.getElementById("browser-content");
|
||||||
|
const list = browserContent.querySelector("ul");
|
||||||
|
|
||||||
|
if (list) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.innerHTML = `<i class="fa-solid fa-file-audio"></i> ${file.name}`;
|
||||||
|
li.setAttribute("draggable", true);
|
||||||
|
|
||||||
|
li.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
playSample(dataURL, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
li.addEventListener("dragstart", (event) => {
|
||||||
|
event.dataTransfer.setData("text/plain", dataURL);
|
||||||
|
event.dataTransfer.effectAllowed = "copy";
|
||||||
|
});
|
||||||
|
|
||||||
|
list.prepend(li);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
export function getSamplePathMap() {
|
export function getSamplePathMap() {
|
||||||
return samplePathMap;
|
return samplePathMap;
|
||||||
}
|
}
|
||||||
@@ -89,15 +126,11 @@ export function renderApp() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="track-controls">
|
<div class="track-controls">
|
||||||
<div class="knob-container">
|
<div class="knob-container">
|
||||||
<div class="knob" data-control="volume" data-track-id="${trackData.id}">
|
<div class="knob" data-control="volume" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
|
||||||
<div class="knob-indicator"></div>
|
|
||||||
</div>
|
|
||||||
<span>VOL</span>
|
<span>VOL</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="knob-container">
|
<div class="knob-container">
|
||||||
<div class="knob" data-control="pan" data-track-id="${trackData.id}">
|
<div class="knob" data-control="pan" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
|
||||||
<div class="knob-indicator"></div>
|
|
||||||
</div>
|
|
||||||
<span>PAN</span>
|
<span>PAN</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,11 +139,25 @@ export function renderApp() {
|
|||||||
|
|
||||||
trackLane.addEventListener('click', () => {
|
trackLane.addEventListener('click', () => {
|
||||||
if (appState.activeTrackId === trackData.id) return;
|
if (appState.activeTrackId === trackData.id) return;
|
||||||
|
|
||||||
|
// A linha stopPlayback() também foi REMOVIDA daqui
|
||||||
|
|
||||||
appState.activeTrackId = trackData.id;
|
appState.activeTrackId = trackData.id;
|
||||||
document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track'));
|
document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track'));
|
||||||
trackLane.classList.add('active-track');
|
trackLane.classList.add('active-track');
|
||||||
updateGlobalPatternSelector();
|
updateGlobalPatternSelector();
|
||||||
|
|
||||||
|
// Apenas redesenha a UI, sem parar a música
|
||||||
|
const activeTrack = appState.tracks.find(t => t.id === appState.activeTrackId);
|
||||||
|
if (activeTrack) {
|
||||||
|
const activePattern = activeTrack.patterns[activeTrack.activePatternIndex];
|
||||||
|
if (activePattern) {
|
||||||
|
const stepsPerBar = 16;
|
||||||
|
const requiredBars = Math.ceil(activePattern.steps.length / stepsPerBar);
|
||||||
|
document.getElementById("bars-input").value = requiredBars > 0 ? requiredBars : 1;
|
||||||
redrawSequencer();
|
redrawSequencer();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); });
|
trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); });
|
||||||
@@ -152,15 +199,12 @@ export function redrawSequencer() {
|
|||||||
const trackData = appState.tracks.find((t) => t.id == trackId);
|
const trackData = appState.tracks.find((t) => t.id == trackId);
|
||||||
|
|
||||||
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) {
|
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) {
|
||||||
sequencerContainer.innerHTML = "";
|
sequencerContainer.innerHTML = ""; return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A CORREÇÃO FINAL: Usa o índice GLOBAL para desenhar CADA faixa
|
|
||||||
const activePattern = trackData.patterns[appState.activePatternIndex];
|
const activePattern = trackData.patterns[appState.activePatternIndex];
|
||||||
if (!activePattern) {
|
if (!activePattern) {
|
||||||
sequencerContainer.innerHTML = "";
|
sequencerContainer.innerHTML = ""; return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const patternSteps = activePattern.steps;
|
const patternSteps = activePattern.steps;
|
||||||
|
|
||||||
@@ -315,7 +359,11 @@ export function highlightStep(stepIndex, isActive) {
|
|||||||
export async function loadAndRenderSampleBrowser() {
|
export async function loadAndRenderSampleBrowser() {
|
||||||
const browserContent = document.getElementById("browser-content");
|
const browserContent = document.getElementById("browser-content");
|
||||||
try {
|
try {
|
||||||
const response = await fetch("metadata/samples-manifest.json");
|
// --- CORREÇÃO AQUI ---
|
||||||
|
// Adiciona um timestamp à URL para evitar que o navegador use uma versão antiga (em cache) do arquivo.
|
||||||
|
const response = await fetch(`metadata/samples-manifest.json?v=${Date.now()}`);
|
||||||
|
// --- FIM DA CORREÇÃO ---
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Arquivo samples-manifest.json não encontrado.");
|
throw new Error("Arquivo samples-manifest.json não encontrado.");
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-113
@@ -21,29 +21,14 @@
|
|||||||
|
|
||||||
<header class="global-toolbar">
|
<header class="global-toolbar">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<i
|
<i class="fa-solid fa-file" id="new-project-btn" title="Novo Projeto"></i>
|
||||||
class="fa-solid fa-file"
|
<i class="fa-solid fa-folder-open" id="open-mmp-btn" title="Abrir Projeto do Servidor"></i>
|
||||||
id="new-project-btn"
|
<i class="fa-solid fa-save" id="save-mmp-btn" title="Salvar Projeto (.mmp)"></i>
|
||||||
title="Novo Projeto"
|
<i class="fa-solid fa-upload" id="upload-sample-btn" title="Carregar Sample do Computador"></i>
|
||||||
></i>
|
|
||||||
<i
|
|
||||||
class="fa-solid fa-folder-open"
|
|
||||||
id="open-mmp-btn"
|
|
||||||
title="Abrir Projeto (.mmp)"
|
|
||||||
></i>
|
|
||||||
<i
|
|
||||||
class="fa-solid fa-save"
|
|
||||||
id="save-mmp-btn"
|
|
||||||
title="Salvar Projeto (.mmp)"
|
|
||||||
></i>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<i
|
<i class="fa-solid fa-backward-step" id="rewind-btn" title="Voltar ao Início"></i>
|
||||||
class="fa-solid fa-backward-step"
|
|
||||||
id="rewind-btn"
|
|
||||||
title="Voltar ao Início"
|
|
||||||
></i>
|
|
||||||
<i class="fa-solid fa-play" title="Play/Pause Global (Futuro)"></i>
|
<i class="fa-solid fa-play" title="Play/Pause Global (Futuro)"></i>
|
||||||
<i class="fa-solid fa-stop" title="Stop Global (Futuro)"></i>
|
<i class="fa-solid fa-stop" title="Stop Global (Futuro)"></i>
|
||||||
<i class="fa-solid fa-circle-dot" title="Gravar"></i>
|
<i class="fa-solid fa-circle-dot" title="Gravar"></i>
|
||||||
@@ -52,96 +37,38 @@
|
|||||||
<div class="info-display-group">
|
<div class="info-display-group">
|
||||||
<div class="info-display">
|
<div class="info-display">
|
||||||
<div class="interactive-input-container">
|
<div class="interactive-input-container">
|
||||||
<button class="adjust-btn" data-target="bpm" data-step="-1">
|
<button class="adjust-btn" data-target="bpm" data-step="-1">-</button>
|
||||||
-</button
|
<input type="text" class="value-input" id="bpm-input" value="140" data-min="20" data-max="400"/>
|
||||||
><input
|
<button class="adjust-btn" data-target="bpm" data-step="1">+</button>
|
||||||
type="text"
|
|
||||||
class="value-input"
|
|
||||||
id="bpm-input"
|
|
||||||
value="140"
|
|
||||||
data-min="20"
|
|
||||||
data-max="400"
|
|
||||||
/><button class="adjust-btn" data-target="bpm" data-step="1">
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="label">ANDAMENTO/BPM</div>
|
<div class="label">ANDAMENTO/BPM</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-display">
|
<div class="info-display">
|
||||||
<div class="interactive-input-container">
|
<div class="interactive-input-container">
|
||||||
<button class="adjust-btn" data-target="bars" data-step="-1">
|
<button class="adjust-btn" data-target="bars" data-step="-1">-</button>
|
||||||
-</button
|
<input type="text" class="value-input" id="bars-input" value="1" data-min="1" data-max="64"/>
|
||||||
><input
|
<button class="adjust-btn" data-target="bars" data-step="1">+</button>
|
||||||
type="text"
|
|
||||||
class="value-input"
|
|
||||||
id="bars-input"
|
|
||||||
value="1"
|
|
||||||
data-min="1"
|
|
||||||
data-max="64"
|
|
||||||
/><button class="adjust-btn" data-target="bars" data-step="1">
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="label">COMPASSOS</div>
|
<div class="label">COMPASSOS</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-display">
|
<div class="info-display">
|
||||||
<div class="interactive-input-container">
|
<div class="interactive-input-container">
|
||||||
<div class="compasso-group">
|
<div class="compasso-group">
|
||||||
<button
|
<button class="adjust-btn" data-target="compasso-a" data-step="-1">-</button>
|
||||||
class="adjust-btn"
|
<input type="text" class="value-input compasso-input" id="compasso-a-input" value="4" data-min="1" data-max="16"/>
|
||||||
data-target="compasso-a"
|
<button class="adjust-btn" data-target="compasso-a" data-step="1">+</button>
|
||||||
data-step="-1"
|
|
||||||
>
|
|
||||||
-</button
|
|
||||||
><input
|
|
||||||
type="text"
|
|
||||||
class="value-input compasso-input"
|
|
||||||
id="compasso-a-input"
|
|
||||||
value="4"
|
|
||||||
data-min="1"
|
|
||||||
data-max="16"
|
|
||||||
/><button
|
|
||||||
class="adjust-btn"
|
|
||||||
data-target="compasso-a"
|
|
||||||
data-step="1"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="compasso-separator">/</span>
|
<span class="compasso-separator">/</span>
|
||||||
<div class="compasso-group">
|
<div class="compasso-group">
|
||||||
<button
|
<button class="adjust-btn" data-target="compasso-b" data-step="-1">-</button>
|
||||||
class="adjust-btn"
|
<input type="text" class="value-input compasso-input" id="compasso-b-input" value="4" data-min="1" data-max="16"/>
|
||||||
data-target="compasso-b"
|
<button class="adjust-btn" data-target="compasso-b" data-step="1">+</button>
|
||||||
data-step="-1"
|
|
||||||
>
|
|
||||||
-</button
|
|
||||||
><input
|
|
||||||
type="text"
|
|
||||||
class="value-input compasso-input"
|
|
||||||
id="compasso-b-input"
|
|
||||||
value="4"
|
|
||||||
data-min="1"
|
|
||||||
data-max="16"
|
|
||||||
/><button
|
|
||||||
class="adjust-btn"
|
|
||||||
data-target="compasso-b"
|
|
||||||
data-step="1"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="label">COMPASSO</div>
|
<div class="label">COMPASSO</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-display">
|
<div class="info-display">
|
||||||
<div
|
<div id="timer-display" class="interactive-input-container" style="font-size: 0.7rem; color: var(--text-dark)">00:00:00</div>
|
||||||
id="timer-display"
|
|
||||||
class="interactive-input-container"
|
|
||||||
style="font-size: 0.7rem; color: var(--text-dark)"
|
|
||||||
>
|
|
||||||
00:00:00
|
|
||||||
</div>
|
|
||||||
<div class="label">MIN:SEC:MSEC</div>
|
<div class="label">MIN:SEC:MSEC</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,18 +76,13 @@
|
|||||||
<button id="metronome-btn" title="Metrônomo On/Off">Metrônomo</button>
|
<button id="metronome-btn" title="Metrônomo On/Off">Metrônomo</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<div class="control-group master-controls">
|
<div class="control-group master-controls">
|
||||||
<div class="knob-container">
|
<div class="knob-container">
|
||||||
<div class="knob" id="master-volume-knob">
|
<div class="knob" id="master-volume-knob"><div class="knob-indicator"></div></div>
|
||||||
<div class="knob-indicator"></div>
|
|
||||||
</div>
|
|
||||||
<span>VOL MASTER</span>
|
<span>VOL MASTER</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="knob-container">
|
<div class="knob-container">
|
||||||
<div class="knob" id="master-pan-knob">
|
<div class="knob" id="master-pan-knob"><div class="knob-indicator"></div></div>
|
||||||
<div class="knob-indicator"></div>
|
|
||||||
</div>
|
|
||||||
<span>PAN MASTER</span>
|
<span>PAN MASTER</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,8 +93,7 @@
|
|||||||
<div class="editor-header">
|
<div class="editor-header">
|
||||||
Mostrar/esconder Editor de Bases
|
Mostrar/esconder Editor de Bases
|
||||||
<div class="window-controls">
|
<div class="window-controls">
|
||||||
<i class="fa-solid fa-minus"></i><i class="fa-regular fa-square"></i
|
<i class="fa-solid fa-minus"></i><i class="fa-regular fa-square"></i><i class="fa-solid fa-xmark"></i>
|
||||||
><i class="fa-solid fa-xmark"></i>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-toolbar">
|
<div class="editor-toolbar">
|
||||||
@@ -189,26 +110,18 @@
|
|||||||
<button id="remove-pattern-btn" class="pattern-btn">-</button>
|
<button id="remove-pattern-btn" class="pattern-btn">-</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tool-icons">
|
<div class="tool-icons">
|
||||||
<i class="fa-solid fa-table-cells"></i
|
<i class="fa-solid fa-table-cells"></i><i class="fa-solid fa-bars-staggered"></i><i class="fa-solid fa-wave-square enabled"></i><i class="fa-solid fa-plus" id="add-bar-btn" title="Adicionar 1 Compasso"></i>
|
||||||
><i class="fa-solid fa-bars-staggered"></i
|
|
||||||
><i class="fa-solid fa-wave-square enabled"></i
|
|
||||||
><i class="fa-solid fa-plus" id="add-bar-btn" title="Adicionar 1 Compasso"></i>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="zoom-controls">
|
<div class="zoom-controls">
|
||||||
<i class="fa-solid fa-minus" id="remove-instrument-btn"></i
|
<i class="fa-solid fa-minus" id="remove-instrument-btn"></i><i class="fa-solid fa-plus" id="add-instrument-btn"></i>
|
||||||
><i class="fa-solid fa-plus" id="add-instrument-btn"></i>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="track-container"></div>
|
<div id="track-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<input
|
<input type="file" id="mmp-file-input" accept=".mmp, .mmpz" style="display: none"/>
|
||||||
type="file"
|
<input type="file" id="sample-file-input" accept=".wav,.flac,.ogg,.mp3" style="display: none"/>
|
||||||
id="mmp-file-input"
|
|
||||||
accept=".mmp, .mmpz"
|
|
||||||
style="display: none"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="modal-overlay" id="open-project-modal">
|
<div class="modal-overlay" id="open-project-modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -229,6 +142,6 @@
|
|||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||||
|
|
||||||
<script src="assets/js/creations/main.js" type="module" defer></script>
|
<script src="assets/js/creations/main.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# ----------------------- // --------------------------
|
||||||
|
|
||||||
|
# Área de Desenvolvimento
|
||||||
|
O site está sendo desenvolvido em um venv, para executá-lo de forma correta, é necessário acessar através do comando:
|
||||||
|
source venv/bin/activate
|
||||||
|
Isso ativará o ambiente de desenvolvimento.
|
||||||
|
|
||||||
|
# ----------------------- // --------------------------
|
||||||
|
|
||||||
|
# Serviço Watchdog para verificar alterações nas pastas de samples
|
||||||
|
Foi criado um serviço(/etc/systemd/system/mmpCreator-upload-server.service) com nome "mmpCreator-upload-server.service" para verificar alterações na pasta src/samples se há alguma mudança. Caso tenha, ele fará um novo build do site para que seja atualizado em tempo real todas as alterações. (tempo de 5 em 5 segundos)
|
||||||
|
A atualização no site é feita a partir de build do site feito pelo script.
|
||||||
|
|
||||||
|
# ----------------------- // --------------------------
|
||||||
|
|
||||||
|
# Servidor de Upload de samples
|
||||||
|
O servidor "upload_server.py" é responsável por receber o upload dos arquivos de áudio (samples) para a plataforma.
|
||||||
|
É um servidor Flask, utilizando a biblioteca watchdog do Python para monitorar as modificações nas pastas de samples.
|
||||||
|
Após qualquer alteração identificada, o script fará a atualização dos caminhos de arquivos de áudio através dos json (samples-manifest.json e mmp-manifest.json) para que todos os arquivos tenham sua exibição correta, assim como sua execução.
|
||||||
|
|
||||||
|
http://127.0.0.1:5000
|
||||||
|
É necessário verificar o quão seguro isso é, para que seja validado como ferramenta oficial ou não do projeto.
|
||||||
|
Inicialmente, está em produção para fazer testes.
|
||||||
|
|
||||||
|
# ----------------------- // --------------------------
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
- Verificar projeto MMP antes de aceitar o upload
|
||||||
|
- Verificar se são samples nativos;
|
||||||
|
- Corrigir caminhos dos samples para que não haja problemas durante o download e execução na plataforma.
|
||||||
|
- Envio de samples gravados com o microfone do dispositivo
|
||||||
|
- Possibilitar a execução e recorte do sample antes de salvá-lo de fato no servidor (evitar muito lixo salvo)
|
||||||
|
- Possibilita a gravação de músicas na plataforma, mesmo que sejam sem mixagem.
|
||||||
|
- Ativar e desativar samples nas patterns, para executar apenas samples específicos ou alternar entre ativo e inativo
|
||||||
|
- Retirar botões inúteis do controle de patterns
|
||||||
|
- Deixar o código mais bem documentado, facilitando o entendimento de externos ao desenvolvimento
|
||||||
|
- Possibilitar a alteração dos nomes das patterns (tanto em novos projetos, quanto em projetos que foram baixados/carregados)
|
||||||
|
- Corrigir a parte de notas do projeto, deixando rastros da plataforma MMPCreator, informações de autor e etc
|
||||||
|
- Adicionar atalhos, espaço pra play/pause, + e - para alternar entre as patterns, etc
|
||||||
|
- Possibilitar a alteração do nome do sample (?) (não sei se é tão válido, porque pode gerar inúmeros samples repetidos)
|
||||||
|
- Verificar se já há algum sample com o mesmo nome, para não ficar duplicando samples
|
||||||
|
- Verificar se já há algum projeto com o mesmo nome, para não ficar duplicando projetos
|
||||||
|
- Resetar valores dos botões utilizando o scroll do mouse
|
||||||
|
- Na hora de salvar projeto, verificar se quer baixar o projeto (possibilitando escolher o nome) ou salvar no servidor
|
||||||
|
|
||||||
|
# ----------------------- // --------------------------
|
||||||
|
|
||||||
|
# O que já é possível fazer
|
||||||
|
- Criar projeto;
|
||||||
|
- Carregar projeto do PC ou do Servidor;
|
||||||
|
- Salvar projeto de forma externa (no servidor ainda não tá salvando);
|
||||||
|
- Editar projetos já existentes (não alterando o original);
|
||||||
|
- Usar o metrônomo;
|
||||||
|
- Criar patterns;
|
||||||
|
- Excluir patterns;
|
||||||
|
- Alternar entre as patterns em tempo real;
|
||||||
|
- Aumentar/diminuir volume das patterns;
|
||||||
|
- Alterar pan das patterns;
|
||||||
|
- Aumentar/diminuir volume dos instrumentos;
|
||||||
|
- Alterar pan dos instrumentos;
|
||||||
|
- Aumentar/diminuir volume global;
|
||||||
|
- Alterar pan global;
|
||||||
|
- Alterar a quantidade de compassos da música;
|
||||||
|
- Alterar o formato do compasso da música;
|
||||||
|
- Alterar o BPM;
|
||||||
|
- Ver o tempo de duração da pattern através do timmer.
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from threading import Thread
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from flask_cors import CORS
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
|
# --- Configurações (sem alterações) ---
|
||||||
|
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
print(f"Raiz do projeto detectada em: {PROJECT_ROOT}")
|
||||||
|
|
||||||
|
CONFIGS = [
|
||||||
|
{
|
||||||
|
"source_dir": "src/samples",
|
||||||
|
"output_file": "metadata/samples-manifest.json",
|
||||||
|
"scan_type": "tree"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source_dir": "mmp",
|
||||||
|
"output_file": "metadata/mmp-manifest.json",
|
||||||
|
"scan_type": "list",
|
||||||
|
"extensions": ['.mmp', '.mmpz']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
UPLOAD_FOLDER = os.path.join(PROJECT_ROOT, "src", "samples", "samples")
|
||||||
|
WATCH_FOLDER = os.path.join(PROJECT_ROOT, "src", "samples") # Pasta a ser vigiada
|
||||||
|
ALLOWED_EXTENSIONS = {'wav', 'ogg', 'flac', 'mp3'}
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app, origins=["https://alice.ufsj.edu.br", "http://localhost:8000", "http://127.0.0.1:5500"])
|
||||||
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||||
|
|
||||||
|
# --- Funções do Gerador de Manifesto (sem alterações) ---
|
||||||
|
def scan_directory_tree(path):
|
||||||
|
tree = {}
|
||||||
|
if not os.path.isdir(path): return tree
|
||||||
|
for item in sorted(os.listdir(path)):
|
||||||
|
full_path = os.path.join(path, item)
|
||||||
|
if os.path.isdir(full_path):
|
||||||
|
tree[item] = scan_directory_tree(full_path)
|
||||||
|
else:
|
||||||
|
if item.lower().endswith(('.wav', '.ogg', '.flac', '.mp3')):
|
||||||
|
tree[item] = {"_isFile": True}
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def scan_directory_list(path, allowed_extensions):
|
||||||
|
file_list = []
|
||||||
|
if not os.path.isdir(path): return file_list
|
||||||
|
for item in os.listdir(path):
|
||||||
|
full_path = os.path.join(path, item)
|
||||||
|
if os.path.isfile(full_path) and any(item.lower().endswith(ext) for ext in allowed_extensions):
|
||||||
|
file_list.append(item)
|
||||||
|
return sorted(file_list)
|
||||||
|
|
||||||
|
def run_jekyll_build():
|
||||||
|
print("\nExecutando o comando de build do Jekyll...")
|
||||||
|
destination_path = "/nethome/jotachina/public_html/mmpSearch/"
|
||||||
|
jekyll_args = ["--destination", destination_path, "--force"]
|
||||||
|
try:
|
||||||
|
command = ["bundle", "exec", "jekyll", "build"] + jekyll_args
|
||||||
|
result = subprocess.run(command, cwd=PROJECT_ROOT, capture_output=True, text=True, check=False)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("Falha com 'bundle', tentando 'jekyll build' diretamente...")
|
||||||
|
command = ["jekyll", "build"] + jekyll_args
|
||||||
|
result = subprocess.run(command, cwd=PROJECT_ROOT, capture_output=True, text=True, check=False)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("SUCESSO: Jekyll build concluído.")
|
||||||
|
else:
|
||||||
|
print(f"ERRO no Jekyll build: {result.stderr}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERRO inesperado durante o Jekyll build: {e}")
|
||||||
|
|
||||||
|
def generate_manifests():
|
||||||
|
print("\nIniciando geração de arquivos de manifesto...")
|
||||||
|
# ... (lógica interna sem alterações) ...
|
||||||
|
for config in CONFIGS:
|
||||||
|
source_dir_abs = os.path.join(PROJECT_ROOT, config["source_dir"])
|
||||||
|
output_file_abs = os.path.join(PROJECT_ROOT, config["output_file"])
|
||||||
|
if not os.path.isdir(source_dir_abs): continue
|
||||||
|
result_data = None
|
||||||
|
if config["scan_type"] == "tree": result_data = scan_directory_tree(source_dir_abs)
|
||||||
|
elif config["scan_type"] == "list": result_data = scan_directory_list(source_dir_abs, config.get("extensions", []))
|
||||||
|
if result_data is not None:
|
||||||
|
output_dir = os.path.dirname(output_file_abs)
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
with open(output_file_abs, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(result_data, f, indent=2, ensure_ascii=False)
|
||||||
|
print(f"SUCESSO: Arquivo '{output_file_abs}' gerado!")
|
||||||
|
print("\nGeração de manifestos concluída.")
|
||||||
|
run_jekyll_build()
|
||||||
|
|
||||||
|
# --- Lógica do Servidor Flask (sem alterações) ---
|
||||||
|
def allowed_file(filename):
|
||||||
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
@app.route('/upload-sample', methods=['POST'])
|
||||||
|
def upload_file():
|
||||||
|
if 'sampleFile' not in request.files: return jsonify({"error": "Nenhum arquivo enviado"}), 400
|
||||||
|
file = request.files['sampleFile']
|
||||||
|
if file.filename == '': return jsonify({"error": "Nenhum arquivo selecionado"}), 400
|
||||||
|
if file and allowed_file(file.filename):
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||||
|
save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||||
|
try:
|
||||||
|
file.save(save_path)
|
||||||
|
generate_manifests()
|
||||||
|
return jsonify({"success": True, "message": f"Arquivo '{filename}' salvo e site atualizado!"}), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
return jsonify({"error": "Tipo de arquivo não permitido"}), 400
|
||||||
|
|
||||||
|
# --- NOVO: LÓGICA DO "VIGIA" DE ARQUIVOS (WATCHDOG) ---
|
||||||
|
|
||||||
|
class ManifestEventHandler(FileSystemEventHandler):
|
||||||
|
"""Um manipulador de eventos que gera os manifestos quando um arquivo muda."""
|
||||||
|
def __init__(self):
|
||||||
|
self.last_triggered = 0
|
||||||
|
|
||||||
|
def on_any_event(self, event):
|
||||||
|
# Desconsidera eventos em diretórios
|
||||||
|
if event.is_directory:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Mecanismo simples para evitar múltiplas execuções seguidas (debounce)
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self.last_triggered > 5: # Espera 5 segundos entre as atualizações
|
||||||
|
print(f"\n[VIGIA] Mudança detectada: {event.src_path}")
|
||||||
|
generate_manifests()
|
||||||
|
self.last_triggered = current_time
|
||||||
|
|
||||||
|
def start_file_watcher():
|
||||||
|
"""Inicia o observador de arquivos em uma thread separada."""
|
||||||
|
print(f"\n[VIGIA] Iniciando monitoramento da pasta: {WATCH_FOLDER}")
|
||||||
|
event_handler = ManifestEventHandler()
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(event_handler, WATCH_FOLDER, recursive=True)
|
||||||
|
observer.start()
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
observer.stop()
|
||||||
|
observer.join()
|
||||||
|
|
||||||
|
# --- ATUALIZAÇÃO: INICIALIZAÇÃO PRINCIPAL ---
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Gera os manifestos uma vez ao iniciar o servidor
|
||||||
|
generate_manifests()
|
||||||
|
|
||||||
|
# Inicia o "vigia" de arquivos em uma thread separada (em segundo plano)
|
||||||
|
watcher_thread = Thread(target=start_file_watcher, daemon=True)
|
||||||
|
watcher_thread.start()
|
||||||
|
|
||||||
|
# Inicia o servidor Flask (thread principal)
|
||||||
|
print("\n[FLASK] Iniciando servidor de upload...")
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=False) # Debug mode deve ser False para evitar que rode duas vezes
|
||||||
Reference in new issue
Block a user