correct application of playlists rules
This commit is contained in:
@@ -3,6 +3,13 @@ import streamlit as st
|
||||
from views.label_views import video_filter_sidebar, video_list_view
|
||||
from playlists import playlist_page
|
||||
|
||||
import argparse
|
||||
|
||||
# --- Parse arguments (avant tout Streamlit UI) ---
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument("--unlabeled", action="store_true", help="Afficher uniquement les vidéos sans labels")
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
# ==========================
|
||||
# 🧭 Configuration
|
||||
# ==========================
|
||||
@@ -20,7 +27,7 @@ page = st.sidebar.radio(
|
||||
# ==========================
|
||||
if page == "Vidéos":
|
||||
st.title("🎬 Gestion et annotation des vidéos")
|
||||
filters = video_filter_sidebar()
|
||||
filters = video_filter_sidebar(unlabeled=args.unlabeled)
|
||||
video_list_view(filters)
|
||||
|
||||
# ==========================
|
||||
|
||||
23
app/cache/video_summary.py
vendored
23
app/cache/video_summary.py
vendored
@@ -1,26 +1,9 @@
|
||||
# cache/video_summary.py
|
||||
import db
|
||||
from playlists import playlist_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.execute("DELETE FROM video_summary_materialized;")
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS video_summary_materialized AS SELECT * FROM video_summary;")
|
||||
conn.commit()
|
||||
|
||||
86
app/db.py
86
app/db.py
@@ -104,12 +104,19 @@ def search_videos(
|
||||
end_date=None,
|
||||
difficulty=None,
|
||||
label_logic="OR",
|
||||
include_playlists=None,
|
||||
exclude_playlists=None,
|
||||
logic="OR", # logique entre playlists incluses
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Retourne une DataFrame filtrée selon les critères fournis.
|
||||
label_logic: "OR" (au moins un label) ou "AND" (tous les labels).
|
||||
label_logic: "OR" (au moins un label) ou "AND" (tous les labels)
|
||||
logic: "OR" ou "AND" pour la combinaison de playlists incluses
|
||||
"""
|
||||
label_names = label_names or []
|
||||
include_playlists = include_playlists or []
|
||||
exclude_playlists = exclude_playlists or []
|
||||
params = []
|
||||
|
||||
base_query = """
|
||||
@@ -121,7 +128,6 @@ def search_videos(
|
||||
# 🔖 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 (
|
||||
@@ -135,7 +141,6 @@ def search_videos(
|
||||
"""
|
||||
params.extend(label_names)
|
||||
else:
|
||||
# OR : au moins une étiquette correspond
|
||||
placeholders = ",".join("?" * len(label_names))
|
||||
base_query += f"""
|
||||
AND v.file_name IN (
|
||||
@@ -147,6 +152,47 @@ def search_videos(
|
||||
"""
|
||||
params.extend(label_names)
|
||||
|
||||
# 🎵 Filtres par playlists incluses
|
||||
if include_playlists:
|
||||
placeholders = ",".join("?" * len(include_playlists))
|
||||
if logic == "AND":
|
||||
# Vidéos présentes dans TOUTES les playlists
|
||||
base_query += f"""
|
||||
AND v.file_name IN (
|
||||
SELECT vp.video_file_name
|
||||
FROM video_playlists vp
|
||||
JOIN playlists p ON p.id = vp.playlist_id
|
||||
WHERE p.id IN ({placeholders})
|
||||
GROUP BY vp.video_file_name
|
||||
HAVING COUNT(DISTINCT p.id) = {len(include_playlists)}
|
||||
)
|
||||
"""
|
||||
params.extend(include_playlists)
|
||||
else:
|
||||
# Vidéos présentes dans AU MOINS une playlist
|
||||
base_query += f"""
|
||||
AND v.file_name IN (
|
||||
SELECT vp.video_file_name
|
||||
FROM video_playlists vp
|
||||
JOIN playlists p ON p.id = vp.playlist_id
|
||||
WHERE p.id IN ({placeholders})
|
||||
)
|
||||
"""
|
||||
params.extend(include_playlists)
|
||||
|
||||
# ❌ Filtres par playlists exclues
|
||||
if exclude_playlists:
|
||||
placeholders = ",".join("?" * len(exclude_playlists))
|
||||
base_query += f"""
|
||||
AND v.file_name NOT IN (
|
||||
SELECT vp.video_file_name
|
||||
FROM video_playlists vp
|
||||
JOIN playlists p ON p.id = vp.playlist_id
|
||||
WHERE p.id IN ({placeholders})
|
||||
)
|
||||
"""
|
||||
params.extend(exclude_playlists)
|
||||
|
||||
# 📆 Jour de la semaine
|
||||
if day_of_week:
|
||||
base_query += " AND v.day_of_week = ?"
|
||||
@@ -177,6 +223,7 @@ def search_videos(
|
||||
return pd.read_sql_query(base_query, conn, params=params)
|
||||
|
||||
|
||||
|
||||
def get_video_playlists(file_name):
|
||||
with get_conn() as conn:
|
||||
query = """
|
||||
@@ -187,18 +234,41 @@ def get_video_playlists(file_name):
|
||||
"""
|
||||
return [row[0] for row in conn.execute(query, (file_name,))]
|
||||
|
||||
def get_videos_in_playlist(playlist_id):
|
||||
def get_video_file_names_in_playlist(playlist_id):
|
||||
"""Retourne un set de file_name pour une playlist donnée."""
|
||||
with get_conn() as conn:
|
||||
query = """
|
||||
rows = conn.execute(
|
||||
# "SELECT video_file_name FROM video_playlists WHERE playlist_id = ?",
|
||||
"SELECT video_file_name FROM playlist_videos WHERE playlist_id = ?",
|
||||
(playlist_id,)
|
||||
).fetchall()
|
||||
names = []
|
||||
for r in rows:
|
||||
if hasattr(r, "keys"):
|
||||
names.append(r["video_file_name"])
|
||||
else:
|
||||
names.append(r[0])
|
||||
return set(names)
|
||||
|
||||
|
||||
def get_videos_in_playlist(playlist_id):
|
||||
"""Retourne une liste d'objets Video complets pour une playlist donnée."""
|
||||
import pandas as pd
|
||||
from models import Video
|
||||
|
||||
with get_conn() as conn:
|
||||
df = pd.read_sql_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()
|
||||
ORDER BY vp.position ASC
|
||||
""", conn, params=(playlist_id,))
|
||||
return [Video(**row) for _, row in df.iterrows()]
|
||||
|
||||
|
||||
def add_video_to_playlist(playlist_id, file_name):
|
||||
print(dict(playlist_id=playlist_id, video_file_name=file_name),flush=True,)
|
||||
with get_conn() as conn:
|
||||
conn.execute("""
|
||||
INSERT OR IGNORE INTO video_playlists (video_file_name, playlist_id, position)
|
||||
|
||||
@@ -4,65 +4,135 @@ 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 views.video_views import show_video_row
|
||||
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}")
|
||||
|
||||
# Filtres standard pour explorer les vidéos
|
||||
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]
|
||||
# Précharge les vidéos déjà incluses dans la playlist
|
||||
playlist_video_ids = db.get_video_file_names_in_playlist(playlist.id)
|
||||
|
||||
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}")
|
||||
# Récupère les vidéos filtrées
|
||||
df_videos = db.search_videos(
|
||||
label_names=filters["selected_labels"],
|
||||
day_of_week=filters["day_filter"],
|
||||
difficulty=filters["difficulty_filter"],
|
||||
**filters
|
||||
)
|
||||
if df_videos.empty:
|
||||
st.info("Aucune vidéo trouvée avec ces filtres.")
|
||||
return
|
||||
|
||||
videos = [Video(**row) for _, row in df_videos.iterrows()]
|
||||
|
||||
st.write(f"🎬 {len(videos)} vidéo(s) disponibles.")
|
||||
|
||||
# Affiche les vidéos avec boutons d’ajout/retrait à droite
|
||||
summary_map = summary_map = get_video_summary_cached()
|
||||
for video in videos[:50]: # limite de sécurité
|
||||
summary = summary_map.get(video.file_name, {"labels": [], "playlists": []})
|
||||
preselected = summary["labels"]
|
||||
playlists = summary["playlists"]
|
||||
show_video_row(
|
||||
video,
|
||||
preselected_labels=preselected,
|
||||
editable_labels=False,
|
||||
editable_difficulty=False,
|
||||
playlist=playlist,
|
||||
playlist_video_ids=playlist_video_ids,
|
||||
video_playlists=playlists
|
||||
)
|
||||
if st.button("📦 Charger plus"):
|
||||
st.session_state.video_page += 1
|
||||
st.rerun()
|
||||
|
||||
@st.cache_data(ttl=30)
|
||||
def get_video_summary_cached():
|
||||
return playlist_db.load_video_summary_map()
|
||||
|
||||
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)
|
||||
|
||||
all_playlists = playlist_db.load_all_playlists()
|
||||
name_to_id = {p.name: p.id for p in all_playlists}
|
||||
id_to_name = {p.id: p.name for p in all_playlists}
|
||||
|
||||
default_include = [id_to_name.get(pid) for pid in rules.include_playlists if pid in id_to_name]
|
||||
default_exclude = [id_to_name.get(pid) for pid in rules.exclude_playlists if pid in id_to_name]
|
||||
|
||||
selected_includes = st.multiselect("Inclure playlists", id_to_name.values(), default=default_include)
|
||||
selected_excludes = st.multiselect("Exclure playlists", id_to_name.values(), default=default_exclude)
|
||||
|
||||
rules.include_playlists = [name_to_id[name] for name in selected_includes]
|
||||
rules.exclude_playlists = [name_to_id[name] for name in selected_excludes]
|
||||
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
use_delta = st.checkbox("⏱️ Utiliser un delta de jours", value=bool(rules.date_delta_days))
|
||||
if use_delta:
|
||||
rules.date_delta_days = st.number_input(
|
||||
"Nombre de jours depuis aujourd’hui (négatif pour passé)",
|
||||
value=rules.date_delta_days or -15
|
||||
)
|
||||
rules.date_after = None
|
||||
rules.date_before = None
|
||||
else:
|
||||
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.date_delta_days = None
|
||||
|
||||
with col2:
|
||||
rules.logic = st.radio("Logique de combinaison", ["AND", "OR"], index=0 if rules.logic == "AND" else 1)
|
||||
rules.label_logic = st.radio("Logique de combinaison entre labels", ["AND", "OR"], index=0 if rules.label_logic == "AND" else 1)
|
||||
|
||||
# --- Enregistrement ---
|
||||
if st.button("💾 Enregistrer les règles"):
|
||||
playlist.rules = rules
|
||||
playlist.save()
|
||||
st.success("Règles mises à jour ✅")
|
||||
st.rerun()
|
||||
(st.rerun if hasattr(st, "rerun") else st.experimental_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)
|
||||
st.write(f"🎬 {len(videos)} vidéo(s) trouvée(s).")
|
||||
|
||||
playlist_video_ids = db.get_video_file_names_in_playlist(playlist.id)
|
||||
|
||||
summary_map = summary_map = get_video_summary_cached()
|
||||
for v in videos[:50]:
|
||||
summary = summary_map.get(v.file_name, {"labels": [], "playlists": []})
|
||||
preselected = summary["labels"]
|
||||
playlists = summary["playlists"]
|
||||
show_video_row(
|
||||
v,
|
||||
preselected_labels=preselected,
|
||||
editable_labels=False,
|
||||
editable_difficulty=False,
|
||||
playlist=playlist,
|
||||
playlist_video_ids=playlist_video_ids,
|
||||
video_playlists=playlists
|
||||
)
|
||||
if st.button("📦 Charger plus"):
|
||||
st.session_state.video_page += 1
|
||||
st.rerun()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# playlists/playlist_db.py
|
||||
import db
|
||||
import json
|
||||
import pandas as pd
|
||||
from playlists.playlist_model import Playlist, RuleSet
|
||||
from playlists.sql_builder import build_sql_from_rules
|
||||
|
||||
def load_all_playlists():
|
||||
"""Retourne une liste de Playlist (Pydantic) — tolérant aux rules_json nuls / vides."""
|
||||
@@ -28,12 +30,16 @@ def load_all_playlists():
|
||||
row_dict = dict(zip(cols, row))
|
||||
|
||||
try:
|
||||
created_at = str(row["created_at"]) if row["created_at"] is not None else None
|
||||
updated_at = str(row["updated_at"]) if row["updated_at"] is not None else None
|
||||
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")
|
||||
id=row["id"],
|
||||
name=row["name"],
|
||||
description=row["description"],
|
||||
type=row["type"],
|
||||
rules=row["rules_json"],
|
||||
created_at=created_at,
|
||||
updated_at=updated_at
|
||||
)
|
||||
playlists.append(pl)
|
||||
except Exception as e:
|
||||
@@ -47,50 +53,31 @@ def delete_playlist(playlist_id: int):
|
||||
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:
|
||||
def get_videos_for_playlist(playlist):
|
||||
"""Retourne les vidéos correspondant aux règles d'une playlist dynamique."""
|
||||
if playlist.type == "manual":
|
||||
q = """
|
||||
with db.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(q, (playlist.id,)).fetchall()
|
||||
return conn.execute(query, (playlist.id,)).fetchall()
|
||||
else:
|
||||
rules = playlist.rules
|
||||
clauses = []
|
||||
params = []
|
||||
sql, params = build_sql_from_rules(playlist.rules)
|
||||
with db.get_conn() as conn:
|
||||
return conn.execute(sql, params).fetchall()
|
||||
|
||||
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()
|
||||
def load_video_summary_map():
|
||||
"""Retourne un dict {file_name: {'labels': [...], 'playlists': [...]}} depuis la vue video_summary."""
|
||||
with db.get_conn() as conn:
|
||||
df = pd.read_sql_query("SELECT file_name, labels, playlists FROM video_summary", conn)
|
||||
summary = {}
|
||||
for _, row in df.iterrows():
|
||||
summary[row["file_name"]] = {
|
||||
"labels": row["labels"].split(",") if row["labels"] else [],
|
||||
"playlists": row["playlists"].split(",") if row["playlists"] else [],
|
||||
}
|
||||
return summary
|
||||
@@ -1,25 +1,67 @@
|
||||
# playlists/playlist_model.py
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from pydantic import BaseModel, Field, validator, field_validator
|
||||
from typing import List, Optional, Literal
|
||||
from datetime import date, datetime
|
||||
import json
|
||||
import db
|
||||
|
||||
|
||||
from typing import Optional, List, Literal
|
||||
from pydantic import BaseModel, Field
|
||||
import json
|
||||
|
||||
class RuleSet(BaseModel):
|
||||
include_labels: List[str] = []
|
||||
exclude_labels: List[str] = []
|
||||
include_playlists: List[str] = []
|
||||
exclude_playlists: List[str] = []
|
||||
include_playlists: List[int] = []
|
||||
exclude_playlists: List[int] = []
|
||||
date_after: Optional[str] = None
|
||||
date_before: Optional[str] = None
|
||||
date_delta_days: Optional[int] = None
|
||||
difficulty: Optional[str] = None
|
||||
day_of_week: Optional[str] = None
|
||||
address_keyword: Optional[str] = None
|
||||
label_logic: Literal["AND", "OR"] = "AND"
|
||||
logic: Literal["AND", "OR"] = "AND"
|
||||
|
||||
# --- Normalisation des dates (entrée) ---
|
||||
@field_validator("date_after", "date_before", mode="before")
|
||||
def normalize_date(cls, v):
|
||||
"""Convertit automatiquement date/datetime en str ISO avant stockage."""
|
||||
if isinstance(v, (date, datetime)):
|
||||
return v.isoformat()
|
||||
return v
|
||||
|
||||
def model_dump(self, *args, **kwargs):
|
||||
"""Force la sortie JSON-safe (dates converties en str)."""
|
||||
data = super().model_dump(*args, **kwargs)
|
||||
for key in ["date_after", "date_before"]:
|
||||
v = data.get(key)
|
||||
if isinstance(v, (date, datetime)):
|
||||
data[key] = v.isoformat()
|
||||
return data
|
||||
|
||||
@validator("date_after", "date_before", pre=True, always=True)
|
||||
def convert_date(cls, v):
|
||||
"""Convertit automatiquement les objets date/datetime en ISO string."""
|
||||
if isinstance(v, (date, datetime)):
|
||||
return v.isoformat()
|
||||
return v
|
||||
|
||||
def dict(self, *args, **kwargs):
|
||||
"""S’assure que toutes les dates sont des chaînes sérialisables."""
|
||||
data = super().dict(*args, **kwargs)
|
||||
for key in ["date_after", "date_before"]:
|
||||
v = data.get(key)
|
||||
if isinstance(v, (date, datetime)):
|
||||
data[key] = v.isoformat()
|
||||
return data
|
||||
|
||||
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):
|
||||
|
||||
@@ -21,14 +21,31 @@ def main():
|
||||
# Appliquer les filtres
|
||||
filtered = []
|
||||
for p in playlists:
|
||||
# filtrage texte
|
||||
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()):
|
||||
# filtrage date de création
|
||||
if date_filter:
|
||||
created = p.created_at
|
||||
if isinstance(created, datetime):
|
||||
created_dt = created
|
||||
elif isinstance(created, (str, bytes)):
|
||||
try:
|
||||
created_dt = datetime.fromisoformat(created)
|
||||
except ValueError:
|
||||
continue # ignore invalid date
|
||||
else:
|
||||
continue
|
||||
|
||||
if created_dt < 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]
|
||||
if "new_playlist_name" in st.session_state:
|
||||
st.session_state["playlist_select"] = st.session_state.pop("new_playlist_name")
|
||||
selected_name = st.selectbox("Sélectionnez une playlist", names, key="playlist_select")
|
||||
|
||||
# --- Création ---
|
||||
@@ -43,11 +60,8 @@ def main():
|
||||
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.session_state["new_playlist_name"] = pl.name # on stocke temporairement
|
||||
st.rerun()
|
||||
else:
|
||||
st.experimental_rerun()
|
||||
return
|
||||
|
||||
# --- Mode édition ---
|
||||
|
||||
109
app/playlists/sql_builder.py
Normal file
109
app/playlists/sql_builder.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# playlists/sql_builder.py
|
||||
from typing import Tuple, List
|
||||
from playlists.playlist_model import RuleSet
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def build_sql_from_rules(rules: RuleSet) -> Tuple[str, List]:
|
||||
"""
|
||||
Construit une requête SQL complète (SELECT * FROM videos ...)
|
||||
en fonction d'un RuleSet.
|
||||
Retourne (sql_query, params).
|
||||
"""
|
||||
where = ["1=1"]
|
||||
params = []
|
||||
# --- DATES (delta prioritaire) ---
|
||||
if rules.date_delta_days is not None:
|
||||
try:
|
||||
delta_days = int(rules.date_delta_days)
|
||||
date_after = (datetime.now() + timedelta(days=delta_days)).strftime("%Y-%m-%d")
|
||||
where.append("record_datetime >= ?")
|
||||
params.append(date_after)
|
||||
except (ValueError, TypeError) as e:
|
||||
print(f"⚠️ [SQL Builder] Delta invalide ({rules.date_delta_days!r}) : {e}")
|
||||
else:
|
||||
if rules.date_after:
|
||||
where.append("record_datetime >= ?")
|
||||
params.append(rules.date_after)
|
||||
if rules.date_before:
|
||||
where.append("record_datetime <= ?")
|
||||
params.append(rules.date_before)
|
||||
|
||||
# --- LABELS ---
|
||||
if rules.include_labels:
|
||||
placeholders = ",".join("?" * len(rules.include_labels))
|
||||
if rules.label_logic == "AND":
|
||||
where.append(f"""
|
||||
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(rules.include_labels)}
|
||||
)
|
||||
""")
|
||||
else:
|
||||
where.append(f"""
|
||||
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(rules.include_labels)
|
||||
|
||||
if rules.exclude_labels:
|
||||
placeholders = ",".join("?" * len(rules.exclude_labels))
|
||||
where.append(f"""
|
||||
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.extend(rules.exclude_labels)
|
||||
|
||||
# --- PLAYLISTS ---
|
||||
if rules.include_playlists:
|
||||
placeholders = ",".join("?" * len(rules.include_playlists))
|
||||
where.append(f"""
|
||||
file_name IN (
|
||||
SELECT vp.video_file_name
|
||||
FROM video_playlists vp
|
||||
JOIN playlists p ON p.id = vp.playlist_id
|
||||
WHERE p.name IN ({placeholders})
|
||||
)
|
||||
""")
|
||||
params.extend(rules.include_playlists)
|
||||
|
||||
if rules.exclude_playlists:
|
||||
placeholders = ",".join("?" * len(rules.exclude_playlists))
|
||||
where.append(f"""
|
||||
file_name NOT IN (
|
||||
SELECT vp.video_file_name
|
||||
FROM video_playlists vp
|
||||
JOIN playlists p ON p.id = vp.playlist_id
|
||||
WHERE p.name IN ({placeholders})
|
||||
)
|
||||
""")
|
||||
params.extend(rules.exclude_playlists)
|
||||
|
||||
# --- DIFFICULTÉ ---
|
||||
if rules.difficulty and rules.difficulty != "Tous":
|
||||
where.append("difficulty_level = ?")
|
||||
params.append(rules.difficulty)
|
||||
|
||||
# --- JOUR ---
|
||||
if rules.day_of_week:
|
||||
where.append("day_of_week = ?")
|
||||
params.append(rules.day_of_week)
|
||||
|
||||
# --- ADRESSE ---
|
||||
if rules.address_keyword:
|
||||
where.append("address NOT LIKE '%unknown%' AND address LIKE ?")
|
||||
params.append(f"%{rules.address_keyword}%")
|
||||
|
||||
sql = f"SELECT * FROM videos WHERE {' AND '.join(where)} ORDER BY record_datetime DESC"
|
||||
return sql, params
|
||||
@@ -4,7 +4,7 @@ from models import Video
|
||||
from views.video_views import show_video_row
|
||||
from controllers.label_controller import label_widget
|
||||
|
||||
def video_filter_sidebar():
|
||||
def video_filter_sidebar(unlabeled=False):
|
||||
"""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)
|
||||
@@ -25,7 +25,7 @@ def video_filter_sidebar():
|
||||
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")
|
||||
show_unlabeled_only = st.sidebar.checkbox("Afficher uniquement les vidéos sans labels", value=unlabeled)
|
||||
|
||||
return dict(
|
||||
max_height=max_height,
|
||||
@@ -39,7 +39,7 @@ def video_filter_sidebar():
|
||||
show_unlabeled_only=show_unlabeled_only
|
||||
)
|
||||
|
||||
def video_list_view(filters: dict, editable_labels=True, editable_difficulty=True):
|
||||
def video_list_view(filters: dict, editable_labels=True, editable_difficulty=True, playlist=None):
|
||||
"""Affiche les vidéos selon les filtres fournis."""
|
||||
st.markdown(f"""
|
||||
<style>
|
||||
@@ -93,7 +93,7 @@ def video_list_view(filters: dict, editable_labels=True, editable_difficulty=Tru
|
||||
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)
|
||||
show_video_row(video, preselected, editable_labels, editable_difficulty, playlist)
|
||||
st.markdown("</div>", unsafe_allow_html=True)
|
||||
|
||||
# lazy loading bouton
|
||||
|
||||
@@ -1,32 +1,129 @@
|
||||
# video_views.py
|
||||
import os
|
||||
import streamlit as st
|
||||
from controllers.label_controller import label_widget
|
||||
import db
|
||||
from models import Video
|
||||
from controllers.label_controller import label_widget
|
||||
|
||||
def show_video_row(video, preselected_labels, editable_labels=True, editable_difficulty=True):
|
||||
|
||||
def show_video_row(
|
||||
video: Video,
|
||||
preselected_labels,
|
||||
editable_labels=True,
|
||||
editable_difficulty=True,
|
||||
playlist=None,
|
||||
playlist_video_ids=None,
|
||||
video_playlists=None,
|
||||
):
|
||||
"""
|
||||
Affiche une ligne Streamlit pour une vidéo :
|
||||
- miniature + lecture conditionnelle
|
||||
- métadonnées et labels
|
||||
- édition des labels et difficulté
|
||||
- (optionnel) ajout/retrait d'une playlist
|
||||
"""
|
||||
|
||||
# --- Vérifie si la vidéo est dans la playlist ---
|
||||
in_playlist = False
|
||||
if playlist and playlist_video_ids is not None:
|
||||
in_playlist = video.file_name in playlist_video_ids
|
||||
elif playlist:
|
||||
playlist_video_ids = db.get_video_file_names_in_playlist(playlist.id)
|
||||
in_playlist = video.file_name in playlist_video_ids
|
||||
|
||||
# --- Layout : miniature / infos / édition / action ---
|
||||
if playlist:
|
||||
col1, col2, col3, col4 = st.columns([1, 3, 2, 0.8])
|
||||
else:
|
||||
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)
|
||||
col4 = None
|
||||
|
||||
# --- Colonne 1 : miniature + boutons Play/Stop ---
|
||||
play_key = f"playing_{video.file_name}"
|
||||
st.session_state.setdefault(play_key, False)
|
||||
|
||||
with col1:
|
||||
if getattr(video, "thumbnail_file", None) and os.path.exists(video.thumbnail_file):
|
||||
st.image(video.thumbnail_file)
|
||||
else:
|
||||
st.caption("Pas de miniature")
|
||||
|
||||
st.caption(video.file_name or video.mp4_file_name)
|
||||
|
||||
c1, c2 = st.columns(2)
|
||||
with c1:
|
||||
if st.button("▶️ Lire", key=f"play_{video.file_name}"):
|
||||
st.session_state[play_key] = True
|
||||
(st.rerun if hasattr(st, "rerun") else st.experimental_rerun)()
|
||||
with c2:
|
||||
if st.button("⏸️ Stop", key=f"stop_{video.file_name}"):
|
||||
st.session_state[play_key] = False
|
||||
(st.rerun if hasattr(st, "rerun") else st.experimental_rerun)()
|
||||
|
||||
if st.session_state[play_key]:
|
||||
mp4_path = getattr(video, "mp4_file", None)
|
||||
if mp4_path and os.path.exists(mp4_path):
|
||||
st.video(mp4_path)
|
||||
else:
|
||||
st.warning("Fichier vidéo introuvable.")
|
||||
|
||||
# --- Colonne 2 : métadonnées ---
|
||||
with col2:
|
||||
st.markdown(f"**📅 {video.record_datetime or ''}** — {video.day_of_week or ''}")
|
||||
st.write(f"📍 {video.address or 'Inconnue'}")
|
||||
st.write(f"💪 Difficulté : {video.difficulty_display}")
|
||||
st.text(f"🏷️ Labels: {', '.join(preselected_labels) or 'Aucun'}")
|
||||
|
||||
if video_playlists:
|
||||
st.text(f"🎵 Playlists: {', '.join(video_playlists)}")
|
||||
else:
|
||||
playlists = db.get_video_playlists(video.file_name)
|
||||
if playlists:
|
||||
st.text(f"🎵 Playlists: {', '.join(playlists)}")
|
||||
|
||||
# --- Colonne 3 : édition ---
|
||||
with col3:
|
||||
if editable_labels:
|
||||
label_widget(video, preselected=preselected_labels)
|
||||
if editable_difficulty:
|
||||
levels = ["Tout niveau", "Débutant", "Intermédiaire", "Avancé", "Star"]
|
||||
try:
|
||||
idx = levels.index(video.difficulty_display)
|
||||
except ValueError:
|
||||
idx = 0
|
||||
new_level = st.selectbox(
|
||||
"🎚 Niveau",
|
||||
levels,
|
||||
index=levels.index(video.difficulty_display),
|
||||
index=idx,
|
||||
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}")
|
||||
|
||||
# --- Colonne 4 : action playlist ---
|
||||
if col4 and playlist:
|
||||
with col4:
|
||||
key_toggle = f"toggle_{playlist.id}_{video.file_name}"
|
||||
prev_state_key = f"{key_toggle}_prev"
|
||||
prev_state = st.session_state.get(prev_state_key, in_playlist)
|
||||
|
||||
toggled = st.toggle(
|
||||
"🎵",
|
||||
value=in_playlist,
|
||||
key=key_toggle,
|
||||
help="Inclure dans la playlist"
|
||||
)
|
||||
|
||||
if toggled != prev_state:
|
||||
if toggled:
|
||||
db.add_video_to_playlist(file_name=video.file_name, playlist_id=playlist.id)
|
||||
st.toast(f"✅ {video.file_name} ajouté à {playlist.name}")
|
||||
else:
|
||||
db.remove_video_from_playlist(file_name=video.file_name, playlist_id=playlist.id)
|
||||
st.toast(f"🗑️ {video.file_name} retiré de {playlist.name}")
|
||||
|
||||
st.session_state[prev_state_key] = toggled
|
||||
(st.rerun if hasattr(st, "rerun") else st.experimental_rerun)()
|
||||
else:
|
||||
st.session_state[prev_state_key] = toggled
|
||||
|
||||
@@ -22,6 +22,6 @@ register_video() {
|
||||
fi
|
||||
local mp4_file_name=$(basename $(dirname $mp4_file))
|
||||
|
||||
sqlite3 $DANCE_VIDEOS_DB "INSERT OR REPLACE INTO videos (file_name, raw_file, duration, mp4_file, mp4_file_name, rotated_file, thumbnail_file, record_datetime, day_of_week, lat, long, address) VALUES('$file_name','$raw_file', '$duration', '$mp4_file', '$mp4_file_name', '$rotated_file', '$thumbnail_file', $record_datetime, '$day_of_week', $lat, $long, '$address')"
|
||||
sqlite3 $DANCE_VIDEOS_DB "PRAGMA busy_timeout = 10000; INSERT OR REPLACE INTO videos (file_name, raw_file, duration, mp4_file, mp4_file_name, rotated_file, thumbnail_file, record_datetime, day_of_week, lat, long, address) VALUES('$file_name','$raw_file', '$duration', '$mp4_file', '$mp4_file_name', '$rotated_file', '$thumbnail_file', $record_datetime, '$day_of_week', $lat, $long, '$address');"
|
||||
}
|
||||
export -f register_video
|
||||
@@ -1,4 +1,229 @@
|
||||
CREATE TABLE IF NOT EXISTS video_summary AS
|
||||
-- ============================================================================
|
||||
-- VIEW : playlist_videos
|
||||
-- ============================================================================
|
||||
DROP VIEW IF EXISTS playlist_videos;
|
||||
|
||||
CREATE VIEW playlist_videos AS
|
||||
WITH
|
||||
-- 1️⃣ Playlists manuelles
|
||||
manual_playlists_videos AS (
|
||||
SELECT
|
||||
p.id AS playlist_id,
|
||||
p.name AS playlist_name,
|
||||
'manual' AS playlist_type,
|
||||
vp.video_file_name
|
||||
FROM playlists p
|
||||
JOIN video_playlists vp ON p.id = vp.playlist_id
|
||||
WHERE p.type = 'manual'
|
||||
),
|
||||
|
||||
-- 2️⃣ Dynamiques sans include/exclude
|
||||
dynamic_playlist_videos_base AS (
|
||||
SELECT DISTINCT
|
||||
p.id AS playlist_id,
|
||||
p.name AS playlist_name,
|
||||
'dynamic' AS playlist_type,
|
||||
v.file_name AS video_file_name
|
||||
FROM playlists p
|
||||
JOIN videos v
|
||||
WHERE p.type = 'dynamic'
|
||||
-- Labels inclus (AND/OR)
|
||||
AND (
|
||||
json_array_length(json_extract(p.rules_json, '$.include_labels')) = 0
|
||||
OR (
|
||||
json_extract(p.rules_json, '$.label_logic') = 'OR'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM json_each(json_extract(p.rules_json, '$.include_labels')) jl
|
||||
JOIN labels l ON l.name = jl.value
|
||||
JOIN video_labels vl ON vl.label_id = l.id AND vl.video_file_name = v.file_name
|
||||
)
|
||||
)
|
||||
OR (
|
||||
json_extract(p.rules_json, '$.label_logic') = 'AND'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM json_each(json_extract(p.rules_json, '$.include_labels')) jl
|
||||
WHERE jl.value NOT IN (
|
||||
SELECT l2.name
|
||||
FROM labels l2
|
||||
JOIN video_labels vl2 ON l2.id = vl2.label_id
|
||||
WHERE vl2.video_file_name = v.file_name
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
-- Labels exclus
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM json_each(json_extract(p.rules_json, '$.exclude_labels')) je
|
||||
JOIN labels lx ON lx.name = je.value
|
||||
JOIN video_labels vlx ON vlx.label_id = lx.id AND vlx.video_file_name = v.file_name
|
||||
)
|
||||
-- Dates
|
||||
AND (
|
||||
json_extract(p.rules_json, '$.date_after') IS NULL
|
||||
OR v.record_datetime >= json_extract(p.rules_json, '$.date_after')
|
||||
OR (
|
||||
json_extract(p.rules_json, '$.date_delta_days') IS NOT NULL
|
||||
AND v.record_datetime >= date('now', (json_extract(p.rules_json, '$.date_delta_days') || ' days'))
|
||||
)
|
||||
)
|
||||
AND (
|
||||
json_extract(p.rules_json, '$.date_before') IS NULL
|
||||
OR v.record_datetime <= json_extract(p.rules_json, '$.date_before')
|
||||
)
|
||||
-- Difficulty / day / address
|
||||
AND (
|
||||
json_extract(p.rules_json, '$.difficulty') IS NULL
|
||||
OR v.difficulty_level = json_extract(p.rules_json, '$.difficulty')
|
||||
)
|
||||
AND (
|
||||
json_extract(p.rules_json, '$.day_of_week') IS NULL
|
||||
OR v.day_of_week = json_extract(p.rules_json, '$.day_of_week')
|
||||
)
|
||||
AND (
|
||||
json_extract(p.rules_json, '$.address_keyword') IS NULL
|
||||
OR (
|
||||
v.address NOT LIKE '%unknown%'
|
||||
AND v.address LIKE '%' || json_extract(p.rules_json, '$.address_keyword') || '%'
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
-- 3️⃣ Inclusions directes (parent → enfant)
|
||||
playlist_direct_includes AS (
|
||||
SELECT
|
||||
p.id AS parent_playlist_id,
|
||||
CAST(inc.value AS INTEGER) AS child_playlist_id
|
||||
FROM playlists p
|
||||
JOIN json_each(json_extract(p.rules_json, '$.include_playlists')) inc
|
||||
ON json_type(inc.value) IN ('integer', 'text')
|
||||
WHERE json_array_length(json_extract(p.rules_json, '$.include_playlists')) > 0
|
||||
),
|
||||
|
||||
-- 4️⃣ Récursion : parent → descendant (transitif)
|
||||
playlist_includes_recursive AS (
|
||||
WITH RECURSIVE rec(parent_playlist_id, child_playlist_id) AS (
|
||||
SELECT parent_playlist_id, child_playlist_id FROM playlist_direct_includes
|
||||
UNION ALL
|
||||
SELECT d.parent_playlist_id, di.child_playlist_id
|
||||
FROM playlist_direct_includes d
|
||||
JOIN rec di ON di.parent_playlist_id = d.child_playlist_id
|
||||
)
|
||||
SELECT DISTINCT parent_playlist_id AS parent_id, child_playlist_id AS child_id FROM rec
|
||||
),
|
||||
|
||||
-- 5️⃣ Exclusions directes et récursives
|
||||
playlist_direct_excludes AS (
|
||||
SELECT
|
||||
p.id AS parent_playlist_id,
|
||||
CAST(exc.value AS INTEGER) AS child_playlist_id
|
||||
FROM playlists p
|
||||
JOIN json_each(json_extract(p.rules_json, '$.exclude_playlists')) exc
|
||||
ON json_type(exc.value) IN ('integer', 'text')
|
||||
WHERE json_array_length(json_extract(p.rules_json, '$.exclude_playlists')) > 0
|
||||
),
|
||||
playlist_excludes_recursive AS (
|
||||
WITH RECURSIVE rec_ex(parent_playlist_id, child_playlist_id) AS (
|
||||
SELECT parent_playlist_id, child_playlist_id FROM playlist_direct_excludes
|
||||
UNION ALL
|
||||
SELECT d.parent_playlist_id, di.child_playlist_id
|
||||
FROM playlist_direct_excludes d
|
||||
JOIN rec_ex di ON di.parent_playlist_id = d.child_playlist_id
|
||||
)
|
||||
SELECT DISTINCT parent_playlist_id AS parent_id, child_playlist_id AS child_id FROM rec_ex
|
||||
),
|
||||
|
||||
-- 6️⃣ Vidéos issues des playlists incluses
|
||||
playlist_included_videos AS (
|
||||
SELECT pir.parent_id AS parent_playlist_id, mpv.video_file_name
|
||||
FROM playlist_includes_recursive pir
|
||||
JOIN manual_playlists_videos mpv ON mpv.playlist_id = pir.child_id
|
||||
UNION
|
||||
SELECT pir.parent_id AS parent_playlist_id, dpb.video_file_name
|
||||
FROM playlist_includes_recursive pir
|
||||
JOIN dynamic_playlist_videos_base dpb ON dpb.playlist_id = pir.child_id
|
||||
),
|
||||
|
||||
-- 7️⃣ Inclusion logique OR
|
||||
playlist_includes_union AS (
|
||||
SELECT DISTINCT db.playlist_id, db.playlist_name, db.playlist_type, db.video_file_name
|
||||
FROM dynamic_playlist_videos_base db
|
||||
JOIN playlists p ON p.id = db.playlist_id
|
||||
WHERE json_extract(p.rules_json, '$.logic') = 'OR'
|
||||
UNION
|
||||
SELECT DISTINCT iv.parent_playlist_id AS playlist_id,
|
||||
(SELECT name FROM playlists WHERE id = iv.parent_playlist_id) AS playlist_name,
|
||||
'dynamic' AS playlist_type,
|
||||
iv.video_file_name
|
||||
FROM playlist_included_videos iv
|
||||
JOIN playlists p ON p.id = iv.parent_playlist_id
|
||||
WHERE json_extract(p.rules_json, '$.logic') = 'OR'
|
||||
),
|
||||
|
||||
-- 8️⃣ Inclusion logique AND (+ fallback si 0 include)
|
||||
playlist_includes_intersection AS (
|
||||
SELECT DISTINCT db.playlist_id, db.playlist_name, db.playlist_type, db.video_file_name
|
||||
FROM dynamic_playlist_videos_base db
|
||||
JOIN playlists p ON p.id = db.playlist_id
|
||||
WHERE json_extract(p.rules_json, '$.logic') = 'AND'
|
||||
AND json_array_length(json_extract(p.rules_json, '$.include_playlists')) > 0
|
||||
AND db.video_file_name IN (
|
||||
SELECT iv.video_file_name
|
||||
FROM playlist_included_videos iv
|
||||
WHERE iv.parent_playlist_id = db.playlist_id
|
||||
)
|
||||
UNION ALL
|
||||
SELECT DISTINCT db.playlist_id, db.playlist_name, db.playlist_type, db.video_file_name
|
||||
FROM dynamic_playlist_videos_base db
|
||||
JOIN playlists p ON p.id = db.playlist_id
|
||||
WHERE json_extract(p.rules_json, '$.logic') = 'AND'
|
||||
AND (
|
||||
json_array_length(json_extract(p.rules_json, '$.include_playlists')) IS NULL
|
||||
OR json_array_length(json_extract(p.rules_json, '$.include_playlists')) = 0
|
||||
)
|
||||
),
|
||||
|
||||
-- 9️⃣ Fusion des deux logiques d’inclusion
|
||||
playlist_after_includes AS (
|
||||
SELECT * FROM playlist_includes_union
|
||||
UNION ALL
|
||||
SELECT * FROM playlist_includes_intersection
|
||||
),
|
||||
|
||||
-- 🔟 Vidéos exclues (via exclude_playlists récursif)
|
||||
playlist_excluded_videos AS (
|
||||
SELECT per.parent_id AS parent_playlist_id, mpv.video_file_name
|
||||
FROM playlist_excludes_recursive per
|
||||
JOIN manual_playlists_videos mpv ON mpv.playlist_id = per.child_id
|
||||
UNION
|
||||
SELECT per.parent_id AS parent_playlist_id, dpb.video_file_name
|
||||
FROM playlist_excludes_recursive per
|
||||
JOIN dynamic_playlist_videos_base dpb ON dpb.playlist_id = per.child_id
|
||||
),
|
||||
|
||||
-- 1️⃣1️⃣ Application des exclusions
|
||||
playlist_after_excludes AS (
|
||||
SELECT pai.playlist_id, pai.playlist_name, pai.playlist_type, pai.video_file_name
|
||||
FROM playlist_after_includes pai
|
||||
LEFT JOIN playlist_excluded_videos pev
|
||||
ON pev.parent_playlist_id = pai.playlist_id AND pev.video_file_name = pai.video_file_name
|
||||
WHERE pev.parent_playlist_id IS NULL
|
||||
)
|
||||
|
||||
-- 1️⃣2️⃣ Résultat final : union manuelles + dynamiques
|
||||
SELECT playlist_id, playlist_name, playlist_type, video_file_name
|
||||
FROM manual_playlists_videos
|
||||
UNION ALL
|
||||
SELECT playlist_id, playlist_name, playlist_type, video_file_name
|
||||
FROM playlist_after_excludes;
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- TABLE AGRÉGÉE : video_summary
|
||||
-- ============================================================================
|
||||
DROP VIEW IF EXISTS video_summary;
|
||||
|
||||
CREATE VIEW video_summary AS
|
||||
SELECT
|
||||
v.file_name,
|
||||
v.raw_file,
|
||||
@@ -8,129 +233,12 @@ SELECT
|
||||
v.difficulty_level,
|
||||
v.address,
|
||||
GROUP_CONCAT(DISTINCT l.name) AS labels,
|
||||
GROUP_CONCAT(DISTINCT p.name) AS playlists
|
||||
GROUP_CONCAT(DISTINCT pv.playlist_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
|
||||
LEFT JOIN playlist_videos pv ON pv.video_file_name = 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;
|
||||
CREATE TABLE IF NOT EXISTS video_summary_materialized AS
|
||||
SELECT * FROM video_summary;
|
||||
236
model/views.sql
Normal file
236
model/views.sql
Normal file
@@ -0,0 +1,236 @@
|
||||
-- 1) Vidéos manuelles (source unique, simple)
|
||||
DROP VIEW IF EXISTS manual_playlists_videos;
|
||||
CREATE VIEW manual_playlists_videos AS
|
||||
SELECT
|
||||
p.id AS playlist_id,
|
||||
p.name AS playlist_name,
|
||||
'manual' AS playlist_type,
|
||||
vp.video_file_name
|
||||
FROM playlists p
|
||||
JOIN video_playlists vp ON p.id = vp.playlist_id
|
||||
WHERE p.type = 'manual';
|
||||
|
||||
-- 2) Vidéos dynamiques : tout sauf include_playlists / exclude_playlists
|
||||
-- (ceci applique labels, date_after/date_before/date_delta_days, difficulty, day_of_week, address_keyword)
|
||||
DROP VIEW IF EXISTS dynamic_playlist_videos_base;
|
||||
CREATE VIEW dynamic_playlist_videos_base AS
|
||||
SELECT DISTINCT
|
||||
p.id AS playlist_id,
|
||||
p.name AS playlist_name,
|
||||
'dynamic' AS playlist_type,
|
||||
v.file_name AS video_file_name
|
||||
FROM playlists p
|
||||
JOIN videos v
|
||||
WHERE p.type = 'dynamic'
|
||||
|
||||
/* --- include_labels (AND/OR) --- */
|
||||
AND (
|
||||
json_array_length(json_extract(p.rules_json, '$.include_labels')) = 0
|
||||
OR (
|
||||
json_extract(p.rules_json, '$.label_logic') = 'OR'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM json_each(json_extract(p.rules_json, '$.include_labels')) jl
|
||||
JOIN labels l ON l.name = jl.value
|
||||
JOIN video_labels vl ON vl.label_id = l.id AND vl.video_file_name = v.file_name
|
||||
)
|
||||
)
|
||||
OR (
|
||||
json_extract(p.rules_json, '$.label_logic') = 'AND'
|
||||
AND NOT EXISTS (
|
||||
-- il existe un label requis qui n'est pas présent pour la video
|
||||
SELECT 1
|
||||
FROM json_each(json_extract(p.rules_json, '$.include_labels')) jl
|
||||
WHERE jl.value NOT IN (
|
||||
SELECT l2.name
|
||||
FROM labels l2
|
||||
JOIN video_labels vl2 ON l2.id = vl2.label_id
|
||||
WHERE vl2.video_file_name = v.file_name
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
/* --- exclude_labels --- */
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM json_each(json_extract(p.rules_json, '$.exclude_labels')) je
|
||||
JOIN labels lx ON lx.name = je.value
|
||||
JOIN video_labels vlx ON vlx.label_id = lx.id AND vlx.video_file_name = v.file_name
|
||||
)
|
||||
|
||||
/* --- date_after / date_delta_days / date_before --- */
|
||||
AND (
|
||||
json_extract(p.rules_json, '$.date_after') IS NULL
|
||||
OR v.record_datetime >= json_extract(p.rules_json, '$.date_after')
|
||||
OR (
|
||||
json_extract(p.rules_json, '$.date_delta_days') IS NOT NULL
|
||||
AND v.record_datetime >= date('now', (json_extract(p.rules_json, '$.date_delta_days') || ' days'))
|
||||
)
|
||||
)
|
||||
AND (
|
||||
json_extract(p.rules_json, '$.date_before') IS NULL
|
||||
OR v.record_datetime <= json_extract(p.rules_json, '$.date_before')
|
||||
)
|
||||
|
||||
/* --- difficulty, day_of_week, address_keyword --- */
|
||||
AND (
|
||||
json_extract(p.rules_json, '$.difficulty') IS NULL
|
||||
OR v.difficulty_level = json_extract(p.rules_json, '$.difficulty')
|
||||
)
|
||||
AND (
|
||||
json_extract(p.rules_json, '$.day_of_week') IS NULL
|
||||
OR v.day_of_week = json_extract(p.rules_json, '$.day_of_week')
|
||||
)
|
||||
AND (
|
||||
json_extract(p.rules_json, '$.address_keyword') IS NULL
|
||||
OR (
|
||||
v.address NOT LIKE '%unknown%'
|
||||
AND v.address LIKE '%' || json_extract(p.rules_json, '$.address_keyword') || '%'
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
-- 3) Mapping direct parent -> child playlist ids (extrait JSON include_playlists)
|
||||
DROP VIEW IF EXISTS playlist_direct_includes;
|
||||
CREATE VIEW playlist_direct_includes AS
|
||||
SELECT
|
||||
p.id AS parent_playlist_id,
|
||||
CAST(inc.value AS INTEGER) AS child_playlist_id
|
||||
FROM playlists p
|
||||
JOIN json_each(json_extract(p.rules_json, '$.include_playlists')) inc
|
||||
ON json_type(inc.value) IN ('integer', 'text')
|
||||
WHERE json_array_length(json_extract(p.rules_json, '$.include_playlists')) > 0;
|
||||
|
||||
|
||||
-- 4) Fermeture transitive des inclusions : parent -> descendant child
|
||||
DROP VIEW IF EXISTS playlist_includes_recursive;
|
||||
CREATE VIEW playlist_includes_recursive AS
|
||||
WITH RECURSIVE rec(parent_playlist_id, child_playlist_id) AS (
|
||||
-- base : les inclusions directes
|
||||
SELECT parent_playlist_id, child_playlist_id FROM playlist_direct_includes
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- récursion : si A inclut B et B inclut C, alors A inclut C
|
||||
SELECT d.parent_playlist_id, di.child_playlist_id
|
||||
FROM playlist_direct_includes d
|
||||
JOIN rec di ON di.parent_playlist_id = d.child_playlist_id
|
||||
)
|
||||
SELECT DISTINCT parent_playlist_id AS parent_id, child_playlist_id AS child_id FROM rec;
|
||||
|
||||
|
||||
-- 5) De même pour exclusions directes et récursives
|
||||
DROP VIEW IF EXISTS playlist_direct_excludes;
|
||||
CREATE VIEW playlist_direct_excludes AS
|
||||
SELECT
|
||||
p.id AS parent_playlist_id,
|
||||
CAST(exc.value AS INTEGER) AS child_playlist_id
|
||||
FROM playlists p
|
||||
JOIN json_each(json_extract(p.rules_json, '$.exclude_playlists')) exc
|
||||
ON json_type(exc.value) IN ('integer', 'text')
|
||||
WHERE json_array_length(json_extract(p.rules_json, '$.exclude_playlists')) > 0;
|
||||
|
||||
DROP VIEW IF EXISTS playlist_excludes_recursive;
|
||||
CREATE VIEW playlist_excludes_recursive AS
|
||||
WITH RECURSIVE rec_ex(parent_playlist_id, child_playlist_id) AS (
|
||||
SELECT parent_playlist_id, child_playlist_id FROM playlist_direct_excludes
|
||||
UNION ALL
|
||||
SELECT d.parent_playlist_id, di.child_playlist_id
|
||||
FROM playlist_direct_excludes d
|
||||
JOIN rec_ex di ON di.parent_playlist_id = d.child_playlist_id
|
||||
)
|
||||
SELECT DISTINCT parent_playlist_id AS parent_id, child_playlist_id AS child_id FROM rec_ex;
|
||||
|
||||
|
||||
-- 6) vidéos apportées par les playlists incluses (manuelles + dynamic_base)
|
||||
-- For each parent, collect videos that belong to any included child playlist (direct or transitive).
|
||||
-- union logique (rules.logic = 'OR')
|
||||
DROP VIEW IF EXISTS playlist_includes_union;
|
||||
CREATE VIEW playlist_includes_union AS
|
||||
-- union de base + vidéos incluses
|
||||
SELECT DISTINCT db.playlist_id, db.playlist_name, db.playlist_type, db.video_file_name
|
||||
FROM dynamic_playlist_videos_base db
|
||||
WHERE
|
||||
json_extract((SELECT rules_json FROM playlists WHERE id = db.playlist_id), '$.logic') = 'OR'
|
||||
|
||||
UNION
|
||||
|
||||
SELECT DISTINCT iv.parent_playlist_id AS playlist_id,
|
||||
(SELECT name FROM playlists WHERE id = iv.parent_playlist_id) AS playlist_name,
|
||||
'dynamic' AS playlist_type,
|
||||
iv.video_file_name
|
||||
FROM playlist_included_videos iv
|
||||
JOIN playlists p ON p.id = iv.parent_playlist_id
|
||||
WHERE json_extract(p.rules_json, '$.logic') = 'OR';
|
||||
|
||||
|
||||
|
||||
-- 7) intersection logique (rules.logic = 'AND')
|
||||
DROP VIEW IF EXISTS playlist_includes_intersection;
|
||||
CREATE VIEW playlist_includes_intersection AS
|
||||
-- Cas 1️⃣ : AND + playlists incluses => intersection stricte
|
||||
SELECT DISTINCT db.playlist_id, db.playlist_name, db.playlist_type, db.video_file_name
|
||||
FROM dynamic_playlist_videos_base db
|
||||
WHERE json_extract((SELECT rules_json FROM playlists WHERE id = db.playlist_id), '$.logic') = 'AND'
|
||||
AND json_array_length(json_extract((SELECT rules_json FROM playlists WHERE id = db.playlist_id), '$.include_playlists')) > 0
|
||||
AND db.video_file_name IN (
|
||||
SELECT iv.video_file_name
|
||||
FROM playlist_included_videos iv
|
||||
WHERE iv.parent_playlist_id = db.playlist_id
|
||||
)
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Cas 2️⃣ : AND + aucune playlist incluse => garder la base telle quelle
|
||||
SELECT DISTINCT db.playlist_id, db.playlist_name, db.playlist_type, db.video_file_name
|
||||
FROM dynamic_playlist_videos_base db
|
||||
WHERE json_extract((SELECT rules_json FROM playlists WHERE id = db.playlist_id), '$.logic') = 'AND'
|
||||
AND (
|
||||
json_array_length(json_extract((SELECT rules_json FROM playlists WHERE id = db.playlist_id), '$.include_playlists')) IS NULL
|
||||
OR json_array_length(json_extract((SELECT rules_json FROM playlists WHERE id = db.playlist_id), '$.include_playlists')) = 0
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
-- 8) regroupement des deux logiques
|
||||
DROP VIEW IF EXISTS playlist_after_includes;
|
||||
CREATE VIEW playlist_after_includes AS
|
||||
SELECT * FROM playlist_includes_union
|
||||
UNION ALL
|
||||
SELECT * FROM playlist_includes_intersection;
|
||||
|
||||
|
||||
|
||||
-- 9) exclusions
|
||||
DROP VIEW IF EXISTS playlist_excluded_videos;
|
||||
CREATE VIEW playlist_excluded_videos AS
|
||||
SELECT per.parent_id AS parent_playlist_id, mpv.video_file_name
|
||||
FROM playlist_excludes_recursive per
|
||||
JOIN manual_playlists_videos mpv ON mpv.playlist_id = per.child_id
|
||||
|
||||
UNION
|
||||
|
||||
SELECT per.parent_id AS parent_playlist_id, dpb.video_file_name
|
||||
FROM playlist_excludes_recursive per
|
||||
JOIN dynamic_playlist_videos_base dpb ON dpb.playlist_id = per.child_id;
|
||||
|
||||
|
||||
|
||||
-- 10) Appliquer exclusions
|
||||
DROP VIEW IF EXISTS playlist_after_excludes;
|
||||
CREATE VIEW playlist_after_excludes AS
|
||||
SELECT pai.playlist_id, pai.playlist_name, pai.playlist_type, pai.video_file_name
|
||||
FROM playlist_after_includes pai
|
||||
LEFT JOIN playlist_excluded_videos pev
|
||||
ON pev.parent_playlist_id = pai.playlist_id AND pev.video_file_name = pai.video_file_name
|
||||
WHERE pev.parent_playlist_id IS NULL;
|
||||
|
||||
-- 11) résultat final « flat »
|
||||
|
||||
DROP VIEW IF EXISTS playlist_videos_flat;
|
||||
CREATE VIEW playlist_videos_flat AS
|
||||
SELECT playlist_id, playlist_name, playlist_type, video_file_name
|
||||
FROM manual_playlists_videos
|
||||
UNION ALL
|
||||
SELECT playlist_id, playlist_name, playlist_type, video_file_name
|
||||
FROM playlist_after_excludes;
|
||||
50
program1/append_with_lock.sh
Normal file
50
program1/append_with_lock.sh
Normal file
@@ -0,0 +1,50 @@
|
||||
# Fonction pour ajouter une ligne à un fichier avec verrouillage (bloquant + timeout + gestion des signaux)
|
||||
append_with_lock() {
|
||||
local line="$1"
|
||||
local file="$2"
|
||||
local lock_dir="/tmp/dancevideos_moved_files.lock"
|
||||
local timeout=30 # Timeout en secondes (ajustable)
|
||||
local start_time=$(date +%s)
|
||||
local got_lock=false
|
||||
|
||||
# Fonction de nettoyage du verrou (appelée en cas de signal ou de sortie)
|
||||
cleanup_lock() {
|
||||
if [ -d "$lock_dir" ]; then
|
||||
rmdir "$lock_dir" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Piège les signaux d'interruption pour libérer le verrou
|
||||
trap cleanup_lock INT TERM EXIT
|
||||
|
||||
# Attendre le verrou avec timeout
|
||||
while true; do
|
||||
# Vérifier le timeout
|
||||
local current_time=$(date +%s)
|
||||
if [ $((current_time - start_time)) -ge $timeout ]; then
|
||||
echo "Timeout atteint pour l'obtention du verrou sur $file" >&2
|
||||
trap - INT TERM EXIT # Désactive le piège pour éviter un nettoyage double
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Tentative de verrouillage
|
||||
if mkdir "$lock_dir" 2>/dev/null; then
|
||||
got_lock=true
|
||||
break
|
||||
else
|
||||
sleep 0.1 # Attendre avant de réessayer
|
||||
fi
|
||||
done
|
||||
|
||||
# Écriture dans le fichier (si verrou obtenu)
|
||||
if $got_lock; then
|
||||
set -x
|
||||
echo "$line" >> "$file"
|
||||
echo "$line" >> "$file" # write them twice because of bug where a line is skipped
|
||||
set +x
|
||||
# Libérer le verrou
|
||||
rmdir "$lock_dir"
|
||||
trap - INT TERM EXIT # Désactive le piège après utilisation
|
||||
fi
|
||||
}
|
||||
export -f append_with_lock
|
||||
@@ -2,11 +2,49 @@
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
# === Gestion des arguments ===
|
||||
PROCESS_ALL=false
|
||||
PRINT_ERR=false
|
||||
|
||||
# Fonction pour afficher l'aide
|
||||
usage() {
|
||||
echo "Usage: $0 [--all|-a]"
|
||||
echo "Options:"
|
||||
echo " --all, -a Traiter tous les fichiers (ignore la liste de fichiers)"
|
||||
echo " --help, -h Afficher cette aide"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Traitement des arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--all|-a)
|
||||
PROCESS_ALL=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
;;
|
||||
--print-err)
|
||||
PRINT_ERR=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Option inconnue : $1" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
SCRIPTS_DIR=$(dirname `realpath ${BASH_SOURCE[0]}`)
|
||||
|
||||
# === CONFIGURATION ===
|
||||
export DOSSIER_SOURCE="${HOME}/Downloads"
|
||||
export DOSSIER_DESTINATION_RAW="${HOME}/Documents/.DanceVideos/raw"
|
||||
export TEMP_FILE="/tmp/dancevideos_moved_files.txt"
|
||||
|
||||
# Initialiser le fichier temporaire
|
||||
> "$TEMP_FILE"
|
||||
|
||||
sanitize_name() {
|
||||
local name="$1"
|
||||
@@ -17,6 +55,8 @@ sanitize_name() {
|
||||
}
|
||||
export -f sanitize_name
|
||||
|
||||
source $SCRIPTS_DIR/append_with_lock.sh
|
||||
|
||||
# Fonction pour vérifier et déplacer un fichier
|
||||
check_and_move() {
|
||||
local fichier="$1"
|
||||
@@ -28,6 +68,9 @@ check_and_move() {
|
||||
if [ "$taille1" -eq "$taille2" ] && [ "$taille2" -gt 0 ]; then
|
||||
echo "Déplacement de $(basename "$fichier")"
|
||||
rsync -av --remove-source-files "$fichier" "$DOSSIER_DESTINATION_RAW/$(sanitize_name "$(basename "$fichier")")"
|
||||
if ! append_with_lock "$DOSSIER_DESTINATION_RAW/$(sanitize_name "$(basename "$fichier")")" "$TEMP_FILE"; then
|
||||
echo "Échec de l'écriture dans $TEMP_FILE (timeout)" >&2
|
||||
fi
|
||||
else
|
||||
echo "Fichier $(basename "$fichier") encore en cours de réception."
|
||||
fi
|
||||
@@ -90,6 +133,10 @@ process_raw_file() {
|
||||
local ct weekday duration lat lon address
|
||||
IFS="|" read -r ct weekday duration lat lon address <<<"$(process_video "$raw")"
|
||||
|
||||
# récupérer les infos eventuelles dans la BDD
|
||||
# si fichier videos existent toujours ingorer, sinon écraser
|
||||
# TODO
|
||||
|
||||
local dir=$(mp4_dir $raw $weekday "$address")
|
||||
|
||||
local thumbnail=${dir}/thumbnail.jpg
|
||||
@@ -125,7 +172,8 @@ export -f whatsapp_video
|
||||
|
||||
convert_raws() {
|
||||
|
||||
# ##_##.mov
|
||||
if $PROCESS_ALL; then
|
||||
|
||||
find "$DOSSIER_DESTINATION_RAW" -type f \
|
||||
-name "*.mov" \
|
||||
-print0 | parallel -0 -j 4 iphone_video
|
||||
@@ -137,6 +185,39 @@ convert_raws() {
|
||||
find "$DOSSIER_DESTINATION_RAW" -type f \
|
||||
-name '*[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]*.mp4' \
|
||||
-print0 | parallel -0 -j 4 whatsapp_video
|
||||
|
||||
else
|
||||
if [ -f "$TEMP_FILE" ]; then
|
||||
while IFS= read -r file || [ -n "$file" ]; do
|
||||
file=$(echo "$file" | tr -d '\r' | xargs)
|
||||
# Ignorer les lignes vides
|
||||
if [ -z "$file" ]; then
|
||||
continue
|
||||
fi
|
||||
echo "Dealing with $file"
|
||||
local filename=$(basename "$file")
|
||||
if [[ "$filename" == *.mov ]]; then
|
||||
iphone_video "$file"
|
||||
elif [[ "$filename" == screenrecording*.mp4 ]]; then
|
||||
screen_video "$file"
|
||||
elif [[ "$filename" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.mp4$ ]]; then
|
||||
whatsapp_video "$file"
|
||||
else
|
||||
echo "$file didn't match any pattern"
|
||||
fi
|
||||
done < "$TEMP_FILE"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
$PRINT_ERR || exec 2> /tmp/DanceVideos.stderr
|
||||
|
||||
convert_raws
|
||||
|
||||
set -x
|
||||
if [ 0 -lt $(wc -l /tmp/dancevideos_moved_files.txt | awk '{print $1}') ]; then
|
||||
|
||||
STREAMLIT_PID=$(ps aux | grep streamlit | grep -v 'grep' | awk '{print $2}'); [ ! -z "$STREAMLIT_PID" ] && kill $STREAMLIT_PID
|
||||
(cd $SCRIPTS_DIR/..; source .venv/bin/activate; streamlit run app/app.py -- --unlabeled)
|
||||
|
||||
fi
|
||||
@@ -2,25 +2,21 @@ get_rotation_filter() {
|
||||
local f="$1"
|
||||
local rotation
|
||||
rotation=$(exiftool -Rotation -n "$f" | awk '{print $3}')
|
||||
# echo $rotation $f >> "rotations.txt"
|
||||
case "$rotation" in
|
||||
# 90) echo "transpose=1" ;;
|
||||
# 270) echo "transpose=2" ;;
|
||||
# 180) echo "hflip,vflip" ;;
|
||||
# *) echo "" ;;
|
||||
*) echo "transpose=1" ;;
|
||||
esac
|
||||
}
|
||||
export -f get_rotation_filter
|
||||
|
||||
reencode_with_rotation() {
|
||||
set -x
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
local filter
|
||||
filter="$(get_rotation_filter "$src")"
|
||||
|
||||
if [ -n "$filter" ]; then
|
||||
echo " Correction d’orientation (rotation=${filter})"
|
||||
#echo " Correction d’orientation (rotation=${filter})"
|
||||
if ffmpeg -encoders 2>/dev/null | grep -q 'h264_videotoolbox'; then
|
||||
ffmpeg -nostdin -i "$src" -vf "$filter" \
|
||||
-c:v h264_videotoolbox -b:v 5M -c:a aac -map_metadata -1 -y "$dst"
|
||||
@@ -42,5 +38,6 @@ reencode_with_rotation() {
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
set +x
|
||||
}
|
||||
export -f reencode_with_rotation
|
||||
@@ -18,9 +18,9 @@ mkdir -p "$DOSSIER_PLAYLIST"
|
||||
|
||||
# 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;
|
||||
SELECT pv.playlist_id, pv.playlist_name, pv.video_file_name, v.rotated_file
|
||||
FROM playlist_videos pv JOIN videos v ON pv.video_file_name=v.file_name
|
||||
WHERE v.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
|
||||
|
||||
Reference in New Issue
Block a user