Commit 5acf72f7 by PLN (Algolia)

feat(backlog_setlists): recover gig setlists from backlog.md → resolved .tidal

The site tracks.json covers only 23/37 gigs; the rest live in backlog.md as
codename-keyed setlist blocks. New mechanical parser cleans the noisy lines
(ASCII-art, emoji, emphasis, inline [bpm]/{transition} notes, commented-out
tracks) and resolves names to canonical .tidal paths via the catalog's own
alias index (DRY). Gig-codename→slug mapping is authored (confirm:-flagged
when uncertain); suspect_bleed flags blocks that swallowed a neighbour list.
6 gigs cleanly recovered (52 track-slots); seam is larger (dozens of setlists).
parent 6815967f
{
"schema": "backlog setlists (recovered gig→tracks; resolved to .tidal)",
"as_of": "2026-06-06",
"source": "backlog.md",
"n_gigs": 9,
"n_pending_confirm": 3,
"gigs": {
"2023/mephisteuf": {
"slug": "2023/mephisteuf",
"confirm": false,
"header": "## Live @MephisTeuf 😈‍💻😈",
"anchor_line": 710,
"n_tracks": 63,
"n_resolved": 44,
"suspect_bleed": true,
"tracks": [
{
"raw": "TOP HATS",
"track": null,
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Something about us",
"track": null,
"bpm": null,
"transition": null,
"section": "3 Live Love 3"
},
{
"raw": "Best Part",
"track": null,
"bpm": null,
"transition": null,
"section": "3 Live Love 3"
},
{
"raw": "Dream a little dream of Me",
"track": null,
"bpm": null,
"transition": null,
"section": "3 Live Love 3"
},
{
"raw": "Until I found U",
"track": null,
"bpm": null,
"transition": null,
"section": "3 Live Love 3"
},
{
"raw": "Killing me Softly",
"track": null,
"bpm": null,
"transition": null,
"section": "3 Live Love 3"
},
{
"raw": "Can't Help Falling in Love",
"track": null,
"bpm": null,
"transition": null,
"section": "3 Live Love 3"
},
{
"raw": "U remind me",
"track": null,
"bpm": null,
"transition": null,
"section": "3 Live Love 3"
},
{
"raw": "Invoque l'Été",
"track": "live/midi/nova/lounge/invoque_ete.tidal",
"bpm": null,
"transition": null,
"section": "Intro Meme l'hiver a une fin"
},
{
"raw": "Solar",
"track": "live/midi/nova/breaks/solar.tidal",
"bpm": null,
"transition": null,
"section": "Le jour est court"
},
{
"raw": "Contre Visite",
"track": "live/midi/nova/ambient/contre_visite.tidal",
"bpm": null,
"transition": null,
"section": "La nuit est LONGUE"
},
{
"raw": "Empreinte du Numerique",
"track": "live/midi/nova/ambient/empreinte_du_numerique.tidal",
"bpm": null,
"transition": null,
"section": "La nuit est LONGUE"
},
{
"raw": "Break the Loop",
"track": "live/midi/nova/dnb/break_the_loop.tidal",
"bpm": null,
"transition": null,
"section": "La nuit est LONGUE"
},
{
"raw": "Alerte Verte",
"track": "live/midi/nova/dnb/alerte_verte.tidal",
"bpm": null,
"transition": null,
"section": "La nuit est LONGUE"
},
{
"raw": "Invoque l'été",
"track": "live/midi/nova/lounge/invoque_ete.tidal",
"bpm": null,
"transition": null,
"section": "_Aperitif_"
},
{
"raw": "Solar",
"track": "live/midi/nova/breaks/solar.tidal",
"bpm": null,
"transition": null,
"section": "_Entree_"
},
{
"raw": "Lunar",
"track": "live/midi/nova/breaks/lunar.tidal",
"bpm": null,
"transition": null,
"section": "_Entree_"
},
{
"raw": "Michael",
"track": "live/midi/nova/lounge/michael.tidal",
"bpm": null,
"transition": null,
"section": "_Accompagnement_"
},
{
"raw": "Nightly Repair",
"track": "live/techno/nightly_repair.tidal",
"bpm": null,
"transition": null,
"section": "_Accompagnement_"
},
{
"raw": "Contre visite",
"track": "live/midi/nova/ambient/contre_visite.tidal",
"bpm": null,
"transition": null,
"section": "_Accompagnement_"
},
{
"raw": "Burn this Book",
"track": "live/techno/noir/burn_this_book.tidal",
"bpm": null,
"transition": null,
"section": "_Plat de resistance_"
},
{
"raw": "Break the Loop",
"track": "live/midi/nova/dnb/break_the_loop.tidal",
"bpm": null,
"transition": null,
"section": "_Dessert_"
},
{
"raw": "Atari-ght",
"track": "live/chip/ataright.tidal",
"bpm": null,
"transition": null,
"section": "_Dessert_"
},
{
"raw": "Alerte Verte",
"track": "live/midi/nova/dnb/alerte_verte.tidal",
"bpm": null,
"transition": null,
"section": "_Dernier Verre_"
},
{
"raw": "It's About Time",
"track": "live/midi/nova/breaks/its_about_time.tidal",
"bpm": null,
"transition": null,
"section": "__Digestif__"
},
{
"raw": "Premiere Grillade",
"track": "live/midi/nova/lofi/premiere_grillade.tidal",
"bpm": null,
"transition": null,
"section": "Live toi toi mon toit"
},
{
"raw": "Break Dynasty",
"track": "live/midi/nova/breaks/break_dynasty.tidal",
"bpm": null,
"transition": null,
"section": "Live toi toi mon toit"
},
{
"raw": "Clameur",
"track": "live/hip/darkside/clameur.tidal",
"bpm": null,
"transition": null,
"section": "Live toi toi mon toit"
},
{
"raw": "Nouveau Soleil",
"track": "live/midi/nova/dnb/nouveau_soleil.tidal",
"bpm": null,
"transition": null,
"section": "Live toi toi mon toit"
},
{
"raw": "It's About Time",
"track": "live/midi/nova/breaks/its_about_time.tidal",
"bpm": null,
"transition": null,
"section": "Live toi toi mon toit"
},
{
"raw": "Accueil",
"track": null,
"bpm": null,
"transition": null,
"section": "Pour _Demeter_ 3"
},
{
"raw": "Paradis perdu",
"track": null,
"bpm": null,
"transition": null,
"section": "Pour _Demeter_ 3"
},
{
"raw": "Véneration",
"track": null,
"bpm": null,
"transition": null,
"section": "Pour _Demeter_ 3"
},
{
"raw": "Idoles",
"track": null,
"bpm": null,
"transition": null,
"section": "Pour _Demeter_ 3"
},
{
"raw": "Nuit",
"track": null,
"bpm": null,
"transition": null,
"section": "Pour _Demeter_ 3"
},
{
"raw": "Bouquet",
"track": null,
"bpm": null,
"transition": null,
"section": "Pour _Demeter_ 3"
},
{
"raw": "Rite Final",
"track": null,
"bpm": null,
"transition": null,
"section": "Pour _Demeter_ 3"
},
{
"raw": "Invoque l'été",
"track": "live/midi/nova/lounge/invoque_ete.tidal",
"bpm": null,
"transition": null,
"section": "Pour _Demeter_ 3"
},
{
"raw": "Alerte VERTE",
"track": "live/midi/nova/dnb/alerte_verte.tidal",
"bpm": null,
"transition": null,
"section": "Pour _Demeter_ 3"
},
{
"raw": "Nouveau Soleil",
"track": "live/midi/nova/dnb/nouveau_soleil.tidal",
"bpm": null,
"transition": null,
"section": "CANCELED Live Orleans - Labomedia's festival fire"
},
{
"raw": "Mauerpark",
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"bpm": null,
"transition": null,
"section": "CANCELED Live Orleans - Labomedia's festival fire"
},
{
"raw": "SlowMo",
"track": "live/hip/lofi/slow_mo.tidal",
"bpm": null,
"transition": null,
"section": "PreShow with Chuck 15mn"
},
{
"raw": "Lendemain Divin",
"track": "live/midi/nova/lofi/lendemain_divin.tidal",
"bpm": null,
"transition": null,
"section": "PreShow with Chuck 15mn"
},
{
"raw": "Invoque l'été",
"track": "live/midi/nova/lounge/invoque_ete.tidal",
"bpm": null,
"transition": null,
"section": "PreShow with Chuck 15mn"
},
{
"raw": "Before",
"track": null,
"bpm": null,
"transition": null,
"section": "Sessions Break 15mn"
},
{
"raw": "Nouveau Soleil",
"track": "live/midi/nova/dnb/nouveau_soleil.tidal",
"bpm": null,
"transition": null,
"section": "Sessions Break 15mn"
},
{
"raw": "Premiere Grillade",
"track": "live/midi/nova/lofi/premiere_grillade.tidal",
"bpm": null,
"transition": null,
"section": "Sessions Break 15mn"
},
{
"raw": "Été à Mauerpark",
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"bpm": null,
"transition": null,
"section": "Sessions Break 15mn"
},
{
"raw": "Nouveau soleil",
"track": "live/midi/nova/dnb/nouveau_soleil.tidal",
"bpm": null,
"transition": null,
"section": "REPAIR MULTILINE"
},
{
"raw": "Prestance",
"track": "live/midi/nova/ambient/prestance.tidal",
"bpm": null,
"transition": null,
"section": "Adapt to DAW removing midiG calls"
},
{
"raw": "Oct4 Glitch Sauvages",
"track": "live/midi/nova/beatober/oct_glitchs_sauvages.tidal",
"bpm": null,
"transition": null,
"section": "Adapt to DAW removing midiG calls"
},
{
"raw": "About time",
"track": "live/midi/nova/breaks/its_about_time.tidal",
"bpm": null,
"transition": null,
"section": "Adapt to DAW removing midiG calls"
},
{
"raw": "Toxic",
"track": "live/boeuf/algorythm/fdlm2022/toxic.tidal",
"bpm": null,
"transition": null,
"section": "Adapt to DAW removing midiG calls"
},
{
"raw": "Venons ensemble",
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"bpm": null,
"transition": null,
"section": "Adapt to DAW removing midiG calls"
},
{
"raw": "Reboot",
"track": "live/midi/nova/lofi/reboot.tidal",
"bpm": null,
"transition": null,
"section": "READY FOR MIX"
},
{
"raw": "Premiere Grillade",
"track": "live/midi/nova/lofi/premiere_grillade.tidal",
"bpm": null,
"transition": null,
"section": "READY FOR MIX"
},
{
"raw": "Rainy day",
"track": "live/collab/josh/oct_18_rainy_day.tidal",
"bpm": null,
"transition": null,
"section": "READY FOR MIX"
},
{
"raw": "Rainy day",
"track": "live/collab/josh/oct_18_rainy_day.tidal",
"bpm": null,
"transition": null,
"section": "READY FOR TAKEOFF"
},
{
"raw": "Bain bouillant",
"track": "live/midi/nova/nujazz/cafe_bouillant.tidal",
"bpm": null,
"transition": null,
"section": "VELOCITE"
},
{
"raw": "Acid RAIN",
"track": null,
"bpm": null,
"transition": null,
"section": "VELOCITE"
},
{
"raw": "No Time to Die",
"track": null,
"bpm": null,
"transition": null,
"section": "VELOCITE"
},
{
"raw": "Haunted house",
"track": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"bpm": null,
"transition": null,
"section": "VELOCITE"
},
{
"raw": "Finale",
"track": null,
"bpm": null,
"transition": null,
"section": "VELOCITE"
}
]
},
"2024/opal-festival-2024": {
"slug": "2024/opal-festival-2024",
"confirm": false,
"header": "## Opal <3🦉",
"anchor_line": 1031,
"n_tracks": 12,
"n_resolved": 11,
"suspect_bleed": false,
"tracks": [
{
"raw": "Cafe Glacé",
"track": "live/midi/nova/nujazz/cafe_glace.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Café Bouillant",
"track": "live/midi/nova/nujazz/cafe_bouillant.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Cafe Tiède",
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Force MOTRICE",
"track": "live/midi/nova/dnb/force_motrice.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Alerte verte",
"track": "live/midi/nova/dnb/alerte_verte.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Nouveau Punk",
"track": "live/collab/raph/nouveau_punk.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Jeudrill",
"track": "live/collab/raph/jeudrill.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Bain Bouillant",
"track": "live/midi/nova/nujazz/cafe_bouillant.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Haunted House",
"track": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Blue Gold",
"track": "live/collab/ccc/ccc0.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "L'été à MauerPark",
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Passation",
"track": null,
"bpm": null,
"transition": null,
"section": null
}
]
},
"2024/toplap-solstice-2024": {
"slug": "2024/toplap-solstice-2024",
"confirm": false,
"header": "### TopLap Solstice Stream 2024",
"anchor_line": 1169,
"n_tracks": 4,
"n_resolved": 3,
"suspect_bleed": false,
"tracks": [
{
"raw": "Deck the Hall",
"track": null,
"bpm": null,
"transition": null,
"section": "Tracklist"
},
{
"raw": "Something about DRUMS",
"track": "live/midi/nova/dnb/something_about_drums.tidal",
"bpm": null,
"transition": null,
"section": "Tracklist"
},
{
"raw": "La fin de l'insouciance",
"track": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"bpm": null,
"transition": null,
"section": "Tracklist"
},
{
"raw": "Blue Gold",
"track": "live/collab/ccc/ccc0.tidal",
"bpm": null,
"transition": null,
"section": "Tracklist"
}
]
},
"2024/38c3-house-of-tea": {
"slug": "2024/38c3-house-of-tea",
"confirm": false,
"header": "#### Day 2 - HOUSE OF Tea",
"anchor_line": 1194,
"n_tracks": 12,
"n_resolved": 11,
"suspect_bleed": false,
"tracks": [
{
"raw": "Contre Visite",
"track": "live/midi/nova/ambient/contre_visite.tidal",
"bpm": null,
"transition": null,
"section": "TRACKLIST"
},
{
"raw": "Bain Electrique",
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"bpm": null,
"transition": null,
"section": "TRACKLIST"
},
{
"raw": "Salut Nu",
"track": "live/midi/nova/nujazz/salut_nu.tidal",
"bpm": null,
"transition": null,
"section": "TRACKLIST"
},
{
"raw": "Cafe Tiede",
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"bpm": null,
"transition": null,
"section": "TRACKLIST"
},
{
"raw": "Force Motrice",
"track": "live/midi/nova/dnb/force_motrice.tidal",
"bpm": null,
"transition": null,
"section": "TRACKLIST"
},
{
"raw": "Cafe Glace",
"track": "live/midi/nova/nujazz/cafe_glace.tidal",
"bpm": null,
"transition": null,
"section": "TRACKLIST"
},
{
"raw": "CCC0",
"track": null,
"bpm": null,
"transition": null,
"section": "TRACKLIST"
},
{
"raw": "ACIDULE",
"track": "live/collab/raph/acidule.tidal",
"bpm": null,
"transition": null,
"section": "TRACKLIST"
},
{
"raw": "Something about Drums",
"track": "live/midi/nova/dnb/something_about_drums.tidal",
"bpm": null,
"transition": null,
"section": "TRACKLIST"
},
{
"raw": "Alerte Verte",
"track": "live/midi/nova/dnb/alerte_verte.tidal",
"bpm": null,
"transition": null,
"section": "TRACKLIST"
},
{
"raw": "Permanence",
"track": "live/collab/raph/permanence.tidal",
"bpm": null,
"transition": null,
"section": "TRACKLIST"
},
{
"raw": "L'or Bleu",
"track": "live/collab/mousquetaires/blue_gold.tidal",
"bpm": null,
"transition": null,
"section": "TRACKLIST"
}
]
},
"2024/38c3-toilet": {
"slug": "2024/38c3-toilet",
"confirm": false,
"header": "#### Day 3 - Hardcore in the Toilet Club",
"anchor_line": 1242,
"n_tracks": 21,
"n_resolved": 12,
"suspect_bleed": true,
"tracks": [
{
"raw": "GHOSTS IN THE TOILETS",
"track": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "NOUVEAU PUNK",
"track": "live/collab/raph/nouveau_punk.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "PITBULL PUNK",
"track": null,
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "ACIDULE",
"track": "live/collab/raph/acidule.tidal",
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Si Bon 142bpm",
"track": null,
"bpm": 142,
"transition": null,
"section": "Samedi au Paradis"
},
{
"raw": "Because it's there 110bpm",
"track": null,
"bpm": 110,
"transition": null,
"section": "Samedi au Paradis"
},
{
"raw": "L'amour d'abord 120bpm",
"track": null,
"bpm": 120,
"transition": null,
"section": "Samedi au Paradis"
},
{
"raw": "Ton Numero",
"track": "live/midi/nova/breaks/ton_numero.tidal",
"bpm": null,
"transition": null,
"section": "Boboche Session2"
},
{
"raw": "Because it's there",
"track": "copycat/because_its_there.tidal",
"bpm": null,
"transition": null,
"section": "Boboche Session2"
},
{
"raw": "Sunny Side Up",
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"bpm": null,
"transition": null,
"section": "Boboche Session2"
},
{
"raw": "Larmes Salees",
"track": null,
"bpm": null,
"transition": null,
"section": "Boboche Session2"
},
{
"raw": "Blues du Mic",
"track": null,
"bpm": null,
"transition": null,
"section": "Boboche Session2"
},
{
"raw": "Sept1",
"track": "live/collab/baba/sept1.tidal",
"bpm": null,
"transition": null,
"section": "Boboche Session2"
},
{
"raw": "Ton Numero",
"track": "live/midi/nova/breaks/ton_numero.tidal",
"bpm": null,
"transition": null,
"section": "ALGOLIA's CKO 2025"
},
{
"raw": "Paris you want",
"track": null,
"bpm": null,
"transition": null,
"section": "ALGOLIA's CKO 2025"
},
{
"raw": "Sunny Side Up",
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"bpm": null,
"transition": null,
"section": "ALGOLIA's CKO 2025"
},
{
"raw": "Cafe Glace",
"track": "live/midi/nova/nujazz/cafe_glace.tidal",
"bpm": null,
"transition": null,
"section": "ALGOLIA's CKO 2025"
},
{
"raw": "Cafe Bouillant",
"track": "live/midi/nova/nujazz/cafe_bouillant.tidal",
"bpm": null,
"transition": null,
"section": "ALGOLIA's CKO 2025"
},
{
"raw": "Force motrice",
"track": "live/midi/nova/dnb/force_motrice.tidal",
"bpm": null,
"transition": null,
"section": "ALGOLIA's CKO 2025"
},
{
"raw": "Sept1 Mode mineur",
"track": null,
"bpm": null,
"transition": null,
"section": "ALGOLIA's CKO 2025"
},
{
"raw": "Finish",
"track": null,
"bpm": null,
"transition": null,
"section": "ALGOLIA's CKO 2025"
}
]
},
"2025/ensad": {
"slug": "2025/ensad",
"confirm": false,
"header": "# Set @ENSAD 2025",
"anchor_line": 1441,
"n_tracks": 9,
"n_resolved": 7,
"suspect_bleed": false,
"tracks": [
{
"raw": "Quand on décolle",
"track": "live/midi/nova/ambient/quand_on_decolle.tidal",
"bpm": null,
"transition": "0 - / 11",
"section": null
},
{
"raw": "Parce qu'Elle est là",
"track": null,
"bpm": null,
"transition": "7",
"section": null
},
{
"raw": "Ere de jeu",
"track": "live/midi/nova/techno/ere_de_jeu.tidal",
"bpm": null,
"transition": "123- 9",
"section": null
},
{
"raw": "l'Été à Mauerpark",
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"bpm": null,
"transition": "9",
"section": null
},
{
"raw": "Sept1",
"track": "live/collab/baba/sept1.tidal",
"bpm": null,
"transition": "11",
"section": null
},
{
"raw": "Sunny Side Up",
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"bpm": null,
"transition": "11 /",
"section": null
},
{
"raw": "L'or Bleu",
"track": "live/collab/mousquetaires/blue_gold.tidal",
"bpm": null,
"transition": "5",
"section": null
},
{
"raw": "Cafe Tiede",
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"bpm": null,
"transition": "5",
"section": null
},
{
"raw": "Finale",
"track": null,
"bpm": null,
"transition": null,
"section": "- Atelier de Force Motrice - 57"
}
]
},
"2025/opal-festival-2025": {
"slug": "2025/opal-festival-2025",
"confirm": false,
"header": "# OPAL 2025",
"anchor_line": 1632,
"n_tracks": 78,
"n_resolved": 49,
"suspect_bleed": true,
"tracks": [
{
"raw": "Drifting soul",
"track": "live/collab/jane/drifting_soul.tidal",
"bpm": 80,
"transition": "10+5->4+11",
"section": "INTRO"
},
{
"raw": "Lendemain Divin",
"track": "live/midi/nova/lofi/lendemain_divin.tidal",
"bpm": 95,
"transition": "4,5,6->6",
"section": "INTRO"
},
{
"raw": "Quand on Décolle",
"track": "live/midi/nova/ambient/quand_on_decolle.tidal",
"bpm": 120,
"transition": "11+12->11",
"section": "INTRO"
},
{
"raw": "The Secret",
"track": null,
"bpm": 80,
"transition": "11.3->3.12",
"section": "LA DRILL BEBE"
},
{
"raw": "Jeudi Drill",
"track": "live/collab/raph/jeudrill.tidal",
"bpm": 70,
"transition": "3..10->10..3",
"section": "LA DRILL BEBE"
},
{
"raw": "1er Septembre Pour Elle",
"track": null,
"bpm": 60,
"transition": "9.11.->5",
"section": "LE SOMMET"
},
{
"raw": "Sunny Side Up",
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"bpm": 120,
"transition": "4.5->5..11...",
"section": "LE SOMMET"
},
{
"raw": "Chere Mireille",
"track": null,
"bpm": 120,
"transition": "11+6..4",
"section": "LE SOMMET"
},
{
"raw": "So G00D",
"track": null,
"bpm": 140,
"transition": "3.4->11...",
"section": "LE SOMMET"
},
{
"raw": "Nuit d'Orage",
"track": "live/midi/nova/techno/techno_orage.tidal",
"bpm": 104,
"transition": "11.3.4->",
"section": "LE SOMMET"
},
{
"raw": "Something About Drums",
"track": "live/midi/nova/dnb/something_about_drums.tidal",
"bpm": 160,
"transition": "10.5..9->5.9",
"section": "LE SOMMET"
},
{
"raw": "Ton Numero",
"track": "live/midi/nova/breaks/ton_numero.tidal",
"bpm": 99,
"transition": "RTFM",
"section": "REDESCENTE"
},
{
"raw": "Finale",
"track": null,
"bpm": 138,
"transition": null,
"section": "REDESCENTE"
},
{
"raw": "JEROME",
"track": null,
"bpm": null,
"transition": null,
"section": "REDESCENTE"
},
{
"raw": "Cafes du plus chaud au plus froid",
"track": null,
"bpm": null,
"transition": null,
"section": "REDESCENTE"
},
{
"raw": "El Mundo Interior",
"track": null,
"bpm": null,
"transition": null,
"section": "INTRO LATINO"
},
{
"raw": "Fabuleux",
"track": "live/midi/nova/lounge/fabuleux.tidal",
"bpm": null,
"transition": null,
"section": "INTRO LATINO"
},
{
"raw": "Contre Visite",
"track": "live/midi/nova/ambient/contre_visite.tidal",
"bpm": null,
"transition": null,
"section": "INTRO LATINO"
},
{
"raw": "Jeudi Drill",
"track": "live/collab/raph/jeudrill.tidal",
"bpm": null,
"transition": null,
"section": "INTRO LATINO"
},
{
"raw": "Ouais je Funk",
"track": null,
"bpm": null,
"transition": null,
"section": "BASS MUSIC BROTHERS"
},
{
"raw": "WAP",
"track": "live/midi/nova/dnb/wap.tidal",
"bpm": null,
"transition": null,
"section": "BASS MUSIC BROTHERS"
},
{
"raw": "Sunny Side Up",
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"bpm": null,
"transition": null,
"section": "BASS MUSIC BROTHERS"
},
{
"raw": "Sept1",
"track": "live/collab/baba/sept1.tidal",
"bpm": null,
"transition": null,
"section": "BASS MUSIC BROTHERS"
},
{
"raw": "Finale",
"track": null,
"bpm": null,
"transition": null,
"section": "BASS MUSIC BROTHERS"
},
{
"raw": "Contre Visite",
"track": "live/midi/nova/ambient/contre_visite.tidal",
"bpm": null,
"transition": null,
"section": "NON-ANNIVERSAIRE"
},
{
"raw": "Orage",
"track": "live/midi/nova/techno/techno_orage.tidal",
"bpm": null,
"transition": null,
"section": "NON-ANNIVERSAIRE"
},
{
"raw": "Ouais je Funk",
"track": null,
"bpm": null,
"transition": null,
"section": "NON-ANNIVERSAIRE"
},
{
"raw": "WAP",
"track": "live/midi/nova/dnb/wap.tidal",
"bpm": null,
"transition": null,
"section": "NON-ANNIVERSAIRE"
},
{
"raw": "WET HARD PUSSY",
"track": null,
"bpm": null,
"transition": null,
"section": "NON-ANNIVERSAIRE"
},
{
"raw": "Nouveau PUNK",
"track": "live/collab/raph/nouveau_punk.tidal",
"bpm": null,
"transition": null,
"section": "NON-ANNIVERSAIRE"
},
{
"raw": "PUNKACHIEN",
"track": "live/collab/raph/punkachien.tidal",
"bpm": null,
"transition": null,
"section": "NON-ANNIVERSAIRE"
},
{
"raw": "Ghosts in the Toilets",
"track": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"bpm": null,
"transition": null,
"section": "NON-ANNIVERSAIRE"
},
{
"raw": "Sept1",
"track": "live/collab/baba/sept1.tidal",
"bpm": null,
"transition": null,
"section": "NON-ANNIVERSAIRE"
},
{
"raw": "JeuDrill",
"track": "live/collab/raph/jeudrill.tidal",
"bpm": null,
"transition": null,
"section": "INTRO FEERIE DU VOYAGE"
},
{
"raw": "Sunny Side Up",
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"bpm": null,
"transition": null,
"section": "INTRO FEERIE DU VOYAGE"
},
{
"raw": "Savoir Voyager",
"track": null,
"bpm": null,
"transition": null,
"section": "INTRO FEERIE DU VOYAGE"
},
{
"raw": "Menthe Givrée",
"track": null,
"bpm": null,
"transition": null,
"section": "INTRO FEERIE DU VOYAGE"
},
{
"raw": "Ouais je funk",
"track": null,
"bpm": null,
"transition": null,
"section": "COEUR FEERIE NOCTURNE"
},
{
"raw": "WAP",
"track": "live/midi/nova/dnb/wap.tidal",
"bpm": null,
"transition": null,
"section": "COEUR FEERIE NOCTURNE"
},
{
"raw": "Long Way",
"track": "live/collab/raph/long_way.tidal",
"bpm": null,
"transition": null,
"section": "COEUR FEERIE NOCTURNE"
},
{
"raw": "Esperluette",
"track": "live/collab/raph/esperluette.tidal",
"bpm": null,
"transition": null,
"section": "COEUR FEERIE NOCTURNE"
},
{
"raw": "Piment Bresilien",
"track": "live/collab/raph/piment_bresilien.tidal",
"bpm": null,
"transition": null,
"section": "COEUR FEERIE NOCTURNE"
},
{
"raw": "Biscuit Acide",
"track": "live/collab/raph/biscuit_acide.tidal",
"bpm": null,
"transition": null,
"section": "COEUR FEERIE NOCTURNE"
},
{
"raw": "Des Efforts",
"track": "live/collab/raph/des_efforts.tidal",
"bpm": null,
"transition": null,
"section": "COEUR FEERIE NOCTURNE"
},
{
"raw": "AUXiliaire",
"track": null,
"bpm": null,
"transition": null,
"section": "COEUR FEERIE NOCTURNE"
},
{
"raw": "Quand on Decolle",
"track": "live/midi/nova/ambient/quand_on_decolle.tidal",
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "Fabuleux",
"track": "live/midi/nova/lounge/fabuleux.tidal",
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "Paris",
"track": "live/midi/nova/breaks/madeleine_de_paris.tidal",
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "Liquid Finale",
"track": null,
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "Sunshine",
"track": null,
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "WAP",
"track": "live/midi/nova/dnb/wap.tidal",
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "The Secret",
"track": null,
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "Bain Electrique",
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "La Fin de l'insouciance",
"track": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "Piment Bresilien",
"track": "live/collab/raph/piment_bresilien.tidal",
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "PERFECT",
"track": null,
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "L'or Bleu",
"track": "live/collab/mousquetaires/blue_gold.tidal",
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "Sunny Side Up",
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "L'été à MauerPark",
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"bpm": null,
"transition": null,
"section": "2025 11 SovieTeuf"
},
{
"raw": "Sept",
"track": null,
"bpm": null,
"transition": "1",
"section": "2025 11 SovieTeuf"
},
{
"raw": "Quand on decolle",
"track": "live/midi/nova/ambient/quand_on_decolle.tidal",
"bpm": null,
"transition": null,
"section": "NOUVEAU Novembre set"
},
{
"raw": "Liquid Finale",
"track": null,
"bpm": null,
"transition": null,
"section": "NOUVEAU Novembre set"
},
{
"raw": "Sept1",
"track": "live/collab/baba/sept1.tidal",
"bpm": null,
"transition": null,
"section": "NOUVEAU Novembre set"
},
{
"raw": "I'M PERFECT",
"track": null,
"bpm": null,
"transition": null,
"section": "NOUVEAU Novembre set"
},
{
"raw": "SoGood",
"track": "live/collab/nass/sogood.tidal",
"bpm": null,
"transition": null,
"section": "NOUVEAU Novembre set"
},
{
"raw": "Perfect Party",
"track": null,
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "WAP",
"track": "live/midi/nova/dnb/wap.tidal",
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "Piment Bresilien",
"track": "live/collab/raph/piment_bresilien.tidal",
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "JeuDrill",
"track": "live/collab/raph/jeudrill.tidal",
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "L'or Bleu",
"track": "live/collab/mousquetaires/blue_gold.tidal",
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "Sept1",
"track": "live/collab/baba/sept1.tidal",
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "Sunny Side Up",
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "My Sunshine",
"track": null,
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "L'été à MauerPark",
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "TECHNOLIGARCHIE",
"track": null,
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "My Sunshine",
"track": null,
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "Suns of Gold",
"track": null,
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
},
{
"raw": "END",
"track": null,
"bpm": null,
"transition": null,
"section": "Day 1 - Secret Toilet Rave"
}
]
},
"2025/39c3-house-of-tea": {
"slug": "2025/39c3-house-of-tea",
"confirm": false,
"header": "# CCC 39c3 ALGORAVE <3",
"anchor_line": 1786,
"n_tracks": 14,
"n_resolved": 12,
"suspect_bleed": false,
"tracks": [
{
"raw": "Quand on décolle",
"track": "live/midi/nova/ambient/quand_on_decolle.tidal",
"bpm": null,
"transition": null,
"section": "INTRO"
},
{
"raw": "Because it's there",
"track": "copycat/because_its_there.tidal",
"bpm": null,
"transition": null,
"section": "INTRO"
},
{
"raw": "Ere de jeu",
"track": "live/midi/nova/techno/ere_de_jeu.tidal",
"bpm": null,
"transition": null,
"section": "INTRO"
},
{
"raw": "Sunny",
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"bpm": null,
"transition": null,
"section": "TECHNO HIGH"
},
{
"raw": "TechnOrage",
"track": null,
"bpm": null,
"transition": null,
"section": "TECHNO HIGH"
},
{
"raw": "L'Ete a Mauerpark",
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"bpm": null,
"transition": null,
"section": "TECHNO HIGH"
},
{
"raw": "Piment Bresilien",
"track": "live/collab/raph/piment_bresilien.tidal",
"bpm": null,
"transition": null,
"section": "TECHNO HIGH"
},
{
"raw": "Venons Ensemble",
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"bpm": null,
"transition": null,
"section": "TECHNO HIGH"
},
{
"raw": "Paris you want",
"track": null,
"bpm": null,
"transition": null,
"section": "DNBish"
},
{
"raw": "Cafe Bouillant",
"track": "live/midi/nova/nujazz/cafe_bouillant.tidal",
"bpm": null,
"transition": null,
"section": "Nujazz Low"
},
{
"raw": "Cafe Tiede",
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"bpm": null,
"transition": null,
"section": "Nujazz Low"
},
{
"raw": "Force motrice",
"track": "live/midi/nova/dnb/force_motrice.tidal",
"bpm": null,
"transition": null,
"section": "Nujazz Low"
},
{
"raw": "Cafe Glacé",
"track": "live/midi/nova/nujazz/cafe_glace.tidal",
"bpm": null,
"transition": null,
"section": "Nujazz Low"
},
{
"raw": "Sept1",
"track": "live/collab/baba/sept1.tidal",
"bpm": null,
"transition": null,
"section": "OUTRO"
}
]
},
"2026/le-vortex": {
"slug": "2026/le-vortex",
"confirm": false,
"header": "# VORTEX26",
"anchor_line": 1816,
"n_tracks": 9,
"n_resolved": 8,
"suspect_bleed": false,
"tracks": [
{
"raw": "Quand on décolle",
"track": "live/midi/nova/ambient/quand_on_decolle.tidal",
"bpm": null,
"transition": null,
"section": "INTRO"
},
{
"raw": "Because it's there",
"track": "copycat/because_its_there.tidal",
"bpm": null,
"transition": null,
"section": "INTRO"
},
{
"raw": "Ere de jeu",
"track": "live/midi/nova/techno/ere_de_jeu.tidal",
"bpm": null,
"transition": null,
"section": "INTRO"
},
{
"raw": "Take 5 Drops",
"track": "live/midi/nova/lounge/take_5_drops.tidal",
"bpm": null,
"transition": null,
"section": "TECHNO HIGH"
},
{
"raw": "Sunny",
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"bpm": null,
"transition": null,
"section": "TECHNO HIGH"
},
{
"raw": "TechnOrage",
"track": null,
"bpm": null,
"transition": null,
"section": "TECHNO HIGH"
},
{
"raw": "L'Ete a Mauerpark",
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"bpm": null,
"transition": null,
"section": "TECHNO HIGH"
},
{
"raw": "Piment Bresilien",
"track": "live/collab/raph/piment_bresilien.tidal",
"bpm": null,
"transition": null,
"section": "TECHNO HIGH"
},
{
"raw": "Venons Ensemble",
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"bpm": null,
"transition": null,
"section": "TECHNO HIGH"
}
]
}
},
"pending": {
"2024/algolia-fdlm": {
"slug": "2024/algolia-fdlm",
"confirm": true,
"header": "## Algolia FDLM2024",
"anchor_line": 988,
"n_tracks": 26,
"n_resolved": 25,
"suspect_bleed": true,
"tracks": [
{
"raw": "Lendemain Divin",
"track": "live/midi/nova/lofi/lendemain_divin.tidal",
"bpm": null,
"transition": null,
"section": "Intro"
},
{
"raw": "Contre Visite",
"track": "live/midi/nova/ambient/contre_visite.tidal",
"bpm": null,
"transition": null,
"section": "2022 throwback"
},
{
"raw": "Nightly Repair",
"track": "live/techno/nightly_repair.tidal",
"bpm": null,
"transition": null,
"section": "2022 throwback"
},
{
"raw": "Invoque l'Été",
"track": "live/midi/nova/lounge/invoque_ete.tidal",
"bpm": null,
"transition": null,
"section": "2022 throwback"
},
{
"raw": "Venons Ensemble",
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"bpm": null,
"transition": null,
"section": "2024 new thingies"
},
{
"raw": "JeuDrill",
"track": "live/collab/raph/jeudrill.tidal",
"bpm": null,
"transition": null,
"section": "2024 new thingies"
},
{
"raw": "PERMANENCE",
"track": "live/collab/raph/permanence.tidal",
"bpm": null,
"transition": null,
"section": "2024 new thingies"
},
{
"raw": "Force motrice",
"track": "live/midi/nova/dnb/force_motrice.tidal",
"bpm": null,
"transition": null,
"section": "2024 new thingies"
},
{
"raw": "Café Tiède",
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"bpm": null,
"transition": null,
"section": "2024 new thingies"
},
{
"raw": "Café Bouillant",
"track": "live/midi/nova/nujazz/cafe_bouillant.tidal",
"bpm": null,
"transition": null,
"section": "2024 new thingies"
},
{
"raw": "Café Glaçé",
"track": "live/midi/nova/nujazz/cafe_glace.tidal",
"bpm": null,
"transition": null,
"section": "2024 new thingies"
},
{
"raw": "Salut Nu",
"track": "live/midi/nova/nujazz/salut_nu.tidal",
"bpm": null,
"transition": null,
"section": "2024 new thingies"
},
{
"raw": "Nuit agitée",
"track": "live/midi/nova/breaks/nuit_agitee.tidal",
"bpm": null,
"transition": null,
"section": "2024 new thingies"
},
{
"raw": "MauerPark",
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"bpm": null,
"transition": null,
"section": "2024 new thingies"
},
{
"raw": "Fabuleux",
"track": "live/midi/nova/lounge/fabuleux.tidal",
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
},
{
"raw": "Blue Gold",
"track": "live/collab/ccc/ccc0.tidal",
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
},
{
"raw": "Haunted House",
"track": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
},
{
"raw": "Force Motrice",
"track": "live/midi/nova/dnb/force_motrice.tidal",
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
},
{
"raw": "Cafe Tiede",
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
},
{
"raw": "Cafe Bouillant",
"track": "live/midi/nova/nujazz/cafe_bouillant.tidal",
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
},
{
"raw": "Lendemain Divin",
"track": "live/midi/nova/lofi/lendemain_divin.tidal",
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
},
{
"raw": "Invoque l'été",
"track": "live/midi/nova/lounge/invoque_ete.tidal",
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
},
{
"raw": "Nuit agitée",
"track": "live/midi/nova/breaks/nuit_agitee.tidal",
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
},
{
"raw": "L'été à MauerPark",
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
},
{
"raw": "Permanence",
"track": "live/collab/raph/permanence.tidal",
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
},
{
"raw": "Encore",
"track": null,
"bpm": null,
"transition": null,
"section": "Sur le Toit 3"
}
]
},
"2025/algorave-lyon": {
"slug": "2025/algorave-lyon",
"confirm": true,
"header": "⠀⠀⠀⠹⣟⠀⠀⠀ GZ 2025 LYON ⠀⣰⠏⠀⠀",
"anchor_line": 1550,
"n_tracks": 0,
"n_resolved": 0,
"suspect_bleed": false,
"tracks": []
},
"2026/ete-surprise": {
"slug": "2026/ete-surprise",
"confirm": true,
"header": "## SURPRISE IT'S A DANCEFLOOR",
"anchor_line": 1831,
"n_tracks": 11,
"n_resolved": 7,
"suspect_bleed": false,
"tracks": [
{
"raw": "Perfect PARTY",
"track": null,
"bpm": null,
"transition": null,
"section": null
},
{
"raw": "Paris you want",
"track": null,
"bpm": null,
"transition": null,
"section": "DNBish"
},
{
"raw": "Plosive1",
"track": null,
"bpm": null,
"transition": null,
"section": "DNBish"
},
{
"raw": "L'amour d'Abord",
"track": null,
"bpm": null,
"transition": null,
"section": "DNBish"
},
{
"raw": "Cafe Bouillant",
"track": "live/midi/nova/nujazz/cafe_bouillant.tidal",
"bpm": null,
"transition": null,
"section": "Nujazz Low"
},
{
"raw": "Cafe Tiede",
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"bpm": null,
"transition": null,
"section": "Nujazz Low"
},
{
"raw": "Force motrice",
"track": "live/midi/nova/dnb/force_motrice.tidal",
"bpm": null,
"transition": null,
"section": "Nujazz Low"
},
{
"raw": "Cafe Glacé",
"track": "live/midi/nova/nujazz/cafe_glace.tidal",
"bpm": null,
"transition": null,
"section": "Nujazz Low"
},
{
"raw": "Sept1",
"track": "live/collab/baba/sept1.tidal",
"bpm": null,
"transition": null,
"section": "OUTRO"
},
{
"raw": "Ton Numero",
"track": "live/midi/nova/breaks/ton_numero.tidal",
"bpm": null,
"transition": null,
"section": "Rappel Bonus"
},
{
"raw": "Contre Visite",
"track": "live/midi/nova/ambient/contre_visite.tidal",
"bpm": null,
"transition": null,
"section": "Rappel Bonus"
}
]
}
}
}
\ No newline at end of file
#!/usr/bin/env python3
"""Recover gig setlists from backlog.md — the informal but canonical source PLN keeps.
The site `tracks.json` only covers 23/37 gigs; the rest live in `backlog.md` as
codename-keyed setlist blocks (e.g. `# VORTEX26`, `## Opal <3🦉`, `# Set @ENSAD 2025`).
Those blocks are noisy: ASCII-art banners, emoji, markdown emphasis, inline `[bpm]` and
`{transition-routing}` annotations, HTML-commented (dropped) tracks, section sub-headers.
This parser is mechanical (parsers-over-copy): it extracts each anchored block, cleans
the track lines, and RESOLVES names to canonical `.tidal` paths via the catalog's own
alias index (DRY — same names that feed the triangle). Gig-codename→site-slug mapping is
risky to guess, so it lives in an AUTHORED anchor table; uncertain anchors are flagged
`confirm:` and excluded from the emitted artifact until PLN confirms.
Emits `backlog_setlists.json`: per gig → resolved tracks (+ raw line, bpm, transition, section),
unmatched names surfaced honestly. Never invents a track that doesn't resolve.
"""
import json
import re
import sys
import unicodedata
from pathlib import Path
HERE = Path(__file__).parent
BACKLOG = HERE.parent.parent / "backlog.md"
CATALOG_VIEW = HERE / "catalog_view.json"
OUT = HERE / "backlog_setlists.json"
AS_OF = "2026-06-06"
# ── Authored gig anchors: (1-based line of the header in backlog.md) → site slug ──
# `confirm:`-prefixed slugs are NOT yet trusted (ambiguous header / needs PLN's ear);
# they are parsed and reported but withheld from the emitted artifact.
ANCHORS = {
1441: "2025/ensad", # "# Set @ENSAD 2025"
1816: "2026/le-vortex", # "# VORTEX26"
1031: "2024/opal-festival-2024", # "## Opal <3🦉" / OPAL 2024
1632: "2025/opal-festival-2025", # "# OPAL 2025"
710: "2023/mephisteuf", # "## Live @MephisTeuf"
1194: "2024/38c3-house-of-tea", # "#### Day 2 - HOUSE OF Tea" (under 38C3)
1242: "2024/38c3-toilet", # "#### Day 3 - Hardcore in the Toilet Club"
1786: "2025/39c3-house-of-tea", # "# CCC 39c3 ALGORAVE ... Pour Un Thé Dansant"
1169: "2024/toplap-solstice-2024", # "### TopLap Solstice Stream 2024"
988: "confirm:2024/algolia-fdlm", # "## Algolia FDLM2024" — already has tracks.json
1550: "confirm:2025/algorave-lyon", # "GZ 2025 LYON ALGORAVE" then "# LABENNE LIVE" (?)
1831: "confirm:2026/ete-surprise", # "## SURPRISE IT'S A DANCEFLOOR" — slug guess
}
# Gigs still unanchored (no clear backlog block found): 2022/algolia-fdlm-2022,
# 2024/38c3-chaos-music-club, 2025/39c3-toilet-rave, 2025/toplap-fromscratch-2025.
# Lines that are decoration / structure, never tracks.
_ASCII_ART = re.compile(r"[⠀-⣿▓▒░█▀▄■◆◢◣◤◥╔╗╚╝║═]")
_SECTION_HINT = re.compile(
r"^(intro|outro|techno|dnb|nujazz|drill|drums|tempête|tempete|sommet|"
r"surprise|la\s|le\s|les\s|day\s|samedi|mission|rappel|bonus|final|"
r"pause|sunset|sunrise|darkness)\b", re.I)
def deaccent(s: str) -> str:
return "".join(c for c in unicodedata.normalize("NFKD", s)
if not unicodedata.combining(c))
def norm_name(s: str) -> str:
"""Normalize a track name for matching: deaccent, lowercase, alnum-only."""
return re.sub(r"[^a-z0-9]+", "", deaccent(s).lower())
def build_name_index():
"""norm(name) → .tidal path, from the catalog's existing alias set (DRY)."""
cv = json.load(open(CATALOG_VIEW))
idx = {}
for t in cv["tracks"]:
for nm in t["names"]:
idx.setdefault(norm_name(nm), t["track"])
return idx, cv
def clean_track_line(raw: str):
"""Return (name, bpm, transition, ok) from a raw list line, or ok=False to skip."""
s = raw.rstrip("\n")
if "<!--" in s or "-->" in s: # commented-out track → dropped
return None, None, None, False
if _ASCII_ART.search(s): # banner art
return None, None, None, False
m = re.match(r"\s*[-+*]+\s*(.+)$", s) # must be a list item
if not m:
return None, None, None, False
body = m.group(1)
body = re.sub(r"<+/?3+", " ", body) # <3 / <33 hearts → space (else leak a stray digit)
# pull inline annotations before stripping punctuation
bpm = None
bm = re.search(r"\[?\b(\d{2,3})\s*bpm", body, re.I) or re.search(r"\[(\d{2,3})(?:[-.]\d{2,3})*\]", body)
if bm:
bpm = int(bm.group(1))
transition = None
om = re.search(r"\{([^}]+)\}", body)
if om:
transition = om.group(1).strip()
# strip annotations, emphasis, emoji, trailing decoration
body = re.sub(r"\{[^}]*\}", "", body)
body = re.sub(r"\[[^\]]*\]", "", body)
body = re.sub(r"\([^)]*\)", "", body) # (Chloe cover) etc
body = re.sub(r"\bfeat\b.*$", "", body, flags=re.I)
body = body.replace("*", "").replace("_", "").replace("`", "")
body = re.sub(r":\s*.*$", "", body) # "Café tiède: note" → drop gloss after colon
# remove emoji / symbol chars, keep letters/digits/space/'-/&
body = "".join(ch for ch in body
if ch.isalnum() or ch in " '-/&." or unicodedata.category(ch).startswith("L"))
# dash/arrow transition tail (ENSAD style "Quand on décolle 0 - / 11")
tt = _TRANS_TAIL.search(body)
if tt and tt.group(0).strip():
if not transition:
transition = tt.group(0).strip(" -–")
body = body[:tt.start()]
body = re.sub(r"\s+", " ", body).strip(" .-")
if len(body) < 2:
return None, None, None, False
return body, bpm, transition, True
def block_span(lines, start_idx):
"""Collect a gig block. Headers in backlog.md are DECORATIVE (stylized banners,
repeated `# PARVAGUES`, sub-labels) not hierarchical, so we don't scope by level.
Instead: from the anchor, walk to the next ANCHOR, but terminate early once the
setlist proper ends — a run of ≥3 blank lines after we've collected ≥2 items
(journal prose resumes). Decorative headers are kept as section context, not breaks."""
anchor_lines = sorted(k - 1 for k in ANCHORS)
hard_end = next((a for a in anchor_lines if a > start_idx), len(lines))
out, items, blanks = [], 0, 0
for raw in lines[start_idx + 1:hard_end]:
if not raw.strip():
blanks += 1
if blanks >= 3 and items >= 2:
break
continue
blanks = 0
if re.match(r"\s*[-+*]+\s*\S", raw) and not _ASCII_ART.search(raw):
items += 1
out.append(raw)
return out
# transition tails appended to a track name: "-> 5+7", " 0 - / 11", "- 9"
_TRANS_TAIL = re.compile(r"\s*(->|[-–])\s*[\d\s./+,]*$|\s+[\d][\d\s./+,>-]*$")
def parse(lines, name_idx):
gigs = {}
for ln1, slug in sorted(ANCHORS.items()):
confirm = slug.startswith("confirm:")
real_slug = slug.split(":", 1)[1] if confirm else slug
i = ln1 - 1
header = lines[i].strip()
section = None
tracks, unmatched = [], []
for raw in block_span(lines, i):
hm = re.match(r"\s*#{1,6}\s+(.+)$", raw)
if hm: # subsection label
section = re.sub(r"[^\w\s'-]", "", deaccent(hm.group(1))).strip() or None
continue
name, bpm, transition, ok = clean_track_line(raw)
if not ok:
continue
if _SECTION_HINT.match(name) and norm_name(name) not in name_idx:
continue
track = name_idx.get(norm_name(name))
entry = {"raw": name, "track": track, "bpm": bpm, "transition": transition, "section": section}
(tracks if track else unmatched).append(entry)
if not track:
tracks.append(entry) # keep, but track=None
gigs[real_slug] = {
"slug": real_slug, "confirm": confirm, "header": header, "anchor_line": ln1,
"n_tracks": len(tracks),
"n_resolved": sum(1 for t in tracks if t["track"]),
# >20 items between anchors → almost certainly swallowed a neighbour setlist
# (backlog has many densely-packed lists; needs a tighter anchor). Flag, don't hide.
"suspect_bleed": len(tracks) > 20,
"tracks": tracks,
}
return gigs
def build():
name_idx, _ = build_name_index()
lines = BACKLOG.read_text().splitlines()
gigs = parse(lines, name_idx)
confirmed = {k: v for k, v in gigs.items() if not v["confirm"]}
pending = {k: v for k, v in gigs.items() if v["confirm"]}
return {
"schema": "backlog setlists (recovered gig→tracks; resolved to .tidal)",
"as_of": AS_OF, "source": "backlog.md",
"n_gigs": len(confirmed), "n_pending_confirm": len(pending),
"gigs": confirmed, "pending": pending,
}
def main():
out = build()
OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1))
print(f"✓ {OUT}")
print(f" {out['n_gigs']} gigs recovered, {out['n_pending_confirm']} pending confirm\n")
for grp, label in ((out["gigs"], "RECOVERED"), (out["pending"], "PENDING-CONFIRM")):
for slug, g in sorted(grp.items()):
print(f" [{label}] {slug} «{g['header'][:48]}» "
f"{g['n_resolved']}/{g['n_tracks']} resolved")
for t in g["tracks"]:
mark = "✓" if t["track"] else "✗"
extra = " ".join(x for x in (f"{t['bpm']}bpm" if t['bpm'] else "",
f"{{{t['transition']}}}" if t['transition'] else "") if x)
tgt = Path(t["track"]).stem if t["track"] else "— UNMATCHED —"
print(f" {mark} {t['raw']:<32} → {tgt} {extra}")
print()
if __name__ == "__main__":
main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment