Compare commits

...

3 Commits

Author SHA1 Message Date
Gabriel Radureau
d2e2028610 program2 exports the playlists 2025-10-13 17:31:57 +02:00
Gabriel Radureau
65d63ec828 playlist edition 2025-10-13 16:53:15 +02:00
Gabriel Radureau
0fa5a30809 infinite scroll 2025-10-13 15:05:54 +02:00
24 changed files with 957 additions and 292 deletions

View File

@@ -1,38 +1,30 @@
# app.py
import streamlit as st import streamlit as st
import os from views.label_views import video_filter_sidebar, video_list_view
import db from playlists import playlist_page
from views import show_video_thumbnail
from controllers import label_widget
# ==========================
# 🧭 Configuration
# ==========================
st.set_page_config(page_title="Dance Video Manager", layout="wide") st.set_page_config(page_title="Dance Video Manager", layout="wide")
st.title("💃 Dance Video Manager") st.sidebar.title("💃 Menu principal")
# --- Barre latérale : hauteur max --- page = st.sidebar.radio(
st.sidebar.header("⚙️ Apparence") "Navigation",
max_height = st.sidebar.slider("Hauteur maximale (px)", 100, 800, 300, 50) ["Vidéos", "Playlists"],
st.markdown( key="nav_main"
f"""
<style>
img, video {{
max-height: {max_height}px !important;
object-fit: contain;
transition: all 0.3s ease-in-out;
border-radius: 8px;
}}
</style>
""",
unsafe_allow_html=True
) )
# --- Charger données --- # ==========================
videos = db.load_videos() # 🎬 PAGE : VIDÉOS
labels = db.load_labels() # ==========================
if page == "Vidéos":
st.title("🎬 Gestion et annotation des vidéos")
filters = video_filter_sidebar()
video_list_view(filters)
if videos.empty: # ==========================
st.warning("Aucune vidéo trouvée dans la base.") # 🎵 PAGE : PLAYLISTS
else: # ==========================
for _, video in videos.iterrows(): elif page == "Playlists":
col3 = show_video_thumbnail(video) playlist_page.main()
preselected = db.load_video_labels(video["mp4_file_name"])
with col3:
label_widget(video["mp4_file_name"], preselected=preselected)

View File

@@ -1,155 +0,0 @@
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"""
<style>
/* Limite globale de hauteur */
img, video {{
max-height: {max_height}px !important;
object-fit: contain;
transition: all 0.3s ease-in-out;
}}
</style>
""",
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 dun 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()

26
app/cache/video_summary.py vendored Normal file
View 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()

View File

View File

@@ -1,45 +1,38 @@
# controllers.py
import streamlit as st import streamlit as st
import db import db
def label_widget(video_name, preselected=None): def label_widget(video, preselected=None):
"""Widget multiselect labels avec création dynamique et réactivité.""" """Widget multiselect pour labels, avec création dynamique."""
preselected = preselected or [] preselected = preselected or []
key_multiselect = f"labels_{video.id}"
key_input = f"new_label_{video.id}"
key_multiselect = f"labels_{video_name}"
key_input = f"new_label_{video_name}"
# --- Charger labels depuis DB ---
labels = db.load_labels() labels = db.load_labels()
# --- Initialisation session_state ---
if key_multiselect not in st.session_state: if key_multiselect not in st.session_state:
st.session_state[key_multiselect] = preselected st.session_state[key_multiselect] = preselected
# --- Si "Autre…" dans sélection, afficher input ---
current_selected = st.session_state[key_multiselect] current_selected = st.session_state[key_multiselect]
# Si "Autre…" dans sélection, afficher input
if "Autre…" in current_selected: if "Autre…" in current_selected:
new_label = st.text_input("Entrer un label personnalisé", value="", key=key_input) new_label = st.text_input("Entrer un label personnalisé", value="", key=key_input)
if new_label.strip(): if new_label.strip():
# Ajouter le nouveau label à la DB
db.create_labels([new_label.strip()]) db.create_labels([new_label.strip()])
# Mettre à jour options avant de créer le multiselect
labels = db.load_labels() 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()] 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 st.session_state[key_multiselect] = current_selected
# --- Multiselect ---
selected = st.multiselect( selected = st.multiselect(
"Labels", "Labels",
options=labels + ["Autre…"], options=labels + ["Autre…"],
default=st.session_state[key_multiselect], default=st.session_state[key_multiselect],
key=key_multiselect key=key_multiselect,
) )
# --- Sauvegarde --- if st.button("💾 Sauvegarder labels", key=f"save_{video.id}"):
if st.button("💾 Sauvegarder labels", key=f"save_{video_name}"): video.save_labels(selected)
db.save_video_labels(video_name, selected) st.success(f"{len(selected)} label(s) enregistré(s) pour {video.mp4_file_name}")
st.success(f"{len(selected)} label(s) enregistré(s) pour {video_name}")
return selected return selected

