You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

523 lines
21 KiB

{% extends "base.html" %}
{% block content %}
<header class="topbar reveal">
<div>
<h1>Stockage audio</h1>
</div>
<div class="topbar-actions">
<a href="{{ url_for('ui.dashboard') }}" class="ghost-link">Retour au tableau de bord</a>
<a href="{{ url_for('ui.logout') }}" class="ghost-link">Déconnexion</a>
</div>
</header>
<main class="dashboard-grid">
<section class="panel reveal delay-1" style="grid-column: span 12;">
<form method="post" action="{{ url_for('ui.upload_audio') }}" enctype="multipart/form-data" class="upload-row" id="upload-audio-form">
<input type="hidden" name="return_to" value="audio_storage" />
<input type="file" name="audio_file" id="audio_file_input" accept=".mp3,.wav,.ogg,.flac,.aac,.m4a" required />
<button type="submit">Téléverser</button>
</form>
<form method="post" action="{{ url_for('ui.upload_audio') }}" class="upload-row" id="upload-youtube-form" style="margin-top: 0.75rem;">
<input type="hidden" name="return_to" value="audio_storage" />
<input type="hidden" name="youtube_filename" id="youtube_filename_input" value="" />
<input type="url" name="youtube_url" id="youtube_url_input" placeholder="Lien YouTube (https://...)" required />
<button type="submit">Extraire via yt-dlp</button>
</form>
{% if audio_files %}
<ul class="audio-list">
{% for filename in audio_files %}
<li>
<span>{{ filename }}</span>
<div class="audio-actions">
{% set is_used_by_trigger = filename|lower in used_audio_files %}
<button
type="button"
class="small-button js-play-browser"
data-audio-url="{{ url_for('ui.stream_audio', filename=filename) }}"
data-audio-name="{{ filename }}"
>
Lire
</button>
<a href="{{ url_for('ui.download_audio', filename=filename) }}" class="small-button">Télécharger</a>
<form method="post" action="{{ url_for('ui.delete_audio') }}" class="inline-form js-delete-audio-form" data-filename="{{ filename }}">
<input type="hidden" name="filename" value="{{ filename }}" />
<input type="hidden" name="return_to" value="audio_storage" />
<button
type="submit"
class="small danger"
{% if is_used_by_trigger %}disabled title="Impossible: fichier utilisé par un trigger"{% endif %}
>Supprimer</button>
</form>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="muted">Aucun fichier audio dans le stockage.</p>
{% endif %}
</section>
</main>
<div class="modal-backdrop" id="duplicate-name-modal" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="duplicate-name-title">
<h3 id="duplicate-name-title">Nom déjà utilisé</h3>
<p id="duplicate-name-message">Ce nom existe déjà dans les fichiers enregistrés.</p>
<div class="modal-actions">
<button type="button" class="ghost-link" id="duplicate-name-close">Fermer</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="youtube-rename-modal" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="youtube-rename-title">
<h3 id="youtube-rename-title">Renommer avant téléchargement</h3>
<p id="youtube-rename-message">Vous pouvez modifier le nom proposé avant de lancer le téléchargement.</p>
<input type="text" id="youtube-rename-input" placeholder="nom_fichier.mp3" style="width: 100%; margin-top: 0.35rem;" />
<div class="modal-actions">
<button type="button" class="ghost-link" id="youtube-rename-cancel">Annuler</button>
<button type="button" id="youtube-rename-confirm">Télécharger</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="youtube-error-modal" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="youtube-error-title">
<h3 id="youtube-error-title">Erreur import YouTube</h3>
<p id="youtube-error-message">Une erreur est survenue lors de la préparation du téléchargement.</p>
<div class="modal-actions">
<button type="button" class="ghost-link" id="youtube-error-close">Fermer</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="youtube-progress-modal" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="youtube-progress-title">
<h3 id="youtube-progress-title">Téléchargement YouTube en cours</h3>
<div class="youtube-progress-wrap">
<div class="youtube-spinner" aria-hidden="true"></div>
<p id="youtube-progress-percent" class="youtube-progress-percent">0%</p>
</div>
<div class="youtube-progress-track" aria-hidden="true">
<div class="youtube-progress-bar" id="youtube-progress-bar"></div>
</div>
<p id="youtube-progress-message" class="muted">Préparation du téléchargement...</p>
<div class="modal-actions">
<button type="button" class="ghost-link" id="youtube-progress-close">Masquer</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="delete-audio-modal" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="delete-audio-title">
<h3 id="delete-audio-title">Confirmer la suppression</h3>
<p id="delete-audio-message">Voulez-vous vraiment supprimer ce fichier ?</p>
<div class="modal-actions">
<button type="button" class="ghost-link" id="delete-audio-cancel">Annuler</button>
<button type="button" class="danger" id="delete-audio-confirm">Supprimer</button>
</div>
</div>
</div>
<div class="modal-backdrop" id="browser-audio-modal" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="browser-audio-title">
<h3 id="browser-audio-title">Lecture audio</h3>
<div class="audio-player-card">
<p id="audio-now-playing" class="muted">Aucune lecture en cours.</p>
<audio id="browser-audio-player" controls preload="metadata"></audio>
<p class="audio-time" id="audio-time">Temps de lecture: 00:00 / 00:00</p>
</div>
<div class="modal-actions">
<button type="button" class="ghost-link" id="browser-audio-close">Fermer</button>
</div>
</div>
</div>
<script>
(function () {
const form = document.getElementById("upload-youtube-form");
const youtubeUrlInput = document.getElementById("youtube_url_input");
const youtubeFilenameInput = document.getElementById("youtube_filename_input");
const modal = document.getElementById("youtube-rename-modal");
const renameInput = document.getElementById("youtube-rename-input");
const cancelBtn = document.getElementById("youtube-rename-cancel");
const confirmBtn = document.getElementById("youtube-rename-confirm");
const errorModal = document.getElementById("youtube-error-modal");
const errorMessage = document.getElementById("youtube-error-message");
const errorClose = document.getElementById("youtube-error-close");
const progressModal = document.getElementById("youtube-progress-modal");
const progressPercent = document.getElementById("youtube-progress-percent");
const progressBar = document.getElementById("youtube-progress-bar");
const progressMessage = document.getElementById("youtube-progress-message");
const progressClose = document.getElementById("youtube-progress-close");
if (!form || !youtubeUrlInput || !youtubeFilenameInput || !modal || !renameInput || !cancelBtn || !confirmBtn) return;
const submitButton = form.querySelector("button[type='submit']");
if (!submitButton) return;
let pollTimer = null;
let activeJobId = "";
const setLoading = (loading, label) => {
submitButton.disabled = loading;
submitButton.textContent = loading ? (label || "Analyse...") : "Extraire via yt-dlp";
};
const openModal = (filename) => {
renameInput.value = filename;
modal.classList.add("is-open");
modal.setAttribute("aria-hidden", "false");
renameInput.focus();
renameInput.select();
};
const closeModal = () => {
modal.classList.remove("is-open");
modal.setAttribute("aria-hidden", "true");
};
const openErrorModal = (message) => {
if (!errorModal || !errorMessage || !errorClose) return;
errorMessage.textContent = message;
errorModal.classList.add("is-open");
errorModal.setAttribute("aria-hidden", "false");
errorClose.focus();
};
const closeErrorModal = () => {
if (!errorModal) return;
errorModal.classList.remove("is-open");
errorModal.setAttribute("aria-hidden", "true");
};
const openProgressModal = (percent, message) => {
if (!progressModal || !progressPercent || !progressBar || !progressMessage) return;
progressPercent.textContent = Math.max(0, Math.min(100, percent)).toFixed(1) + "%";
progressBar.style.width = Math.max(0, Math.min(100, percent)) + "%";
progressMessage.textContent = message || "Téléchargement en cours...";
progressModal.classList.add("is-open");
progressModal.setAttribute("aria-hidden", "false");
};
const closeProgressModal = () => {
if (!progressModal) return;
progressModal.classList.remove("is-open");
progressModal.setAttribute("aria-hidden", "true");
};
const stopPolling = () => {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
activeJobId = "";
};
const updateProgress = (percent, message) => {
if (!progressPercent || !progressBar || !progressMessage) return;
const safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
progressPercent.textContent = safePercent.toFixed(1) + "%";
progressBar.style.width = safePercent + "%";
progressMessage.textContent = message || "Téléchargement en cours...";
};
const statusUrlTemplate = "{{ url_for('ui.youtube_download_status', job_id='__JOB_ID__') }}";
const pollStatus = async () => {
if (!activeJobId) return;
try {
const response = await fetch(statusUrlTemplate.replace("__JOB_ID__", encodeURIComponent(activeJobId)), {
headers: { "X-Requested-With": "fetch" },
});
const payload = await response.json();
if (!response.ok || !payload.ok) {
stopPolling();
setLoading(false);
closeProgressModal();
openErrorModal(payload.error || "Impossible de suivre le téléchargement.");
return;
}
updateProgress(payload.percent, payload.message);
if (payload.status === "completed") {
stopPolling();
setLoading(false);
updateProgress(100, payload.message || "Téléchargement terminé.");
setTimeout(() => {
window.location.reload();
}, 500);
return;
}
if (payload.status === "error") {
stopPolling();
setLoading(false);
closeProgressModal();
openErrorModal(payload.message || "Échec du téléchargement YouTube.");
}
} catch (_) {
stopPolling();
setLoading(false);
closeProgressModal();
openErrorModal("Erreur réseau pendant le suivi du téléchargement.");
}
};
const startYoutubeDownload = async (filename) => {
const youtubeUrl = (youtubeUrlInput.value || "").trim();
if (!youtubeUrl) return;
youtubeFilenameInput.value = filename;
setLoading(true, "Démarrage...");
openProgressModal(0, "Préparation du téléchargement...");
try {
const response = await fetch("{{ url_for('ui.youtube_download_start') }}", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"X-Requested-With": "fetch",
},
body: new URLSearchParams({
youtube_url: youtubeUrl,
youtube_filename: filename,
}),
});
const payload = await response.json();
if (!response.ok || !payload.ok || !payload.job_id) {
setLoading(false);
closeProgressModal();
openErrorModal(payload.error || "Impossible de démarrer le téléchargement.");
return;
}
activeJobId = payload.job_id;
pollTimer = setInterval(pollStatus, 900);
pollStatus();
} catch (_) {
setLoading(false);
closeProgressModal();
openErrorModal("Erreur réseau lors du démarrage du téléchargement.");
}
};
form.addEventListener("submit", async (event) => {
event.preventDefault();
const youtubeUrl = (youtubeUrlInput.value || "").trim();
if (!youtubeUrl) return;
setLoading(true, "Analyse...");
try {
const response = await fetch("{{ url_for('ui.youtube_proposed_name') }}", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"X-Requested-With": "fetch",
},
body: new URLSearchParams({ youtube_url: youtubeUrl }),
});
const payload = await response.json();
if (!response.ok || !payload.ok) {
openErrorModal(payload.error || "Impossible de proposer un nom de fichier.");
return;
}
openModal(payload.filename || "audio_youtube.mp3");
} catch (_) {
openErrorModal("Erreur réseau lors de la récupération du nom proposé.");
} finally {
setLoading(false);
}
});
cancelBtn.addEventListener("click", closeModal);
confirmBtn.addEventListener("click", () => {
const filename = (renameInput.value || "").trim();
if (!filename) {
renameInput.focus();
return;
}
closeModal();
startYoutubeDownload(filename);
});
modal.addEventListener("click", (event) => {
if (event.target === modal) closeModal();
});
if (errorClose) {
errorClose.addEventListener("click", closeErrorModal);
}
if (errorModal) {
errorModal.addEventListener("click", (event) => {
if (event.target === errorModal) closeErrorModal();
});
}
if (progressClose) {
progressClose.addEventListener("click", closeProgressModal);
}
if (progressModal) {
progressModal.addEventListener("click", (event) => {
if (event.target === progressModal) closeProgressModal();
});
}
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && modal.classList.contains("is-open")) closeModal();
if (errorModal && event.key === "Escape" && errorModal.classList.contains("is-open")) closeErrorModal();
if (progressModal && event.key === "Escape" && progressModal.classList.contains("is-open")) closeProgressModal();
});
})();
(function () {
const player = document.getElementById("browser-audio-player");
const timeLabel = document.getElementById("audio-time");
const nowPlaying = document.getElementById("audio-now-playing");
const modal = document.getElementById("browser-audio-modal");
const closeBtn = document.getElementById("browser-audio-close");
if (!player || !timeLabel || !nowPlaying || !modal || !closeBtn) return;
const openModal = () => {
modal.classList.add("is-open");
modal.setAttribute("aria-hidden", "false");
};
const closeModal = () => {
player.pause();
modal.classList.remove("is-open");
modal.setAttribute("aria-hidden", "true");
};
const formatTime = (seconds) => {
if (!Number.isFinite(seconds) || seconds < 0) return "00:00";
const total = Math.floor(seconds);
const minutes = Math.floor(total / 60);
const remain = total % 60;
return String(minutes).padStart(2, "0") + ":" + String(remain).padStart(2, "0");
};
const updateTimeLabel = () => {
const current = formatTime(player.currentTime || 0);
const total = formatTime(player.duration || 0);
timeLabel.textContent = "Temps de lecture: " + current + " / " + total;
};
player.addEventListener("timeupdate", updateTimeLabel);
player.addEventListener("loadedmetadata", updateTimeLabel);
player.addEventListener("ended", updateTimeLabel);
const playButtons = document.querySelectorAll(".js-play-browser");
playButtons.forEach((button) => {
button.addEventListener("click", () => {
const src = button.dataset.audioUrl;
const name = button.dataset.audioName || "Fichier audio";
if (!src) return;
if (player.src !== src) {
player.src = src;
}
player.play().catch(() => {});
nowPlaying.textContent = "Lecture: " + name;
updateTimeLabel();
openModal();
});
});
closeBtn.addEventListener("click", closeModal);
modal.addEventListener("click", (event) => {
if (event.target === modal) closeModal();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && modal.classList.contains("is-open")) closeModal();
});
})();
(function () {
const uploadForm = document.getElementById("upload-audio-form");
const fileInput = document.getElementById("audio_file_input");
const duplicateModal = document.getElementById("duplicate-name-modal");
const duplicateClose = document.getElementById("duplicate-name-close");
if (!uploadForm || !fileInput || !duplicateModal || !duplicateClose) return;
const existing = new Set([
{% for filename in audio_files %}
"{{ filename|lower|replace('\\', '\\\\')|replace('"', '\\"') }}",
{% endfor %}
]);
const openDuplicateModal = () => {
duplicateModal.classList.add("is-open");
duplicateModal.setAttribute("aria-hidden", "false");
duplicateClose.focus();
};
const closeDuplicateModal = () => {
duplicateModal.classList.remove("is-open");
duplicateModal.setAttribute("aria-hidden", "true");
};
uploadForm.addEventListener("submit", (event) => {
const file = fileInput.files && fileInput.files[0];
if (!file) return;
if (existing.has(file.name.toLowerCase())) {
event.preventDefault();
openDuplicateModal();
}
});
duplicateClose.addEventListener("click", closeDuplicateModal);
duplicateModal.addEventListener("click", (event) => {
if (event.target === duplicateModal) closeDuplicateModal();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && duplicateModal.classList.contains("is-open")) closeDuplicateModal();
});
})();
(function () {
const modal = document.getElementById("delete-audio-modal");
const message = document.getElementById("delete-audio-message");
const cancelBtn = document.getElementById("delete-audio-cancel");
const confirmBtn = document.getElementById("delete-audio-confirm");
if (!modal || !message || !cancelBtn || !confirmBtn) return;
let pendingForm = null;
const openModal = (form, filename) => {
pendingForm = form;
message.textContent = "Voulez-vous vraiment supprimer le fichier \"" + filename + "\" ?";
modal.classList.add("is-open");
modal.setAttribute("aria-hidden", "false");
confirmBtn.focus();
};
const closeModal = () => {
modal.classList.remove("is-open");
modal.setAttribute("aria-hidden", "true");
pendingForm = null;
};
document.querySelectorAll(".js-delete-audio-form").forEach((form) => {
form.addEventListener("submit", (event) => {
event.preventDefault();
const filename = form.dataset.filename || "ce fichier";
openModal(form, filename);
});
});
cancelBtn.addEventListener("click", closeModal);
modal.addEventListener("click", (event) => {
if (event.target === modal) closeModal();
});
confirmBtn.addEventListener("click", () => {
if (pendingForm) pendingForm.submit();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && modal.classList.contains("is-open")) closeModal();
});
})();
</script>
{% endblock %}