emacsconf.org/2023/talks/emacsconf
Starting from the next slide, you can press “play” once in the audio controls (or type “a”) to start the presentation, which advances automatically afterwards.
How we use Org Mode and TRAMP to organize and run a multi-track conference
Revisit scrambled-together code and document things along the way
Show the process of thinking about a complex project and building it up
Point to more information
https://emacsconf.org/2023/talks/emacsconf
E-mail: Notmuch + Emacs
Private conf.org
Public webpages
Mailing lists
Backstage area
Public media files
Configuration, etc.
* WAITING_FOR_PREREC EmacsConf.org: How we use Org Mode and TRAMP to organize and run a multi-track conference SCHEDULED: <2023-12-03 Sun 14:55-15:15> :PROPERTIES: :CUSTOM_ID: emacsconf :SLUG: emacsconf :NAME: Sacha Chua :NAME_SHORT: Sacha :END:
(defun emacsconf-mail-parse-submission (body) "Extract data from EmacsConf submissions in BODY." (when (listp body) (setq body (plist-get (car body) :content))) (let* ((data (list :body body)) (fields '((:title "^[* ]*Talk title") (:description "^[* ]*Talk description") (:format "^[* ]*Format") (:intro "^[* ]*Introduction for you and your talk") (:name "^[* ]*Speaker name") (:availability "^[* ]*Speaker availability") (:q-and-a "^[* ]*Preferred Q&A approach") (:public "^[* ]*Public contact information") (:private "^[* ]*Private emergency contact information") (:release "^[* ]*Please include this speaker release"))) field (field-regexp (mapconcat (lambda (o) (concat "\\(?:" (cadr o) "\\)")) fields "\\|"))) (with-temp-buffer (insert body) (goto-char (point-min)) ;; Try to parse it (catch 'done (while (not (eobp)) ;; skip the field title (unless (looking-at field-regexp) (unless (re-search-forward field-regexp nil t) (throw 'done nil))) (goto-char (match-beginning 0)) (setq field (seq-find (lambda (o) (looking-at (cadr o))) fields)) (when field ;; get the text between this and the next field (re-search-forward "\\(:[ \t\n]+\\|\n\n\\)" nil t) (setq data (plist-put data (car field) (buffer-substring (point) (or (and (re-search-forward field-regexp nil t) (goto-char (match-beginning 0)) (point)) (point-max)))))))) (if (string-match "[0-9]+" (or (plist-get data :format) "")) (plist-put data :time (match-string 0 (or (plist-get data :format) "")))) data)))
(defun my-org-set-property (property value) "In the current entry, set PROPERTY to VALUE. Use the region if active." (interactive (list (org-read-property-name) (when (region-active-p) (replace-regexp-in-string "[ \n\t]+" " " (buffer-substring (point) (mark)))))) (org-set-property property value))
:FILE_PREFIX: emacsconf-year-slug--title--speakers
(defun emacsconf-set-file-prefixes () "Set the FILE_PREFIX property for each talk entry that needs it." (interactive) (org-map-entries (lambda () (org-entry-put (point) "FILE_PREFIX" (emacsconf-file-prefix (emacsconf-get-talk-info-for-subtree)))) "SLUG={.}-FILE_PREFIX={.}"))
(defun emacsconf-rename-files (talk &optional filename) "Rename the marked files or the current file to match TALK. If FILENAME is specified, use that as the extra part of the filename after the prefix. This is useful for distinguishing files with the same extension. Return the list of new filenames." (interactive (list (emacsconf-complete-talk-info))) (prog1 (mapcar (lambda (file) (let* ((extra (or filename (read-string (format "Filename (%s): " (file-name-base file))))) (new-filename (expand-file-name (concat (plist-get talk :file-prefix) (if (string= extra "") "" (concat "--" extra)) "." (file-name-extension file)) (file-name-directory file)))) (rename-file file new-filename t) new-filename)) (or (dired-get-marked-files) (list (buffer-file-name)))) (when (derived-mode-p 'dired-mode) (revert-buffer))))
(defun emacsconf-publish-talks-json () "Return JSON format with a subset of talk information." (let ((emacsconf-publishing-phase 'conference)) (json-encode (list :talks (mapcar (lambda (o) (apply 'list (cons :start-time (format-time-string "%FT%T%z" (plist-get o :start-time) t)) (cons :end-time (format-time-string "%FT%T%z" (plist-get o :end-time) t)) (mapcar (lambda (field) (cons field (plist-get o field))) '(:slug :title :speakers :pronouns :pronunciation :url :track :file-prefix :qa-url :qa-type :qa-backstage-url)))) (emacsconf-filter-talks (emacsconf-get-talk-info)))))))
roles/prerec/templates/rename-original.sh
#!/bin/bash # {{ ansible_managed }} # Usage: rename-original.sh $slug $file [$extra] [$talks-json] SLUG=$1 FILE=$2 TALKS_JSON=${4:-~/current/talks.json} EXTRA="" if [ -z ${3-unset} ]; then EXTRA="" elif [ -n "$3" ]; then EXTRA="$3" elif echo "$FILE" | grep -e '\(webm\|mp4\|mov\)'; then EXTRA="--original" fi filename=$(basename -- "$FILE") extension="${filename##*.}" filename="${filename%.*}" FILE_PREFIX=$(jq -r '.talks[] | select(.slug=="'$SLUG'")["file-prefix"]' < $TALKS_JSON) if echo "$FILE" | grep -q \\. ; then mv "$FILE" $FILE_PREFIX$EXTRA.$extension echo $FILE_PREFIX$EXTRA.$extension else mv "$FILE" $FILE_PREFIX$EXTRA echo $FILE_PREFIX$EXTRA fi # Copy to original if needed if [ -f $FILE_PREFIX--original.webm ] && [ ! -f $FILE_PREFIX--main.$extension ]; then cp $FILE_PREFIX--original.$extension $FILE_PREFIX--main.webm fi
(defun emacsconf-timezone-set (timezone) "Set the timezone for the current Org entry." (interactive (list (progn (require 'tzc) (completing-read "Timezone: " tzc-time-zones)))) (org-entry-put (point) "TIMEZONE" timezone))
<= 11:00 EST
As of the time I write this e-mail, your tentative schedule is: Improving compiler diagnostics with Overlays Dec 2 Sat 1:00 pm EST translated to your local timezone US/Pacific: Dec 2 Sat 10:00 am PST I'm using ">= 11:00 -0500 (08:00 US/Pacific)" as the availability constraint for you when planning the talks, but since I sometimes mess up encoding these things, could you please doublecheck that this works for you?
format-time-string
(let ((x 1)) (s-lex-format "x is: ${x}"))
(defun emacsconf-replace-plist-in-string (attrs string) "Replace ${keyword} from ATTRS in STRING." (let ((a attrs) name val) (while a (setq name (pop a) val (pop a)) (when (stringp val) (setq string (replace-regexp-in-string (regexp-quote (concat "${" (substring (symbol-name name) 1) "}")) (or val "") string t t)))) string))
(emacsconf-replace-plist-in-string '(:test "Hello" :another-var "world") "Test 1: ${test} Test 2: ${another-var}") ;; Test 1: Hello Test 2: world
(defun emacsconf-get-talk-info () (with-current-buffer (find-file-noselect emacsconf-org-file) (save-excursion (save-restriction (widen) (let (results) (org-map-entries (lambda () (when (or (org-entry-get (point) "TIME") (org-entry-get (point) "SLUG") (org-entry-get (point) "INCLUDE_IN_INFO")) (setq results (cons (emacsconf-get-talk-info-for-subtree) results))))) (nreverse results))))))
(defun emacsconf-get-talk-info-for-subtree () "Run `emacsconf-talk-info-functions' to extract the info for this entry." (seq-reduce (lambda (prev val) (save-excursion (save-restriction (funcall val prev)))) emacsconf-talk-info-functions nil))
(defvar emacsconf-talk-info-functions '(emacsconf-get-talk-info-from-properties emacsconf-get-talk-categories emacsconf-get-talk-abstract-from-subtree emacsconf-get-talk-logbook emacsconf-add-talk-status emacsconf-add-checkin-time emacsconf-add-timezone-conversions emacsconf-add-speakers-with-pronouns emacsconf-add-live-info) "Functions to collect information.")
(defun emacsconf-get-talk-abstract-from-subtree (o) "Add the abstract from a subheading. The subheading should match `emacsconf-abstract-heading-regexp'." (plist-put o :abstract (substring-no-properties (or (emacsconf-get-subtree-entry "abstract") ""))))
(defun emacsconf-add-talk-status (o) "Add status label and public info." (plist-put o :status-label (or (assoc-default (plist-get o :status) emacsconf-status-types 'string= "") (plist-get o :status))) (when (or (member (plist-get o :status) (split-string "PLAYING CLOSED_Q OPEN_Q UNSTREAMED_Q TO_ARCHIVE TO_EXTRACT TO_FOLLOW_UP DONE")) (time-less-p (plist-get o :start-time) (current-time))) (plist-put o :public t)) o)
memoize for performance
;; from emacsconf-publish-format-talk-schedule-info (emacsconf-replace-plist-in-string (append o (list :irc-info (format "Discuss on IRC: [#%s](%s) \n" (plist-get o :channel) (plist-get o :webchat-url)) ; ... more entries )) "[[!toc ]] Format: ${format} ${pad-info}${irc-info}${status-info}${schedule-info}\n ${alternate-apac-info}\n")
The Ikiwiki markup looks like this:
[[!template id=sched time="""20""" q-and-a="""<a href="https://media.emacsconf.org/2023/current/bbb-emacsconf.html">BBB</a>""" startutc="""2023-12-02T19:50:00+0000""" endutc="""2023-12-02T20:10:00+0000""" start="""2:50""" end="""3:10""" title="""EmacsConf.org: How we use Org Mode and TRAMP to organize and run a multi-track conference""" url="""/2023/talks/emacsconf""" speakers="""Sacha Chua""" track="""Development""" watch="""https://emacsconf.org/2023/watch/dev""" slug="""emacsconf""" note=""""""]]
emacsconf-publish-sched-directive
in emacsconf-publish.el
(format "[[!template id=sched%s]]" (let ((result "") (attrs ;; ... some code to make a property list based on the talk ... )) (while attrs (let ((field (pop attrs)) (val (pop attrs))) (when val (setq result (concat result " " (substring (symbol-name field) 1) "=\"\"\"" val "\"\"\""))))) result))
(defun emacsconf-publish-nav-pages (&optional talks) "Generate links to the next and previous talks. During the schedule and conference phase, the talks are sorted by time. Otherwise, they're sorted by track and then schedule." (interactive (list (emacsconf-publish-prepare-for-display (or emacsconf-schedule-draft (emacsconf-get-talk-info))))) (let* ((next-talks (cdr talks)) (prev-talks (cons nil talks)) (label (if (member emacsconf-publishing-phase '(schedule conference)) "time" "track"))) (unless (file-directory-p (expand-file-name "info" (expand-file-name emacsconf-year emacsconf-directory))) (mkdir (expand-file-name "info" (expand-file-name emacsconf-year emacsconf-directory)))) (while talks (let* ((o (pop talks)) (next-talk (emacsconf-format-talk-link (pop next-talks))) (prev-talk (emacsconf-format-talk-link (pop prev-talks)))) (with-temp-file (expand-file-name (format "%s-nav.md" (plist-get o :slug)) (expand-file-name "info" (expand-file-name emacsconf-year emacsconf-directory))) (insert (concat "\n<div class=\"talk-nav\"> Back to the [[talks]] \n" (if prev-talk (format "Previous by %s: %s \n" label prev-talk) "") (if next-talk (format "Next by %s: %s \n" label next-talk) "") (if (plist-get o :track) ; tagging doesn't work here because ikiwiki will list the nav page (format "Track: <span class=\"sched-track %s\">%s</span> \n" (plist-get o :track) (plist-get o :track)) "") "</div> ")))))))
(defun emacsconf-pad-set-html (pad-id html) "Set PAD-ID contents to the given HTML." (interactive "MPad ID: \nMHTML: ") (let ((url-request-data (concat "html=" (url-hexify-string html))) (url-request-method "POST") (url-request-extra-headers '(("Content-Type" . "application/x-www-form-urlencoded")))) (emacsconf-pad-json-request (format "%sapi/1/setHTML?apikey=%s&padID=%s" emacsconf-pad-base (url-hexify-string emacsconf-pad-api-key) (url-hexify-string pad-id)) (called-interactively-p 'any))))
<div> <div>All talks: ${talks}</div> <div><strong>${title}</strong></div> <div>${base-url}${url} - ${speakers} - Track: ${track}</div> <div>Watch/participate: ${watch}</div> ${bbb-info} <div>IRC: ${irc-nick-details} https://chat.emacsconf.org/#/connect?join=emacsconf,emacsconf-${track-id} or #emacsconf-${track-id} on libera.chat network</div> <div>Guidelines for conduct: ${base-url}conduct</div> <div>See end of file for license (CC Attribution-ShareAlike 4.0 + GPLv3 or later)</div> <div>----------------------------------------------------------------</div> <div>Notes, discussions, links, feedback:</div> <ul>${notes}</ul> <div>----------------------------------------------------------------</div> ...
(defun emacsconf-pad-prepopulate-talk-pad (o) "Fill in the pad for O." (interactive (list (let ((info (emacsconf-include-next-talks (emacsconf-get-talk-info) emacsconf-pad-number-of-next-talks))) (emacsconf-complete-talk-info info)))) (let ((pad-id (emacsconf-pad-id o))) (emacsconf-pad-create-pad pad-id) (when (or emacsconf-pad-force-all (not (emacsconf-pad-modified-p pad-id)) (progn (browse-url (emacsconf-pad-url o)) (y-or-n-p (format "%s might have been modified. Reset? " (plist-get o :slug))))) (emacsconf-pad-set-html pad-id (emacsconf-pad-initial-content o)) (save-window-excursion (emacsconf-with-talk-heading (plist-get o :slug) (let-alist (emacsconf-pad-get-last-edited pad-id) (org-entry-put (point) "PAD_RESET" (number-to-string .data.lastEdited))))))))
(defun emacsconf-mail-review (talk) "Let the speaker know we've received their proposal." (interactive (list (emacsconf-complete-talk-info))) (emacsconf-mail-prepare '(:subject "${conf-name} ${year} review: ${title}" :cc "emacsconf-submit@gnu.org" :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" :body " Hi, ${speakers-short}! Thanks for submitting your proposal! (ZZZ TODO: feedback) We'll wait a week (~ ${notification-date}) in case the other volunteers want to chime in regarding your talk. =) ${signature} ") (plist-get talk :email) (list :user-email user-mail-address :signature user-full-name :title (plist-get talk :title) :email (plist-get talk :email) :conf-name emacsconf-name :speakers-short (plist-get talk :speakers-short) :year emacsconf-year :notification-date (plist-get talk :date-to-notify))))
(defun emacsconf-mail-upload-and-backstage-info (group) "E-mail upload and backstage access information to GROUP." (interactive (list (emacsconf-mail-complete-email-group))) (emacsconf-mail-prepare (list :subject "${conf-name} ${year}: Upload instructions, backstage info" :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" :log-note "sent backstage and upload information" :body "${email-notes}Hi ${name}! Hope things are going well for you! I got the upload service up and running so you can upload your video${plural} and other talk resources (ex: speaker notes, Org files, slides, etc.). You can access it at ${upload-url} with the password \"${upload-password}\". Please let me know if you run into technical issues.${fill} If you can get your file(s) uploaded by ${video-target-date}, that would give us plenty of time to reencode it, edit captions, and so on. If life has gotten a bit busier than expected, that's okay. You can upload it when you can or we can plan to do your presentation live. (Late submissions and live presentations are a bit more stressful, but it'll probably work out.)${fill} We've also set up ${backstage} as the backstage area where you can view the videos and resources uploaded so far. You can access it with the username \"${backstage-user}\" and the password \"${backstage-password}\". Please keep the backstage password and other speakers' talk resources secret. This is a manual process, so your talk won't immediately show up there once you've upload it. Once we've processed your talk, you can use the backstage area to check if your talk has been correctly reencoded and see the progress on captions. You can also check out other people's talks. Enjoy!${fill} Thank you so much for contributing to ${conf-name} ${year}! ${signature}") (car group) (list :email-notes (emacsconf-surround "ZZZ: " (plist-get (cadr group) :email-notes) "\n\n" "") :backstage (emacsconf-replace-plist-in-string (list :year emacsconf-year) "https://media.emacsconf.org/${year}/backstage/") :plural (if (= 1 (length (cdr group))) "" "s") :backstage-user emacsconf-backstage-user :backstage-password emacsconf-backstage-password :upload-url (concat "https://ftp-upload.emacsconf.org/?sid=" emacsconf-upload-password "-" (mapconcat (lambda (o) (plist-get o :slug)) (cdr group) "-")) :upload-password emacsconf-upload-password :video-target-date emacsconf-video-target-date :name (plist-get (cadr group) :speakers-short) :email (car group) :user-email user-mail-address :signature user-full-name :conf-name emacsconf-name :year emacsconf-year)))
(defun emacsconf-spookfox-create-bbb (group) "Create a BBB room for this group of talks. GROUP is (email . (talk talk talk)). Needs a Spookfox connection." (let* ((bbb-name (format "%s (%s) - %s%s" (mapconcat (lambda (o) (plist-get o :slug)) (cdr group) ", ") (plist-get (cadr group) :speakers) emacsconf-id emacsconf-year)) path (retrieve-command (format "window.location.origin + [...document.querySelectorAll('h4.room-name-text')].find((o) => o.textContent.trim() == '%s').closest('tr').querySelector('.delete-room').getAttribute('data-path')" bbb-name)) (create-command (format "document.querySelector('#create-room-block').click(); document.querySelector('#create-room-name').value = \"%s\"; document.querySelector('#room_mute_on_join').click(); document.querySelector('.create-room-button').click();" bbb-name))) (setq path (spookfox-js-injection-eval-in-active-tab retrieve-command t)) (unless path (dolist (cmd (split-string create-command ";")) (spookfox-js-injection-eval-in-active-tab cmd t) (sleep-for 2)) (sleep-for 2) (setq path (spookfox-js-injection-eval-in-active-tab retrieve-command t))) (when path (dolist (talk (cdr group)) (save-window-excursion (emacsconf-with-talk-heading talk (org-entry-put (point) "ROOM" path)))) (cons bbb-name path))))
(defun emacsconf-mail-checkin-instructions-to-all () "Draft check-in instructions for all speakers." (interactive) (let* ((talks (seq-filter (lambda (o) (and (plist-get o :email) (plist-get o :q-and-a))) (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) (by-attendance (seq-group-by (lambda (o) (null (string-match "after" (plist-get o :q-and-a)))) talks))) (dolist (group (emacsconf-mail-groups (assoc-default nil by-attendance))) (emacsconf-mail-checkin-instructions-for-nonattending-speakers group)) (dolist (group (emacsconf-mail-groups (assoc-default t by-attendance))) (emacsconf-mail-checkin-instructions-for-attending-speakers group))))
(defun emacsconf-publish-bbb-redirect (talk &optional status) "Update the publicly-available redirect for TALK." (interactive (list (emacsconf-complete-talk-info))) (let ((bbb-filename (expand-file-name (format "bbb-%s.html" (plist-get talk :slug)) emacsconf-publish-current-dir)) (bbb-redirect-url (concat "https://media.emacsconf.org/" emacsconf-year "/current/bbb-" (plist-get talk :slug) ".html")) (status (or status (emacsconf-bbb-status (if (boundp 'org-state) (append (list :status org-state) talk) talk))))) (with-temp-file bbb-filename (insert (emacsconf-replace-plist-in-string (append talk (list :base-url emacsconf-base-url :bbb-redirect-url bbb-redirect-url)) (pcase status ('before "<html><head><meta http-equiv=\"refresh\" content=\"5; URL=${bbb-redirect-url}\"></head><body> The Q&A room for ${title} is not yet open. This page will refresh every 5 seconds until the BBB room is marked as open, or you can refresh it manually.</body></html>") ('open "<html><head><meta http-equiv=\"refresh\" content=\"0; URL=${bbb-room}\"></head><body> The live Q&A room for ${title} is now open. You should be redirected to <a href=\"${bbb-room}\">${bbb-room}</a> automatically, but if not, please visit the URL manually to join the Q&A.</body></html>") ('after "<html><head><body> The Q&A room for ${title} has finished. You can find more information about the talk at <a href=\"${base-url}${url}\">${base-url}${url}</a>.</body></html>") (_ "<html><head><body> There is no live Q&A room for ${title}. You can find more information about the talk at <a href=\"${base-url}${url}\">${base-url}${url}</a>.</body></html>" )))))))
(defun emacsconf-publish-bbb-static-redirects () "Create emergency redirects that can be copied over the right location." (interactive) (mapc (lambda (state) (let ((emacsconf-publish-current-dir (expand-file-name state (expand-file-name "redirects" (expand-file-name "assets" emacsconf-backstage-dir))))) (unless (file-directory-p emacsconf-publish-current-dir) (make-directory emacsconf-publish-current-dir t)) (mapc (lambda (talk) (emacsconf-publish-bbb-redirect talk (intern state))) (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))) '("before" "open" "after")))
(defun emacsconf-mail-add-to-logbook (email note) "Add to logbook for all matching talks from this speaker." (interactive (let* ((email (mail-strip-quoted-names (plist-get (plist-get (notmuch-show-get-message-properties) :headers) :From))) (talks (emacsconf-mail-talks email))) (list email (read-string (format "Note for %s: " (mapconcat (lambda (o) (plist-get o :slug)) talks", ")))))) (save-window-excursion (mapc (lambda (talk) (emacsconf-add-to-talk-logbook talk note)) (emacsconf-mail-talks email))))
(setq emacsconf-backstage-dir "/ssh:orga@media.emacsconf.org:/var/www/media.emacsconf.org/2023/backstage")
(setq filename (or filename (expand-file-name "index.html" emacsconf-backstage-dir))) ;; ... (with-temp-file filename ;; ... )
Using timers and TRAMP to play talks automatically
Using crontab to play talks automatically
PATH=/usr/local/bin:/usr/bin MAILTO="" XDG_RUNTIME_DIR="/run/user/2002" 35 11 26 10 * /usr/bin/screen -dmS play-sat-open bash -c "DISPLAY=:5 TEST_MODE=1 /usr/local/bin/handle-session sat-open | tee -a ~/track.log" 36 11 26 10 * /usr/bin/screen -dmS play-uni bash -c "DISPLAY=:5 TEST_MODE=1 /usr/local/bin/handle-session uni | tee -a ~/track.log" 38 11 26 10 * /usr/bin/screen -dmS play-adventure bash -c "DISPLAY=:5 TEST_MODE=1 /usr/local/bin/handle-session adventure | tee -a ~/track.log"
#!/bin/bash # {{ ansible_managed }} # # Handle the intro/talk/Q&A for a session # Usage: handle-session $SLUG YEAR="{{ emacsconf_year }}" BASE_DIR="{{ emacsconf_caption_dir }}" FIREFOX_NAME=firefox-esr SLUG=$1 # Kill background music if playing if screen -list | grep -q background; then screen -S background -X quit fi # Update the status sudo -u {{ emacsconf_user }} talk $SLUG PLAYING & # Update the overlay overlay $SLUG # Play the intro if it exists. If it doesn't exist, switch to the intro slide and stop processing. if [[ -f $BASE_DIR/assets/intros/$SLUG.webm ]]; then killall -s TERM $FIREFOX_NAME mpv $BASE_DIR/assets/intros/$SLUG.webm else firefox --kiosk $BASE_DIR/assets/in-between/$SLUG.png exit 0 fi # Play the video if it exists. If it doesn't exist, switch to the BBB room and stop processing. if [ "x$TEST_MODE" = "x" ]; then LIST=($BASE_DIR/assets/stream/{{ emacsconf_id }}-{{ emacsconf_year }}-$SLUG*--main.webm) else LIST=($BASE_DIR/assets/test/{{ emacsconf_id }}-{{ emacsconf_year }}-$SLUG*--main.webm) fi FILE="${LIST[0]}" if [ ! -f "$FILE" ]; then # Is there an original file? LIST=($BASE_DIR/assets/stream/{{ emacsconf_id }}-{{ emacsconf_year }}-$SLUG*--original.{webm,mp4,mov}) FILE="${LIST[0]}" fi if [[ -f $FILE ]]; then killall -s TERM $FIREFOX_NAME mpv $FILE else /usr/local/bin/bbb $SLUG exit 0 fi sudo -u {{ emacsconf_user }} talk $SLUG CLOSED_Q & # Open the appropriate Q&A URL QA=$(jq -r '.talks[] | select(.slug=="'$SLUG'")["qa-backstage-url"]' < $BASE_DIR/talks.json) QA_TYPE=$(jq -r '.talks[] | select(.slug=="'$SLUG'")["qa-type"]' < $BASE_DIR/talks.json) echo "QA_TYPE $QA_TYPE QA $QA" if [ "$QA_TYPE" = "live" ]; then /usr/local/bin/bbb $SLUG elif [ "$QA" != "null" ]; then /usr/local/bin/music & /usr/bin/firefox $QA # i3-msg 'layout splith' fi wait
# ... from handle-session ... # Open the appropriate Q&A URL QA=$(jq -r '.talks[] | select(.slug=="'$SLUG'")["qa-backstage-url"]' < $BASE_DIR/talks.json) QA_TYPE=$(jq -r '.talks[] | select(.slug=="'$SLUG'")["qa-type"]' < $BASE_DIR/talks.json) echo "QA_TYPE $QA_TYPE QA $QA" if [ "$QA_TYPE" = "live" ]; then /usr/local/bin/bbb $SLUG elif [ "$QA" != "null" ]; then /usr/local/bin/music & /usr/bin/firefox $QA # i3-msg 'layout splith' fi
b#!/bin/bash # Open the Big Blue Button room using the backstage link # {{ ansible_managed }} # Kill the background music if playing if screen -list | grep -q background; then screen -S background -X quit fi # Update the overlay SLUG=$1 overlay $SLUG killall -s TERM firefox-esr firefox https://media.emacsconf.org/{{ emacsconf_year }}/backstage/assets/redirects/open/bbb-$SLUG.html & sleep 5 xdotool search --class firefox windowactivate --sync xdotool key Return xdotool key F11 wait
(defun emacsconf-stream-format-crontab (track talks &optional test-mode) "Return crontab entries for TALKS. Use the display specified in TRACK. If TEST-MODE is non-nil, load the videos from the test directory." (concat (format "PATH=/usr/local/bin:/usr/bin MAILTO=\"\" XDG_RUNTIME_DIR=\"/run/user/%d\" " (plist-get track :uid)) (mapconcat (lambda (talk) (format "%s /usr/bin/screen -dmS play-%s bash -c \"DISPLAY=%s TEST_MODE=%s /usr/local/bin/handle-session %s | tee -a ~/track.log\"\n" ;; cron times are UTC (format-time-string "%-M %-H %-d %m *" (plist-get talk :start-time)) (plist-get talk :slug) (plist-get track :vnc-display) (if test-mode "1" "") (plist-get talk :slug))) (emacsconf-filter-talks talks))))
(defun emacsconf-stream-crontabs (&optional test-mode info) "Write the streaming users' crontab files. If TEST-MODE is non-nil, use the videos in the test directory. If INFO is non-nil, use that as the schedule instead." (interactive) (let ((emacsconf-publishing-phase 'conference)) (setq info (or info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) (dolist (track emacsconf-tracks) (let ((talks (seq-filter (lambda (talk) (string= (plist-get talk :track) (plist-get track :name))) info)) (crontab (expand-file-name (concat (plist-get track :id) ".crontab") (concat (plist-get track :tramp) "~")))) (with-temp-file crontab (when (plist-get track :autopilot) (insert (emacsconf-stream-format-crontab track talks test-mode)))) (emacsconf-stream-track-ssh track (concat "crontab ~/" (plist-get track :id) ".crontab"))))))
(defun emacsconf-stream-cancel-crontab (track) "Remove crontab for TRACK." (interactive (list (emacsconf-complete-track))) (plist-put track :autopilot nil) (emacsconf-stream-track-ssh track "crontab -r"))
#!/bin/bash # {{ ansible_managed }} # How to use: talk slug from-status-regexp to-status # or talk slug to-status SLUG="$1" FROM_STATUS="$2" TO_STATUS="$3" XDG_RUNTIME_DIR=/run/user/{{ getent_passwd[emacsconf_user].1 }} if [ "x$TO_STATUS" == "x" ]; then FROM_STATUS=. TO_STATUS="$2" fi cd {{ emacsconf_private_dir }} #echo "Pulling conf.org..." #git pull echo "Updating status..." XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR emacsclient --eval "(emacsconf-with-todo-hooks (emacsconf-update-talk-status \"$SLUG\" \"$FROM_STATUS\" \"$TO_STATUS\"))" -a emacs #echo "Committing and pushing in the background" #git commit -m "Update task status for $SLUG from $FROM_STATUS to $TO_STATUS" conf.org #git push &
(defun emacsconf-org-after-todo-state-change () "Run all the hooks in `emacsconf-todo-hooks'. If an `emacsconf-todo-hooks' entry is a list, run it only for the tracks with the ID in the cdr of that list." (let* ((talk (emacsconf-get-talk-info-for-subtree)) (track (emacsconf-get-track (plist-get talk :track)))) (mapc (lambda (hook-entry) (cond ((symbolp hook-entry) (funcall hook-entry talk)) ((member (plist-get track :id) (cdr hook-entry)) (funcall (car hook-entry) talk)))) emacsconf-todo-hooks)))
(defun emacsconf-erc-announce-on-change (talk) "Announce talk." (let ((emacsconf-publishing-phase 'conference) (func (pcase org-state ("PLAYING" #'erc-cmd-NOWPLAYING) ("CLOSED_Q" #'erc-cmd-NOWCLOSEDQ) ("OPEN_Q" #'erc-cmd-NOWOPENQ) ("UNSTREAMED_Q" #'erc-cmd-NOWUNSTREAMEDQ) ("TO_ARCHIVE" #'erc-cmd-NOWDONE)))) (when func (funcall func talk))))
(defun erc-cmd-NOWPLAYING (talk) "Set the channel topics to announce TALK." (interactive (list (emacsconf-complete-talk-info))) (when (stringp talk) (setq talk (or (emacsconf-find-talk-info talk) (error "Could not find talk %s" talk)))) ;; Announce it in the track's channel (if (emacsconf-erc-recently-announced (format "---- %s:" (plist-get talk :slug))) (message "Recently announced, skipping") (when (plist-get talk :track) (emacsconf-erc-with-channels (list (concat "#" (plist-get talk :channel))) (erc-cmd-TOPIC (format "%s: %s (%s) pad: %s Q&A: %s | %s" (plist-get talk :slug) (plist-get talk :title) (plist-get talk :speakers) (plist-get talk :pad-url) (plist-get talk :qa-info) (car (assoc-default (concat "#" (plist-get talk :channel)) emacsconf-topic-templates)))) (erc-send-message (format "---- %s: %s - %s ----" (plist-get talk :slug) (plist-get talk :title) (plist-get talk :speakers-with-pronouns))) (erc-send-message (concat "Add your notes/questions to the pad: " (plist-get talk :pad-url))) (cond ((string-match "live" (or (plist-get talk :q-and-a) "")) (erc-send-message (concat "Live Q&A: " (plist-get talk :bbb-redirect)))) ((plist-get talk :irc) (erc-send-message (format "or discuss the talk on IRC (nick: %s)" (plist-get talk :irc))))))) ;; Short announcement in #emacsconf (emacsconf-erc-with-channels (list emacsconf-erc-hallway emacsconf-erc-org) (erc-send-message (format "-- %s track: %s: %s (watch: %s, pad: %s, channel: #%s)" (plist-get talk :track) (plist-get talk :slug) (plist-get talk :title) (plist-get talk :watch-url) (plist-get talk :pad-url) (plist-get talk :channel))))))
Happy hacking!