correct application of playlists rules

This commit is contained in:
Gabriel Radureau
2025-10-16 17:18:58 +02:00
parent d2e2028610
commit 78313ffbef
17 changed files with 1098 additions and 247 deletions

View File

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

View File

@@ -1,136 +1,244 @@
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;
-- ============================================================================
-- VIEW : playlist_videos
-- ============================================================================
DROP VIEW IF EXISTS playlist_videos;
DROP VIEW playlist_videos;
CREATE VIEW playlist_videos AS
WITH
-- Sous-requête pour les playlists manuelles
manual_playlist_videos AS (
-- 1⃣ Playlists manuelles
manual_playlists_videos AS (
SELECT
p.id AS playlist_id,
p.name AS playlist_name,
vp.video_file_name,
v.*,
'manual' AS playlist_type
'manual' AS playlist_type,
vp.video_file_name
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 (
-- 2⃣ Dynamiques sans include/exclude
dynamic_playlist_videos_base 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
'dynamic' AS playlist_type,
v.file_name AS video_file_name
FROM playlists p
JOIN videos v
WHERE p.type = 'dynamic'
-- Inclure les vidéos qui ont les labels requis
-- Labels inclus (AND/OR)
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 || ']'
)
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
)
)
)
)
-- Exclure les vidéos qui ont les labels exclus
-- 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 || ']'
)
)
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
)
-- 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
-- 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 dinclusion
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
)
-- Union des deux types de playlists
SELECT * FROM manual_playlist_videos
-- 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 * FROM dynamic_playlist_videos;
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;

236
model/views.sql Normal file
View 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;