210
app/db.py
View File

@@ -1,53 +1,37 @@
# db.py (modifié)
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
import pandas as pd import pandas as pd
DB_PATH = Path.home() / "Documents/.DanceVideos/db.sqlite" DB_PATH = Path.home() / "Documents/.DanceVideos/db.sqlite"
# --- Connexion SQLite avec timeout ---
def get_conn(): def get_conn():
""" conn = sqlite3.connect(DB_PATH, timeout=30, check_same_thread=False)
Retourne une connexion SQLite avec timeout de 30s. conn.row_factory = sqlite3.Row
Utiliser un context manager pour garantir la fermeture. conn.execute("PRAGMA foreign_keys = ON;")
""" return conn
return sqlite3.connect(DB_PATH, timeout=30, check_same_thread=False)
# --- Vidéos ---
def load_videos(): def load_videos():
"""
Retourne toutes les vidéos dans un DataFrame pandas.
"""
with get_conn() as conn: with get_conn() as conn:
return pd.read_sql_query("SELECT * FROM videos ORDER BY record_datetime DESC", conn) return pd.read_sql_query("SELECT * FROM videos ORDER BY record_datetime DESC", conn)
# --- Labels ---
def load_labels(): def load_labels():
"""
Retourne tous les labels existants dans une liste.
"""
with get_conn() as conn: with get_conn() as conn:
df = pd.read_sql_query("SELECT name FROM labels ORDER BY name", conn) df = pd.read_sql_query("SELECT name FROM labels ORDER BY name", conn)
return df["name"].tolist() return df["name"].tolist()
def create_labels(label_names): def create_labels(label_names):
"""
Crée tous les labels de la liste s'ils n'existent pas.
"""
if not label_names: if not label_names:
return return
with get_conn() as conn: with get_conn() as conn:
cursor = conn.cursor() conn.executemany(
cursor.executemany(
"INSERT OR IGNORE INTO labels (name) VALUES (?)", "INSERT OR IGNORE INTO labels (name) VALUES (?)",
[(name,) for name in label_names] [(name,) for name in label_names]
) )
conn.commit() conn.commit()
def get_label_ids(label_names): def get_label_ids(label_names):
"""
Retourne un dictionnaire {label_name: label_id}.
"""
label_ids = {} label_ids = {}
with get_conn() as conn: with get_conn() as conn:
cursor = conn.cursor() cursor = conn.cursor()
@@ -58,11 +42,7 @@ def get_label_ids(label_names):
label_ids[name] = row[0] label_ids[name] = row[0]
return label_ids return label_ids
# --- Vidéo / Labels --- def load_video_labels(file_name):
def load_video_labels(video_file_name):
"""
Retourne la liste des labels associés à une vidéo.
"""
with get_conn() as conn: with get_conn() as conn:
cursor = conn.cursor() cursor = conn.cursor()
query = """ query = """
@@ -71,44 +51,162 @@ def load_video_labels(video_file_name):
JOIN video_labels vl ON l.id = vl.label_id JOIN video_labels vl ON l.id = vl.label_id
WHERE vl.video_file_name = ? WHERE vl.video_file_name = ?
""" """
return [row[0] for row in cursor.execute(query, (video_file_name,))] return [row[0] for row in cursor.execute(query, (file_name,))]
def save_video_labels(video_file_name, label_names): def save_video_labels(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: if label_names is None:
label_names = [] label_names = []
# 1⃣ Créer tous les labels
create_labels(label_names) create_labels(label_names)
# 2⃣ Récupérer les IDs
label_ids = get_label_ids(label_names) label_ids = get_label_ids(label_names)
# 3⃣ Associer la vidéo aux labels dans une seule transaction
with get_conn() as conn: with get_conn() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM video_labels WHERE video_file_name = ?", (file_name,))
# 🔹 Supprimer les anciennes associations for lid in label_ids.values():
cursor.execute( cursor.execute(
"DELETE FROM video_labels WHERE video_file_name = ?", "INSERT OR REPLACE INTO video_labels (video_file_name, label_id) VALUES (?, ?)",
(video_file_name,) (file_name, lid),
) )
# 🔹 Ajouter les nouvelles associations
for lid in label_ids.values():
cursor.execute(""" cursor.execute("""
INSERT INTO video_labels (video_file_name, label_id) DELETE FROM labels
VALUES (?, ?) WHERE id NOT IN (SELECT DISTINCT label_id FROM video_labels)
""", (video_file_name, lid)) """)
conn.commit()
def update_video_difficulty(file_name, level):
with get_conn() as conn:
conn.execute("UPDATE videos SET difficulty_level = ? WHERE file_name = ?", (level, file_name))
conn.commit() conn.commit()
cursor = conn.cursor()
for lid in label_ids.values(): def get_unique_days():
cursor.execute(""" """Retourne la liste unique des jours de la semaine enregistrés dans la base."""
INSERT OR REPLACE INTO video_labels (video_file_name, label_id) with get_conn() as conn:
VALUES (?, ?) df = pd.read_sql_query("SELECT DISTINCT day_of_week FROM videos WHERE day_of_week IS NOT NULL ORDER BY day_of_week", conn)
""", (video_file_name, lid)) return [d for d in df["day_of_week"].dropna().tolist() if d.strip()]
def get_unique_difficulties():
"""Retourne la liste des niveaux de difficulté existants."""
with get_conn() as conn:
df = pd.read_sql_query("SELECT DISTINCT difficulty_level FROM videos WHERE difficulty_level IS NOT NULL ORDER BY difficulty_level", conn)
return [d for d in df["difficulty_level"].dropna().tolist() if d.strip()]
def get_unique_addresses():
"""Retourne les adresses connues (exclut 'unknown')."""
with get_conn() as conn:
df = pd.read_sql_query("SELECT DISTINCT address FROM videos WHERE address NOT LIKE '%unknown%' ORDER BY address", conn)
return [a for a in df["address"].dropna().tolist() if a.strip()]
def search_videos(
label_names=None,
day_of_week=None,
address_keyword=None,
start_date=None,
end_date=None,
difficulty=None,
label_logic="OR",
):
"""
Retourne une DataFrame filtrée selon les critères fournis.
label_logic: "OR" (au moins un label) ou "AND" (tous les labels).
"""
label_names = label_names or []
params = []
base_query = """
SELECT DISTINCT v.*
FROM videos v
WHERE 1=1
"""
# 🔖 Filtres par label
if label_names:
if label_logic == "AND":
# Toutes les étiquettes doivent être présentes
placeholders = ",".join("?" * len(label_names))
base_query += f"""
AND v.file_name IN (
SELECT vl.video_file_name
FROM video_labels vl
JOIN labels l ON l.id = vl.label_id
WHERE l.name IN ({placeholders})
GROUP BY vl.video_file_name
HAVING COUNT(DISTINCT l.name) = {len(label_names)}
)
"""
params.extend(label_names)
else:
# OR : au moins une étiquette correspond
placeholders = ",".join("?" * len(label_names))
base_query += f"""
AND v.file_name IN (
SELECT vl.video_file_name
FROM video_labels vl
JOIN labels l ON l.id = vl.label_id
WHERE l.name IN ({placeholders})
)
"""
params.extend(label_names)
# 📆 Jour de la semaine
if day_of_week:
base_query += " AND v.day_of_week = ?"
params.append(day_of_week)
# 🗺️ Mot-clé d'adresse (et exclusion unknown)
if address_keyword:
base_query += " AND v.address NOT LIKE '%unknown%' AND v.address LIKE ?"
params.append(f"%{address_keyword}%")
# 📅 Plage de dates
if start_date:
base_query += " AND v.record_datetime >= ?"
params.append(start_date)
if end_date:
base_query += " AND v.record_datetime <= ?"
params.append(end_date)
# 💪 Niveau de difficulté
if difficulty and difficulty != "Tous":
base_query += " AND v.difficulty_level = ?"
params.append(difficulty)
# 🔽 Tri
base_query += " ORDER BY v.record_datetime DESC"
with get_conn() as conn:
return pd.read_sql_query(base_query, conn, params=params)
def get_video_playlists(file_name):
with get_conn() as conn:
query = """
SELECT p.name
FROM playlists p
JOIN video_playlists vp ON vp.playlist_id = p.id
WHERE vp.video_file_name = ?
"""
return [row[0] for row in conn.execute(query, (file_name,))]
def get_videos_in_playlist(playlist_id):
with get_conn() as conn:
query = """
SELECT v.*
FROM videos v
JOIN video_playlists vp ON vp.video_file_name = v.file_name
WHERE vp.playlist_id = ?
ORDER BY vp.position
"""
return conn.execute(query, (playlist_id,)).fetchall()
def add_video_to_playlist(playlist_id, file_name):
with get_conn() as conn:
conn.execute("""
INSERT OR IGNORE INTO video_playlists (video_file_name, playlist_id, position)
VALUES (?, ?, COALESCE((SELECT MAX(position)+1 FROM video_playlists WHERE playlist_id=?), 0))
""", (file_name, playlist_id, playlist_id))
conn.commit()
def remove_video_from_playlist(playlist_id, file_name):
with get_conn() as conn:
conn.execute("DELETE FROM video_playlists WHERE playlist_id=? AND video_file_name=?", (playlist_id, file_name))
conn.commit() conn.commit()

55
app/models.py Normal file
View File

@@ -0,0 +1,55 @@
# models.py
from pydantic import BaseModel, Field, validator
from typing import Optional, List
import db
class Video(BaseModel):
file_name: str
raw_file: str = Field(..., description="Identifiant unique de la vidéo")
duration: Optional[float] = None
mp4_file: Optional[str] = None
mp4_file_name: Optional[str] = None
rotated_file: Optional[str] = None
thumbnail_file: Optional[str] = None
record_datetime: Optional[str] = None
day_of_week: Optional[str] = None
lat: Optional[float] = None
long: Optional[float] = None
address: Optional[str] = None
difficulty_level: Optional[str] = Field("Tout niveau", description="Niveau de difficulté")
# --- propriétés pratiques ---
@property
def id(self) -> str:
"""Identifiant unique (basé sur file_name)."""
return self.file_name
@property
def title(self) -> str:
"""Nom court à afficher."""
return self.mp4_file_name or self.file_name
@property
def difficulty_display(self) -> str:
return self.difficulty_level or "Tout niveau"
# --- Méthodes métiers ---
def load_labels(self) -> List[str]:
return db.load_video_labels(self.file_name)
def save_labels(self, label_names: List[str]):
db.save_video_labels(self.file_name, label_names)
# --- Validation optionnelle ---
@validator("raw_file")
def validate_raw_file(cls, v):
if not v or not isinstance(v, str):
raise ValueError("raw_file doit être une chaîne non vide")
return v
@validator("duration")
def validate_duration(cls, v):
if v is not None and v < 0:
raise ValueError("duration ne peut pas être négative")
return v

View File

View File

@@ -0,0 +1,68 @@
# playlists/playlist_controller.py
import streamlit as st
import db
from playlists.playlist_model import Playlist, RuleSet
from playlists import playlist_db
from views.label_views import video_filter_sidebar, video_list_view
from models import Video
def playlist_manual_editor(playlist: Playlist):
"""Permet de gérer les vidéos d'une playlist manuelle."""
st.subheader(f"🎞️ Édition manuelle : {playlist.name}")
filters = video_filter_sidebar()
videos = video_list_view(filters, editable_labels=False, editable_difficulty=False)
existing_videos = db.get_videos_in_playlist(playlist.id)
existing_names = [v["file_name"] for v in existing_videos]
for video in videos:
in_playlist = video.file_name in existing_names
button_label = " Ajouter" if not in_playlist else "❌ Retirer"
if st.button(button_label, key=f"toggle_{playlist.id}_{video.file_name}"):
if in_playlist:
db.remove_video_from_playlist(playlist.id, video.file_name)
st.success(f"Supprimée de {playlist.name}")
else:
db.add_video_to_playlist(playlist.id, video.file_name)
st.success(f"Ajoutée à {playlist.name}")
st.rerun()
def playlist_dynamic_editor(playlist: Playlist):
"""Édite les règles d'une playlist dynamique et affiche le rendu en temps réel."""
st.subheader(f"⚙️ Playlist dynamique : {playlist.name}")
rules = playlist.rules or RuleSet()
labels = db.load_labels()
playlists = [p.name for p in playlist_db.load_all_playlists()]
rules.include_labels = st.multiselect("Inclure labels", labels, default=rules.include_labels)
rules.exclude_labels = st.multiselect("Exclure labels", labels, default=rules.exclude_labels)
rules.include_playlists = st.multiselect("Inclure playlists", playlists, default=rules.include_playlists)
rules.exclude_playlists = st.multiselect("Exclure playlists", playlists, default=rules.exclude_playlists)
col1, col2 = st.columns(2)
with col1:
rules.date_after = st.date_input("📅 Après le", value=rules.date_after or None)
with col2:
rules.date_before = st.date_input("📅 Avant le", value=rules.date_before or None)
rules.logic = st.radio("Logique de combinaison", ["AND", "OR"], index=0 if rules.logic == "AND" else 1)
if st.button("💾 Enregistrer les règles"):
playlist.rules = rules
playlist.save()
st.success("Règles mises à jour ✅")
st.rerun()
st.markdown("---")
st.subheader("🧩 Rendu de la playlist")
rows = playlist_db.get_videos_for_playlist(playlist)
if not rows:
st.info("Aucune vidéo ne correspond aux règles actuelles.")
return
videos = [Video(**row) for row in rows]
st.write(f"{len(videos)} vidéo(s) trouvée(s).")
from views.video_views import show_video_row
for v in videos[:30]:
show_video_row(v, db.load_video_labels(v.file_name), editable_labels=False, editable_difficulty=False)

View File

@@ -0,0 +1,96 @@
# playlists/playlist_db.py
import db
import json
from playlists.playlist_model import Playlist, RuleSet
def load_all_playlists():
"""Retourne une liste de Playlist (Pydantic) — tolérant aux rules_json nuls / vides."""
with db.get_conn() as conn:
# s'assurer que les lignes sont accessibles par nom (si get_conn ne l'a pas fait)
try:
conn.row_factory = conn.row_factory # no-op si déjà réglé
except Exception:
pass
rows = conn.execute("SELECT * FROM playlists ORDER BY created_at DESC").fetchall()
playlists = []
for row in rows:
# si row est sqlite3.Row, on peut accéder par nom, sinon c'est un tuple et on mappe par index
if hasattr(row, "__getitem__") and isinstance(row, dict) is False and getattr(row, "keys", None):
# sqlite3.Row behaves like mapping
row_dict = {k: row[k] for k in row.keys()}
elif isinstance(row, dict):
row_dict = row
else:
# fallback: convert tuple -> dict using cursor.description
# but here we assume conn.row_factory set in db.get_conn; keep robust fallback
cols = [d[0] for d in conn.execute("PRAGMA table_info(playlists)").fetchall()]
row_dict = dict(zip(cols, row))
try:
pl = Playlist(
id=row_dict.get("id"),
name=row_dict.get("name") or "",
description=row_dict.get("description") or "",
type=row_dict.get("type") or "manual",
rules=row_dict.get("rules_json")
)
playlists.append(pl)
except Exception as e:
# Ne bloque pas toute la lecture : logue et passe à la suivante
print(f"⚠️ Ignored invalid playlist row (id={row_dict.get('id')}, name={row_dict.get('name')}): {e}")
return playlists
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):
"""Retourne les vidéos selon le type"""
with db.get_conn() as conn:
if playlist.type == "manual":
q = """
SELECT v.*
FROM videos v
JOIN video_playlists vp ON vp.video_file_name = v.file_name
WHERE vp.playlist_id = ?
ORDER BY vp.position
"""
return conn.execute(q, (playlist.id,)).fetchall()
else:
rules = playlist.rules
clauses = []
params = []
if rules.include_labels:
placeholders = ",".join("?" * len(rules.include_labels))
clauses.append(f"v.file_name IN (SELECT vl.video_file_name FROM video_labels vl JOIN labels l ON l.id=vl.label_id WHERE l.name IN ({placeholders}))")
params += rules.include_labels
if rules.exclude_labels:
placeholders = ",".join("?" * len(rules.exclude_labels))
clauses.append(f"v.file_name NOT IN (SELECT vl.video_file_name FROM video_labels vl JOIN labels l ON l.id=vl.label_id WHERE l.name IN ({placeholders}))")
params += rules.exclude_labels
if rules.include_playlists:
placeholders = ",".join("?" * len(rules.include_playlists))
clauses.append(f"v.file_name IN (SELECT vp.video_file_name FROM video_playlists vp JOIN playlists p ON vp.playlist_id=p.id WHERE p.name IN ({placeholders}))")
params += rules.include_playlists
if rules.exclude_playlists:
placeholders = ",".join("?" * len(rules.exclude_playlists))
clauses.append(f"v.file_name NOT IN (SELECT vp.video_file_name FROM video_playlists vp JOIN playlists p ON vp.playlist_id=p.id WHERE p.name IN ({placeholders}))")
params += rules.exclude_playlists
if rules.date_after:
clauses.append("v.record_datetime >= ?")
params.append(rules.date_after)
if rules.date_before:
clauses.append("v.record_datetime <= ?")
params.append(rules.date_before)
where_clause = f" {' AND ' if rules.logic == 'AND' else ' OR '} ".join(clauses) if clauses else "1=1"
q = f"SELECT v.* FROM videos v WHERE {where_clause} ORDER BY v.record_datetime DESC"
return conn.execute(q, params).fetchall()

