diff --git a/app/app.py b/app/app.py
index 9f42a4f..a05c3f1 100644
--- a/app/app.py
+++ b/app/app.py
@@ -1,155 +1,38 @@
-import os
-import base64
-import sqlite3
import streamlit as st
-import pandas as pd
-from glob import glob
+import os
+import db
+from views import show_video_thumbnail
+from controllers import label_widget
-# --- chemins ---
-ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
-MODEL_DIR = os.path.join(ROOT_DIR, "model")
-DB_PATH = os.path.expanduser("~/Documents/.DanceVideos/db.sqlite")
-VIDEO_DIR = os.path.expanduser("~/Documents/.DanceVideos/videos")
+st.set_page_config(page_title="Dance Video Manager", layout="wide")
+st.title("💃 Dance Video Manager")
-# --- initialisation de la base ---
-def init_db():
- if not os.path.exists(DB_PATH):
- st.warning("⚠️ Base de données non trouvée, création d'une nouvelle...")
- conn = sqlite3.connect(DB_PATH)
- sql_files = sorted(glob(os.path.join(MODEL_DIR, "*.sql")))
- for sql_file in sql_files:
- with open(sql_file, "r", encoding="utf-8") as f:
- sql = f.read()
- try:
- conn.executescript(sql)
- # st.info(f"✅ Exécuté : {os.path.basename(sql_file)}")
- except sqlite3.Error as e:
- st.error(f"Erreur dans {sql_file} : {e}")
- conn.commit()
- conn.close()
+# --- Barre latérale : hauteur max ---
+st.sidebar.header("⚙️ Apparence")
+max_height = st.sidebar.slider("Hauteur maximale (px)", 100, 800, 300, 50)
+st.markdown(
+ f"""
+
+ """,
+ unsafe_allow_html=True
+)
-# --- utilitaires base ---
-def get_conn():
- return sqlite3.connect(DB_PATH, check_same_thread=False)
+# --- Charger données ---
+videos = db.load_videos()
+labels = db.load_labels()
-# --- chargement des vidéos ---
-def load_videos():
- conn = get_conn()
- try:
- return pd.read_sql_query("SELECT * FROM videos", conn)
- except Exception as e:
- st.error(f"Erreur chargement vidéos : {e}")
- return pd.DataFrame()
- finally:
- conn.close()
-
-
-
-# --- interface principale ---
-def main():
- st.set_page_config(page_title="Dance Video Manager", layout="wide")
- st.title("💃 Dance Video Manager")
-
- # --- Barre latérale : réglage utilisateur ---
- st.sidebar.header("⚙️ Apparence")
- max_height = st.sidebar.slider("Hauteur maximale (px)", 100, 800, 300, 50)
- st.markdown(
- f"""
-
- """,
- unsafe_allow_html=True
- )
-
- # if st.button("🔄 Initialiser / synchroniser la base"):
- init_db()
-
- st.sidebar.header("Navigation")
- page = st.sidebar.radio("Choisir une vue :", ["Vidéos", "Playlists", "Labels"])
-
- videos = load_videos()
- if videos.empty:
- st.warning("Aucune vidéo trouvée dans la base.")
- return
-
- if page == "Vidéos":
- conn = get_conn()
- try:
- # Charger tous les labels existants
- existing_labels = pd.read_sql_query("SELECT name FROM labels ORDER BY name", conn)["name"].tolist()
-
- for _, row in videos.iterrows():
- col1, col2, col3 = st.columns([1, 2, 1])
-
- with col1:
- thumb = row["thumbnail_file"]
- if os.path.exists(thumb):
- st.image(thumb, width="stretch")
- st.caption(row["file_name"])
-
- with col2:
- mp4 = row["mp4_file"]
- video_name = row["file_name"]
- if os.path.exists(mp4):
- if st.button(f"▶️ 📅 {row.get('record_datetime', '')} — 🕒 {row.get('day_of_week', '')}", key=f"play_{video_name}"):
- st.video(mp4)
-
- with col3:
- # Sélecteur de label
- label_selected = st.selectbox(
- "Label",
- options=existing_labels + ["Autre…"],
- key=f"label_select_{video_name}"
- )
-
- # Création d’un label personnalisé
- if label_selected == "Autre…":
- label_selected = st.text_input(
- "Entrer un label personnalisé",
- value="",
- key=f"label_input_{video_name}"
- )
-
- # Sauvegarde
- if label_selected:
- if st.button("💾 Sauvegarder", key=f"save_label_{video_name}"):
- cursor = conn.cursor()
- # Crée le label s'il n'existe pas
- cursor.execute("INSERT OR IGNORE INTO labels (name) VALUES (?)", (label_selected,))
- # Récupère l'ID
- cursor.execute("SELECT id FROM labels WHERE name=?", (label_selected,))
- label_id = cursor.fetchone()[0]
- # Associe à la vidéo
- cursor.execute("""
- INSERT OR REPLACE INTO video_labels (video_file_name, label_id)
- VALUES (?, ?)
- """, (video_name, label_id))
- conn.commit()
- st.success(f"Label '{label_selected}' enregistré pour {video_name}")
-
- finally:
- conn.close()
-
- elif page == "Playlists":
- st.subheader("📜 Gestion des playlists")
- conn = get_conn()
- playlists = pd.read_sql_query("SELECT * FROM playlists", conn)
- st.dataframe(playlists)
- conn.close()
-
- elif page == "Labels":
- st.subheader("🏷️ Gestion des labels")
- conn = get_conn()
- labels = pd.read_sql_query("SELECT * FROM labels", conn)
- st.dataframe(labels)
- conn.close()
-
-
-if __name__ == "__main__":
- main()
+if videos.empty:
+ st.warning("Aucune vidéo trouvée dans la base.")
+else:
+ for _, video in videos.iterrows():
+ col3 = show_video_thumbnail(video)
+ preselected = db.load_video_labels(video["mp4_file_name"])
+ with col3:
+ label_widget(video["mp4_file_name"], preselected=preselected)
diff --git a/app/app.py.old b/app/app.py.old
new file mode 100644
index 0000000..9f42a4f
--- /dev/null
+++ b/app/app.py.old
@@ -0,0 +1,155 @@
+import os
+import base64
+import sqlite3
+import streamlit as st
+import pandas as pd
+from glob import glob
+
+# --- chemins ---
+ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+MODEL_DIR = os.path.join(ROOT_DIR, "model")
+DB_PATH = os.path.expanduser("~/Documents/.DanceVideos/db.sqlite")
+VIDEO_DIR = os.path.expanduser("~/Documents/.DanceVideos/videos")
+
+# --- initialisation de la base ---
+def init_db():
+ if not os.path.exists(DB_PATH):
+ st.warning("⚠️ Base de données non trouvée, création d'une nouvelle...")
+ conn = sqlite3.connect(DB_PATH)
+ sql_files = sorted(glob(os.path.join(MODEL_DIR, "*.sql")))
+ for sql_file in sql_files:
+ with open(sql_file, "r", encoding="utf-8") as f:
+ sql = f.read()
+ try:
+ conn.executescript(sql)
+ # st.info(f"✅ Exécuté : {os.path.basename(sql_file)}")
+ except sqlite3.Error as e:
+ st.error(f"Erreur dans {sql_file} : {e}")
+ conn.commit()
+ conn.close()
+
+# --- utilitaires base ---
+def get_conn():
+ return sqlite3.connect(DB_PATH, check_same_thread=False)
+
+# --- chargement des vidéos ---
+def load_videos():
+ conn = get_conn()
+ try:
+ return pd.read_sql_query("SELECT * FROM videos", conn)
+ except Exception as e:
+ st.error(f"Erreur chargement vidéos : {e}")
+ return pd.DataFrame()
+ finally:
+ conn.close()
+
+
+
+# --- interface principale ---
+def main():
+ st.set_page_config(page_title="Dance Video Manager", layout="wide")
+ st.title("💃 Dance Video Manager")
+
+ # --- Barre latérale : réglage utilisateur ---
+ st.sidebar.header("⚙️ Apparence")
+ max_height = st.sidebar.slider("Hauteur maximale (px)", 100, 800, 300, 50)
+ st.markdown(
+ f"""
+
+ """,
+ unsafe_allow_html=True
+ )
+
+ # if st.button("🔄 Initialiser / synchroniser la base"):
+ init_db()
+
+ st.sidebar.header("Navigation")
+ page = st.sidebar.radio("Choisir une vue :", ["Vidéos", "Playlists", "Labels"])
+
+ videos = load_videos()
+ if videos.empty:
+ st.warning("Aucune vidéo trouvée dans la base.")
+ return
+
+ if page == "Vidéos":
+ conn = get_conn()
+ try:
+ # Charger tous les labels existants
+ existing_labels = pd.read_sql_query("SELECT name FROM labels ORDER BY name", conn)["name"].tolist()
+
+ for _, row in videos.iterrows():
+ col1, col2, col3 = st.columns([1, 2, 1])
+
+ with col1:
+ thumb = row["thumbnail_file"]
+ if os.path.exists(thumb):
+ st.image(thumb, width="stretch")
+ st.caption(row["file_name"])
+
+ with col2:
+ mp4 = row["mp4_file"]
+ video_name = row["file_name"]
+ if os.path.exists(mp4):
+ if st.button(f"▶️ 📅 {row.get('record_datetime', '')} — 🕒 {row.get('day_of_week', '')}", key=f"play_{video_name}"):
+ st.video(mp4)
+
+ with col3:
+ # Sélecteur de label
+ label_selected = st.selectbox(
+ "Label",
+ options=existing_labels + ["Autre…"],
+ key=f"label_select_{video_name}"
+ )
+
+ # Création d’un label personnalisé
+ if label_selected == "Autre…":
+ label_selected = st.text_input(
+ "Entrer un label personnalisé",
+ value="",
+ key=f"label_input_{video_name}"
+ )
+
+ # Sauvegarde
+ if label_selected:
+ if st.button("💾 Sauvegarder", key=f"save_label_{video_name}"):
+ cursor = conn.cursor()
+ # Crée le label s'il n'existe pas
+ cursor.execute("INSERT OR IGNORE INTO labels (name) VALUES (?)", (label_selected,))
+ # Récupère l'ID
+ cursor.execute("SELECT id FROM labels WHERE name=?", (label_selected,))
+ label_id = cursor.fetchone()[0]
+ # Associe à la vidéo
+ cursor.execute("""
+ INSERT OR REPLACE INTO video_labels (video_file_name, label_id)
+ VALUES (?, ?)
+ """, (video_name, label_id))
+ conn.commit()
+ st.success(f"Label '{label_selected}' enregistré pour {video_name}")
+
+ finally:
+ conn.close()
+
+ elif page == "Playlists":
+ st.subheader("📜 Gestion des playlists")
+ conn = get_conn()
+ playlists = pd.read_sql_query("SELECT * FROM playlists", conn)
+ st.dataframe(playlists)
+ conn.close()
+
+ elif page == "Labels":
+ st.subheader("🏷️ Gestion des labels")
+ conn = get_conn()
+ labels = pd.read_sql_query("SELECT * FROM labels", conn)
+ st.dataframe(labels)
+ conn.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/app/controllers.py b/app/controllers.py
new file mode 100644
index 0000000..584f11f
--- /dev/null
+++ b/app/controllers.py
@@ -0,0 +1,45 @@
+import streamlit as st
+import db
+
+def label_widget(video_name, preselected=None):
+ """Widget multiselect labels avec création dynamique et réactivité."""
+ preselected = preselected or []
+
+ key_multiselect = f"labels_{video_name}"
+ key_input = f"new_label_{video_name}"
+
+ # --- Charger labels depuis DB ---
+ labels = db.load_labels()
+
+ # --- Initialisation session_state ---
+ if key_multiselect not in st.session_state:
+ st.session_state[key_multiselect] = preselected
+
+ # --- Si "Autre…" dans sélection, afficher input ---
+ current_selected = st.session_state[key_multiselect]
+ if "Autre…" in current_selected:
+ new_label = st.text_input("Entrer un label personnalisé", value="", key=key_input)
+ if new_label.strip():
+ # Ajouter le nouveau label à la DB
+ db.create_labels([new_label.strip()])
+ # Mettre à jour options avant de créer le multiselect
+ labels = db.load_labels()
+ # Remplacer "Autre…" par le nouveau label dans selection
+ current_selected = [l for l in current_selected if l != "Autre…"] + [new_label.strip()]
+ # MAINTENANT c'est sûr de passer current_selected comme default
+ st.session_state[key_multiselect] = current_selected
+
+ # --- Multiselect ---
+ selected = st.multiselect(
+ "Labels",
+ options=labels + ["Autre…"],
+ default=st.session_state[key_multiselect],
+ key=key_multiselect
+ )
+
+ # --- Sauvegarde ---
+ if st.button("💾 Sauvegarder labels", key=f"save_{video_name}"):
+ db.save_video_labels(video_name, selected)
+ st.success(f"{len(selected)} label(s) enregistré(s) pour {video_name}")
+
+ return selected
diff --git a/app/db.py b/app/db.py
new file mode 100644
index 0000000..5e00281
--- /dev/null
+++ b/app/db.py
@@ -0,0 +1,114 @@
+import sqlite3
+from pathlib import Path
+import pandas as pd
+
+DB_PATH = Path.home() / "Documents/.DanceVideos/db.sqlite"
+
+# --- Connexion SQLite avec timeout ---
+def get_conn():
+ """
+ Retourne une connexion SQLite avec timeout de 30s.
+ Utiliser un context manager pour garantir la fermeture.
+ """
+ return sqlite3.connect(DB_PATH, timeout=30, check_same_thread=False)
+
+# --- Vidéos ---
+def load_videos():
+ """
+ Retourne toutes les vidéos dans un DataFrame pandas.
+ """
+ with get_conn() as conn:
+ return pd.read_sql_query("SELECT * FROM videos ORDER BY record_datetime DESC", conn)
+
+# --- Labels ---
+def load_labels():
+ """
+ Retourne tous les labels existants dans une liste.
+ """
+ with get_conn() as conn:
+ df = pd.read_sql_query("SELECT name FROM labels ORDER BY name", conn)
+ return df["name"].tolist()
+
+def create_labels(label_names):
+ """
+ Crée tous les labels de la liste s'ils n'existent pas.
+ """
+ if not label_names:
+ return
+
+ with get_conn() as conn:
+ cursor = conn.cursor()
+ cursor.executemany(
+ "INSERT OR IGNORE INTO labels (name) VALUES (?)",
+ [(name,) for name in label_names]
+ )
+ conn.commit()
+
+def get_label_ids(label_names):
+ """
+ Retourne un dictionnaire {label_name: label_id}.
+ """
+ label_ids = {}
+ with get_conn() as conn:
+ cursor = conn.cursor()
+ for name in label_names:
+ cursor.execute("SELECT id FROM labels WHERE name=?", (name,))
+ row = cursor.fetchone()
+ if row:
+ label_ids[name] = row[0]
+ return label_ids
+
+# --- Vidéo / Labels ---
+def load_video_labels(video_file_name):
+ """
+ Retourne la liste des labels associés à une vidéo.
+ """
+ with get_conn() as conn:
+ cursor = conn.cursor()
+ query = """
+ SELECT l.name
+ FROM labels l
+ JOIN video_labels vl ON l.id = vl.label_id
+ WHERE vl.video_file_name = ?
+ """
+ return [row[0] for row in cursor.execute(query, (video_file_name,))]
+
+def save_video_labels(video_file_name, label_names):
+ """
+ Associe une vidéo à plusieurs labels.
+ Supprime d'abord les labels précédents pour cette vidéo.
+ """
+ if label_names is None:
+ label_names = []
+
+ # 1️⃣ Créer tous les labels
+ create_labels(label_names)
+
+ # 2️⃣ Récupérer les IDs
+ label_ids = get_label_ids(label_names)
+
+ # 3️⃣ Associer la vidéo aux labels dans une seule transaction
+ with get_conn() as conn:
+ cursor = conn.cursor()
+
+ # 🔹 Supprimer les anciennes associations
+ cursor.execute(
+ "DELETE FROM video_labels WHERE video_file_name = ?",
+ (video_file_name,)
+ )
+
+ # 🔹 Ajouter les nouvelles associations
+ for lid in label_ids.values():
+ cursor.execute("""
+ INSERT INTO video_labels (video_file_name, label_id)
+ VALUES (?, ?)
+ """, (video_file_name, lid))
+
+ conn.commit()
+ cursor = conn.cursor()
+ for lid in label_ids.values():
+ cursor.execute("""
+ INSERT OR REPLACE INTO video_labels (video_file_name, label_id)
+ VALUES (?, ?)
+ """, (video_file_name, lid))
+ conn.commit()
diff --git a/app/views.py b/app/views.py
new file mode 100644
index 0000000..85c15b0
--- /dev/null
+++ b/app/views.py
@@ -0,0 +1,20 @@
+import streamlit as st
+import os
+
+def show_video_thumbnail(video):
+ col1, col2, col3 = st.columns([1, 2, 1])
+ video_name = video["mp4_file_name"]
+ thumb = video["thumbnail_file"]
+ mp4 = video["mp4_file"]
+
+ with col1:
+ if os.path.exists(thumb):
+ st.image(thumb, width="content")
+ st.caption(video_name)
+
+ with col2:
+ if os.path.exists(mp4):
+ if st.button(f"▶️ Lire 📅 {video.get('record_datetime', '')} — 🕒 {video.get('day_of_week', '')}", key=f"play_{video_name}"):
+ st.video(mp4)
+
+ return col3