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
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 %}
|
|
|