View File

@@ -0,0 +1,65 @@
# playlists/playlist_model.py
from pydantic import BaseModel, Field, validator
from typing import List, Optional, Literal
import json
import db
class RuleSet(BaseModel):
include_labels: List[str] = []
exclude_labels: List[str] = []
include_playlists: List[str] = []
exclude_playlists: List[str] = []
date_after: Optional[str] = None
date_before: Optional[str] = None
logic: Literal["AND", "OR"] = "AND"
def to_json(self) -> str:
return json.dumps(self.dict(), ensure_ascii=False, indent=2)
@classmethod
def from_json(cls, raw):
"""Accepte None, str vide ou dict JSON."""
if not raw:
return cls()
if isinstance(raw, dict):
return cls(**raw)
try:
return cls(**json.loads(raw))
except Exception:
return cls()
class Playlist(BaseModel):
id: Optional[int] = None
name: str
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):
"""Transforme la valeur de rules_json (None, str ou dict) en RuleSet."""
if isinstance(v, RuleSet):
return v
return RuleSet.from_json(v)
def save(self):
"""Insert or update playlist"""
with db.get_conn() as conn:
cur = conn.cursor()
if self.id:
cur.execute("""
UPDATE playlists
SET name=?, description=?, type=?, rules_json=?, updated_at=CURRENT_TIMESTAMP
WHERE id=?
""", (self.name, self.description, self.type, self.rules.to_json(), self.id))
else:
cur.execute("""
INSERT INTO playlists (name, description, type, rules_json)
VALUES (?, ?, ?, ?)
""", (self.name, self.description, self.type, self.rules.to_json()))
self.id = cur.lastrowid
conn.commit()

