program2 exports the playlists
This commit is contained in:
26
app/cache/video_summary.py
vendored
Normal file
26
app/cache/video_summary.py
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# cache/video_summary.py
|
||||||
|
import db
|
||||||
|
|
||||||
|
def rebuild_video_summary():
|
||||||
|
with db.get_conn() as conn:
|
||||||
|
conn.execute("DROP TABLE IF EXISTS video_summary")
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE video_summary AS
|
||||||
|
SELECT
|
||||||
|
v.file_name,
|
||||||
|
v.raw_file,
|
||||||
|
v.mp4_file,
|
||||||
|
v.record_datetime,
|
||||||
|
v.day_of_week,
|
||||||
|
v.difficulty_level,
|
||||||
|
v.address,
|
||||||
|
GROUP_CONCAT(DISTINCT l.name) AS labels,
|
||||||
|
GROUP_CONCAT(DISTINCT p.name) AS playlists
|
||||||
|
FROM videos v
|
||||||
|
LEFT JOIN video_labels vl ON vl.video_file_name = v.file_name
|
||||||
|
LEFT JOIN labels l ON l.id = vl.label_id
|
||||||
|
LEFT JOIN video_playlists vp ON vp.video_file_name = v.file_name
|
||||||
|
LEFT JOIN playlists p ON p.id = vp.playlist_id
|
||||||
|
GROUP BY v.file_name
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
@@ -41,9 +41,10 @@ def load_all_playlists():
|
|||||||
print(f"⚠️ Ignored invalid playlist row (id={row_dict.get('id')}, name={row_dict.get('name')}): {e}")
|
print(f"⚠️ Ignored invalid playlist row (id={row_dict.get('id')}, name={row_dict.get('name')}): {e}")
|
||||||
return playlists
|
return playlists
|
||||||
|
|
||||||
def delete_playlist(playlist_id):
|
def delete_playlist(playlist_id: int):
|
||||||
with db.get_conn() as conn:
|
with db.get_conn() as conn:
|
||||||
conn.execute("DELETE FROM playlists WHERE id = ?", (playlist_id,))
|
conn.execute("DELETE FROM playlists WHERE id = ?", (playlist_id,))
|
||||||
|
conn.execute("DELETE FROM video_playlists WHERE playlist_id = ?", (playlist_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
def get_videos_for_playlist(playlist: Playlist):
|
def get_videos_for_playlist(playlist: Playlist):
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class Playlist(BaseModel):
|
|||||||
description: Optional[str] = ""
|
description: Optional[str] = ""
|
||||||
type: Literal["manual", "dynamic"] = "manual"
|
type: Literal["manual", "dynamic"] = "manual"
|
||||||
rules: RuleSet = Field(default_factory=RuleSet)
|
rules: RuleSet = Field(default_factory=RuleSet)
|
||||||
|
created_at: Optional[str] = None
|
||||||
|
updated_at: Optional[str] = None
|
||||||
|
|
||||||
@validator("rules", pre=True, always=True)
|
@validator("rules", pre=True, always=True)
|
||||||
def ensure_rules(cls, v):
|
def ensure_rules(cls, v):
|
||||||
|
|||||||
@@ -1,19 +1,39 @@
|
|||||||
# playlists/playlist_page.py
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
from playlists import playlist_db
|
from playlists import playlist_db
|
||||||
from playlists.playlist_model import Playlist
|
from playlists.playlist_model import Playlist, RuleSet
|
||||||
from playlists.playlist_controller import playlist_manual_editor, playlist_dynamic_editor
|
from playlists.playlist_controller import playlist_manual_editor, playlist_dynamic_editor
|
||||||
from playlists.playlist_controller import RuleSet
|
from datetime import datetime
|
||||||
|
from cache.video_summary import rebuild_video_summary
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
st.title("🎵 Gestion des Playlists")
|
st.title("🎵 Gestion des Playlists")
|
||||||
|
if st.button("🔁 Recalculer le cache vidéo"):
|
||||||
|
rebuild_video_summary()
|
||||||
|
st.success("Cache mis à jour !")
|
||||||
|
|
||||||
|
# --- Barre latérale : recherche & filtres ---
|
||||||
|
st.sidebar.header("🔎 Recherche de playlists")
|
||||||
|
search_term = st.sidebar.text_input("Filtrer par nom ou description")
|
||||||
|
date_filter = st.sidebar.date_input("Créées après", value=None)
|
||||||
|
|
||||||
playlists = playlist_db.load_all_playlists()
|
playlists = playlist_db.load_all_playlists()
|
||||||
selected = st.selectbox("Playlists existantes", ["(Nouvelle)"] + [p.name for p in playlists])
|
|
||||||
|
|
||||||
# playlists/playlist_page.py (partie création)
|
# Appliquer les filtres
|
||||||
if selected == "(Nouvelle)":
|
filtered = []
|
||||||
st.subheader("➕ Nouvelle playlist")
|
for p in playlists:
|
||||||
|
if search_term.lower() not in p.name.lower() and search_term.lower() not in (p.description or "").lower():
|
||||||
|
continue
|
||||||
|
if date_filter and datetime.fromisoformat(p.created_at) < datetime.combine(date_filter, datetime.min.time()):
|
||||||
|
continue
|
||||||
|
filtered.append(p)
|
||||||
|
|
||||||
|
# --- Sélection ou création ---
|
||||||
|
names = ["(➕ Nouvelle playlist)"] + [p.name for p in filtered]
|
||||||
|
selected_name = st.selectbox("Sélectionnez une playlist", names, key="playlist_select")
|
||||||
|
|
||||||
|
# --- Création ---
|
||||||
|
if selected_name == "(➕ Nouvelle playlist)":
|
||||||
|
st.subheader("Créer une nouvelle playlist")
|
||||||
name = st.text_input("Nom")
|
name = st.text_input("Nom")
|
||||||
desc = st.text_area("Description")
|
desc = st.text_area("Description")
|
||||||
type_choice = st.radio("Type", ["manual", "dynamic"])
|
type_choice = st.radio("Type", ["manual", "dynamic"])
|
||||||
@@ -21,15 +41,53 @@ def main():
|
|||||||
if not name.strip():
|
if not name.strip():
|
||||||
st.error("Le nom ne peut pas être vide.")
|
st.error("Le nom ne peut pas être vide.")
|
||||||
else:
|
else:
|
||||||
pl = Playlist(name=name.strip(), description=desc or "", type=type_choice, rules=RuleSet())
|
pl = Playlist(name=name.strip(), description=desc, type=type_choice, rules=RuleSet())
|
||||||
pl.save()
|
pl.save()
|
||||||
st.success("Playlist créée ✅")
|
st.session_state["playlist_select"] = pl.name # ✅ sélectionne automatiquement
|
||||||
# Recharge immédiate : rerun force tout à re-exécuter
|
if hasattr(st, "rerun"):
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
else:
|
||||||
|
st.experimental_rerun()
|
||||||
|
return
|
||||||
|
|
||||||
else:
|
# --- Mode édition ---
|
||||||
current = next(p for p in playlists if p.name == selected)
|
current = next((p for p in playlists if p.name == selected_name), None)
|
||||||
if current.type == "manual":
|
if not current:
|
||||||
playlist_manual_editor(current)
|
st.warning("Aucune playlist sélectionnée.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Barre d’actions ---
|
||||||
|
st.subheader(f"🎞️ Playlist : {current.name}")
|
||||||
|
new_name = st.text_input("Renommer", value=current.name)
|
||||||
|
new_desc = st.text_area("Description", value=current.description or "")
|
||||||
|
if st.button("💾 Sauvegarder les métadonnées"):
|
||||||
|
current.name = new_name
|
||||||
|
current.description = new_desc
|
||||||
|
current.save()
|
||||||
|
st.success("Mise à jour enregistrée ✅")
|
||||||
|
if hasattr(st, "rerun"):
|
||||||
|
st.rerun()
|
||||||
else:
|
else:
|
||||||
playlist_dynamic_editor(current)
|
st.experimental_rerun()
|
||||||
|
|
||||||
|
col1, col2, col3 = st.columns([1, 1, 2])
|
||||||
|
with col1:
|
||||||
|
if st.button("🗑️ Supprimer"):
|
||||||
|
playlist_db.delete_playlist(current.id)
|
||||||
|
st.success("Playlist supprimée ✅")
|
||||||
|
if hasattr(st, "rerun"):
|
||||||
|
st.rerun()
|
||||||
|
else:
|
||||||
|
st.experimental_rerun()
|
||||||
|
with col2:
|
||||||
|
if st.button("⏪ Retour à la liste"):
|
||||||
|
st.session_state.pop("playlist_select", None)
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
# --- Éditeur selon le type ---
|
||||||
|
if current.type == "manual":
|
||||||
|
playlist_manual_editor(current)
|
||||||
|
else:
|
||||||
|
playlist_dynamic_editor(current)
|
||||||
|
|||||||
@@ -1,10 +1,136 @@
|
|||||||
CREATE TABLE IF NOT EXISTS video_summary AS
|
CREATE TABLE IF NOT EXISTS video_summary AS
|
||||||
SELECT v.file_name,
|
SELECT
|
||||||
GROUP_CONCAT(DISTINCT l.name) AS labels,
|
v.file_name,
|
||||||
GROUP_CONCAT(DISTINCT p.name) AS playlists
|
v.raw_file,
|
||||||
|
v.mp4_file,
|
||||||
|
v.record_datetime,
|
||||||
|
v.day_of_week,
|
||||||
|
v.difficulty_level,
|
||||||
|
v.address,
|
||||||
|
GROUP_CONCAT(DISTINCT l.name) AS labels,
|
||||||
|
GROUP_CONCAT(DISTINCT p.name) AS playlists
|
||||||
FROM videos v
|
FROM videos v
|
||||||
LEFT JOIN video_labels vl ON vl.video_file_name = v.file_name
|
LEFT JOIN video_labels vl ON vl.video_file_name = v.file_name
|
||||||
LEFT JOIN labels l ON l.id = vl.label_id
|
LEFT JOIN labels l ON l.id = vl.label_id
|
||||||
LEFT JOIN video_playlists vp ON vp.video_file_name = v.file_name
|
LEFT JOIN video_playlists vp ON vp.video_file_name = v.file_name
|
||||||
LEFT JOIN playlists p ON p.id = vp.playlist_id
|
LEFT JOIN playlists p ON p.id = vp.playlist_id
|
||||||
GROUP BY v.file_name;
|
GROUP BY v.file_name;
|
||||||
|
|
||||||
|
DROP VIEW playlist_videos;
|
||||||
|
CREATE VIEW playlist_videos AS
|
||||||
|
WITH
|
||||||
|
-- Sous-requête pour les playlists manuelles
|
||||||
|
manual_playlist_videos AS (
|
||||||
|
SELECT
|
||||||
|
p.id AS playlist_id,
|
||||||
|
p.name AS playlist_name,
|
||||||
|
vp.video_file_name,
|
||||||
|
v.*,
|
||||||
|
'manual' AS playlist_type
|
||||||
|
FROM playlists p
|
||||||
|
JOIN video_playlists vp ON p.id = vp.playlist_id
|
||||||
|
JOIN videos v ON vp.video_file_name = v.file_name
|
||||||
|
WHERE p.type = 'manual'
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Sous-requête pour les playlists dynamiques
|
||||||
|
dynamic_playlist_videos AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
p.id AS playlist_id,
|
||||||
|
p.name AS playlist_name,
|
||||||
|
v.file_name AS video_file_name,
|
||||||
|
v.*,
|
||||||
|
'dynamic' AS playlist_type
|
||||||
|
FROM playlists p
|
||||||
|
JOIN videos v
|
||||||
|
WHERE p.type = 'dynamic'
|
||||||
|
-- Inclure les vidéos qui ont les labels requis
|
||||||
|
AND (
|
||||||
|
json_array_length(json_extract(p.rules_json, '$.include_labels')) = 0
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM labels l
|
||||||
|
JOIN video_labels vl ON l.id = vl.label_id
|
||||||
|
WHERE vl.video_file_name = v.file_name
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM (
|
||||||
|
SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
||||||
|
-- Ajoute plus de lignes si tes tableaux JSON ont plus de 5 éléments
|
||||||
|
) n
|
||||||
|
WHERE n.n < json_array_length(json_extract(p.rules_json, '$.include_labels'))
|
||||||
|
AND l.name = json_extract(
|
||||||
|
json_extract(p.rules_json, '$.include_labels'),
|
||||||
|
'$[' || n.n || ']'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
-- Exclure les vidéos qui ont les labels exclus
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM labels l
|
||||||
|
JOIN video_labels vl ON l.id = vl.label_id
|
||||||
|
WHERE vl.video_file_name = v.file_name
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM (
|
||||||
|
SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
||||||
|
) n
|
||||||
|
WHERE n.n < json_array_length(json_extract(p.rules_json, '$.exclude_labels'))
|
||||||
|
AND l.name = json_extract(
|
||||||
|
json_extract(p.rules_json, '$.exclude_labels'),
|
||||||
|
'$[' || n.n || ']'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
-- Inclure les vidéos qui sont dans les playlists requises
|
||||||
|
AND (
|
||||||
|
json_array_length(json_extract(p.rules_json, '$.include_playlists')) = 0
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM playlists pl
|
||||||
|
JOIN video_playlists vp ON pl.id = vp.playlist_id
|
||||||
|
WHERE vp.video_file_name = v.file_name
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM (
|
||||||
|
SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
||||||
|
) n
|
||||||
|
WHERE n.n < json_array_length(json_extract(p.rules_json, '$.include_playlists'))
|
||||||
|
AND pl.name = json_extract(
|
||||||
|
json_extract(p.rules_json, '$.include_playlists'),
|
||||||
|
'$[' || n.n || ']'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
-- Exclure les vidéos qui sont dans les playlists exclues
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM playlists pl
|
||||||
|
JOIN video_playlists vp ON pl.id = vp.playlist_id
|
||||||
|
WHERE vp.video_file_name = v.file_name
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM (
|
||||||
|
SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
||||||
|
) n
|
||||||
|
WHERE n.n < json_array_length(json_extract(p.rules_json, '$.exclude_playlists'))
|
||||||
|
AND pl.name = json_extract(
|
||||||
|
json_extract(p.rules_json, '$.exclude_playlists'),
|
||||||
|
'$[' || n.n || ']'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
-- Filtrer par date si nécessaire
|
||||||
|
AND (
|
||||||
|
json_extract(p.rules_json, '$.date_after') IS NULL
|
||||||
|
OR v.record_datetime >= json_extract(p.rules_json, '$.date_after')
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
json_extract(p.rules_json, '$.date_before') IS NULL
|
||||||
|
OR v.record_datetime <= json_extract(p.rules_json, '$.date_before')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Union des deux types de playlists
|
||||||
|
SELECT * FROM manual_playlist_videos
|
||||||
|
UNION ALL
|
||||||
|
SELECT * FROM dynamic_playlist_videos;
|
||||||
|
|||||||
@@ -1,14 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SCRIPTS_DIR=$(dirname `realpath ${BASH_SOURCE[0]}`)
|
|
||||||
|
|
||||||
export DANCE_VIDEOS_DB="${HOME}/Documents/.DanceVideos/db.sqlite"
|
SCRIPTS_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
|
||||||
export DOSSIER_PLAYLIST="$(dirname $DANCE_VIDEOS_DB)/playlists"
|
DANCE_VIDEOS_DB="${HOME}/Documents/.DanceVideos/db.sqlite"
|
||||||
|
DOSSIER_PLAYLIST="$(dirname "$DANCE_VIDEOS_DB")/playlists"
|
||||||
|
|
||||||
rm -rf $DOSSIER_PLAYLIST
|
# Nettoyer et recréer le dossier des playlists
|
||||||
|
rm -rf "$DOSSIER_PLAYLIST"
|
||||||
|
mkdir -p "$DOSSIER_PLAYLIST"
|
||||||
|
|
||||||
export PLAYLIST_ALL=$DOSSIER_PLAYLIST/all
|
# Créer un dossier "all" avec toutes les vidéos
|
||||||
mkdir -p $PLAYLIST_ALL
|
# PLAYLIST_ALL="$DOSSIER_PLAYLIST/all"
|
||||||
|
# mkdir -p "$PLAYLIST_ALL"
|
||||||
|
# while IFS= read -r v; do
|
||||||
|
# ln -s "$v" "$PLAYLIST_ALL/$(basename "$(dirname "$v")").mp4"
|
||||||
|
# done < <(sqlite3 "$DANCE_VIDEOS_DB" "SELECT rotated_file FROM videos WHERE rotated_file IS NOT NULL;")
|
||||||
|
|
||||||
for v in $(sqlite3 $DANCE_VIDEOS_DB "select rotated_file from videos"); do
|
# Pour chaque playlist, créer un dossier et ajouter les liens symboliques
|
||||||
ln -s $v $PLAYLIST_ALL/$(basename $(dirname $v)).mp4
|
sqlite3 -separator '|' "$DANCE_VIDEOS_DB" "
|
||||||
done
|
SELECT playlist_id, playlist_name, video_file_name, rotated_file
|
||||||
|
FROM playlist_videos
|
||||||
|
WHERE rotated_file IS NOT NULL;
|
||||||
|
" | while IFS='|' read -r playlist_id playlist_name video_file_name rotated_file; do
|
||||||
|
# Sauter l'en-tête
|
||||||
|
if [[ "$playlist_id" == "playlist_id" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ignorer les lignes vides
|
||||||
|
if [[ -z "$playlist_id" || -z "$rotated_file" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
set -x
|
||||||
|
# Créer le dossier de la playlist
|
||||||
|
PLAYLIST_DIR="$DOSSIER_PLAYLIST/$playlist_name"
|
||||||
|
mkdir -p "$PLAYLIST_DIR"
|
||||||
|
|
||||||
|
# Créer le lien symbolique
|
||||||
|
ln -sf "$rotated_file" "$PLAYLIST_DIR/$(basename "$(dirname "$rotated_file")").mp4"
|
||||||
|
set +x
|
||||||
|
done
|
||||||
|
|||||||
Reference in New Issue
Block a user