From e9c2ef0b6c965d01d805c50ea6efcb00e39a7dcf Mon Sep 17 00:00:00 2001 From: christian Date: Tue, 27 Oct 2026 11:57:14 -0400 Subject: [PATCH] START saves every song every week (old one parked z/zz), logs SAVED/PARKED on the key; SONG KEYS no longer warns ONLY ; install.sh and the location kit ship ableton_start.py (NA had the 10/22 copy) Co-Authored-By: Claude Opus 5.5 --- README.md | 9 +++++---- ableton_start.py | 15 +++++++-------- install.sh | 2 +- readiness.py | 8 ++++---- settings/campus.py | 2 +- tests/test_readiness.py | 7 +++---- tests/test_start.py | 24 +++++++++--------------- 7 files changed, 30 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index ff94755..ba50c26 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ everything on the page at once. | FOH MIX | **NOT MIXED** (amber): every track of a song is still at 0 dB. **TO MASTER**: a stem goes to Master instead of out to the console | looks again | | LIGHTS | the `LIGHTING CUES` track has a clip under every song | looks again | | LIGHT TIMING | every blinder flash and FX cue (notes 30-39 on `LIGHTING CUES`: Blinder Flash 1-3, Blinder FX 1-5, named from `Ableton/LIGHTING CUES MIDI`) lands on the hit in the `Drums (Live)` stem under it, even when that clip is deactivated because the drummer plays live. Each cue's moment in the stem comes from the tempo map; the Tracks Mac listens around it (less than half a 16th note either way) for the strongest hit. **3 CUES OFF** (amber): a strong hit more than 25 ms before or after the cue while the cue itself sits on a quiet spot; the details say `JEHOVAH bar 329.2.1: Blinder Flash 2 is 37 ms before the hit` (bars as Live shows them, 4/4). Listened to once per saved set. | looks again (move the note in Live by hand: Live's control script cannot edit notes in the arrangement) | -| SONG KEYS | every song is in the key PCO gives it: the set's locator for it (`3- JEHOVAH - B`), the pad clip under it on `AMBIENT PADS` (`B - WARM - CHURCHFRONT PADS`) and a `Finished Songs` / `Finished Custom Songs` folder (`JEHOVAH [B]`). Eb and D# are the same key. **JEHOVAH B NOT Db** (red): the locator is in another key. **JEHOVAH PAD D NOT B** (red): the wrong pad. **JEHOVAH ONLY Db** (amber): Finished Songs has the song, but not in this key (save the new version there). A song that is not in Finished Songs at all is only noted. | looks again | +| SONG KEYS | every song is in the key PCO gives it: the set's locator for it (`3- JEHOVAH - B`), the pad clip under it on `AMBIENT PADS` (`B - WARM - CHURCHFRONT PADS`) and a `Finished Songs` / `Finished Custom Songs` folder (`JEHOVAH [B]`). Eb and D# are the same key. **JEHOVAH B NOT Db** (red): the locator is in another key. **JEHOVAH PAD D NOT B** (red): the wrong pad. Finished Songs having the song only in another key is fine (START transposes it). A song that is not in Finished Songs at all is only noted. | looks again | | W TUNE | the `WAVES TUNE` clip that starts each song (a clip from `Ableton/Waves Tune - midi Program Change`, sent to Waves Tune as a program change) is the song's PCO key; a minor song is fine on its relative major. **JEHOVAH TUNE Db** names the song; the details say which clip to put at which bar. **TUNE PGM**: a renamed clip that sends another key's program. | looks again (Live's control script cannot change a clip in the arrangement: swap the clip by hand) | | ROUTING | read **live** from Ableton (AbletonOSC, so tracks just dragged in count before the set is saved): every stem goes where `ready.routing` says (Piano, Keys, Organ → `Ext. Out 5/6 Keys`; AG, EG, Bass → `7/8 GTRS`; Drums, Perc, FX, Synth Bass → `9/10 PERC / FX`; BGVS, Choir, Vox FX → `11/12 VOX`). The table was taken from 261025 NA Project and is edited on the settings page; "EG 13" goes where every other EG goes. **PIANO TO MASTER** (red) names the stem; **NEW STEM BANJO** (amber): a stem name the table does not know. | puts every stem back where the table says (and IN EARS' sends to 0 dB). Runs by itself in the auto-fix hours. | | IN EARS | read live: no stem in a song group has its in-ear send (send A, to `A-IN EAR MIX`) at -inf, as songs dragged in from another set do. Levels someone set are left alone; `RESI TRACKS` and `MIDI GROUP` (`ready.ears_skip_groups`) are skipped. | sets those sends to 0 dB. Runs by itself in the auto-fix hours. | @@ -194,9 +194,10 @@ other special service `Projects/NA/ NA Project`. `ableton_start.py` then runs on the Tracks Mac (detached, at low priority), doing the workflow PDF's steps: 1. **Last weekend's songs to Finished Songs.** Each song group of the newest weekend set goes to `Finished Songs/TITLE [KEY]` (key from its locator), with its arrangement markers, lighting cues and guide inside - the group and its audio copied in. A song that matches a saved version (any key, parked or not) is left alone. - A changed one replaces the version it was made from (`CAN'T HELP BUT PRAISE LM [Eb]` for a group called - `CAN'T HELP BUT PRAISE`), which is parked first as `zCAN'T HELP BUT PRAISE LM [Eb]`. + the group and its audio copied in. Every song is saved again every week, changed or not: it replaces the version + it was made from (`CAN'T HELP BUT PRAISE LM [Eb]` for a group called `CAN'T HELP BUT PRAISE`), which is parked + first as `zCAN'T HELP BUT PRAISE LM [Eb]` (`zz...` when that name is taken), so the old ones sort to the bottom. + START's details list each `SAVED` and `PARKED` folder. 2. **A new set from the newest weekend set:** its songs, pads, Waves Tune cues and marker, guide, loop-guide and lighting clips go; the groups, the numbered locators (and their computer keys 1-9), the loop locators and the loop cues stay. Empty loop cues are dropped. diff --git a/ableton_start.py b/ableton_start.py index ff694e8..3d559f5 100644 --- a/ableton_start.py +++ b/ableton_start.py @@ -5,8 +5,8 @@ does them by hand on Mondays. readiness.py sends this file to the Tracks Mac and every special service (First / Together Tuesday, Morning Worship, ...). 1 the previous weekend set's songs go to Finished Songs as "TITLE [KEY]": the song group with its - arrangement markers, lighting cues and guide inside it, the audio copied in. A song that changed parks - the old folder as "zTITLE [KEY]" first; an unchanged one is left as it is. + arrangement markers, lighting cues and guide inside it, the audio copied in. Every song, every week: + the old folder is parked as "zTITLE [KEY]" ("zzTITLE [KEY]" when that is taken) first. 2 each new set ("261101 NA Project", "261103 NA First Tuesday Project") starts from the newest weekend set: its songs, pads, Waves Tune cues and marker, guide and lighting clips go; the dedicated groups, the numbered locators (with their computer-key mappings), the loop locators and the loop cues stay. @@ -604,7 +604,7 @@ def helper_copy(src, gid, new_id, keep_from, keep_to, name): def export_songs(prev_path, cfg, status, log): - """Step 1: every song of the previous weekend set to Finished Songs (new or changed ones only).""" + """Step 1: every song of the previous weekend set to Finished Songs, the old version parked.""" root = load(prev_path) ls = root.find("LiveSet") proj = os.path.dirname(prev_path) @@ -623,11 +623,9 @@ def export_songs(prev_path, cfg, status, log): continue mine = stems_signature(_wrap(fam), proj) versions = list(saved_versions(cfg, dest_dir, title)) - # Saved already, under this name or a near one ("CAN'T HELP BUT PRAISE LM [Eb]" for a group called - # "CAN'T HELP BUT PRAISE"), parked or not: then it came from there and is left as it is. - if any(sig == mine for _, sig in versions): - continue - # Changed: it replaces the version it was made from (most of the same audio, same key), else a new name. + # Every song is saved again every week (user, 2026-10-27): it replaces the version it was made from (most + # of the same audio, same key: "CAN'T HELP BUT PRAISE LM [Eb]" for a group called "CAN'T HELP BUT PRAISE"), + # else "TITLE [KEY]"; the old folder is parked as "zTITLE [KEY]" ("zz..." when that is taken). def overlap(sig): a, b = set(sig), set(mine) return len(a & b) / float(len(a | b) or 1) @@ -684,6 +682,7 @@ def export_songs(prev_path, cfg, status, log): os.rename(building, dest) index[folder.upper()] = folder done.append(folder) + log.append("SAVED %s" % folder) if files.missing: log.append("%s: %d FILES MISSING" % (folder, len(files.missing))) return done diff --git a/install.sh b/install.sh index 7f67508..31bd2a3 100644 --- a/install.sh +++ b/install.sh @@ -19,7 +19,7 @@ SETTINGS_LABEL=org.waterschurch.status-settings DEST="$HOME/Library/Application Support/status-panel" PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" DOMAIN="gui/$(id -u)" -CODE=(status_panel.py readiness.py coremidi.py netgear.py schema.py make_page.py push_page.py nav_keys.py atem_watch.js atem_state.js nav_templates.json prop_guard.py) +CODE=(status_panel.py readiness.py ableton_start.py coremidi.py netgear.py schema.py make_page.py push_page.py nav_keys.py atem_watch.js atem_state.js nav_templates.json prop_guard.py) KEEP_CONFIG=0 SITE="" SITE_NEXT=0 diff --git a/readiness.py b/readiness.py index ce593ee..13ec801 100644 --- a/readiness.py +++ b/readiness.py @@ -1182,9 +1182,8 @@ def eval_keys(rc, plan, look): elif any(same_key(k, want) for _, k in versions): checked += 1 else: - line.fault(WARN, "%s ONLY %s" % (name, "/".join(sorted({k for _, k in versions}))), - "%s has %s, none in %s" % (" / ".join(folders), ", ".join(n for n, _ in versions), want)) - bad = True + # No warning (user, 2026-10-27): START transposes a song saved in another key. + checked += 1 if not checked and not bad: line.fault(WARN, "KEY UNCHECKED " + short(title), "no locator with a key, no pad under it and nothing in Finished Songs " "for %s (%s in PCO)" % (title, want)) @@ -2202,7 +2201,8 @@ def eval_start(rc, services, look, now): % (svc["name"], song, n, "" if n == 1 else "s")) if st.get("state") == "done" and now - (st.get("time") or 0) < 86400: for note in st.get("log") or []: - if "CONSOLIDATE IN LIVE:" in note or note.startswith("OPEN ") or "NOT CONSOLIDATED" in note or "DID NOT SAVE" in note: + if "CONSOLIDATE IN LIVE:" in note or note.startswith(("OPEN ", "SAVED ", "ALREADY SAVED ", "PARKED ", "NOT SAVED ", "RENAMED ")) \ + or "NOT CONSOLIDATED" in note or "DID NOT SAVE" in note: line.notes.append(note) line.info = " ".join(x["label"] for x in services) or "NO SERVICES" return line diff --git a/settings/campus.py b/settings/campus.py index 5f25069..5dd8545 100644 --- a/settings/campus.py +++ b/settings/campus.py @@ -36,7 +36,7 @@ TRIGGER_NAME = "flash_tick_500" LOOK_EVERY = 600 SEEN, LAST = "discover-seen.json", "discover-last.json" # What the program kit carries (no settings, keys or history of the location that serves it). -KIT_FILES = ("status_panel.py", "readiness.py", "coremidi.py", "netgear.py", "schema.py", "make_page.py", "push_page.py", +KIT_FILES = ("status_panel.py", "readiness.py", "ableton_start.py", "coremidi.py", "netgear.py", "schema.py", "make_page.py", "push_page.py", "nav_keys.py", "atem_watch.js", "atem_state.js", "nav_templates.json", "prop_guard.py") KIT_DIRS = ("settings", "remote-access", "companion-objects", "backup") diff --git a/tests/test_readiness.py b/tests/test_readiness.py index 49e8e73..e5aaa3c 100644 --- a/tests/test_readiness.py +++ b/tests/test_readiness.py @@ -302,10 +302,9 @@ class TestKeys(unittest.TestCase): def test_the_set_in_another_key(self): keys = dict(GOOD, **{"Holy Forever": "C"}) line = rd.eval_keys(RC, keyed(keys), with_pads_and_finished()) - # Rejoice Again: Finished Songs only has it in B (PCO and the set say Bb) - self.assertEqual(msgs(line), ["REJOICE AGAIN ONLY B", "HOLY FOREVER B NOT C", "HOLY FOREVER PAD B NOT C", - "HOLY FOREVER ONLY B"]) - self.assertEqual([lv for lv, _, _ in line.faults], [rd.WARN, rd.ERR, rd.ERR, rd.WARN]) + # Rejoice Again: Finished Songs only has it in B (PCO and the set say Bb): no warning, START transposes it + self.assertEqual(msgs(line), ["HOLY FOREVER B NOT C", "HOLY FOREVER PAD B NOT C"]) + self.assertEqual([lv for lv, _, _ in line.faults], [rd.ERR, rd.ERR]) def test_a_song_pco_has_no_key_for(self): line = rd.eval_keys(RC, keyed(dict(GOOD, **{"Holy Forever": ""})), with_pads_and_finished()) diff --git a/tests/test_start.py b/tests/test_start.py index a14c543..05f069b 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -349,22 +349,16 @@ class TestBuild(unittest.TestCase): self.assertEqual(st["step"], "NOTHING TO DO") self.assertEqual(os.listdir(os.path.join(self.proj, "NA", "261101 NA Project")), []) - def test_an_unchanged_song_is_not_saved_again_and_a_changed_one_parks_the_old(self): + def test_every_song_is_saved_every_week_and_the_old_ones_parked(self): + """User, 2026-10-27: every song saved new every week; the old one goes to z, zz, ... (the bottom of the folder).""" self.run_job() - shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project")) - st = self.run_job() - self.assertEqual(st["saved"], []) - self.assertFalse(os.path.exists(os.path.join(self.fin, "zOLD SONG [Eb]"))) - # A re-consolidated stem (new file) is a new version: the old folder is parked as z. - base = os.path.join(self.proj, "NA", "261025 NA Project", "261025 NA Project.als") - xml = gzip.open(base).read().decode() - i = xml.index("21-Piano") - xml = xml[:i] + xml[i:].replace('Value="1000"', 'Value="2000"', 1) - write_als(base, xml) - shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project")) - st = self.run_job() - self.assertEqual(st["saved"], ["OLD SONG [Eb]"]) - self.assertTrue(os.path.isdir(os.path.join(self.fin, "zOLD SONG [Eb]"))) + for parked in ("zOLD SONG [Eb]", "zzOLD SONG [Eb]"): + shutil.rmtree(os.path.join(self.proj, "NA", "261101 NA Project")) + st = self.run_job() # unchanged, saved again all the same + self.assertEqual(st["saved"], ["OLD SONG [Eb]"]) + self.assertIn("SAVED OLD SONG [Eb]", st["log"]) + self.assertTrue(os.path.isdir(os.path.join(self.fin, parked))) + self.assertTrue(os.path.exists(os.path.join(self.fin, "OLD SONG [Eb]", "OLD SONG.als"))) def test_a_changed_song_replaces_the_version_it_was_made_from(self): # Last week's "OLD SONG" group was made from "OLD SONG LIVE [Eb]", then edited (a stem swapped).