View File

@@ -0,0 +1,93 @@
import streamlit as st
from playlists import playlist_db
from playlists.playlist_model import Playlist, RuleSet
from playlists.playlist_controller import playlist_manual_editor, playlist_dynamic_editor
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()
# 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"])
if st.button("Créer"):
if not name.strip():
st.error("Le nom ne peut pas être vide.")
else:
pl = Playlist(name=name.strip(), description=desc, type=type_choice, rules=RuleSet())
pl.save()
st.session_state["playlist_select"] = pl.name # ✅ sélectionne automatiquement
if hasattr(st, "rerun"):
st.rerun()
else:
st.experimental_rerun()
return
# --- 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 dactions ---
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:
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)

View File

@@ -0,0 +1,15 @@
# playlists/playlist_views.py
import streamlit as st
from playlists import playlist_db
from models import Video
from views import show_video_thumbnail
def preview_playlist(playlist):
st.subheader(f"🎬 Aperçu de la playlist : {playlist.name}")
rows = playlist_db.get_videos_for_playlist(playlist)
videos = [Video(**row) for row in rows]
if not videos:
st.info("Aucune vidéo correspondante.")
return
for video in videos[:30]: # limiter pour performance
show_video_thumbnail(video)

View File

@@ -1,4 +1,5 @@
altair==5.5.0 altair==5.5.0
annotated-types==0.7.0
attrs==25.4.0 attrs==25.4.0
blinker==1.9.0 blinker==1.9.0
cachetools==6.2.0 cachetools==6.2.0
@@ -21,6 +22,8 @@ pillow==11.3.0
pluggy==1.6.0 pluggy==1.6.0
protobuf==6.32.1 protobuf==6.32.1
pyarrow==21.0.0 pyarrow==21.0.0
pydantic==2.12.0
pydantic-core==2.41.1
pydeck==0.9.1 pydeck==0.9.1
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pytz==2025.2 pytz==2025.2
@@ -37,5 +40,6 @@ tenacity==9.1.2
toml==0.10.2 toml==0.10.2
tornado==6.5.2 tornado==6.5.2
typing-extensions==4.15.0 typing-extensions==4.15.0
typing-inspection==0.4.2
tzdata==2025.2 tzdata==2025.2
urllib3==2.5.0 urllib3==2.5.0

View File

@@ -1,20 +1,18 @@
# views.py
import streamlit as st import streamlit as st
import os import os
def show_video_thumbnail(video): def show_video_thumbnail(video):
col1, col2, col3 = st.columns([1, 2, 1]) col1, col2, col3 = st.columns([1, 2, 1])
video_name = video["mp4_file_name"]
thumb = video["thumbnail_file"]
mp4 = video["mp4_file"]
with col1: with col1:
if os.path.exists(thumb): if video.thumbnail_file and os.path.exists(video.thumbnail_file):
st.image(thumb, width="content") st.image(video.thumbnail_file, width="content")
st.caption(video_name) st.caption(video.mp4_file_name)
with col2: with col2:
if os.path.exists(mp4): if video.mp4_file and os.path.exists(video.mp4_file):
if st.button(f"▶️ Lire 📅 {video.get('record_datetime', '')} — 🕒 {video.get('day_of_week', '')}", key=f"play_{video_name}"): if st.button(f"▶️ Lire 📅 {video.record_datetime or ''} — 🕒 {video.day_of_week or ''} - 📍 {video.address or ''}", key=f"play_{video.id}"):
st.video(mp4) st.video(video.mp4_file)
return col3 return col3

0
app/views/__init__.py Normal file
View File

107
app/views/label_views.py Normal file
View File

