↻ ◁ || ▷ ↺ Musique - Générateur de playlist
Web - Générateur playlist musical
Toutes les opérations sont effectuées sur le serveur cwwk (192.168.0.205)
“Playlists uniquement avec dossier absolu pour les applis de type Subsonic”
Ce code exporte des chemins absolus issus demusic-path:
/sharenfs/multimedia/Music/musicyan/.../fichier.mp3
Dossier travail: /sharenfs/rnmkcy/web-music-playlist
Python venv + flask + uwsgi
1
2
3
4
5
6
7
8
# Créer le dossier projet
mkdir -p /sharenfs/rnmkcy/web-music-playlist
chown -R $USER:$USER /sharenfs/rnmkcy/web-music-playlist
cd /sharenfs/rnmkcy/web-music-playlist
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip wheel
pip install flask uwsgi
Écrire app.py (export .m3u avec chemins absolus)
Créer app.py dans /sharenfs/rnmkcy/web-music-playlist/ :
Etendre Réduire app.py
import os
from pathlib import Path
from flask import Flask, request, Response, render_template_string, abort, jsonify
app = Flask(__name__)
MUSIC_ROOT = Path(os.environ.get("MUSIC_ROOT", "/sharenfs/multimedia/Music/musicyan")).resolve()
M3U_ROOT = Path(os.environ.get("M3U_ROOT", "/sharenfs/multimedia/Music/playlists/2")).resolve()
# index : { artist: { album_or_None: [ {name, rel}...] } }
def build_index():
index = {}
if not MUSIC_ROOT.exists():
return index
mp3_exts = {".mp3"}
def is_mp3(p: Path) -> bool:
return p.is_file() and p.suffix.lower() in mp3_exts
for artist_dir in sorted([p for p in MUSIC_ROOT.iterdir() if p.is_dir()]):
artist = artist_dir.name
index.setdefault(artist, {})
# cas : mp3 directement dans /Artist/
direct = []
for f in sorted([p for p in artist_dir.iterdir() if is_mp3(p)]):
rel = f.relative_to(MUSIC_ROOT).as_posix()
direct.append({"name": f.stem, "rel": rel})
if direct:
index[artist]["~direct"] = direct
# cas : /Artist/Album/*.mp3
for album_dir in sorted([p for p in artist_dir.iterdir() if p.is_dir()]):
album = album_dir.name
tracks = []
for f in sorted([p for p in album_dir.iterdir() if is_mp3(p)]):
rel = f.relative_to(MUSIC_ROOT).as_posix()
tracks.append({"name": f.stem, "rel": rel})
if tracks:
index[artist][album] = tracks
return index
INDEX = build_index()
def make_m3u_from_rels(rels, title="Playlist"):
lines = ["#EXTM3U"]
# IMPORTANT : chemins absolus serveur
for rel in rels:
abs_path = (MUSIC_ROOT / rel).resolve()
lines.append(str(abs_path))
return "\n".join(lines) + "\n"
@app.route("/api/index")
def api_index():
# rescan simple à la volée (sinon mets en cache + option)
global INDEX
INDEX = build_index()
return jsonify(INDEX)
@app.route("/export", methods=["POST"])
def export():
global INDEX
if not INDEX:
INDEX = build_index()
# Nom donné par l'utilisateur (prompt JS)
title = (request.form.get("title") or "Playlist").strip()[:120] or "Playlist"
rels = request.form.getlist("track")
# filtrage par sécurité
valid = set()
for _, albums in INDEX.items():
for _, tracks in albums.items():
for t in tracks:
valid.add(t["rel"])
chosen = [r for r in rels if r in valid]
if not chosen:
abort(400, "Aucun morceau sélectionné")
m3u = make_m3u_from_rels(chosen, title=title)
# construction du chemin cible sur le serveur
safe_name = title.replace('"', "").replace("/", "_").replace("\\", "_")
filename = safe_name + ".m3u"
target_dir = M3U_ROOT
target_dir.mkdir(parents=True, exist_ok=True) # au cas où
target_path = target_dir / filename
# écriture du fichier m3u sur le serveur
target_path.write_text(m3u, encoding="utf-8") # Path.write_text [web:29]
# On renvoie juste un petit JSON de confirmation
return jsonify({
"status": "ok",
"filename": filename,
"path": str(target_path),
})
@app.route("/api/playlists", methods=["GET"])
def list_playlists():
M3U_ROOT.mkdir(parents=True, exist_ok=True)
files = []
for p in sorted(M3U_ROOT.glob("*.m3u")):
files.append({
"name": p.name,
"path": str(p),
})
return jsonify(files)
@app.route("/api/playlists/delete", methods=["POST"])
def delete_playlists():
data = request.get_json(silent=True) or {}
names = data.get("names") or []
if not isinstance(names, list):
abort(400, "Format invalide")
deleted = []
errors = []
for name in names:
# sécurité : pas de /, pas de chemin relatif
if "/" in name or "\\" in name or name.startswith("."):
errors.append({"name": name, "error": "Nom invalide"})
continue
p = M3U_ROOT / name
try:
if p.is_file() and p.suffix.lower() == ".m3u":
p.unlink() # Path.unlink pour supprimer [web:31][web:35]
deleted.append(name)
else:
errors.append({"name": name, "error": "Fichier introuvable"})
except Exception as e:
errors.append({"name": name, "error": str(e)})
return jsonify({
"deleted": deleted,
"errors": errors,
})
@app.route("/")
def ui():
return render_template_string("""
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Playlist globale (.m3u)</title>
<style>
:root{
--pad: 16px;
--gap: 16px;
--radius: 8px;
--bg: #ffffff;
--text: #111111;
--muted: #666666;
--box-border: #dddddd;
--box-bg: #ffffff;
--btn-bg: #fafafa;
--btn-border: #cccccc;
--input-border: #cccccc;
}
html[data-theme="dark"]{
--bg: #0f1115;
--text: #e8eaf0;
--muted: #a0a4b3;
--box-border: #2a2f3a;
--box-bg: #141824;
--btn-bg: #1a2130;
--btn-border: #323a4c;
--input-border: #323a4c;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
font-family: sans-serif;
margin: var(--pad);
color: var(--text);
background: var(--bg);
}
h2 { margin-top: 0; }
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--gap);
align-items: start;
}
@media (max-width: 820px){
body { margin: 12px; }
.grid { grid-template-columns: 1fr; }
}
.box {
border: 1px solid var(--box-border);
padding: 12px;
border-radius: var(--radius);
background: var(--box-bg);
}
input[type="search"] {
width: 100%;
padding: 10px 10px;
border-radius: 6px;
border: 1px solid var(--input-border);
background: transparent;
color: var(--text);
outline: none;
}
input[type="search"]::placeholder { color: var(--muted); }
.muted { color: var(--muted); font-size: 12px; }
button {
padding: 10px 14px;
cursor: pointer;
margin-right: 8px;
margin-bottom: 8px;
border-radius: 6px;
border: 1px solid var(--btn-border);
background: var(--btn-bg);
color: var(--text);
}
.actions{
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.topbar{
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.themeBtn{
margin: 0;
white-space: nowrap;
}
ul { list-style: none; padding-left: 0; margin: 10px 0 0 0; }
li { margin: 6px 0; }
/*.track { display: flex; align-items: center; gap: 8px; }*/
/*.track label {
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}*/
/* Taille par défaut un peu plus grande */
.track {
font-size: 15px;
}
/*.track input[type="checkbox"] {
width: 22px;
height: 22px;
}*/
.track input[type="checkbox"] {
transform: scale(1.4);
-webkit-transform: scale(1.4); /* WebKit/mobile */
margin-right: 6px;
}
/* Spécifique aux petits écrans (mobile) */
@media (max-width: 600px){
body {
font-size: 18px;
}
.track {
font-size: 18px;
}
.track input[type="checkbox"] {
width: 26px;
height: 26px;
}
/* Boutons un peu plus gros pour le doigt */
button {
padding: 12px 18px;
font-size: 16px;
}
input[type="search"] {
font-size: 16px;
}
#selection-panel {
font-size: 14px;
}
}
#selection-panel {
max-height: 180px;
overflow: auto;
font-size: 13px;
line-height: 1.4;
}
.spacer { height: 16px; }
</style>
</head>
<body>
<div class="topbar">
<h2 style="margin:0;">Playlist globale (.m3u)</h2>
<button id="themeToggle" class="themeBtn" type="button">Clair</button>
</div>
<div class="grid">
<div class="box">
<b>Recherche</b>
<div class="spacer" style="height:8px"></div>
<input id="q" type="search" placeholder="ex: Paranoid, Thunderstruck..."/>
<div class="spacer" style="height:10px"></div>
<div class="muted">Coche/décoche puis “Exporter .m3u”.</div>
</div>
<div class="box">
<b>Sélection</b>
<div class="spacer" style="height:8px"></div>
<div class="muted" id="count">0 morceaux</div>
<div class="spacer" style="height:12px"></div>
<div class="actions">
<button onclick="exportM3U()">Exporter .m3u</button>
<button onclick="toggleAll(true)">Tout cocher</button>
<button onclick="toggleAll(false)">Tout décocher</button>
<button onclick="openDeleteDialog()">Effacer playlist</button>
</div>
</div>
</div>
<div class="spacer"></div>
<div class="box">
<b>Vue sur la sélection</b>
<div class="spacer" style="height:8px"></div>
<div class="muted"><span id="selection-count">0</span> morceaux cochés</div>
<div class="spacer" style="height:10px"></div>
<div id="selection-panel" style="max-height:180px; overflow:auto; font-size:13px; line-height:1.4;">
<div id="selection-list"></div>
</div>
</div>
<div class="spacer"></div>
<div id="content" class="box"></div>
<div id="delete-dialog" style="
position: fixed;
inset: 0;
background: rgba(0,0,0,0.55);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
">
<div style="
background: var(--box-bg);
color: var(--text);
border-radius: 8px;
padding: 16px;
max-width: 420px;
width: 90%;
border: 1px solid var(--box-border);
">
<h3 style="margin-top:0;">Effacer des playlists</h3>
<div class="muted" style="margin-bottom:8px;">Coche les fichiers .m3u à supprimer :</div>
<div id="delete-list" style="max-height:220px; overflow:auto; margin-bottom:12px; font-size:14px;"></div>
<div style="display:flex; justify-content:flex-end; gap:8px;">
<button type="button" onclick="closeDeleteDialog()">Annuler</button>
<button type="button" onclick="confirmDeletePlaylists()">Supprimer</button>
</div>
</div>
</div>
<script>
let INDEX = null;
let collapsed = {}; // "artist|album"
let lastQuery = "";
let selectedRels = new Set(); // état global des morceaux sélectionnés
// --- thème sombre/claire
(function(){
const saved = localStorage.getItem("theme");
const theme = saved || "dark";
document.documentElement.setAttribute("data-theme", theme);
const btn = document.getElementById("themeToggle");
const label = () => {
const t = document.documentElement.getAttribute("data-theme");
btn.textContent = (t === "dark") ? "Clair" : "Sombre";
};
label();
btn.addEventListener("click", () => {
const cur = document.documentElement.getAttribute("data-theme");
const next = (cur === "dark") ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem("theme", next);
label();
});
})();
// --- helpers
function keyFor(artist, album){ return artist + "|" + album; }
function ensureCollapsedIndex(index){
collapsed = {};
for (const artist of Object.keys(index).sort()){
collapsed[keyFor(artist, "__artist__")] = true;
const albums = index[artist] || {};
for (const album of Object.keys(albums).sort()){
collapsed[keyFor(artist, album)] = true;
}
}
}
function allTrackRels(index){
const rels = [];
for (const artist of Object.keys(index)){
const albums = index[artist] || {};
for (const album of Object.keys(albums)){
const tracks = albums[album] || [];
for (const t of tracks) rels.push(t.rel);
}
}
return rels;
}
function getSelectedRels(){
return new Set(selectedRels);
}
function setSelectedRelsToAll(on){
const rels = allTrackRels(INDEX);
selectedRels = new Set();
if (on) rels.forEach(r => selectedRels.add(r));
render(INDEX, document.getElementById("q").value.toLowerCase() || "");
}
function updateCount(){
const count = selectedRels.size;
document.getElementById("count").textContent = count + " morceaux";
document.getElementById("selection-count").textContent = count;
}
function renderSelectionView(){
const panel = document.getElementById("selection-panel");
if (!panel) return;
const relToTrack = new Map();
for (const artist of Object.keys(INDEX)){
const albums = INDEX[artist] || {};
for (const album of Object.keys(albums)){
const tracks = albums[album] || [];
for (const t of tracks){
relToTrack.set(t.rel, { artist, album, name: t.name });
}
}
}
const selected = Array.from(selectedRels);
const arr = selected.map(rel => {
const meta = relToTrack.get(rel) || { artist:"?", album:"?", name:rel };
const albumLabel = (meta.album === "~direct") ? "(morceaux directs)" : meta.album;
return meta.artist + " / " + albumLabel + " — " + meta.name;
}).sort();
const list = document.getElementById("selection-list");
list.innerHTML = "";
if (arr.length === 0){
const li = document.createElement("div");
li.style.color = "#666";
li.textContent = "Rien sélectionné.";
list.appendChild(li);
return;
}
const maxItems = 200;
const slice = arr.slice(0, maxItems);
slice.forEach(txt => {
const div = document.createElement("div");
div.textContent = txt;
list.appendChild(div);
});
if (arr.length > maxItems){
const more = document.createElement("div");
more.style.color = "#666";
more.textContent = "… et " + (arr.length - maxItems) + " autres";
list.appendChild(more);
}
}
function bindCount(){
document.querySelectorAll('input[type="checkbox"][name="track"]').forEach(cb => {
cb.onchange = () => {
if (cb.checked){
selectedRels.add(cb.value);
} else {
selectedRels.delete(cb.value);
}
updateCount();
renderSelectionView();
};
});
}
// --- render
function render(index, q=""){
lastQuery = q;
const content = document.getElementById("content");
content.innerHTML = "";
const artists = Object.keys(index).sort();
for (const artist of artists){
const albums = index[artist] || {};
const artistReduced = !!collapsed[keyFor(artist, "__artist__")];
const artistBox = document.createElement("div");
artistBox.style.marginBottom = "14px";
const artistHeader = document.createElement("div");
artistHeader.style.display = "flex";
artistHeader.style.alignItems = "center";
artistHeader.style.gap = "8px";
const artistToggle = document.createElement("button");
artistToggle.textContent = (artistReduced ? "+" : "–");
artistToggle.onclick = () => {
collapsed[keyFor(artist, "__artist__")] = !collapsed[keyFor(artist, "__artist__")];
render(INDEX, document.getElementById("q").value.toLowerCase() || "");
};
const artistTitle = document.createElement("div");
artistTitle.innerHTML = "<b>" + artist + "</b>";
artistHeader.appendChild(artistToggle);
artistHeader.appendChild(artistTitle);
artistBox.appendChild(artistHeader);
let any = false;
const searching = !!q;
for (const album of Object.keys(albums).sort()){
const tracks = albums[album] || [];
// Filtrage pour la recherche
const visible = tracks.filter(t => {
if (!q) return true;
const hay = (artist + " " + (album === "~direct" ? "" : album) + " " + t.name).toLowerCase();
return hay.includes(q);
});
if (visible.length === 0) continue;
any = true;
if (artistReduced && !searching) continue;
const collapsedAlbum = !!collapsed[keyFor(artist, album)];
const albumWrapper = document.createElement("div");
const albumHeader = document.createElement("div");
albumHeader.style.display = "flex";
albumHeader.style.alignItems = "center";
albumHeader.style.gap = "8px";
albumHeader.style.marginTop = "8px";
const albumToggle = document.createElement("button");
albumToggle.textContent = (collapsedAlbum ? "+" : "–");
albumToggle.onclick = () => {
collapsed[keyFor(artist, album)] = !collapsed[keyFor(artist, album)];
render(INDEX, document.getElementById("q").value.toLowerCase() || "");
};
const albumTitle = document.createElement("div");
albumTitle.className = "muted";
albumTitle.textContent = (album === "~direct") ? "(morceaux directs)" : album;
// --- nouveaux boutons album qui pilotent selectedRels ---
const albumSelectBtn = document.createElement("button");
albumSelectBtn.textContent = "Cocher titres";
albumSelectBtn.onclick = () => {
// ajouter tous les rels de l'album dans selectedRels
tracks.forEach(t => selectedRels.add(t.rel));
render(INDEX, document.getElementById("q").value.toLowerCase() || "");
};
const albumUnselectBtn = document.createElement("button");
albumUnselectBtn.textContent = "Décocher titres";
albumUnselectBtn.onclick = () => {
// retirer tous les rels de l'album de selectedRels
tracks.forEach(t => selectedRels.delete(t.rel));
render(INDEX, document.getElementById("q").value.toLowerCase() || "");
};
albumHeader.appendChild(albumToggle);
albumHeader.appendChild(albumTitle);
albumHeader.appendChild(albumSelectBtn);
albumHeader.appendChild(albumUnselectBtn);
albumWrapper.appendChild(albumHeader);
if (!collapsedAlbum || searching){
const ul = document.createElement("ul");
for (const t of visible){
const li = document.createElement("li");
li.className = "track";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.name = "track";
cb.value = t.rel;
// l'état visuel dépend de selectedRels
cb.checked = selectedRels.has(t.rel);
const label = document.createElement("label");
label.textContent = t.name;
li.appendChild(cb);
li.appendChild(label);
ul.appendChild(li);
}
albumWrapper.appendChild(ul);
}
artistBox.appendChild(albumWrapper);
}
if (any) content.appendChild(artistBox);
}
// recoller les handlers de changement, mettre à jour les compteurs, vue sélection
bindCount();
updateCount();
renderSelectionView();
}
function toggleAll(on){
setSelectedRelsToAll(on);
}
// --- export
async function exportM3U(){
let rels = Array.from(selectedRels);
if (rels.length === 0){ alert("Aucun morceau sélectionné."); return; }
// mélange optionnel
for (let i = rels.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[rels[i], rels[j]] = [rels[j], rels[i]];
}
const title = prompt("Nom du fichier playlist (.m3u) :", "Playlist") || "Playlist";
const form = new URLSearchParams();
form.append("title", title);
rels.forEach(r => form.append("track", r));
const res = await fetch("/export", {
method: "POST",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: form.toString()
});
if (!res.ok){
const txt = await res.text().catch(() => "");
alert("Erreur export: " + res.status + (txt ? " - " + txt : ""));
return;
}
const data = await res.json();
alert("Playlist enregistrée sur le serveur :\\n" +
data.filename + "\\n(" + data.path + ")");
}
// recherche
document.getElementById("q").addEventListener("input", (e) => {
const qq = (e.target.value || "").toLowerCase();
render(INDEX, qq);
});
// init
(async function init(){
const res = await fetch("/api/index");
INDEX = await res.json();
ensureCollapsedIndex(INDEX);
render(INDEX, "");
})();
// logique d’affichage et de suppression playlist
async function openDeleteDialog(){
const dialog = document.getElementById("delete-dialog");
const listDiv = document.getElementById("delete-list");
listDiv.innerHTML = "Chargement...";
try {
const res = await fetch("/api/playlists");
if (!res.ok){
listDiv.textContent = "Erreur lors du listing (" + res.status + ")";
dialog.style.display = "flex";
return;
}
const files = await res.json();
listDiv.innerHTML = "";
if (!files.length){
const p = document.createElement("div");
p.className = "muted";
p.textContent = "Aucun fichier .m3u dans le dossier.";
listDiv.appendChild(p);
} else {
files.forEach(f => {
const row = document.createElement("div");
row.style.display = "flex";
row.style.alignItems = "center";
row.style.gap = "6px";
row.style.marginBottom = "4px";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.value = f.name;
cb.name = "delete_m3u";
const label = document.createElement("span");
label.textContent = f.name;
row.appendChild(cb);
row.appendChild(label);
listDiv.appendChild(row);
});
}
} catch (e){
listDiv.textContent = "Erreur réseau : " + e;
}
dialog.style.display = "flex";
}
function closeDeleteDialog(){
const dialog = document.getElementById("delete-dialog");
dialog.style.display = "none";
}
async function confirmDeletePlaylists(){
const checked = Array.from(
document.querySelectorAll('#delete-list input[type="checkbox"][name="delete_m3u"]:checked')
).map(cb => cb.value);
if (!checked.length){
alert("Aucun fichier sélectionné.");
return;
}
if (!confirm("Supprimer " + checked.length + " fichier(s) .m3u ?")){
return;
}
try {
const res = await fetch("/api/playlists/delete", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ names: checked })
});
if (!res.ok){
const txt = await res.text().catch(() => "");
alert("Erreur suppression: " + res.status + (txt ? " - " + txt : ""));
return;
}
const data = await res.json();
let msg = "";
if (data.deleted && data.deleted.length){
msg += "Supprimé :\\n" + data.deleted.join("\\n") + "\\n";
}
if (data.errors && data.errors.length){
msg += "\\nErreurs :\\n" +
data.errors.map(e => e.name + " -> " + e.error).join("\\n");
}
if (!msg) msg = "Opération terminée.";
alert(msg);
// Rafraîchir la liste après suppression
openDeleteDialog();
} catch (e){
alert("Erreur réseau : " + e);
}
}
</script>
</body>
</html>
""")
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5100, debug=False)Vérification syntaxe et indentation
une commande pour vérifier si aucune erreur de syntaxe ou indentation
1
2
3
4
5
# Si pas le prompt (.venv) yick@alder:/sharenfs/rnmkcy/web-music-playlist$
cd /sharenfs/rnmkcy/web-music-playlist
source .venv/bin/activate
# vérification syntaxe
python -m py_compile app.py # Ne renvoie rien -> OK
Lancer manuellement pour test
1
2
3
source /sharenfs/rnmkcy/web-music-playlist/.venv/bin/activate
cd /sharenfs/rnmkcy/web-music-playlist
python app.py
Si tout est ok
1
2
3
4
5
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5100
Press CTRL+C to quit
Tester depuis PC1
lancer la commande suivante
1
ssh -L 9500:127.0.0.1:5100 yick@192.168.0.205 -p 55205 -i /home/yann/.ssh/yick-ed25519
Ouvrir un navigateur sur localhost:9500
uwsgi.ini
/sharenfs/rnmkcy/web-music-playlist/uwsgi.ini
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[uwsgi]
chdir = /sharenfs/rnmkcy/web-music-playlist
module = app:app
master = true
processes = 1
threads = 2
enable-threads = true
single-interpreter = true
die-on-term = true
need-app = true
vacuum = true
safe-pidfile = /run/uwsgi/web-music-playlist.pid
socket = /run/uwsgi/playlist-web.sock
chmod-socket = 660
chmod-socket = 660
stats = /run/uwsgi/web-music-playlist.stats
buffer-size = 65535
log-date = true
py-autoreload = 0
honour-stdin = true
Ce modèle correspond aux configurations uWSGI standards avec socket UNIX, permissions et mode daemonisé pour nginx
Mettre en service systemd (recommandé)
Créer /etc/systemd/system/web-music-playlist.service :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Unit]
Description=Flask playlist generator
After=network.target
[Service]
Type=notify
User=yick
Group=yick
WorkingDirectory=/sharenfs/rnmkcy/web-music-playlist
RuntimeDirectory=uwsgi
RuntimeDirectoryMode=0755
Environment="PATH=/sharenfs/rnmkcy/web-music-playlist/venv/bin"
ExecStart=/sharenfs/rnmkcy/web-music-playlist/venv/bin/uwsgi --ini /sharenfs/rnmkcy/web-music-playlist/uwsgi.ini
ExecStop=/sharenfs/rnmkcy/web-music-playlist/venv/bin/uwsgi --stop /run/uwsgi/web-music-playlist.pid
PIDFile=/run/uwsgi/web-music-playlist.pid
Restart=always
KillSignal=SIGQUIT
TimeoutStartSec=30
TimeoutStopSec=20
[Install]
WantedBy=multi-user.target
Puis :
1
2
3
4
sudo systemctl daemon-reload
sudo systemctl enable --now web-music-playlist
sudo systemctl status web-music-playlist --no-pager
sudo journalctl -u web-music-playlist -f --no-pager
Après toutes modifications
1
sudo systemctl restart web-music-playlist
Nginx (vhost)
exposition /music/ + reverse proxy Flask)
Créer /etc/nginx/conf.d/playlist.rnmkcy.eu.conf :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name playlist.rnmkcy.eu;
include /etc/nginx/conf.d/ssl-modern.inc;
location /music/ {
alias /sharenfs/multimedia/Music/musicyan/;
autoindex off;
add_header Cache-Control "public, max-age=31536000";
}
location / {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/playlist-web.sock;
uwsgi_read_timeout 120s;
uwsgi_connect_timeout 30s;
}
}
Vérifier puis reload :
1
2
sudo nginx -t
sudo systemctl reload nginx
Lien :

