diff --git a/app/cache/video_summary.py b/app/cache/video_summary.py new file mode 100644 index 0000000..28ce6d8 --- /dev/null +++ b/app/cache/video_summary.py @@ -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() diff --git a/app/playlists/playlist_db.py b/app/playlists/playlist_db.py index e76c46a..9f42425 100644 --- a/app/playlists/playlist_db.py +++ b/app/playlists/playlist_db.py @@ -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}") return playlists -def delete_playlist(playlist_id): +def delete_playlist(playlist_id: int): with db.get_conn() as conn: conn.execute("DELETE FROM playlists WHERE id = ?", (playlist_id,)) + conn.execute("DELETE FROM video_playlists WHERE playlist_id = ?", (playlist_id,)) conn.commit() def get_videos_for_playlist(playlist: Playlist): diff --git a/app/playlists/playlist_model.py b/app/playlists/playlist_model.py index 6df0bb6..0874af5 100644 --- a/app/playlists/playlist_model.py +++ b/app/playlists/playlist_model.py @@ -36,6 +36,8 @@ class Playlist(BaseModel): description: Optional[str] = "" type: Literal["manual", "dynamic"] = "manual" rules: RuleSet = Field(default_factory=RuleSet) + created_at: Optional[str] = None + updated_at: Optional[str] = None @validator("rules", pre=True, always=True) def ensure_rules(cls, v): diff --git a/app/playlists/playlist_page.py b/app/playlists/playlist_page.py index ff8787e..198d765 100644 --- a/app/playlists/playlist_page.py +++ b/app/playlists/playlist_page.py @@ -1,19 +1,39 @@ -# playlists/playlist_page.py import streamlit as st 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 RuleSet +from datetime import datetime +from cache.video_summary import rebuild_video_summary def main(): 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() - selected = st.selectbox("Playlists existantes", ["(Nouvelle)"] + [p.name for p in playlists]) - # playlists/playlist_page.py (partie création) - if selected == "(Nouvelle)": - st.subheader("➕ Nouvelle playlist") + # Appliquer les filtres + filtered = [] + 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") desc = st.text_area("Description") type_choice = st.radio("Type", ["manual", "dynamic"]) @@ -21,15 +41,53 @@ def main(): if not name.strip(): st.error("Le nom ne peut pas être vide.") 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() - st.success("Playlist créée ✅") - # Recharge immédiate : rerun force tout à re-exécuter - st.rerun() + st.session_state["playlist_select"] = pl.name # ✅ sélectionne automatiquement + if hasattr(st, "rerun"): + st.rerun() + else: + st.experimental_rerun() + return - else: - current = next(p for p in playlists if p.name == selected) - if current.type == "manual": - playlist_manual_editor(current) + # --- Mode édition --- + current = next((p for p in playlists if p.name == selected_name), None) + if not 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: - 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) diff --git a/model/videos_summary.sql b/model/videos_summary.sql index 7619530..b662876 100644 --- a/model/videos_summary.sql +++ b/model/videos_summary.sql @@ -1,10 +1,136 @@ CREATE TABLE IF NOT EXISTS video_summary AS -SELECT v.file_name, - GROUP_CONCAT(DISTINCT l.name) AS labels, - GROUP_CONCAT(DISTINCT p.name) AS playlists +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; + +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; diff --git a/program2/program2.sh b/program2/program2.sh index 24d1314..d1cba52 100755 --- a/program2/program2.sh +++ b/program2/program2.sh @@ -1,14 +1,42 @@ +#!/bin/bash set -euo pipefail -SCRIPTS_DIR=$(dirname `realpath ${BASH_SOURCE[0]}`) -export DANCE_VIDEOS_DB="${HOME}/Documents/.DanceVideos/db.sqlite" -export DOSSIER_PLAYLIST="$(dirname $DANCE_VIDEOS_DB)/playlists" +SCRIPTS_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +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 -mkdir -p $PLAYLIST_ALL +# Créer un dossier "all" avec toutes les vidéos +# 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 - ln -s $v $PLAYLIST_ALL/$(basename $(dirname $v)).mp4 -done \ No newline at end of file +# Pour chaque playlist, créer un dossier et ajouter les liens symboliques +sqlite3 -separator '|' "$DANCE_VIDEOS_DB" " + 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