@@ -0,0 +1,107 @@
import streamlit as st
import db
from models import Video
from views.video_views import show_video_row
from controllers.label_controller import label_widget
def video_filter_sidebar():
"""Affiche les filtres dans la barre latérale et renvoie les paramètres de recherche."""
st.sidebar.header("⚙️ Filtres et affichage")
max_height = st.sidebar.slider("Hauteur max (px)", 100, 800, 300, 50)
all_labels = db.load_labels()
unique_days = db.get_unique_days()
unique_difficulties = db.get_unique_difficulties()
unique_addresses = db.get_unique_addresses()
selected_labels = st.sidebar.multiselect("Filtrer par labels", all_labels)
label_logic = st.sidebar.radio(
"Logique entre labels",
["OR", "AND"],
help="Détermine si la vidéo doit contenir tous les labels sélectionnés (AND) ou au moins un (OR)"
)
day_filter = st.sidebar.selectbox("Jour de la semaine", ["Tous"] + unique_days)
difficulty_filter = st.sidebar.selectbox("Niveau de difficulté", ["Tous"] + unique_difficulties)
address_keyword = st.sidebar.selectbox("Adresse (mot-clé)", [""] + unique_addresses)
start_date = st.sidebar.date_input("Date de début", value=None)
end_date = st.sidebar.date_input("Date de fin", value=None)
show_unlabeled_only = st.sidebar.checkbox("Afficher uniquement les vidéos sans labels")
return dict(
max_height=max_height,
selected_labels=selected_labels,
label_logic=label_logic,
day_filter=None if day_filter == "Tous" else day_filter,
difficulty_filter=difficulty_filter,
address_keyword=address_keyword if address_keyword else None,
start_date=start_date.isoformat() if start_date else None,
end_date=end_date.isoformat() if end_date else None,
show_unlabeled_only=show_unlabeled_only
)
def video_list_view(filters: dict, editable_labels=True, editable_difficulty=True):
"""Affiche les vidéos selon les filtres fournis."""
st.markdown(f"""
<style>
img, video {{
max-height: {filters["max_height"]}px !important;
object-fit: contain;
border-radius: 8px;
transition: all 0.3s ease-in-out;
}}
.unlabeled {{
border: 3px solid #f39c12;
box-shadow: 0 0 10px #f39c12;
border-radius: 10px;
padding: 5px;
margin-bottom: 10px;
}}
</style>
""", unsafe_allow_html=True)
df_videos = db.search_videos(
label_names=filters["selected_labels"],
label_logic=filters["label_logic"],
day_of_week=filters["day_filter"],
address_keyword=filters["address_keyword"],
start_date=filters["start_date"],
end_date=filters["end_date"],
difficulty=filters["difficulty_filter"]
)
if df_videos.empty:
st.warning("Aucune vidéo trouvée avec ces critères.")
return []
# préchargement des labels
video_labels_map = {row["file_name"]: db.load_video_labels(row["file_name"]) for _, row in df_videos.iterrows()}
if filters["show_unlabeled_only"]:
df_videos = df_videos[df_videos["file_name"].apply(lambda fn: not video_labels_map.get(fn))]
videos = [Video(**row) for _, row in df_videos.iterrows()]
# lazy loading
page_size = 20
st.session_state.setdefault("video_page", 1)
start = 0
end = st.session_state.video_page * page_size
subset = videos[start:end]
# affichage
for video in subset:
preselected = video_labels_map.get(video.file_name, [])
css_class = "unlabeled" if not preselected else ""
with st.container():
st.markdown(f"<div class='{css_class}'>", unsafe_allow_html=True)
show_video_row(video, preselected, editable_labels, editable_difficulty)
st.markdown("</div>", unsafe_allow_html=True)
# lazy loading bouton
if end < len(videos):
if st.button("📦 Charger plus de vidéos"):
st.session_state.video_page += 1
st.rerun()
else:
st.info("✅ Toutes les vidéos sont affichées.")
return subset

