diff --git a/README.md b/README.md index 0464d0b..8435984 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,21 @@ speaker / name / date / notes file · `v` view the description · `y` setup · ` PICTURE / CLEAR PICTURE slides are always kept. When the notes mark a picture ("[PICTURE: …]", ""), they go there with the picture named in the slide notes. +## Other campuses + +`tools/make_kit.sh` builds `dist/Message Notes Kit .zip`: an installer, the program, the +Desktop launcher and `READ ME FIRST.txt`. Send the zip to the campus; on their ProPresenter Mac +they unzip it and double-click `Install Message Notes.command` (it installs Apple's Command Line +Tools first if Python is missing). The first run asks which campus it is and saves it in +`~/Library/Application Support/MessageNotes/settings.json`: + +- The campus code picks the preacher from the notes email ("Pastor Geyser will be preaching live + in AB"); a lone surname matches the most recent speaker with it. +- YouTube text is only written at NA (`"youtube": true`); `tools/deploy.sh` writes NA's settings. +- The ProPresenter folder, network port and template library are read from ProPresenter itself. + Templates are any `_TEMPLATE…DO NOT DELETE….pro` (with GENERIC in the name = generic). +- Without ProPresenter's network on, it never restarts ProPresenter to link the playlist. + ## How notes become slides | In the notes | On the slides | @@ -96,10 +111,11 @@ from the rest of their box keep bold/italic. - `youtube.py` writes the YouTube text (Claude API over HTTPS); `secrets_store.py` keeps the key in the Keychain - `spelling.py` is the typo check (macOS spelling checker via JXA) - `docxnum.py` keeps Word list numbers - - `app.py` is the Terminal app + - `app.py` is the Terminal app; `settings.py` is the per-Mac campus settings - `apps-script/SaveSermonNotes.gs` is the Google Apps Script for email pickup. - `tools/build.sh` builds `dist/message-notes.pyz` and the launcher. `tools/deploy.sh` builds - and installs both on the ProPresenter Mac, via the ingest Mac's SSH key. + and installs both on the NA ProPresenter Mac, via the ingest Mac's SSH key. `tools/make_kit.sh` + builds the kit for other campuses (`tools/kit/` holds its installer and read-me). - `tools/gen_schema.py` regenerates `schema.json` from the community ProPresenter `.proto` files (github.com/greyshirtguy/ProPresenter7-Proto, `autogen-proto`) after a ProPresenter upgrade. - `tests/run_tests.py` runs offline checks against real notes and presentations. Last Sunday's diff --git a/src/app.py b/src/app.py index 65dd6bd..a3d67ad 100644 --- a/src/app.py +++ b/src/app.py @@ -28,19 +28,17 @@ import notes as notesmod import plantext import playlist as playlist_mod import ppctl +import settings import spelling import themes import youtube -VERSION = "1.2.0" -LIBRARY_CANDIDATES = ["/Volumes/MEDIA/ProPresenter/Libraries/Messages", - "~/Documents/ProPresenter/Libraries/Messages"] -API = "http://127.0.0.1:1025" +VERSION = "1.3.0" +API = None # ProPresenter's network API on this Mac - set in run() from its preferences SUPPORT = os.path.expanduser("~/Library/Application Support/MessageNotes") LOG = os.path.expanduser("~/Library/Logs/message-notes.log") # where the email script saves the notes (checked first), then the usual places -NOTES_INBOX = ["~/Library/CloudStorage/GoogleDrive-media@waterschurch.org/My Drive/Message Notes", - "~/Library/CloudStorage/GoogleDrive-christian@waterschurch.org/My Drive/Message Notes"] +NOTES_INBOX = ["~/Library/CloudStorage/GoogleDrive-*/My Drive/Message Notes"] NOTE_DIRS = ["~/Downloads", "~/Desktop", "~/Documents", "~/Library/Mobile Documents/com~apple~TextEdit/Documents", "~/Library/CloudStorage/*/My Drive"] @@ -112,21 +110,11 @@ def short(text, n): # ----------------------------------------------------------------------------- environment def find_library(explicit=None): - for c in ([explicit] if explicit else LIBRARY_CANDIDATES): - p = os.path.expanduser(c) - if os.path.isdir(p): - return p - for p in glob.glob("/Volumes/*/ProPresenter/Libraries/Messages"): - return p - return None + return settings.find_library(explicit=explicit) def find_templates(library): - out = [] - for f in sorted(os.listdir(library)): - if f.lower().endswith(".pro") and not f.startswith("._") and re.match(r"^_?template", f, re.I) \ - and "do not delete" in f.lower(): - out.append(os.path.join(library, f)) + out = [os.path.join(library, f) for f in sorted(os.listdir(library)) if settings.is_template(f)] series = sorted([p for p in out if "generic" not in os.path.basename(p).lower()], key=os.path.getmtime, reverse=True) generic = sorted([p for p in out if "generic" in os.path.basename(p).lower()], key=os.path.getmtime, reverse=True) return series, generic @@ -240,8 +228,10 @@ def speaker_from_email(text, known): last = "" if not SPEAK_RE.search(rest): continue - here = re.search(r"\b(NA|North Attleboro|all locations|all other)\b", rest, re.I) is not None - score = 2 if here else 1 + words = settings.here_words() + here = bool(words) and re.search(r"\b(%s)\b" % "|".join(re.escape(w) for w in words), rest) is not None + everyone = re.search(r"\b(all locations|all other|all campuses)\b", rest, re.I) is not None + score = 3 if here else 2 if everyone else 1 if best is None or score > best[0]: best = (score, first, last) if not best: @@ -251,6 +241,11 @@ def speaker_from_email(text, known): parts = name.split() if parts and parts[0].lower() == first.lower() and (not last or last.lower() in name.lower()): return role, name + if not last: # "Pastor Geyser": a surname - the most recent speaker with it + for role, name in known: + parts = name.split() + if len(parts) > 1 and parts[-1].lower() == first.lower(): + return role, name return ("PASTOR", ("%s %s" % (first, last)).strip().upper()) @@ -267,6 +262,8 @@ def email_date(notes_path): def api_json(path, timeout=5): + if not API: + raise OSError("ProPresenter's network API is off") with urllib.request.urlopen(API + path, timeout=timeout) as r: return json.loads(r.read().decode("utf-8")) @@ -371,6 +368,9 @@ def edit_in_textedit(plan, name): def find_outputs(tpl, library): themes_dir = os.path.join(os.path.dirname(os.path.dirname(library)), "Themes") + if not API: + return [], ("ProPresenter's network API is off, so text was only checked on the slide itself " + "(turn it on in ProPresenter > Settings > Network)") try: return themes.outputs_for(tpl, API, themes_dir), None except (OSError, ValueError, KeyError) as e: @@ -474,12 +474,38 @@ class YouTubeJob: return self.result -def setup_youtube(): +def setup(): say("") - if not youtube.secrets_store.get(youtube.KEY_SERVICE, "api-key"): + say("%sSetup for this ProPresenter Mac%s" % (B, R)) + cur = settings.load() + pp = settings.propresenter_dir() + lib = settings.find_library(pp) + say(" ProPresenter folder: %s" % (pp or "%snot found - is the ProPresenter drive connected?%s" % (RED, R))) + say(" Message templates in: %s" % (lib or "%snot found%s" % (RED, R))) + api = settings.api_base() + say(" ProPresenter network: %s" % (api or "%soff - turn it on in ProPresenter > Settings > Network%s" % (YEL, R))) + codes = list(settings.CAMPUSES) + say("") + say(" Which campus is this?") + for k, c in enumerate(codes, 1): + say(" %d) %s %s" % (k, c, settings.CAMPUSES[c][1])) + default = str(codes.index(cur["campus"]) + 1) if cur.get("campus") in codes else "" + while True: + a = ask(" Campus (number, or type a code)", default).strip() + if a.isdigit() and 1 <= int(a) <= len(codes): + cur["campus"] = codes[int(a) - 1] + break + if re.fullmatch(r"[A-Za-z]{2,4}", a or ""): + cur["campus"] = a.upper() + break + cur["youtube"] = cur["campus"] == settings.STREAMING_CAMPUS + settings.save(cur) + say(" Saved: %s%s" % (cur["campus"], " (writes the YouTube title and description)" if cur["youtube"] else "")) + if cur["youtube"] and not youtube.secrets_store.get(youtube.KEY_SERVICE, "api-key"): + say("") say("Claude API key (from https://console.anthropic.com/settings/keys) - it goes straight into the Keychain:") youtube.secrets_store.prompt_put(youtube.KEY_SERVICE, "api-key") - if ask("Set up picking the notes up from email now? (Enter = yes, s = skip)", "").lower() != "s": + if ask("Set up picking the notes up from email now? (Enter = yes, s = skip - you can do it later with y)", "").lower() != "s": import pkgutil gs = os.path.join(SUPPORT, "SaveSermonNotes.gs") os.makedirs(SUPPORT, exist_ok=True) @@ -490,11 +516,12 @@ def setup_youtube(): subprocess.run(["/usr/bin/open", "https://script.google.com/create"]) say("") say(" The script is copied. In the browser page that just opened (Google Apps Script):") - say(" 1. Check the round picture top right is media@waterschurch.org (switch account if not).") + say(" 1. Check the round picture top right is the Google account that gets the Notes/Slides") + say(" emails AND is signed in to Google Drive on this Mac (switch account if not).") say(" 2. Click in the big code area, press Cmd-A then Cmd-V (replaces the sample code).") say(" 3. Press Cmd-S to save.") say(" 4. At the top, change the dropdown next to 'Debug' from 'myFunction' to 'install', click Run.") - say(" 5. A box says 'Authorization required': Review permissions > media@ > Allow") + say(" 5. A box says 'Authorization required': Review permissions > that account > Allow") say(" (if it says 'Google hasn't verified this app': Advanced > Go to ... > Allow).") say(" Done when the bottom panel says 'Execution completed'.") ask(" Press Enter when that's done", "") @@ -533,7 +560,7 @@ def main(argv=None): ap.add_argument("--out", help="write the .pro here instead of the library (testing)") ap.add_argument("--no-playlist", action="store_true", help="don't link into the service playlist") ap.add_argument("--no-youtube", action="store_true", help="don't write the YouTube title and description") - ap.add_argument("--setup", action="store_true", help="set up the Claude API key and email pickup") + ap.add_argument("--setup", action="store_true", help="set up this Mac (campus, email pickup, Claude key at NA)") ap.add_argument("--yes", action="store_true", help="don't ask for confirmation") ap.add_argument("--version", action="version", version=VERSION) args = ap.parse_args(argv) @@ -615,12 +642,18 @@ def describe_fix(f): def run(args): say("%s\u2501\u2501 Message Notes \u2192 ProPresenter %s \u2501\u2501%s" % (B, VERSION, R)) - if args.setup: - setup_youtube() - return 0 + global API + if args.setup or not os.path.exists(settings.PATH): + setup() + if args.setup: + return 0 + say("") + API = settings.api_base() + if not settings.youtube_enabled(): + args.no_youtube = True library = find_library(args.library) if not library: - raise SystemExit("Can't find the ProPresenter Messages library (is the MEDIA drive connected?).") + raise SystemExit("Can't find ProPresenter's message templates (is the ProPresenter drive connected?).") series, generic = find_templates(library) if not (series or generic): raise SystemExit("No message templates found (files named '_TEMPLATE ... DO NOT DELETE ...pro').") @@ -697,7 +730,7 @@ def run(args): ask("%sPress Enter to go back%s" % (DIM, R), "") continue if a == "y": - setup_youtube() + setup() yt_dirty = True continue if a == "r": @@ -835,7 +868,8 @@ def show_summary(st, tpl, speaker, speaker_src, name, date, svc_date, svc_name, for n in notes: say(" %s\u2022%s %s" % (YEL, R, wrap(re.sub(r"\s+", " ", n), w - 6, 4))) say("") - say(" %sEnter = do it e = edit slides r = undo typo fixes t/s/n/d/f = change template/speaker/name/date/notes v = view description q = quit%s" % (DIM, R)) + say(" %sEnter = do it e = edit slides r = undo typo fixes t/s/n/d/f = change template/speaker/name/date/notes %sy = setup q = quit%s" + % (DIM, "" if args.no_youtube else "v = view description ", R)) def typo_notes(st, tpl): diff --git a/src/ppctl.py b/src/ppctl.py index 653dd33..b20dba7 100644 --- a/src/ppctl.py +++ b/src/ppctl.py @@ -42,6 +42,8 @@ def busy_reason(api, now=None): now = now or datetime.datetime.now() if now.weekday() == 6 and 6 <= now.hour < 14: return "it's Sunday morning" + if not api: + return "ProPresenter's network API is off, so the tool can't see what's on screen" try: layers = _api(api, "/v1/status/layers") or {} slide = _api(api, "/v1/status/slide") or {} @@ -54,7 +56,7 @@ def busy_reason(api, now=None): if t.get("state") in ("running", "overrunning") and "message" in (t.get("id", {}).get("name", "").lower()): return "the Message timer is running" except (OSError, ValueError): - pass + return "ProPresenter didn't answer, so the tool can't see what's on screen" return None @@ -90,6 +92,6 @@ def restart_with(change, api, say=print, quit_timeout=45): subprocess.run(["/usr/bin/open", "-a", path]) elif p: subprocess.run(["/usr/bin/open", "-b", BUNDLE]) - if p and not wait_api(api): + if p and api and not wait_api(api): say("ProPresenter is starting slowly - give it a moment.") return result diff --git a/src/settings.py b/src/settings.py new file mode 100644 index 0000000..7b8303c --- /dev/null +++ b/src/settings.py @@ -0,0 +1,113 @@ +"""Per-machine settings, so the same tool works at every campus. + +Most things are read from ProPresenter itself (its data folder and network API port, from +its preferences); the campus is asked once and saved in settings.json: + campus - "NA", "AB", ... used to read who is preaching from the notes email + youtube - write the YouTube title/description (only the streaming campus) + library - the ProPresenter library holding the message templates (found automatically) +""" +import glob +import json +import os +import subprocess + +SUPPORT = os.path.expanduser("~/Library/Application Support/MessageNotes") +PATH = os.path.join(SUPPORT, "settings.json") +PP_DOMAIN = "com.renewedvision.propresenter" + +# campus code -> words that mean "here" in the notes email ("live from NA with RESI to SC and WS", +# "live in AB"); the second one is shown in setup. A campus not listed can type its own code. +CAMPUSES = { + "NA": ["NA", "North Attleboro"], + "AB": ["AB", "Apollo Beach"], + "WS": ["WS", "Woonsocket"], + "SC": ["SC", "Tiverton", "South Coast"], + "NW": ["NW", "Norwood"], +} +STREAMING_CAMPUS = "NA" + + +def load(): + try: + with open(PATH) as f: + return json.load(f) + except (OSError, ValueError): + return {} + + +def save(d): + os.makedirs(SUPPORT, exist_ok=True) + with open(PATH, "w") as f: + json.dump(d, f, indent=2) + + +def _pref(key): + p = subprocess.run(["/usr/bin/defaults", "read", PP_DOMAIN, key], capture_output=True, text=True) + return p.stdout.strip() if p.returncode == 0 else None + + +def propresenter_dir(): + """ProPresenter's data folder (Libraries, Playlists, Themes...).""" + d = load().get("propresenter_dir") or _pref("applicationShowDirectory") + for cand in ([d] if d else []) + [os.path.expanduser("~/Documents/ProPresenter")] + \ + sorted(glob.glob("/Volumes/*/ProPresenter")): + if cand and os.path.isdir(os.path.join(cand, "Libraries")): + return cand + return None + + +def api_base(): + """ProPresenter's network API on this Mac, or None when it's switched off.""" + if _pref("networkEnabled") == "0": + return None + port = load().get("api_port") or _pref("networkPort") or "1025" + return "http://127.0.0.1:%s" % port + + +def is_template(name): + low = name.lower() + return low.endswith(".pro") and not name.startswith("._") and low.lstrip("_").startswith("template") \ + and "do not delete" in low + + +def find_library(pp_dir=None, explicit=None): + """The library folder with the message templates ('Messages' at every campus so far).""" + if explicit and os.path.isdir(os.path.expanduser(explicit)): + return os.path.expanduser(explicit) + pp_dir = pp_dir or propresenter_dir() + if not pp_dir: + return None + libs = os.path.join(pp_dir, "Libraries") + want = load().get("library") + if want and os.path.isdir(os.path.join(libs, want)): + return os.path.join(libs, want) + with_templates = [] + for name in sorted(os.listdir(libs)): + full = os.path.join(libs, name) + if os.path.isdir(full): + try: + if any(is_template(f) for f in os.listdir(full)): + with_templates.append(full) + except OSError: + continue + for full in with_templates: + if os.path.basename(full).lower() == "messages": + return full + if with_templates: + return with_templates[0] + msgs = os.path.join(libs, "Messages") + return msgs if os.path.isdir(msgs) else None + + +def campus(): + return load().get("campus", "") + + +def youtube_enabled(): + d = load() + return d.get("youtube", d.get("campus") == STREAMING_CAMPUS) + + +def here_words(): + c = campus() + return CAMPUSES.get(c, [c] if c else []) diff --git a/tests/run_tests.py b/tests/run_tests.py index fec550f..f39d306 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -112,6 +112,35 @@ def main(): except spelling.Unavailable as e: print("skip typo checks (%s)" % e) + # 7. other campuses: their template names, and who preaches there according to the email + import app + import settings + for n, want in (("_TEMPLATE - DO NOT DELETE 2608 Lower Thirds.pro", True), + ("_TEMPLATE_GENERIC - DO NOT DELETE 2601.pro", True), + ("_RESI MESSAGE TEMPLATE.pro", False), ("._TEMPLATE - DO NOT DELETE 2608.pro", False), + ("261011 Where Is Your Faith Part 7.pro", False)): + check("template name %s" % n, settings.is_template(n) == want) + known = [("LOCATION PASTOR", "Mehdi Chniouli"), ("PASTOR", "Test Geyser"), ("LEAD PASTOR", "Tim Hatch"), + ("PASTOR", "Other Geyser")] + mail = ("Subject: Notes/Slides - Where Is Your Faith - Part 7\n\nHi all,\nPastor Mehdi will be preaching live " + "from NA with RESI to SC and WS. I have attached the notes/slides. Pastor Geyser will be preaching " + "live in AB. Please have blank notes ready.\n") + real_load = settings.load + try: + for campus, want in (("NA", "Mehdi Chniouli"), ("SC", "Mehdi Chniouli"), ("WS", "Mehdi Chniouli"), + ("AB", "Test Geyser")): + settings.load = lambda c=campus: {"campus": c} + got = app.speaker_from_email(mail, known) + check("speaker at %s" % campus, got and got[1] == want, "got %s" % (got,)) + settings.load = lambda: {"campus": "AB"} + got = app.speaker_from_email("Pastor Tim will be preaching live from NA with RESI to all other locations.", known) + check("speaker at AB from 'all other locations'", got and got[1] == "Tim Hatch", "got %s" % (got,)) + check("youtube only at NA", not settings.youtube_enabled()) + settings.load = lambda: {"campus": "NA"} + check("youtube at NA", settings.youtube_enabled()) + finally: + settings.load = real_load + print() print("FAILED: %s" % failures if failures else "all checks passed") return 1 if failures else 0 diff --git a/tools/Message Notes to ProPresenter.command b/tools/Message Notes to ProPresenter.command index c1c97b0..65bda81 100644 --- a/tools/Message Notes to ProPresenter.command +++ b/tools/Message Notes to ProPresenter.command @@ -6,16 +6,12 @@ # message presentation, built from the series or generic template in the # Messages library - the same slides you'd make by copy and paste. # -# Double-click, then answer the questions: -# 1. which notes file (newest ones are listed; or drag a file in) -# 2. which template (series / generic - it suggests one) -# 3. who is speaking (recent speakers are listed) -# 4. date + presentation name (e.g. "261018 Where Is Your Faith Part 8") -# Then every slide is listed for review. Nothing is written until you press -# Enter at "Create this presentation?". Type q at any prompt to quit. +# Double-click. It picks up the newest notes (from the email folder, or +# Downloads/Desktop/Documents), checks them for typos, picks the template and +# speaker, and shows one summary. Enter creates the presentation and links it +# into the next service's playlist (Message section). Type q at any prompt to quit. # -# Afterwards it checks the new presentation through ProPresenter's API and -# opens a page of slide previews drawn by ProPresenter itself. +# First run asks which campus this is (saved). Run with --setup to change it. # # Program: ~/Library/Application Support/MessageNotes/message-notes.pyz # Log: ~/Library/Logs/message-notes.log diff --git a/tools/deploy.sh b/tools/deploy.sh index 9e76db4..eae7709 100755 --- a/tools/deploy.sh +++ b/tools/deploy.sh @@ -1,17 +1,22 @@ #!/bin/bash -# Build and install on the ProPresenter Mac (via the ingest Mac, which holds the SSH key). +# Build and install on the North Attleboro ProPresenter Mac (via the ingest Mac, which holds the SSH key). # app -> ~/Library/Application Support/MessageNotes/message-notes.pyz # launcher -> ~/Desktop/Message Notes to ProPresenter.command +# settings -> NA + YouTube, only if that Mac has no settings.json yet (so it never asks there) +# Other campuses: tools/make_kit.sh set -euo pipefail cd "$(dirname "$0")/.." tools/build.sh >/dev/null HOP=checklist-ingest PROP=napropresenter@10.1.14.225 -scp -q dist/message-notes.pyz "dist/Message Notes to ProPresenter.command" "$HOP:/tmp/" -ssh "$HOP" "scp -q -i ~/.ssh/id_status_panel /tmp/message-notes.pyz '/tmp/Message Notes to ProPresenter.command' $PROP:/tmp/ \ - && rm -f /tmp/message-notes.pyz '/tmp/Message Notes to ProPresenter.command' \ +printf '{\n "campus": "NA",\n "youtube": true\n}\n' > dist/na-settings.json +scp -q dist/message-notes.pyz "dist/Message Notes to ProPresenter.command" dist/na-settings.json "$HOP:/tmp/" +rm -f dist/na-settings.json +ssh "$HOP" "scp -q -i ~/.ssh/id_status_panel /tmp/message-notes.pyz '/tmp/Message Notes to ProPresenter.command' /tmp/na-settings.json $PROP:/tmp/ \ + && rm -f /tmp/message-notes.pyz '/tmp/Message Notes to ProPresenter.command' /tmp/na-settings.json \ && ssh -i ~/.ssh/id_status_panel $PROP 'set -e; D=\"\$HOME/Library/Application Support/MessageNotes\"; mkdir -p \"\$D\"; \ mv /tmp/message-notes.pyz \"\$D/message-notes.pyz\"; \ mv \"/tmp/Message Notes to ProPresenter.command\" \"\$HOME/Desktop/Message Notes to ProPresenter.command\"; \ chmod +x \"\$HOME/Desktop/Message Notes to ProPresenter.command\"; \ - /usr/bin/python3 \"\$D/message-notes.pyz\" --version; ls -l \"\$D/message-notes.pyz\" \"\$HOME/Desktop/Message Notes to ProPresenter.command\"'" + if [ -f \"\$D/settings.json\" ]; then rm -f /tmp/na-settings.json; else mv /tmp/na-settings.json \"\$D/settings.json\"; fi; \ + /usr/bin/python3 \"\$D/message-notes.pyz\" --version; cat \"\$D/settings.json\"; ls -l \"\$D/message-notes.pyz\" \"\$HOME/Desktop/Message Notes to ProPresenter.command\"'" diff --git a/tools/kit/Install Message Notes.command b/tools/kit/Install Message Notes.command new file mode 100755 index 0000000..368b6ab --- /dev/null +++ b/tools/kit/Install Message Notes.command @@ -0,0 +1,45 @@ +#!/bin/bash +#============================================================================== +# INSTALL MESSAGE NOTES -> PROPRESENTER +# Run this once on the ProPresenter Mac. It puts the program in +# ~/Library/Application Support/MessageNotes, puts "Message Notes to +# ProPresenter" on the Desktop, and asks a few setup questions. +# Running it again updates the program and keeps the settings. +#============================================================================== +cd "$(dirname "$0")" || exit 1 +clear +echo "Installing Message Notes -> ProPresenter" +echo + +finish() { echo; read -r -p "Press Enter to close this window." _; exit "$1"; } + +if [ ! -f message-notes.pyz ] || [ ! -f "Message Notes to ProPresenter.command" ]; then + echo "This installer has to stay in the kit folder, next to message-notes.pyz." + finish 1 +fi + +# macOS's own Python comes with Apple's free Command Line Tools +if ! /usr/bin/xcode-select -p >/dev/null 2>&1 || ! /usr/bin/python3 -c 'import sys; sys.exit(sys.version_info < (3, 8))' >/dev/null 2>&1; then + echo "This Mac first needs Apple's free Command Line Tools (they include Python)." + echo "A box will pop up: click Install and wait until it's finished (5-15 minutes)," + echo "then double-click this installer again." + /usr/bin/xcode-select --install >/dev/null 2>&1 + finish 1 +fi + +D="$HOME/Library/Application Support/MessageNotes" +L="$HOME/Desktop/Message Notes to ProPresenter.command" +mkdir -p "$D" || finish 1 +cp message-notes.pyz "$D/message-notes.pyz" || finish 1 +cp "Message Notes to ProPresenter.command" "$L" || finish 1 +chmod +x "$L" +/usr/bin/xattr -c "$D/message-notes.pyz" "$L" 2>/dev/null # no "downloaded from the internet" warning +echo "Installed version $(/usr/bin/python3 "$D/message-notes.pyz" --version 2>&1)" +echo " program: $D/message-notes.pyz" +echo " Desktop: Message Notes to ProPresenter" + +/usr/bin/python3 "$D/message-notes.pyz" --setup +status=$? +echo +echo "All set. From now on just double-click 'Message Notes to ProPresenter' on the Desktop." +finish $status diff --git a/tools/kit/READ ME FIRST.txt b/tools/kit/READ ME FIRST.txt new file mode 100644 index 0000000..7908ac1 --- /dev/null +++ b/tools/kit/READ ME FIRST.txt @@ -0,0 +1,74 @@ +MESSAGE NOTES -> PROPRESENTER (version VERSION_HERE) +===================================================== + +Turns the pastor's notes ("Slides - .rtf") into the message presentation +in ProPresenter, built from your own message templates, and links it into the +next service's playlist. Runs on the ProPresenter Mac. + + +INSTALL (once, on the ProPresenter Mac) +--------------------------------------- +1. Unzip the kit (double-click the .zip) and open the "Message Notes Kit" folder. +2. Double-click "Install Message Notes.command". + If macOS says it "can't be opened" / "not verified": + click Done, open System Settings > Privacy & Security, scroll down, + click "Open Anyway" next to "Install Message Notes.command", then Open. + (Or: open Terminal, type bash and a space, drag the installer into the + Terminal window, press Return.) +3. If it asks for Apple's Command Line Tools: click Install, wait, run step 2 again. +4. Answer the setup questions (which campus; email pickup - you can skip that). +5. If macOS asks whether Terminal may access Documents, Desktop, Downloads or + Google Drive: click Allow. + +Then use "Message Notes to ProPresenter" on the Desktop every week. +To update later: run the installer from a newer kit (settings are kept). + + +WHAT PROPRESENTER NEEDS +----------------------- +- The templates in a library (normally "Messages"), named like: + _TEMPLATE - DO NOT DELETE 2608 Lower Thirds (series) + _TEMPLATE_GENERIC - DO NOT DELETE 2601 (generic) + Any name works as long as it starts with _TEMPLATE and contains DO NOT DELETE; + the one with GENERIC in the name is the generic one. If there are several, + the most recently changed one is used. +- Network turned on: ProPresenter > Settings > Network > Enable Network. + The tool uses it to check the text fits your output themes and to see that + nothing is live before it restarts ProPresenter to link the playlist. +- The service playlist imported from Planning Center, with a "Message" header. + If it's missing, the tool tells you to import it. + + +EVERY WEEK +---------- +1. Save the notes .rtf from the email into Downloads (or set up email pickup, + below, and skip this). +2. Double-click "Message Notes to ProPresenter" on the Desktop. +3. Check the summary (typos it fixed, template, speaker, service), press Enter. +4. ProPresenter restarts once (to link the playlist) - never while anything is + on screen, on Sunday mornings, or while the message timer runs. If it can't, + it tells you to drag the presentation into the Message section yourself. +5. Check the slides on your outputs. Font sizes are never changed; if text is + too long for an output, the summary says so. + +Keys at the summary: Enter = create, e = edit the slide text, r = undo typo +fixes, t = switch template, s = speaker, n = name, d = date, f = other notes +file, y = run setup again, q = quit. + + +EMAIL PICKUP (optional) +----------------------- +Needs: Google Drive for desktop on this Mac, signed in to the Google account that +receives the "Notes/Slides" emails. During setup (or later with y) the tool copies +a small Google Apps Script and opens script.google.com - paste it, save, run +"install", allow it. From then on the notes land in My Drive/Message Notes every +10 minutes and the tool picks the newest one by itself (it also reads who is +preaching at your campus from the email). + + +FILES +----- +Program: ~/Library/Application Support/MessageNotes/message-notes.pyz +Settings: ~/Library/Application Support/MessageNotes/settings.json +Log: ~/Library/Logs/message-notes.log +Remove: delete the Desktop launcher and the MessageNotes folder above. diff --git a/tools/make_kit.sh b/tools/make_kit.sh new file mode 100755 index 0000000..eef73b7 --- /dev/null +++ b/tools/make_kit.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Build the kit for other campuses: dist/Message Notes Kit .zip +# Install Message Notes.command, message-notes.pyz, the Desktop launcher, READ ME FIRST.txt +# No settings and no keys go in it - each Mac asks its own campus on install. +set -euo pipefail +cd "$(dirname "$0")/.." +tools/build.sh >/dev/null +V=$(/usr/bin/python3 dist/message-notes.pyz --version) +K="dist/Message Notes Kit" +rm -rf "$K" && mkdir -p "$K" +cp dist/message-notes.pyz "dist/Message Notes to ProPresenter.command" "tools/kit/Install Message Notes.command" "$K/" +sed "s/VERSION_HERE/$V/" "tools/kit/READ ME FIRST.txt" > "$K/READ ME FIRST.txt" +chmod +x "$K/Install Message Notes.command" "$K/Message Notes to ProPresenter.command" +Z="dist/Message Notes Kit $V.zip" +rm -f "$Z" +(cd dist && /usr/bin/ditto -c -k --keepParent --norsrc "Message Notes Kit" "Message Notes Kit $V.zip") +rm -rf "$K" +ls -l "$Z"