-- ============================================================================ -- 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, 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 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 playlist_videos pv ON pv.video_file_name = v.file_name GROUP BY v.file_name; CREATE TABLE IF NOT EXISTS video_summary_materialized AS SELECT * FROM video_summary;