32
app/views/video_views.py Normal file
View File

@@ -0,0 +1,32 @@
import streamlit as st
from controllers.label_controller import label_widget
import db
def show_video_row(video, preselected_labels, editable_labels=True, editable_difficulty=True):
col1, col2, col3 = st.columns([1, 3, 2])
with col1:
if video.thumbnail_file:
st.image(video.thumbnail_file)
st.caption(video.mp4_file_name)
with col2:
st.markdown(f"**📅 {video.record_datetime or ''}** — {video.day_of_week or ''}")
st.text(f"🏷️ Labels: {', '.join(preselected_labels) or 'Aucun'}")
playlists = db.get_video_playlists(video.file_name)
if playlists:
st.text(f"🎵 Playlists: {', '.join(playlists)}")
with col3:
if editable_labels:
label_widget(video, preselected=preselected_labels)
if editable_difficulty:
levels = ["Tout niveau", "Débutant", "Intermédiaire", "Avancé", "Star"]
new_level = st.selectbox(
"🎚 Niveau",
levels,
index=levels.index(video.difficulty_display),
key=f"diff_{video.file_name}"
)
if new_level != video.difficulty_display:
db.update_video_difficulty(video.file_name, new_level)
st.success(f"Niveau mis à jour pour {video.file_name}")

View File

@@ -15,6 +15,7 @@ register_video() {
local lat=${9:-0.000000} local lat=${9:-0.000000}
local long=${10:-0.000000} local long=${10:-0.000000}
local address=${11:-Unknown} local address=${11:-Unknown}
address=$(sed "s|'| |g" <<< $address)
if [ -z "$raw_file" ] || [ -z "$mp4_file" ]; then if [ -z "$raw_file" ] || [ -z "$mp4_file" ]; then
echo "Error: raw_file and mp4_file are required" echo "Error: raw_file and mp4_file are required"
exit 1 exit 1

View File

@@ -11,5 +11,7 @@ CREATE TABLE IF NOT EXISTS videos (
record_time TIME GENERATED ALWAYS AS (TIME(record_datetime)) VIRTUAL, record_time TIME GENERATED ALWAYS AS (TIME(record_datetime)) VIRTUAL,
lat DECIMAL(10,6), lat DECIMAL(10,6),
long DECIMAL(11,7), long DECIMAL(11,7),
address VARCHAR(255) address VARCHAR(255),
difficulty_level VARCHAR(255) DEFAULT 'Tout niveau'
); );

View File

@@ -1,17 +1,30 @@
-- Table des playlists -- =========================================================
-- Table principale des playlists
-- =========================================================
CREATE TABLE IF NOT EXISTS playlists ( CREATE TABLE IF NOT EXISTS playlists (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) UNIQUE NOT NULL, name TEXT UNIQUE NOT NULL,
description TEXT, description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP type TEXT CHECK (type IN ('manual', 'dynamic')) NOT NULL DEFAULT 'manual',
rules_json TEXT, -- JSON décrivant les règles pour les playlists dynamiques
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
-- Table d'association entre vidéos et playlists (relation many-to-many) -- =========================================================
-- Table dassociation vidéos ↔ playlists (manuelles uniquement)
-- =========================================================
CREATE TABLE IF NOT EXISTS video_playlists ( CREATE TABLE IF NOT EXISTS video_playlists (
video_file_name VARCHAR(255), video_file_name TEXT NOT NULL,
playlist_id INTEGER, playlist_id INTEGER NOT NULL,
position INTEGER, -- pour gérer lordre dans la playlist position INTEGER DEFAULT 0, -- ordre dans la playlist
PRIMARY KEY (video_file_name, playlist_id), PRIMARY KEY (video_file_name, playlist_id),
FOREIGN KEY (video_file_name) REFERENCES videos(file_name) ON DELETE CASCADE, FOREIGN KEY (video_file_name) REFERENCES videos(file_name) ON DELETE CASCADE,
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE
); );
-- =========================================================
-- Index pour accélérer les recherches
-- =========================================================
CREATE INDEX IF NOT EXISTS idx_playlist_type ON playlists(type);
CREATE INDEX IF NOT EXISTS idx_video_playlists_playlist ON video_playlists(playlist_id);

136
model/videos_summary.sql Normal file
View File

@@ -0,0 +1,136 @@
CREATE TABLE IF NOT EXISTS 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;
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;

View File

@@ -71,11 +71,9 @@ mp4_dir() {
export -f mp4_dir export -f mp4_dir
write_thumbnail() { write_thumbnail() {
set -x
local raw="$1" local raw="$1"
local thumbnail="$2" local thumbnail="$2"
ffmpeg -ss 00:00:03 -i $raw -vframes 1 $thumbnail 2>/dev/null ffmpeg -ss 00:00:03 -i $raw -vframes 1 $thumbnail 2>/dev/null
set +x
} }
export -f write_thumbnail export -f write_thumbnail

View File

@@ -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" "
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 done