diff options
Diffstat (limited to '')
-rw-r--r-- | emacsconf-erc.el | 111 | ||||
-rw-r--r-- | emacsconf-extract.el | 325 | ||||
-rw-r--r-- | emacsconf-mail.el | 663 | ||||
-rw-r--r-- | emacsconf-pad.el | 184 | ||||
-rw-r--r-- | emacsconf-publish.el | 461 | ||||
-rw-r--r-- | emacsconf-schedule.el | 205 | ||||
-rw-r--r-- | emacsconf-stream.el | 143 | ||||
-rw-r--r-- | emacsconf-subed.el | 8 | ||||
-rw-r--r-- | emacsconf.el | 232 |
9 files changed, 1632 insertions, 700 deletions
diff --git a/emacsconf-erc.el b/emacsconf-erc.el index bced63d..71b2478 100644 --- a/emacsconf-erc.el +++ b/emacsconf-erc.el @@ -26,6 +26,15 @@ ;; ;; Commands: ;; +;; /opall +;; /deopall +;; +;; general +;; - /broadcast message +;; - /conftopic message +;; +;; - /checkin nick talk +;; ;; announcements only ;; - /nowplaying slug ;; - /nowclosedq slug @@ -42,10 +51,6 @@ ;; - /markunstreamedq slug ;; - /markdone slug ;; -;; general -;; - /broadcast message -;; - /conftopic message -;; - /checkin nick ;; ;;; Code: @@ -63,12 +68,12 @@ (defcustom emacsconf-erc-org "#emacsconf-org" "Channel for organizers") (defcustom emacsconf-topic-templates - '(("#emacsconf" "Welcome to EmacsConf 2023 | please join our track-specific channels #emacsconf-gen and #emacsconf-dev as well | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") - ("#emacsconf-gen" "General track | https://emacsconf.org/2023/watch/gen/ | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") - ("#emacsconf-dev" "Development track | https://emacsconf.org/2023/watch/dev/ | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") - ("#emacsconf-accessible" "EmacsConf 2023 accessibility - help by describing what's happening | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") - ("#emacsconf-org" "EmacsConf 2023 | Dedicated channel for EmacsConf organizers and speakers | this is intended as an internal, low-traffic channel; for main discussion around EmacsConf, please join #emacsconf | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") - ("#emacsconf-questions" "EmacsConf 2023 | Low-traffic channel for questions if speakers prefer IRC and need help focusing; for main discussion around EmacsConf, please join #emacsconf | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates")) + '(("#emacsconf" "Welcome to EmacsConf 2024 | please join our track-specific channels #emacsconf-gen and #emacsconf-dev as well | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") + ("#emacsconf-gen" "General track | https://emacsconf.org/2024/watch/gen/ | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") + ("#emacsconf-dev" "Development track | https://emacsconf.org/2024/watch/dev/ | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") + ("#emacsconf-accessible" "EmacsConf 2024 accessibility - help by describing what's happening | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") + ("#emacsconf-org" "EmacsConf 2024 | Dedicated channel for EmacsConf organizers and speakers | this is intended as an internal, low-traffic channel; for main discussion around EmacsConf, please join #emacsconf | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") + ("#emacsconf-questions" "EmacsConf 2024 | Low-traffic channel for questions if speakers prefer IRC and need help focusing; for main discussion around EmacsConf, please join #emacsconf | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates")) "List of (channel topic-template) entries for mass-setting channel topics." :group 'emacsconf :type '(repeat (list (string :tag "Channel") @@ -94,19 +99,20 @@ If MESSAGE is not specified, reset the topic to the template." (cadr template))))) emacsconf-topic-templates)) -(defun erc-cmd-CHECKIN (nick) - (let* ((talk (emacsconf-complete-talk-info)) - (q-and-a (plist-get talk :q-and-a) "")) - (save-excursion - (emacsconf-with-talk-heading (plist-get talk :slug) - (org-entry-put (point) "IRC" nick) - (org-entry-put (point) "CHECK_IN" (format-time-string "%H:%M")))) +(defun erc-cmd-CHECKIN (nick &optional talk) + (let* ((talk (if (stringp talk) (emacsconf-resolve-talk talk) (or talk (emacsconf-complete-talk-info)))) + (q-and-a (or (plist-get talk :q-and-a) ""))) + (save-window-excursion + (save-excursion + (emacsconf-with-talk-heading (plist-get talk :slug) + (org-entry-put (point) "IRC" nick) + (org-entry-put (point) "CHECK_IN" (format-time-string "%H:%M"))))) (cond ((string-match "live" q-and-a) (erc-send-message (format "%s: Thanks for checking in! I'll send you some private messages with instructions, so please check there. Let me know if you don't get them." nick)) (erc-cmd-ROOM nick talk)) ((string-match "pad" q-and-a) - (erc-send-message (format "%s: Thanks for checking in! The collaborative pad we'll be using for questions is at %s . We'll collect questions and put them there. If you'd like to open it, you can keep an eye on questions. Please let us know if you need help, or if you want to switch to live Q&A." nick (plist-get talk :pad-url)))) + (erc-send-message (format "%s: Thanks for checking in! The collaborative pad we'll be using for questions is at %s . We'll collect questions and put them there. If you'd like to open it, you can keep an eye on questions. You can answer questions in any order or skip anything you want to save for later. Please let us know if you need help, or if you want to switch to live Q&A." nick (plist-get talk :pad-url)))) ((string-match "IRC" q-and-a) (erc-send-message (format "#%s: Thanks for checking in! Feel free to keep an eye on %s for questions and discussion, and we'll copy things from the pad to there. If the volume gets overwhelming, let us know and we can /msg you questions or add them to the pad. If you'd like to try Q&A over live video or the collaborative pad instead, or if you need help, please let us know." nick (plist-get (emacsconf-get-track (plist-get talk :track)) :channel)))) @@ -115,11 +121,37 @@ If MESSAGE is not specified, reset the topic to the template." (emacsconf-with-talk-heading (plist-get talk :slug) (emacsconf-upcoming-insert-or-update nil t))))) -(defun erc-cmd-BBB (nick &optional talk) +(defun erc-cmd-BACKSTAGE (nick) + "Send NICK emergency backstage information." + (erc-send-message (format "%s: I'll send you a private message on IRC with the backstage details.")) + (erc-message + "PRIVMSG" + (format "%s You can access the backstage area at %s%s/backstage/ with the username \"%s\" and the password \"%s\" (please keep them secret, thanks!). The general talk index is at %s%s/backstage/index-gen.html and the dev talk index is at %s%s/backstage/index-dev.html" + nick + emacsconf-media-base-url + emacsconf-year + emacsconf-backstage-user + emacsconf-backstage-password + emacsconf-media-base-url + emacsconf-year + emacsconf-media-base-url + emacsconf-year + ))) + +(defun erc-cmd-PAD (nick &optional talk) + (setq talk (if (stringp talk) (emacsconf-resolve-talk talk) + (or talk (emacsconf-complete-talk-info)))) + (erc-message "PRIVMSG" (format "%s The collaborative pad we'll be using for questions is at %s . We'll collect questions from %s and put them there. When you're live, you can answer questions in any order or skip anything you want to save for later. Alternatively, we can read questions to you. Let us know what you'd prefer!" nick (plist-get talk :pad-url) (plist-get talk :channel)))) + +(defun erc-cmd-ROOM (nick &optional talk) "Send live Q&A instructions to NICK." - (setq talk (or talk (emacsconf-complete-talk-info))) - (erc-message "PRIVMSG" (format "%s You can use this BBB room: %s . We'll join you there shortly to set up the room and do the last-minute tech check." nick (plist-get talk :bbb-room))) - (erc-message "PRIVMSG" (format "%s The collaborative pad we'll be using for questions is at %s . We'll collect questions from #emacsconf and put them there. If you'd like to jump to your part of the document, you might be able to keep an eye on questions. Alternatively, we can read questions to you." nick (plist-get talk :pad-url))) + (setq talk (if (stringp talk) (emacsconf-resolve-talk talk) + (or talk (emacsconf-complete-talk-info)))) + (erc-message + "PRIVMSG" + (if (plist-get talk :bbb-mod-code) + (format "%s You can use this BBB room: %s . If you use the moderator access code \"%s\" when you log in, you can get all set up to share your screen if you want. We'll join you there shortly to do the last-minute tech check." nick (plist-get talk :bbb-room) (plist-get talk :bbb-mod-code)) + (format "%s You can use this BBB room: %s . We'll join you there shortly to set up the room and do the last-minute tech check." nick (plist-get talk :bbb-room)))) (erc-message "PRIVMSG" (format "%s The host will join and give you the go-ahead when you go on air. See you in the BBB room!" nick))) (defun erc-cmd-READY (&rest filter) @@ -183,10 +215,10 @@ If MESSAGE is not specified, reset the topic to the template." (emacsconf-erc-with-channels (list (concat "#" (plist-get talk :channel))) (erc-cmd-TOPIC (format - "%s: %s (%s) pad: %s Q&A: %s | %s" + "%s: %s%s pad: %s Q&A: %s | %s" (plist-get talk :slug) (plist-get talk :title) - (plist-get talk :speakers) + (emacsconf-surround " (" (plist-get talk :speakers) ")" " -") (plist-get talk :pad-url) (plist-get talk :qa-info) (car (assoc-default @@ -195,7 +227,7 @@ If MESSAGE is not specified, reset the topic to the template." (erc-send-message (format "---- %s: %s - %s ----" (plist-get talk :slug) (plist-get talk :title) - (plist-get talk :speakers-with-pronouns))) + (emacsconf-surround " - " (plist-get talk :speakers-with-pronouns) "" ""))) (erc-send-message (concat "Add your notes/questions to the pad: " (plist-get talk :pad-url))) (cond @@ -515,5 +547,34 @@ Usage: /conflog keyword notes go here" (cons (cons (match-string-no-properties 1 string) (current-time)) (seq-take emacsconf-erc-recent-announcements (1- emacsconf-erc-recent-announcements-length)))))) +;; (keymap-set erc-mode-map "C-c w" #'emacsconf-erc-copy) +(defun emacsconf-erc-copy (&optional beg end) + "Unwrap and copy the current line to the clipboard. +This makes it easier to paste into the Etherpad." + (interactive + (list + (if (region-active-p) + (min (point) (mark))) + (if (region-active-p) + (max (point) (mark))))) + (setq beg (or beg (if (get-text-property (point) 'erc--ts) + (line-beginning-position) + (prop-match-beginning (text-property-search-backward 'erc--ts))))) + (setq end + (let ((end-field (save-excursion (text-property-search-forward 'field))) + (end-nick (save-excursion (text-property-search-forward 'erc--ts nil nil t)))) + (min (if end-field (prop-match-beginning end-field) most-positive-fixnum) + (if end-nick (prop-match-beginning end-nick) most-positive-fixnum)))) + (let* ((pulse-flag nil)) + (when (fboundp 'pulse-momentary-highlight-region) + (pulse-momentary-highlight-region beg end)) + (kill-new + (string-trim + (replace-regexp-in-string + "\n[ \t]+" " " + (buffer-substring-no-properties + beg + end)))))) + (provide 'emacsconf-erc) ;;; emacsconf-erc.el ends here diff --git a/emacsconf-extract.el b/emacsconf-extract.el index efd2ade..5600494 100644 --- a/emacsconf-extract.el +++ b/emacsconf-extract.el @@ -247,6 +247,11 @@ (gethash "sentences" data))))) ;; (emacsconf-extract-qa-from-assemblyai-sentences "~/proj/emacsconf/rms/sentences") +(defun emacsconf-extract-unescape (s) + (replace-regexp-in-string + "\\\\\\(['\"]\\)" + "\\1" s)) + ;;;###autoload (defun emacsconf-extract-copy-pad-to-wiki () "Copy the notes and questions from the current file to the wiki page for this talk." @@ -268,12 +273,12 @@ nil (re-search-forward "-after)" nil t) (forward-line -1) - (insert "# Discussion\n\n")) - (save-excursion - (unless (string= (or questions "") "") - (insert "## Questions and answers\n\n" questions "\n\n")) - (unless (string= (or notes "") "") - (insert "## Notes\n\n" notes "\n\n"))))) + (insert "# Discussion\n\n") + (save-excursion + (unless (string= (or questions "") "") + (insert "## Questions and answers\n\n" (emacsconf-extract-unescape questions) "\n\n")) + (unless (string= (or notes "") "") + (insert "## Notes\n\n" (emacsconf-extract-unescape notes) "\n\n")))))) (defun emacsconf-extract-question-headings (slug) (with-temp-buffer @@ -308,7 +313,7 @@ "Question: " (emacsconf-extract-question-headings (emacsconf-get-slug-from-string (file-name-base (buffer-file-name))))))) - (insert "NOTE " question "\n\n")) + (subed-set-subtitle-comment (concat "Q: " question))) (defun emacsconf-extract-wget-bbb (o) (when (plist-get o :bbb-playback) @@ -355,7 +360,8 @@ (date-to-time (dom-text (dom-by-tag - (dom-elements dom 'eventname "StopRecordingEvent") + (or (dom-elements dom 'eventname "StopRecordingEvent") + (dom-elements dom 'eventname "EndAndKickAllEvent")) 'date)))) (setq start-ms (* 1000 (time-to-seconds start-recording)) stop-ms (* 1000 (time-to-seconds stop-recording))) @@ -428,11 +434,28 @@ (let ((results "")) (save-excursion (goto-char (point-min)) - (while (re-search-forward "^\\( *- \\([QA]: \\)?\\)\\[[0-9:]+\\] <.*?> \\(.*\n\\)" nil t) - (setq results (concat results (match-string 1) (match-string 3))) + (while (re-search-forward "^\\([qna] *\\| *- +\\([QA]: \\)?\\)\\[[0-9:]+\\] <.*?> \\(.*\n\\)" nil t) + (setq results (concat results + (save-match-data + (pcase (match-string 1) + ((rx "q") "- Q: ") + ((rx "a") "- A: ") + ((rx "n") "- ") + (_ "- "))) + (match-string 3))) (replace-match "" nil t nil 1)) (kill-new results)))) +(defvar-keymap emacsconf-extract-irc-log-map + "<down>" #'forward-line + "<up>" #'previous-line + "<right>" (lambda () (interactive) (insert "-") (forward-line)) + "<left>" #'forward-line) + +(defun emacsconf-extract-irc-log () + (interactive) + (set-transient-map emacsconf-extract-irc-log-map t)) + (defun emacsconf-extract-irc-backward-by-nick () (interactive) (goto-char (line-beginning-position)) @@ -503,29 +526,12 @@ (defun emacsconf-extract-irc-anonymize-log (beg end speakers) (interactive "r\nMNick(s): ") (when (stringp speakers) (setq speakers (split-string speakers))) - (let ((text (buffer-substring beg end)) - nicks) - (with-temp-buffer - (insert text) - (goto-char (point-min)) - ;; make a list of nicks - (while (re-search-forward "^\\[[0-9:]+\\] <\\(.*?\\)>" nil t) - (unless (member (match-string 1) speakers) - (add-to-list 'nicks (match-string 1)))) - (goto-char (point-min)) - (while (re-search-forward "^\\[[0-9:]+\\] <\\(.*?\\)> \\(.+\\)" nil t) - (replace-match - (if (member (match-string 1) speakers) - (concat " - A: " (match-string 2)) - (format "- {{%d}} %s" - (seq-position nicks (match-string 1)) - (propertize (match-string 2) - 'nick (match-string 1)))))) - (goto-char (point-min)) - (perform-replace (regexp-opt nicks) (lambda ())) - (setq text (buffer-string)) - (other-window 1) - (insert text)))) + (save-excursion + (goto-char beg) + (while (re-search-forward "^\\[[0-9:]+\\] <\\(.*?\\)> \\(.+\\)" end t) + (if (member (match-string 1) speakers) + (replace-match (concat "- " (match-string 1) ": " (match-string 2)) t t) + (replace-match (concat "- " (match-string 2)) t t))))) (defun emacsconf-private-qa (&optional info) (seq-remove (lambda (o) @@ -720,7 +726,7 @@ Would you like to help? See [[help_with_chapter_markers]] for more details. You (setq talk (emacsconf-resolve-talk talk)) (expand-file-name "events.xml" (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-raw-dir))) -(defun emacsconf-extract-bbb-report () +(defun emacsconf-extract-bbb-report (&optional event-xml-files) (let* ((max 0) (participant-count 0) (meeting-count 0) @@ -730,39 +736,41 @@ Would you like to help? See [[help_with_chapter_markers]] for more details. You (meeting-events (sort (seq-mapcat - (lambda (talk) - (when (plist-get talk :bbb-meeting-id) - (let ((dom (xml-parse-file (emacsconf-extract-bbb-raw-events-file-name talk))) - participants talking meeting-events) - (mapc (lambda (o) - (pcase (dom-attr o 'eventname) - ("ParticipantJoinEvent" - (cl-pushnew (cons (dom-text (dom-by-tag o 'userId)) - (dom-text (dom-by-tag o 'name))) - participants) - (push (cons (string-to-number (dom-text (dom-by-tag o 'timestampUTC))) - (dom-attr o 'eventname)) - meeting-events)) - ("ParticipantLeftEvent" - (when (string= (dom-attr o 'module) "PARTICIPANT") - (push (cons (string-to-number (dom-text (dom-by-tag o 'timestampUTC))) - (dom-attr o 'eventname)) - meeting-events))) - ("ParticipantTalkingEvent" - (cl-pushnew (assoc-default (dom-text (dom-by-tag o 'participant)) participants) talking)) - ((or - "CreatePresentationPodEvent" - "EndAndKickAllEvent") + (lambda (file) + (let ((dom (xml-parse-file file)) + participants talking meeting-events) + (mapc (lambda (o) + (pcase (dom-attr o 'eventname) + ("ParticipantJoinEvent" + (cl-pushnew (cons (dom-text (dom-by-tag o 'userId)) + (dom-text (dom-by-tag o 'name))) + participants) + (push (cons (string-to-number (dom-text (dom-by-tag o 'timestampUTC))) + (dom-attr o 'eventname)) + meeting-events)) + ("ParticipantLeftEvent" + (when (string= (dom-attr o 'module) "PARTICIPANT") (push (cons (string-to-number (dom-text (dom-by-tag o 'timestampUTC))) (dom-attr o 'eventname)) - meeting-events)))) - (dom-search dom (lambda (o) (dom-attr o 'eventname)))) - (cl-pushnew (list :slug (plist-get talk :slug) - :participants participants - :talking talking) - meeting-participants) - meeting-events))) - (emacsconf-get-talk-info)) + meeting-events))) + ("ParticipantTalkingEvent" + (cl-pushnew (assoc-default (dom-text (dom-by-tag o 'participant)) participants) talking)) + ((or + "CreatePresentationPodEvent" + "EndAndKickAllEvent") + (push (cons (string-to-number (dom-text (dom-by-tag o 'timestampUTC))) + (dom-attr o 'eventname)) + meeting-events)))) + (dom-search dom (lambda (o) (dom-attr o 'eventname)))) + (cl-pushnew (list ;; :slug (plist-get talk :slug) + :participants participants + :talking talking) + meeting-participants) + meeting-events)) + (or event-xml-files + (mapcar #'emacsconf-extract-bbb-raw-events-file-name + (seq-filter (lambda (talk) (plist-get talk :bbb-meeting-id)) + (emacsconf-get-talk-info))))) (lambda (a b) (< (car a) (car b)))))) (dolist (event meeting-events) (pcase (cdr event) @@ -794,13 +802,8 @@ Would you like to help? See [[help_with_chapter_markers]] for more details. You (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir)))) -(defun emacsconf-extract-bbb-parse-events (talk) - "Parse events TALK from raw recordings. -This works with the events.xml from /var/bigbluebutton/raw. -Files should be downloaded to `emacsconf-extract-bbb-raw-dir'." - (setq talk (emacsconf-resolve-talk talk)) - (let* ((xml-file (emacsconf-extract-bbb-raw-events-file-name talk)) - (dom (xml-parse-file xml-file)) +(defun emacsconf-extract-bbb-parse-events (xml-file) + (let* ((dom (xml-parse-file xml-file)) (meeting-name (dom-attr (dom-by-tag dom 'metadata) 'meetingName)) (meeting-id (dom-attr dom 'meeting_id)) (conf-joined (dom-search dom (lambda (o) (and (string= (dom-tag o) "name") (string= (dom-text o) emacsconf-extract-conference-username))))) @@ -886,6 +889,37 @@ Files should be downloaded to `emacsconf-extract-bbb-raw-dir'." (talking . ,(nreverse talking)) (chat . ,(nreverse chat))))) +(defun emacsconf-extract-bbb-talking-report (meeting-xml) + (let ((data (emacsconf-extract-bbb-parse-events meeting-xml))) + (unless (string= "" (alist-get 'meeting-date data)) + (format "- %s %s: %s\n" + (alist-get 'name data) + (format-time-string "%a %I:%M %p" + (date-to-time + (alist-get 'meeting-date data))) + (mapconcat + (lambda (person) + (format "%s (%s)" + (car person) + (/ (cdr person) 60000))) + (sort + (mapcar + (lambda (group) + (cons + (car group) + (apply '+ (mapcar (lambda (o) (- (elt o 2) (elt o 1))) (cdr group))))) + (seq-group-by 'car (alist-get 'talking data))) + :key 'cdr + :reverse t) + ", "))))) + +(defun emacsconf-extract-bbb-parse-events-for-talk (talk) + "Parse events TALK from raw recordings. +This works with the events.xml from /var/bigbluebutton/raw. +Files should be downloaded to `emacsconf-extract-bbb-raw-dir'." + (setq talk (emacsconf-resolve-talk talk)) + (emacsconf-extract-bbb-parse-events (emacsconf-extract-bbb-raw-events-file-name talk))) + (defun emacsconf-extract-bbb-format-chat () (mapconcat (lambda (events) @@ -1127,6 +1161,10 @@ Strategies: ;; To avoid being prompted for the client secret, it's helpful to have a line in ~/.authinfo or ~/.authinfo.gpg with ;; machine https://oauth2.googleapis.com/token username CLIENT_ID password CLIENT_SECRET +;; reset: +;; (setq url-http-oauth--interposed nil url-http-oauth--interposed-regexp nil) +;; and remove the token from ~/.authinfo + (defvar emacsconf-extract-google-client-identifier nil) (defvar emacsconf-extract-youtube-api-channels nil) (defvar emacsconf-extract-youtube-api-categories nil) @@ -1154,7 +1192,7 @@ Strategies: ("access_type" . "offline") ("prompt" . "consent"))) ("access-token-endpoint" . "https://oauth2.googleapis.com/token") - ("scope" . "https://www.googleapis.com/auth/youtube") + ("scope" . "https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl https://www.googleapis.com/auth/youtube.upload") ("client-secret-method" . prompt)))) (setq emacsconf-extract-youtube-api-channels (plz 'get "https://youtube.googleapis.com/youtube/v3/channels?part=contentDetails&mine=true" @@ -1180,6 +1218,106 @@ Strategies: (string-match (regexp-quote emacsconf-year) (let-alist item .snippet.title)))))) +(defun emacsconf-extract-youtube-comment-list () + (seq-mapcat + (lambda (item) + (append + (if (alist-get 'topLevelComment (alist-get 'snippet item)) + (list (alist-get 'topLevelComment (alist-get 'snippet item)))) + (alist-get 'comments (alist-get 'replies item)))) + (alist-get + 'items + (or emacsconf-extract-youtube-comments (emacsconf-extract-youtube-get-channel-comments))))) + +(defun emacsconf-extract-youtube-get-talk-for-video-id (video-id) + (seq-find (lambda (o) + (or (string-match (regexp-quote video-id) (or (plist-get o :youtube-url) "")) + (string-match (regexp-quote video-id) (or (plist-get o :qa-youtube-url) "")))) + (emacsconf-get-talk-info))) + +(defun emacsconf-extract-youtube-comments-after (date) + (interactive (list (org-read-date nil t nil "On or after date: "))) + (when (stringp date) + (setq date (org-read-date nil t date))) + (seq-filter + (lambda (entry) + (time-less-p + date + (date-to-time + (alist-get + 'publishedAt + (alist-get 'snippet entry))))) + (emacsconf-extract-youtube-comment-list))) + +(defun emacsconf-extract-youtube-format-talk-comments (videos) + (mapconcat + (lambda (video) + (format + "- https://youtu.be/%s\n%s\n" + (car video) + (mapconcat + (lambda (comment) + (let-alist comment + (format + " - %s: %s\n" + .snippet.authorDisplayName + (replace-regexp-in-string "\n" "\n " .snippet.textOriginal)))) + (cdr video) + ""))) + videos + "")) + +(defun emacsconf-extract-youtube-comments-by-talk (&optional comments) + (interactive (list + (if current-prefix-arg (emacsconf-extract-youtube-comments-after (org-read-date nil nil nil "Date: "))))) + (setq comments (or comments (emacsconf-extract-youtube-comment-list))) + (let ((by-talk + (seq-group-by + (lambda (group) + (plist-get (emacsconf-extract-youtube-get-talk-for-video-id (car group)) :slug)) + (seq-group-by (lambda (o) + (alist-get 'videoId (alist-get 'snippet o))) + comments)))) + (when (called-interactively-p 'any) + (with-current-buffer (get-buffer-create "*comments*") + (erase-buffer) + (org-mode) + (dolist (group by-talk) + (when (car group) + (insert (format + "* %s\n\n%s\n\n" + (org-link-make-string + (concat "file:" + (expand-file-name + (concat + (car group) ".md") + (expand-file-name + "talks" + (expand-file-name + emacsconf-year + emacsconf-directory)))) + (car group)) + (emacsconf-extract-youtube-format-talk-comments (cdr group)))))) + (display-buffer (current-buffer)))) + by-talk)) + + +;; (emacsconf-extract-youtube-comment-list) + +;; (emacsconf-extract-youtube-comments-after "-2mon") + +(defvar emacsconf-extract-youtube-comments nil) +(defun emacsconf-extract-youtube-get-channel-comments (&optional no-cache) + (setq + emacsconf-extract-youtube-comments + (or (and emacsconf-extract-youtube-comments (not no-cache)) + (plz 'get + (format + "https://youtube.googleapis.com/youtube/v3/commentThreads?part=snippet,replies&allThreadsRelatedToChannelId=%s&maxResults=100" + (alist-get 'id (car (alist-get 'items emacsconf-extract-youtube-api-channels)))) + :headers `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/"))) + :as #'json-read)))) + (defvar emacsconf-extract-youtube-tags '("emacs" "emacsconf")) (defun emacsconf-extract-youtube-object (video-id talk &optional privacy-status qa) "Format the video object for VIDEO-ID using TALK details. @@ -1206,19 +1344,27 @@ If QA is non-nil, treat it as a Q&A video." (let-alist video-object (cond ;; not yet renamed - ((string-match (rx (literal emacsconf-id) " " (literal emacsconf-year) " " - (group (1+ (or (syntax word) "-"))) - " ") - .snippet.title) + ((and .snippet.title (string-match (rx (literal emacsconf-id) " " (literal emacsconf-year) " " + (group (1+ (or (syntax word) "-"))) + " ") + .snippet.title)) (match-string 1 .snippet.title)) ;; renamed, match the description instead - ((string-match (rx (literal emacsconf-base-url) (literal emacsconf-year) "/talks/" - (group (1+ (or (syntax word) "-")))) - .snippet.description) + ((and .snippet.description + (string-match (rx (literal emacsconf-base-url) (literal emacsconf-year) "/talks/" + (group (1+ (or (syntax word) "-")))) + .snippet.description)) (match-string 1 .snippet.description)) (t (plist-get - (seq-find (lambda (o) (string-match (regexp-quote .snippet.resourceId.videoId) (or (plist-get o :youtube-url) ""))) + (seq-find (lambda (o) + (or + (string-match (regexp-quote (or .snippet.videoId + .snippet.resourceId.videoId)) + (or (plist-get o :youtube-url) "")) + (string-match (regexp-quote (or .snippet.videoId + .snippet.resourceId.videoId)) + (or (plist-get o :qa-youtube-url) "")))) (emacsconf-get-talk-info)) :slug))))) @@ -1261,6 +1407,7 @@ If QA is non-nil, treat it as a Q&A video." (if (<= num-pages 0) (setq url null)))) result)) + (defun emacsconf-extract-youtube-api-update-video (video-object &optional qa) "Update VIDEO-OBJECT. If QA is non-nil, treat it as a Q&A video." @@ -1419,6 +1566,8 @@ If QA is non-nil, treat it as a Q&A video." :as #'json-read)) nil))))) + + (defun emacsconf-extract-youtube-duration-msecs (video) (let-alist video (when-let ((duration .contentDetails.duration)) @@ -1672,5 +1821,19 @@ Call with a prefix arg to store the URL as Q&A." url))))) +(defun emacsconf-extract-subed-copy-section-text () + (interactive) + (save-excursion + (subed-copy-region-text + (unless (looking-at "^NOTE") + (if (re-search-backward "^NOTE" nil t) + (point) + (point-min))) + (progn + (forward-line) + (if (re-search-forward "^NOTE" nil t) + (match-beginning 0) + (point-max)))))) + (provide 'emacsconf-extract) ;;; emacsconf-extract.el ends here diff --git a/emacsconf-mail.el b/emacsconf-mail.el index 190ca4b..23743e8 100644 --- a/emacsconf-mail.el +++ b/emacsconf-mail.el @@ -238,10 +238,15 @@ Group by e-mail. With prefix argument (e.g. \\[universal-argument]), insert into the current buffer instead of drafting e-mails." (interactive "P") (let* ((mail-func (emacsconf-mail-complete-template-function)) - (grouped (emacsconf-mail-group-by-email)) + (grouped (emacsconf-filter-talks-by-logbook + (symbol-name mail-func) + (emacsconf-mail-group-by-email))) (emacsconf-mail-prepare-behavior (if arg t 'new-message))) (mapc (lambda (group) - (funcall mail-func group)) + (funcall mail-func group) + (mapc (lambda (talk) + (emacsconf-mail-log-message-when-sent talk (symbol-name mail-func))) + (cdr group))) grouped))) (defun emacsconf-mail-log-message-when-sent (o message) @@ -357,6 +362,7 @@ insert into the current buffer instead of drafting e-mails." (defun emacsconf-mail-parse-submission (body) "Extract data from EmacsConf submissions in BODY." (when (listp body) (setq body (plist-get (car body) :content))) + (when (listp body) (setq body (plist-get (car body) :content))) (let* ((data (list :body body)) (fields '((:title "^[* ]*Talk title") (:description "^[* ]*Talk description") @@ -534,6 +540,22 @@ Include some other things, too, such as emacsconf-year, title, name, email, url, (kill-buffer buffer))))) (buffer-list))) +(defun emacsconf-mail-merge-send-all () + "Send all the unsent messages." + (interactive) + (mapc (lambda (buffer) + (with-current-buffer buffer + (message-send-and-exit))) + (match-buffers "unsent"))) + +(defun emacsconf-mail-merge-clean-up () + "Kill all the sent messages." + (interactive) + (mapc (lambda (buffer) + (when (string-match "^\\*sent mail" (buffer-name buffer)) + (kill-buffer buffer))) + (buffer-list))) + ;;; Notmuch ;;;###autoload @@ -617,6 +639,99 @@ ${signature} :year emacsconf-year :notification-date (plist-get talk :date-to-notify)))) +(defun emacsconf-mail-send-grouped-acceptance (group &optional arg) + "Send acceptance letter to speakers. +GROUP is (email . (talk talk talk)). +If called with ARG, insert into current buffer instead of composing or updating a message." + (interactive (list (emacsconf-mail-complete-email-group + (seq-filter (lambda (o) (and + (plist-get o :email) + (string= (plist-get o :status) "TO_ACCEPT"))) + (emacsconf-get-talk-info))) + current-prefix-arg)) + (let ((emacsconf-mail-prepare-behavior (if arg t emacsconf-mail-prepare-behavior))) + (emacsconf-mail-prepare + '(:subject "${conf-name} ${year} acceptance${plural}: ${titles}" + :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}! + +Looks like all systems are a go for your talk${plural}. Thanks! + +${info} +Please feel free to update the wiki with more information or e-mail us +if you'd like help with any changes.${fill} + +If you want to get started on your talk early, we have some +instructions at ${base}${year}/prepare/ that might help. +We strongly encourage speakers to prepare talk videos by +${video-target-date} in order to reduce technical risks and make +things flow more smoothly. Plus, we might be able to get it captioned +by volunteers, just like the talks last year.${wrap} + +Don't sweat it if you're a few minutes over or under your proposed +time${plural}. If it looks like a much shorter or longer talk once you +start getting into it, let us know and we might be able to +adjust.${wrap} + +${todos} +I'll follow up with the specific schedule for your talk once things +settle down. In the meantime, please let us know if you have any +questions or if there's anything we can do to help out! + +${signature}" + :log-note "accepted talk") + (plist-get (cadr group) :email) + (list + :base emacsconf-base-url + :user-email user-mail-address + :year emacsconf-year + :signature user-full-name + :conf-name emacsconf-name + :email (plist-get (cadr group) :email) + :titles (mapconcat (lambda (o) (plist-get o :title)) (cdr group) "; ") + :info + (mapconcat + (lambda (o) + (emacsconf-replace-plist-in-string + (list :title (plist-get o :title) + :url (concat emacsconf-base-url (plist-get o :url)) + :time (plist-get o :time)) + " ${title}\n ${url}\n ${time} minutes\n")) + (cdr group) + "\n") + :todos + (concat + (if (= 1 (length (cdr group))) "Here's a handy TODO you can use if you want:" "Here are handy TODOs you can use if you want:") + "\n\n" + (mapconcat + (lambda (o) + (emacsconf-replace-plist-in-string + (list :title (plist-get o :title) + :conf-name emacsconf-name + :year emacsconf-year + :video-target-date (format-time-string "%Y-%m-%d %a" (date-to-time emacsconf-video-target-date)) + :submit-email emacsconf-submit-email + :base emacsconf-base-url + :q-and-a (if (emacsconf-schedule-q-and-a-p o) " (+ time afterwards for Q&A)" "") + :time (plist-get o :time) + :url (plist-get o :url)) + "** TODO Prepare \"${title}\" for ${conf-name} ${year} + DEADLINE: <${video-target-date}> + (feel free to send it in earlier; let us know at ${submit-email} you're running late) + Reserved time: ${time} minutes${q-and-a} + Instructions: ${base}${year}/prepare/ + Talk page: ${base}${url} + (remember to use large text for your video!) +")) + (cdr group) "\n\n")) + :plural (if (= 1 (length (cdr group))) "" "s") + :video-target-date (format-time-string "%Y-%m-%d %a" (date-to-time emacsconf-video-target-date)) + :speakers-short (plist-get (cadr group) :speakers-short))))) + (defun emacsconf-mail-accept-talk (talk) "Send acceptance letter." (interactive (list (emacsconf-complete-talk-info))) @@ -651,19 +766,19 @@ questions or if there's anything we can do to help out! ${signature}" :function emacsconf-mail-accept-talk :log-note "accepted talk") - (plist-get talk :email) - (list - :base emacsconf-base-url - :user-email user-mail-address - :year emacsconf-year - :signature user-full-name - :conf-name emacsconf-name - :title (plist-get talk :title) - :email (plist-get talk :email) - :time (plist-get talk :time) - :speakers-short (plist-get talk :speakers-short) - :url (concat emacsconf-base-url (plist-get talk :url)) - :video-target-date emacsconf-video-target-date))) + (plist-get talk :email) + (list + :base emacsconf-base-url + :user-email user-mail-address + :year emacsconf-year + :signature user-full-name + :conf-name emacsconf-name + :title (plist-get talk :title) + :email (plist-get talk :email) + :time (plist-get talk :time) + :speakers-short (plist-get talk :speakers-short) + :url (concat emacsconf-base-url (plist-get talk :url)) + :video-target-date emacsconf-video-target-date))) (defvar emacsconf-mail-bcc-email "*Extra e-mail address to Bcc for delivery confirmation.") @@ -721,6 +836,7 @@ background in the notes that follow.${wrap} As of the time I write this e-mail, your tentative schedule is: ${schedule} + ${availability-note}${timezone-note}I might also be able to move things around if you want to attend any conflicting Q&A sessions or if your availability changes.${wrap} @@ -733,11 +849,11 @@ the updated schedule along with check-in instructions before the conference.${wrap} We plan to announce the schedule to the general public on -${schedule-announcement-date}, so we'd love to incorporate any -schedule feedback before then.${wrap} +${schedule-announcement-date}, so we'd love to incorporate any schedule +feedback before then. Does this schedule work for you, or would something else be better?${wrap} -In the meantime, good luck working on your presentation. ${todos} -Looking forward to ${conf-name} with you! +In the meantime, good luck working on your presentation${plural}. +Feel free to reach out if you have any questions. Looking forward to ${conf-name} with you!${wrap} ${signature} " @@ -751,6 +867,8 @@ so that's totally all right. You don't have to make it to the time your talk is scheduled; this e-mail is just to keep you up to date. =) +${schedule} + You can see the draft schedule at ${base}${year}/organizers-notebook/?highlight=${slugs}#draft-schedule . If you use a Javascript-enabled browser, your talk${plural} @@ -761,7 +879,7 @@ We'll also update the schedule as we get closer to the conference, but I'll let you know if things change a lot. Anyway, that's how things are shaping up. -In the meantime, good luck working on your presentation. ${todos} +In the meantime, good luck working on your presentation${plural}. Looking forward to ${conf-name} with you! ${signature} @@ -778,33 +896,14 @@ ${signature} :conf-name emacsconf-name :speakers-short (plist-get (cadr group) :speakers-short) :plural (if (= 1 (length (cdr group))) "" "s") - :todos - (concat - (if (= 1 (length (cdr group))) "Here's a handy TODO you can use if you want:" "Here are handy TODOs you can use if you want:") - "\n\n" - (mapconcat - (lambda (o) - (emacsconf-replace-plist-in-string - (list :title (plist-get o :title) - :conf-name emacsconf-name - :year emacsconf-year - :video-target-date (format-time-string "%Y-%m-%d %a" (date-to-time emacsconf-video-target-date)) - :submit-email emacsconf-submit-email - :base emacsconf-base-url - :q-and-a (if (emacsconf-schedule-q-and-a-p o) " (+ time afterwards for Q&A)" "") - :time (plist-get o :time) - :url (plist-get o :url)) - "** TODO Prepare \"${title}\" for ${conf-name} ${year} - DEADLINE: <${video-target-date}> - (feel free to send it in earlier; let us know at ${submit-email} you're running late) - Reserved time: ${time} minutes${q-and-a} - Instructions: ${base}${year}/prepare/ - Talk page: ${base}${url} -")) - (cdr group) "\n\n")) :email-notes (emacsconf-surround "ZZZ: " (plist-get (cadr group) :email-notes) "\n\n" "") :schedule - (emacsconf-indent-string (mapconcat #'emacsconf-mail-format-talk-schedule (cdr group) "\n") 2) + (emacsconf-indent-string (mapconcat #'emacsconf-mail-format-talk-schedule + (sort (cdr group) + :key (lambda (o) (plist-get o :start-time)) + :lessp #'time-less-p) + "\n\n") + 2) :availability-note (if (delq nil (emacsconf-schedule-get-time-constraint (cadr group))) (emacsconf-replace-plist-in-string @@ -817,16 +916,17 @@ ${signature} (append (list :renamed-timezone (emacsconf-schedule-rename-etc-timezone (plist-get (cadr group) :timezone))) (cadr group)) - "Just let me know if you want us to use a different timezone for translating times in future e-mails. ") - "I don't think I have a timezone noted for you yet. If you want, I can translate times into your local timezone for you in future e-mails. Just let me know what you would like. "))))) + "Just let me know if you want us to use a different time zone for translating times in future e-mails. ") + "I don't think I have a time zone noted for you yet. If you want, I can translate times into your local time zone for you in future e-mails. Just let me know what you would like. "))))) (defun emacsconf-mail-acknowledge-upload (talk) "Acknowledge uploaded files for TALK." (interactive (list (emacsconf-complete-talk-info))) (emacsconf-mail-prepare '(:subject "${conf-name} ${year}: received uploaded file${plural} for ${title}" - :cc "emacsconf-submit@gnu.org" + ;; :cc "emacsconf-submit@gnu.org" :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" + :log-note "acknowledged submission" :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" :body "Hi, ${speakers-short}! @@ -836,12 +936,13 @@ Now we have the following file${plural} starting with ${file-prefix}: ${file-list} -I've added the video to the processing queue. You can see how -things are going backstage at ${backstage-url-with-auth} . We'll -be working on captioning your talk over the next few weeks. We'll -e-mail again a little closer to the conference with schedule -updates and other useful information. If you want to upload a new -version, you can upload it the same way you did the previous +I've added the video to the processing queue. You can see how things are +going backstage at ${backstage-url-with-auth} . I or another captioning +volunteer will work on captioning your talk over the next few weeks. The +VTT and TXT file are in the backstage area if you want to try editing it +yourself. =) We'll e-mail again a little closer to the conference with +schedule updates and other useful information. If you want to upload a +new version, you can upload it the same way you did the previous one.${fill} Please feel free to e-mail us at ${submit-email} if you need help updating the talk wiki page at ${base}${url} or if you have other questions. @@ -902,6 +1003,7 @@ ${signature}") files "\n"))))) +;;;###autoload (defun emacsconf-mail-captions-for-review (talk) "E-mail captions for TALK so that the speakers can review them." (interactive (list (emacsconf-complete-talk-info @@ -920,6 +1022,7 @@ ${signature}") :subject "${conf-name} ${year}: Captions for ${title}" :to "${email}" :cc "${captioner-email}" + :log-note "sent captions for review" :body "${email-notes}Hi ${speakers-short}! Because you sent in your video before the conference, we were able to @@ -1114,10 +1217,7 @@ people's talks too.")) (defun emacsconf-mail-backstage-info-to-captioning-volunteers () "E-mail backstage info to captioning volunteers." (interactive) - (dolist (volunteer (seq-filter - (lambda (o) - (string-match ":captions:" (assoc-default "ALLTAGS" o 'string= ""))) - (emacsconf-get-volunteer-info))) + (dolist (volunteer (emacsconf-get-volunteer-info "caption")) (emacsconf-mail-backstage-info (list (assoc-default "EMAIL" volunteer) @@ -1165,10 +1265,13 @@ quality of the reencoded video.")) speaker-groups)) volunteer-groups))))) -(defun emacsconf-mail-bbb-tips-and-intro-to-all (&optional types) - "Draft BBB information for all speakers." +(defun emacsconf-mail-intro-to-all (&optional types) + "Ask speakers to review intros and let them know that check-in info will come soon." (interactive) - (let* ((log-note "sent bbb-tips-and-intro") + (let* ((log-note "sent intro") + (talks (emacsconf-filter-talks-by-logbook + log-note + (emacsconf-active-talks (emacsconf-get-talk-info)))) (talk-groups (seq-group-by (lambda (talk) (cond @@ -1183,53 +1286,47 @@ quality of the reencoded video.")) 'after) (t (error "Exception for %s" (plist-get talk :slug))))) (seq-filter (lambda (o) (plist-get o :email)) - (emacsconf-filter-talks-by-logbook - log-note - (emacsconf-active-talks (emacsconf-get-talk-info)))))) + talks))) (base (list :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" :log-note log-note :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}"))) (setq types (or types (mapcar 'car talk-groups))) (dolist (type types) - (dolist (group (emacsconf-mail-groups (cdr (assoc-default type talk-groups)))) - (emacsconf-mail-prepare - (append - base - (pcase type - ('waiting-for-prerec - (list - :subject "${conf-name} ${year} action needed: Options, BigBlueButton info, intro" - :body - "${email-notes}Hi, ${name}! - -${conf-name} is in two weeks. Aaah! I don't think we have your + (when (assoc-default type talk-groups) + (dolist (group (emacsconf-mail-groups (assoc-default type talk-groups))) + (emacsconf-mail-prepare + (append + base + (pcase type + ('waiting-for-prerec + (list + :subject "${conf-name} ${year}: options, please check intro" + :body + "${email-notes}Hi, ${name}! + +${conf-name} is this week. Aaah! I don't think we have your presentation yet, but I'm not panicking (much) because we've got plans and backup plans. Option A: You can upload your presentation before the conference -There's still a little time to squeeze in processing presentations and -possibly even captioning them. If you're getting stuck because you -want your presentation to be totally awesome, it's okay, it doesn't -have to be perfect. If it's too long or too short, that's cool too; we -can manage the time around that. We'll figure things out together. =) +There might still be a little time to squeeze in processing +presentations and possibly even captioning them. If you're getting stuck +because you want your presentation to be totally awesome, it's okay, it +doesn't have to be perfect. If it's too long or too short, that's cool +too; we can manage the time around that. We'll figure things out +together. =) You can upload your file(s) to ${upload-url} (password ${upload-password}) and we'll get things going.${fill} -If you're thinking of doing the Q&A in a live web conference, -please also test the BigBlueButton URL described under Option -B. Thanks! - Option B: You can do it live Sometimes it's easier to do a presentation live, or sometimes you're -making last-minute tweaks and want to play the latest copy of your -video from your own computer. We can do the presentation live over -BigBlueButton. Your room URL is ${bbb-url} , and I've put together -some tips at ${bbb-tips} . Please -help reduce the technical risks by trying out the BigBlueButton setup -before November 28 so that there's time to help troubleshoot.${fill} +making last-minute tweaks and want to play the latest copy of your video +from your own computer. We can do the presentation live over +BigBlueButton. On Thursday or Friday, I'll send you the BigBlueButton +information so you can check in and try things out.${fill} Option C: It's okay, you can cancel @@ -1252,21 +1349,17 @@ e-mail me the corrections.${fill} Best regards, ${signature}")) - ('live - (list - :subject "${conf-name} ${year} action needed: BigBlueButton info, intro" - :body - "${email-notes}Hi, ${name}! + ('live + (list + :subject "${conf-name} ${year}: please check intro pronunciation" + :body + "${email-notes}Hi, ${name}! Thanks again for uploading your presentation early! -Since you're planning to do a live Q&A session, I've set up a -BigBlueButton web conference room for you at ${bbb-url} . I've -also put together some tips at ${bbb-tips} . Please help reduce -the technical risks by trying out the BigBlueButton setup before -November 28 so that there's time to help troubleshoot. Sharing -system audio or multi-monitor setups can sometimes be tricky, so -please let us know if you need help figuring things out. ${fill} +Since you're planning to do a live Q&A session, I'll set up a +BigBlueButton web conference room and I'll e-mail you the information on +Thursday or Friday.${fill} Also, could you please take a minute to check if I pronounce your name correctly in the intro I recorded? The recording is at ${intro-url} , @@ -1278,11 +1371,11 @@ e-mail me the corrections. Best regards, ${signature}")) - ('pad-or-irc - (list - :subject "${conf-name} ${year} action needed: check intro pronunciation" - :body - "${email-notes}Hi, ${name}! + ('pad-or-irc + (list + :subject "${conf-name} ${year}: please check intro pronunciation" + :body + "${email-notes}Hi, ${name}! Thanks again for uploading your presentation early! @@ -1293,17 +1386,17 @@ at ${backstage-url-with-credentials} . If it needs tweaking, you can upload a recording to ${upload-url} (password ${upload-password}) or e-mail me the corrections. -We'll send you check-in instructions a few days before the conference -so that you'll be all set. +We'll send you check-in instructions on Thursday or Friday so that +you'll be all set. Best regards, ${signature}")) - ('after - (list - :subject "${conf-name} ${year} action needed: check intro pronunciation" - :body - "${email-notes}Hi, ${name}! + ('after + (list + :subject "${conf-name} ${year}: please check intro pronunciation" + :body + "${email-notes}Hi, ${name}! Thanks again for uploading your presentation early! @@ -1324,58 +1417,60 @@ us! Best regards, ${signature}")) - (_ (error "Unknown type %s" (symbol-name type))))) - (car group) - (list - :email-notes (or (plist-get (car group) :email-notes) "") - :conf-name emacsconf-name - :year emacsconf-year - :name (plist-get (cadr group) :speakers-short) - :email (car group) - :user-email user-mail-address - :signature user-full-name - :backstage-url-with-credentials - (mapconcat (lambda (talk) - (format "https://%s:%s@media.emacsconf.org/%s/backstage/#%s" - emacsconf-backstage-user - emacsconf-backstage-password - emacsconf-year - (plist-get talk :slug))) - (cdr group) - " , ") - :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 - :bbb-url - (cond - ((null (file-exists-p - (expand-file-name - (format "assets/redirects/open/bbb-%s.html" (plist-get (cadr group) :slug)) - emacsconf-backstage-dir))) - (error "Backstage redirect for %s does not exist" (plist-get (cadr group) :slug))) - ((null (= (length (seq-uniq (mapcar (lambda (o) (plist-get o :bbb-room)) (cdr group)))) 1)) - (error "Number of rooms for %s speaker: %d" - (plist-get (cadr group) :slug) - (length (seq-uniq (mapcar (lambda (o) (plist-get o :bbb-room)) (cdr group)))))) - (t (emacsconf-backstage-url (plist-get (car (cdr group)) :bbb-backstage)))) - :bbb-tips (concat emacsconf-base-url emacsconf-year "/bbb-for-speakers/") - :intro-url (mapconcat - (lambda (talk) - (if (file-exists-p (expand-file-name - (concat (plist-get talk :file-prefix) "--intro.webm") - emacsconf-backstage-dir)) - (format "https://%s:%s@media.emacsconf.org/%s/backstage/%s--intro.webm" - emacsconf-backstage-user - emacsconf-backstage-password - emacsconf-year - (plist-get talk :file-prefix)) - (error "No intro file for %s" (plist-get talk :slug)))) - (cdr group) - " , "))))))) - -(defun emacsconf-mail-checkin-instructions-to-all () + (_ (error "Unknown type %s" (symbol-name type))))) + (car group) + (list + :email-notes (or (plist-get (car group) :email-notes) "") + :conf-name emacsconf-name + :year emacsconf-year + :name (plist-get (cadr group) :speakers-short) + :email (car group) + :user-email user-mail-address + :signature user-full-name + :backstage-url-with-credentials + (mapconcat (lambda (talk) + (format "https://%s:%s@media.emacsconf.org/%s/backstage/#%s" + emacsconf-backstage-user + emacsconf-backstage-password + emacsconf-year + (plist-get talk :slug))) + (cdr group) + " , ") + :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 + ;; :bbb-url + ;; (cond + ;; ((string= (plist-get (cadr group) :qa-type) "none") + ;; "{ZZZ: No BBB URL because Q&A is after the conference}") + ;; ((null (file-exists-p + ;; (expand-file-name + ;; (format "assets/redirects/open/bbb-%s.html" (plist-get (cadr group) :slug)) + ;; emacsconf-backstage-dir))) + ;; (error "Backstage redirect for %s does not exist" (plist-get (cadr group) :slug))) + ;; ((null (= (length (seq-uniq (mapcar (lambda (o) (plist-get o :bbb-room)) (cdr group)))) 1)) + ;; (error "Number of rooms for %s speaker: %d" + ;; (plist-get (cadr group) :slug) + ;; (length (seq-uniq (mapcar (lambda (o) (plist-get o :bbb-room)) (cdr group)))))) + ;; (t (emacsconf-backstage-url (plist-get (car (cdr group)) :bbb-backstage)))) + ;; :bbb-tips (concat emacsconf-base-url emacsconf-year "/bbb-for-speakers/") + :intro-url (mapconcat + (lambda (talk) + (if (file-exists-p (expand-file-name + (emacsconf-talk-file talk "--intro.webm") + emacsconf-backstage-dir)) + (format "https://%s:%s@media.emacsconf.org/%s/backstage/%s--intro.webm" + emacsconf-backstage-user + emacsconf-backstage-password + emacsconf-year + (plist-get talk :file-prefix)) + (error "No intro file for %s" (plist-get talk :slug)))) + (cdr group) + " , ")))))))) + +(defun emacsconf-mail-checkin-instructions-to-all (&optional info) "Draft check-in instructions for all speakers." (interactive) (let* ((talks @@ -1383,14 +1478,51 @@ ${signature}")) "sent check-in information" (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)))) + (emacsconf-publish-prepare-for-display (or info (emacsconf-get-talk-info)))))) + (by-attendance (seq-group-by (lambda (o) (null (string-match "after\\|none" (plist-get o :qa-type)))) talks))) - (dolist (group (emacsconf-mail-groups (assoc-default nil by-attendance))) - (emacsconf-mail-checkin-instructions-for-nonattending-speakers group)) + (when (assoc-default nil by-attendance) + (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-mail-interim-schedule-update (talk) + "E-mail a quick update about the schedule." + (interactive (list (emacsconf-complete-talk-info))) + (emacsconf-mail-prepare + (list + :subject "${conf-name} ${year}: Schedule update ${sched-one-line}" + :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" + :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" + :log-note "sent updated schedule" + :body + "Hello, ${speakers-short}! + +We tweaked the schedule a bit. Your new schedule is: + +${schedule} + +Let us know if you need to reschedule! + +${signature}") + (plist-get talk :email) + (list + :year emacsconf-year + :base-url emacsconf-base-url + :conf-name emacsconf-name + :user-email user-mail-address + :email (plist-get talk :email) + :speakers-short (plist-get talk :speakers-short) + :signature user-full-name + :schedule + (emacsconf-indent-string (emacsconf-mail-format-talk-schedule talk) 2) + :sched-one-line + (emacsconf-timezone-strings-combined + (plist-get talk :start-time) + (plist-get talk :timezone) + "%b %-e %a %-I:%M %#p %Z")))) + (defun emacsconf-mail-schedule-update () "E-mail day-of schedule updates" (interactive) @@ -1490,18 +1622,20 @@ as soon as you can and I'll try to shuffle things around. Thank you!") "Send checkin instructions to speakers who will be there. GROUP is (email . (talk talk))" (interactive (list (emacsconf-mail-complete-email-group - (seq-remove - (lambda (o) - (or - (string= (plist-get o :status) "CANCELLED") - (null (plist-get o :email)) - (string-match "after" (or (plist-get o :q-and-a) "")))) - (emacsconf-get-talk-info))))) + (emacsconf-filter-talks-by-logbook + "sent check-in information" + (seq-remove + (lambda (o) + (or + (string= (plist-get o :status) "CANCELLED") + (null (plist-get o :email)) + (string-match "after" (or (plist-get o :qa-type) "")))) + (emacsconf-get-talk-info)))))) (emacsconf-mail-prepare (list :subject "${conf-name} ${year}: Check-in instructions" - :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" - :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" + :reply-to "${user-email}" + :mail-followup-to "${user-email}" :log-note "sent check-in information for people who will be there" :body "${email-notes}Hello, ${speakers-short}! @@ -1509,9 +1643,9 @@ GROUP is (email . (talk talk))" We're looking forward to having you join us at ${conf-name}! Here's your talk page URL and checkin information: -${checkin-info} +${checkin-info}${bbb-tech-check-note} -Please check in early so that we can deal with scheduling changes +Please check in earlyish so that we can deal with scheduling changes or technical issues, and so that we don't worry too much about missing speakers (aaah!). You can find the check-in process at ${base-url}${year}/speakers/ . ${wrap} @@ -1522,10 +1656,7 @@ my emergency contact information: ${emergency}${wrap} Thank you for sharing your time and energy with the ${conf-name} community! -${signature} - -p.s. If you need to cancel, that's okay too, life happens. Let me know -as soon as you can and I'll try to shuffle things around. Thank you!") +${signature}") (car group) (list :year emacsconf-year @@ -1545,6 +1676,14 @@ as soon as you can and I'll try to shuffle things around. Thank you!") ((= (length waiting) 1) "If you happen to be able to get a pre-recorded video together in the next few days, I think we might be able to still manage that.${fill}\n\n") (t "If you happen to be able to get your pre-recorded videos together in the next few days, I think we might be able to still manage them.${fill}\n\n"))) :signature user-full-name + :bbb-tech-check-note + (if (seq-find (lambda (o) + (or (plist-get o :live) + (null (plist-get o :video-file)) + (string= (plist-get o :qa-type) "live"))) + (cdr group)) + "\n\nWe upgraded BigBlueButton this year, so it might be a good idea to do a tech check to make sure I didn't mess anything up. =) Feel free to connect to your BigBlueButton room before the conference using the URL and moderator code above so that you can try your audio, screensharing (optional), webcam (optional), etc.${wrap}" + "") :checkin-info (mapconcat (lambda (o) @@ -1560,13 +1699,23 @@ as soon as you can and I'll try to shuffle things around. Thank you!") :qa-info-speakers (cond ((or (plist-get o :live) (null (plist-get o :video-file))) ;; intentionally a live talk + (unless (plist-get o :bbb-room) (error "No BBB room for %s" (plist-get o :slug))) + (unless (plist-get o :bbb-mod-code) (error "No BBB mod code for %s" (plist-get o :slug))) (concat "Talk & Q&A BigBlueButton room: " - (emacsconf-backstage-url (plist-get o :bbb-backstage)))) + (emacsconf-backstage-url (plist-get o :bbb-backstage)) + " (moderator code: " + (plist-get o :bbb-mod-code) + ")")) ((string= (plist-get o :qa-type) "none") "Q&A: After the event; we'll collect the questions and e-mail them to you") ((string= (plist-get o :qa-type) "live") + (unless (plist-get o :bbb-room) (error "No BBB room for %s" (plist-get o :slug))) + (unless (plist-get o :bbb-mod-code) (error "No BBB mod code for %s" (plist-get o :slug))) (concat "Q&A BigBlueButton room: " - (emacsconf-backstage-url (plist-get o :bbb-backstage)))) + (emacsconf-backstage-url (plist-get o :bbb-backstage)) + " (moderator code: " + (plist-get o :bbb-mod-code) + ")")) ((string= (plist-get o :qa-type) "irc") (concat "Q&A: On IRC: #" (plist-get o :channel) " ( " (plist-get o :webchat-url) " )")) ((string= (plist-get o :qa-type) "pad") @@ -1801,26 +1950,94 @@ ${transcript} (concat "Automatic captions for " (plist-get talk :title)) "attachment"))) +(defvar emacsconf-sticker-mailer nil "E-mail address of person who sends out stickers.") + +(defun emacsconf-mail-template-mailing-address (group) + (interactive (list (emacsconf-mail-complete-email-group))) + (emacsconf-mail-prepare + (list + :subject "${conf-name} ${year}: Can we send you a sticker or pin of appreciation?" + :reply-to "${user-email}, ${sticker-mailer}, ${email}" + :mail-followup-to "${user-email}, ${sticker-mailer}, ${email}" + :log-note "Asked for mailing address" + :body + "${email-notes}Hi, ${speakers-short}! + +We have swag again this year, thanks to Corwin +Brust! Would you like a sticker or a pin as a +small token of our appreciation? This is what they +look like: + +https://bru.st/i/ecswag.jpg + +(It's also part of our Evil Plan: maybe people +will see the sticker or the pin and talk to you +about Emacs! =) ) + +If you want one, please e-mail your mailing +address and your preference* (sticker or pin) to +corwin@bru.st . We promise to use your address +only for sending it. + +(* While supplies last; Corwin thinks there should +be plenty, but just in case, feel free to send us +your second choice too.) + +Thank you so much for contributing to ${conf-name} ${year}! + +${signature} +") + (car group) + (list + :email-notes (emacsconf-surround "ZZZ: " (string-join (seq-uniq (seq-map (lambda (talk) (plist-get talk :email-notes)) (cdr group))) + ", ") "\n\n" "") + :speakers-short (plist-get (cadr group) :speakers-short) + :conf-name emacsconf-name + :year emacsconf-year + :email (car group) + :base-url emacsconf-base-url + :signature user-full-name + :user-email user-mail-address + :sticker-mailer emacsconf-sticker-mailer))) + +(defun emacsconf-mail-template-mailing-address-to-all () + "Ask for mailing address." + (interactive) + (let* ((log-note "asked for mailing address") + (groups + (emacsconf-mail-groups + (emacsconf-filter-talks-by-logbook + log-note + (seq-filter (lambda (o) (plist-get o :email)) + (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))))) + (dolist (group groups) + (emacsconf-mail-template-mailing-address group)) + (message "Drafted %d messages" (length groups)))) + ;;; Other mail functions -(defun emacsconf-mail-verify-delivery (groups subject) +(defun emacsconf-mail-verify-delivery (subject &optional groups) "Verify that the email addresses in GROUPS have all received an email with SUBJECT." - (interactive (list (emacsconf-mail-groups (seq-filter (lambda (o) (not (string= (plist-get o :status) "CANCELLED"))) - (emacsconf-get-talk-info))) - (read-string "Subject: "))) + (interactive (list (read-string "Subject: "))) + (setq groups (or groups + (emacsconf-mail-groups (seq-filter (lambda (o) + (and (not (string= (plist-get o :status) "CANCELLED")) + (plist-get o :email))) + (emacsconf-get-talk-info))))) (let ((missing (seq-keep (lambda (group) - (and (string= "0" - (string-trim - (shell-command-to-string - (format "notmuch count to:%s and to:%s and subject:%s" - (shell-quote-argument (car group)) - (shell-quote-argument emacsconf-mail-bcc-email) - subject)))) - (car group))) + (let ((cmd (format "notmuch count to:%s and to:%s and subject:\\\"%s\\\"" + (shell-quote-argument (car group)) + (shell-quote-argument emacsconf-mail-bcc-email) + subject))) + (and (string= "0" + (string-trim + (shell-command-to-string + cmd))) + (car group)))) groups))) (if missing - (prin1 missing) + (message "Missing: %s" (string-join missing ", ")) (message "All good.")))) (defun emacsconf-mail-get-all-email-addresses (talk) @@ -1927,11 +2144,13 @@ This minimizes the risk of mail delivery issues and radio silence." (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)))) + (condition-case e + (save-window-excursion + (mapc + (lambda (talk) + (emacsconf-add-to-talk-logbook talk note)) + (emacsconf-mail-talks email))) + (error (message "Error %s" e)))) (defun emacsconf-mail-notmuch-save-attachments-to-cache (talk) "Save the attached files to the cache and backstage dir for TALK." @@ -1956,5 +2175,37 @@ This minimizes the risk of mail delivery issues and radio silence." part (expand-file-name new-filename emacsconf-backstage-dir))))) (mm-dissect-buffer)))) +(defun emacsconf-notmuch-submissions () + "Search for recent submissions." + (interactive) + (notmuch-search emacsconf-submit-email)) + +(defun emacsconf-notmuch-check-sent (query &optional groups) + (interactive "MSubject: ") + (setq groups + (or groups + (emacsconf-mail-groups + (seq-filter (lambda (o) (plist-get o :email)) + (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))))) + (let* ((files + (with-temp-buffer + (notmuch-call-notmuch--helper + t + (list "search" "--output" "files" (format "\"%s\"" query))) + (split-string (string-trim (buffer-string)) "\n"))) + (sent-to + (cl-loop for f in files + collect + (with-temp-buffer + (insert-file-contents f) + (goto-char (point-min)) + (when (re-search-forward "To: \\(.+\\)" nil t) + (match-string 1))))) + (missing (seq-difference (mapcar 'car groups) + sent-to))) + (if missing + (message "Missing: %s" (string-join missing "; ")) + (message "All sent.")))) + (provide 'emacsconf-mail) ;;; emacsconf-mail.el ends here diff --git a/emacsconf-pad.el b/emacsconf-pad.el index 0a49bd6..0a958e1 100644 --- a/emacsconf-pad.el +++ b/emacsconf-pad.el @@ -142,7 +142,9 @@ You can find it in $ETHERPAD_PATH/APIKEY.txt" (format "https://etherpad.wikimedia.org/p/emacsconf-%s-%s" emacsconf-year (plist-get o :slug)) - (concat emacsconf-pad-base emacsconf-pad-directory (emacsconf-pad-id o)))) + (if (and (listp o) (plist-get o :slug)) + (concat emacsconf-pad-base emacsconf-pad-directory (emacsconf-pad-id o)) + (concat emacsconf-pad-base o)))) (defvar emacsconf-pad-number-of-next-talks 3 "Integer limiting the number of next talks to link to from the pad.") @@ -175,6 +177,8 @@ You can find it in $ETHERPAD_PATH/APIKEY.txt" "\n") "</ul></div>") "") + :pronouns (emacsconf-surround " (" (plist-get o :pronouns) ")" "") + :pronunciation (emacsconf-surround " - Pronunciation: " (plist-get o :pronunciation) "" "") :track-id (plist-get (emacsconf-get-track (plist-get o :track)) :id) :watch @@ -196,7 +200,7 @@ You can find it in $ETHERPAD_PATH/APIKEY.txt" "<div> <div>All talks: ${talks}</div> <div><strong>${title}</strong></div> -<div>${base-url}${url} - ${speakers} - Track: ${track}</div> +<div>${base-url}${url} - ${speakers}${pronouns}${pronunciation} - 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> @@ -339,6 +343,7 @@ ${next-talk-list} (defun emacsconf-pad-format-shift-hyperlist (shift info) (let* ((track (emacsconf-get-track (plist-get shift :track))) + (shift-rtmp (seq-find (lambda (entry) (string= (assoc-default "ID" entry) (plist-get shift :id))) emacsconf-rtmp-shifts)) (prefixed (list :start (plist-get shift :start) :end (plist-get shift :end) @@ -353,10 +358,17 @@ ${next-talk-list} :irc-volunteer (format "<em>%s</em>" (emacsconf-surround "IRC-" (plist-get shift :irc) "" "IRC")) :track-id (plist-get track :id) :conf-id emacsconf-id + :channel (concat emacsconf-id "-" (plist-get track :id)) :checkin (format "<em>%s</em>" (emacsconf-surround "CHECKIN-" (plist-get shift :checkin) "" "CHECKIN")) :pad (format "<em>%s</em>" (emacsconf-surround "PAD-" (plist-get shift :pad) "" "PAD")) :coord (format "<em>%s</em>" (emacsconf-surround "COORD-" (plist-get shift :coord) "" "COORD")) - :checkin-pad (concat emacsconf-pad-base "checkin-" (downcase (format-time-string "%a" (date-to-time (plist-get shift :start))))))) + :youtube-rtmp (assoc-default "YouTube" shift-rtmp 'string=) + :youtube-studio-url (assoc-default "YouTube URL" shift-rtmp 'string=) + :youtube-view-url + (replace-regexp-in-string + "https://studio\\.youtube\\.com/video/\\([^/]+\\)/livestreaming" "https://youtube.com/live/\\1" + (assoc-default "YouTube URL" shift-rtmp 'string=)) + :checkin-pad (concat emacsconf-pad-base "private-" emacsconf-private-pad-prefix "-checkin-" (downcase (format-time-string "%a" (date-to-time (plist-get shift :start))))))) (shift-talks (mapcar (lambda (o) (append prefixed o)) (seq-filter @@ -384,29 +396,29 @@ ${next-talk-list} <strong>Setup</strong> <ul> -<li>[ ] ${checkin}: Open ${checkin-pad}</li> -<li>[ ] ${irc-volunteer}: Watch the #emacsconf-${track-id} channel and open ${base-url}${year}/talks for links to the pads</li> -<li>[ ] ${pad}: Open ${base-url}${year}/talks for links to the pads</li> -<li>[ ] ${coord}: ssh orga@live0.emacsconf.org and run screen-fallbacks; confirm that the streams are showing fallbacks</li> -<li>[ ] ${stream}: Start recording with OBS -<li>[ ] Copy the password file if you don't already have it: <strong>scp emacsconf-${track-id}@res.emacsconf.org:~/.vnc/passwd vnc-passwd-${track-id} -p ${ssh-port}</strong></li> -<li>[ ] Forward your local ports: <strong>ssh emacsconf-${track-id}@res.emacsconf.org -N -L ${vnc-port}:127.0.0.1:${vnc-port} -p ${ssh-port} &</strong></li> -<li>[ ] Connect via VNC: <strong>xvncviewer 127.0.0.1:${vnc-port} -shared -geometry 1280x720 -passwd vnc-passwd-${track-id} &</strong> -<ul> -<li>[? Can't connect to VNC]: ssh emacsconf-${track-id}@res.emacsconf.org -p ${ssh-port} /home/${conf-id}-${track-id}/bin/track-vnc</li> -<li>[? Can't find OBS]: track-obs</li></ul></li> -<li>[ ] Start background music via SSH or VNC: <em>music</em> -<ul><li>[? No audio device]: -<ul><li><em>pulseaudio -k; pulseaudio --start</em></li> -<li>quit OBS</li> -<li><em>track-obs</em></li></ul></li> -<li>[ ] Start recording (not streaming). (Alt-2, switch to workspace 2; Alt-Shift-2, move something to workspace 2).</li> -<li>[ ] Watch the stream with MPV on your local system: <strong>mpv https://live0.emacsconf.org/emacsconf/${track-id}.webm &</strong></li> -<li>[ ] Check 480p by viewing it : <strong>mpv https://live0.emacsconf.org/emacsconf/${track-id}-480p.webm &</strong></li> -<li>[ ] Confirm that the streaming user has connected to Mumble, is in the ${channel} channel, and can hear what we say on Mumble.</li> -<li>[ ] Test with a sample video or Q&A session. You can run this command on your local system if you want to do things off-screen: <strong>ssh emacsconf-${track-id}@res.emacsconf.org -p 46668 \"~/bin/track-mpv emacsconf &\"</strong></li> -<li>[ ] ${stream}: Restart the background music via SSH or VNC: <em>music</em> . The background music should automatically get killed when the talks start, but if it doesn't, you can stop it with: <em>screen -S background -X quit</em></li> -</ul></li>" + <li>[ ] ${checkin}: Open ${checkin-pad}</li> + <li>[ ] ${irc-volunteer}: Watch the #emacsconf-${track-id} channel and open ${base-url}${year}/talks for links to the pads</li> + <li>[ ] ${pad}: Open ${base-url}${year}/talks for links to the pads</li> + <li>[ ] Copy the password file if you don't already have it: <strong>scp emacsconf-${track-id}@res.emacsconf.org:~/.vnc/passwd vnc-passwd-${track-id} -p ${ssh-port}</strong></li> + <li>[ ] Forward your local ports: <strong>ssh emacsconf-${track-id}@res.emacsconf.org -N -L ${vnc-port}:127.0.0.1:${vnc-port} -p ${ssh-port} &</strong></li> + <li>[ ] Connect via VNC: <strong>xvncviewer 127.0.0.1:${vnc-port} -shared -geometry 1280x720 -passwd vnc-passwd-${track-id} &</strong> + <ul> + <li>[? Can't connect to VNC]: ssh emacsconf-${track-id}@res.emacsconf.org -p ${ssh-port} /home/${conf-id}-${track-id}/bin/track-vnc</li> + <li>[? Can't find OBS]: track-obs</li></ul></li> + <li>[ ] Start background music via SSH or VNC: <em>music</em> + <ul><li>[? No audio device]: + <ul><li><em>pulseaudio -k; pulseaudio --start</em></li> + <li>quit OBS</li> + <li><em>track-obs</em></li></ul> + </li></ul></li> + <li>[ ] OBS - Settings - update the RTMP stream key: <strong>${youtube-rtmp}</strong></li> + <li>[ ] Start recording AND start streaming. (Alt-2, switch to workspace 2; Alt-Shift-2, move something to workspace 2).</li> + <li>[ ] Watch the stream with MPV on your local system: <strong>mpv https://live0.emacsconf.org/emacsconf/${track-id}.webm &</strong></li> + <li>[ ] Check 480p by viewing it : <strong>mpv https://live0.emacsconf.org/emacsconf/${track-id}-480p.webm &</strong></li> + <li>[ ] Check YouTube: ${youtube-studio-url} and ${youtube-view-url}</li> + <li>[ ] Confirm that the streaming user has connected to Mumble, is in the ${channel} channel, and can hear what we say on Mumble.</li> + <li>[ ] Test with a sample video or Q&A session. You can run this command on your local system if you want to do things off-screen: <strong>ssh emacsconf-${track-id}@res.emacsconf.org -p 46668 \"~/bin/track-mpv emacsconf &\"</strong></li> + <li>[ ] ${stream}: Restart the background music via SSH or VNC: <em>music</em> . The background music should automatically get killed when the talks start, but if it doesn't, you can stop it with: <em>screen -S background -X quit</em></li>" (if emacsconf-restream-youtube "<li>[ ] ${coord}: ssh -t orga@live0.emacsconf.org 'screen -S restream-${track-id}-youtube /home/orga/restream-${track-id}-youtube.sh' and then confirm at ${youtube-url}</li> " "") @@ -430,7 +442,7 @@ ${next-talk-list} "</ul>" "Teardown <ul> -<li>[ ] ${stream}: stop recording</li> +<li>[ ] ${stream}: stop recording and stop streaming</li> " (if emacsconf-restream-youtube " @@ -442,7 +454,7 @@ ${next-talk-list} <li>[ ] ${coord}: stop the restream-${track-id}-toobnix screen on live0: <strong>screen -S restream-${track-id}-toobnix -X quit</strong></li> " "") -" + " <li>[ ] ${coord}: update the status page on live.emacsconf.org by changing emacsconf-tracks and calling emacsconf-stream-update-status-page</li> </ul>")) ))) @@ -488,6 +500,7 @@ ${next-talk-list} <ul><li>[ ] Window or screen can be shared <li>[ ] Text is readable</li></ul> <li>[ ] Webcam sharing (optional)</li></ul></li> +<li>[ ] What kind of facilitation would the speaker like? (Host reads questions, chats a lot, etc.)</li></ul></li> <li>OK to do other things until going live at <strong>${live}</strong></li> <li>People will add questions to the pad or IRC channel; host can read them to you, or you can read them</li> <li>You can answer questions in any order, and you can skip questions if you want. Feel free to take your time to think about answers or to save some for following up later</li> @@ -514,7 +527,7 @@ ${bbb-checklist}</li>") (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))) (mapc (lambda (day) - (let ((pad-id (concat "checkin-" (downcase (format-time-string "%a" (plist-get (cadr day) :checkin-time)))))) + (let ((pad-id (concat "private-" emacsconf-private-pad-prefix "-checkin-" (downcase (format-time-string "%a" (plist-get (cadr day) :checkin-time)))))) (emacsconf-pad-create-pad pad-id) (emacsconf-pad-set-html pad-id @@ -551,7 +564,9 @@ ${bbb-checklist}</li>") :base-url emacsconf-base-url :year emacsconf-year :checkin-list (mapconcat - (lambda (day) (concat "<li>" emacsconf-pad-base "checkin-" + (lambda (day) (concat "<li>" emacsconf-pad-base + id + "-checkin-" (downcase (format-time-string "%a" (plist-get (cadr day) :checkin-time))) "</li>")) (seq-group-by (lambda (talk) @@ -561,18 +576,19 @@ ${bbb-checklist}</li>") "") :shift-list (mapconcat (lambda (shift) - (format "<li>%sprivate-%s-%s-%s</li>" + (format "<li>%s%s-%s</li>" emacsconf-pad-base - emacsconf-private-pad-prefix - emacsconf-year + id (plist-get shift :id))) emacsconf-shifts "") :host-list (mapconcat (lambda (shift) - (format "<li>%shost-%s</li>" + (format "<li>%sprivate-%s-%s-host-%s</li>" emacsconf-pad-base + emacsconf-private-pad-prefix + emacsconf-year (plist-get shift :id))) emacsconf-shifts "") @@ -589,7 +605,14 @@ ${bbb-checklist}</li>") <div>Combined shift info: <ul>${shift-list}</ul></div> -")))) +")) + (emacsconf-pad-url id))) + +(defun emacsconf-pad-shift-hyperlist-id (shift) + (format "private-%s-%s-%s" + emacsconf-private-pad-prefix + emacsconf-year + (plist-get (emacsconf-resolve-shift shift) :id))) (defun emacsconf-pad-prepopulate-shift-hyperlist (shift &optional info) (interactive (list (completing-read "Shift: " @@ -598,15 +621,17 @@ ${bbb-checklist}</li>") (setq shift (seq-find (lambda (o) (string= (plist-get o :id) shift)) emacsconf-shifts))) (unless info (setq info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) (let ((info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))) - (pad-id (format "private-%s-%s-%s" - emacsconf-private-pad-prefix - emacsconf-year - (plist-get shift :id)))) + (pad-id (emacsconf-pad-shift-hyperlist-id shift))) (emacsconf-pad-create-pad pad-id) (emacsconf-pad-set-html pad-id (emacsconf-pad-format-shift-hyperlist shift info)))) +(defun emacsconf-pad-open-shift-hyperlist (shift) + (interactive (list (completing-read "Shift: " + (mapcar (lambda (o) (plist-get o :id)) emacsconf-shifts)))) + (browse-url (emacsconf-pad-url (emacsconf-pad-shift-hyperlist-id shift)))) + (defun emacsconf-pad-prepopulate-host-hyperlists () (interactive) (mapc #'emacsconf-pad-prepopulate-shift-hyperlist-host emacsconf-shifts)) @@ -618,7 +643,9 @@ ${bbb-checklist}</li>") (setq shift (seq-find (lambda (o) (string= (plist-get o :id) shift)) emacsconf-shifts))) (unless info (setq info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) (let ((info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) - (let* ((pad-id (format "host-%s" + (let* ((pad-id (format "private-%s-%s-host-%s" + emacsconf-private-pad-prefix + emacsconf-year (plist-get shift :id))) (shift-talks (seq-filter @@ -667,40 +694,39 @@ ${bbb-checklist}</li>") talk) (concat "${hyperlist-note-info}" - (cond - (;; live talk, join BBB - (null (plist-get talk :video-file)) - "<li><strong>${start-hhmm} ${slug} live talk</strong>: it should play a prerecorded intro, but if it doesn't, join ${bbb-backstage} and introduce talk, then turn it over to speaker for <strong>live talk</strong>: ${expanded-intro} (pronunciation: ${pronunciation})</li>") - (t ;; prerecorded talk - "<li>Backup: ${start-hhmm} ${slug}: it should play a prerecorded intro and talk, but if it doesn't, join ${mumble} in Mumble and introduce talk: ${expanded-intro} (pronunciation: ${pronunciation}); then <em>play ${slug}</em></li>")) + (if (emacsconf-talk-recorded-p talk) + "<li>Backup: ${start-hhmm} ${slug}: it should play a prerecorded intro and talk, but if it doesn't, join ${mumble} in Mumble and introduce talk: ${expanded-intro} (pronunciation: ${pronunciation}); then <em>play ${slug}</em></li>" + ;; live talk, join BBB + "<li><strong>${start-hhmm} ${slug} live talk</strong>: it should play a prerecorded intro, but if it doesn't, join ${bbb-backstage} (mod code <strong>${bbb-mod-code}</strong> ) and introduce talk, then turn it over to speaker for <strong>live talk</strong>: ${expanded-intro} (pronunciation: ${pronunciation})</li>") ;; Q&A - (if (and (null (plist-get talk :video-file)) (not (string= (or (plist-get talk :q-and-a) "none") "none"))) + (if (and (not (emacsconf-talk-recorded-p talk)) + (not (string= (or (plist-get talk :qa-type) "none") "none"))) "<li>Continue in the BBB room for live Q&A because the talk was live</li>" - (pcase (plist-get talk :q-and-a) + (pcase (plist-get talk :qa-type) ((or 'nil "" "none" (rx "after")) (if (plist-get talk :video-file) "<li>[ ] ${qa-hhmm} ${slug} Q&A after: Join ${mumble} in Mumble and say that the speaker will follow up with answers on the talk page afterwards. Read questions. ${pad-url}</li>" "")) - ((rx "IRC") + ((rx "irc") "<li>[ ] ${qa-hhmm} ${slug} Q&A IRC: Join ${mumble} in Mumble. Invite people to put their questions in the ${channel} IRC channel and read questions and answers from there. ${webchat-url} ${pad-url}</li>") ((rx "pad") "<li>[ ] <strong>${qa-hhmm}</strong> ${slug} Q&A pad: Join ${mumble} in Mumble. Invite people to put their questions in the Etherpad and read questions and answers from there. ${pad-url}</li>") - ((rx "Mumble") + ((rx "mumble") "<li>[ ] <strong>${qa-hhmm}</strong> ${slug} Q&A mumble: Join ${mumble} in Mumble. Bring the speaker into the right channel if needed. Invite people to put their questions in the Etherpad and read questions and answers from there. ${pad-url} Paste questions into Mumble chat or read them out loud.</li>") ((rx "live") (concat - "<li>[ ] <strong>${qa-hhmm} ${slug} Q&A live</strong> (on stream until ${end-of-qa}): Join ${bbb-backstage}. START RECORDING. Invite people to put their questions in the Etherpad, and read questions from there. ${pad-url}</li> + "<li>[ ] <strong>${qa-hhmm} ${slug} Q&A live</strong> (on stream until ${end-of-qa}): Join ${bbb-backstage} (mod code <strong>${bbb-mod-code}</strong> ). START RECORDING. Invite people to put their questions in the Etherpad, and read questions from there. ${pad-url}</li> ${open-qa} " - (if next-talk - " + (if next-talk + " <li><strong>${next-talk-in-5}</strong> [? Open Q&A is still going on and it's about five minutes before the next talk] <ul><li>[ ] Let the speaker know about the time and that the Q&A can continue off-stream if people want to join</li></ul></li> -<li><strong>${next-talk-in-1}</strong> [? Open Q&A is still going on and it's about a minute before the next talk] +<li><strong>${next-talk-in-2}</strong> [? Open Q&A is still going on and it's about 2 minutes before the next talk] <ul><li>[ ] Announce that the Q&A will continue if people want to join the BBB room from the talk page, and the stream will now move to the next talk</li></ul></li> " - "")) -))))))) + "")) + ))))))) (emacsconf-include-next-talks shift-talks 1) "\n") "</ul>"))))) @@ -729,10 +755,11 @@ ${bbb-checklist}</li>") (let ((pronoun (pcase (plist-get talk :pronouns) ((rx "she") "She") ((rx "\"ou\"" "Ou")) - ((or 'nil "nil" (rx string-start "he") (rx "him")) "He") + ((or 'nil "nil") nil) + ((or (rx string-start "he") (rx "him")) "He") ((rx "they") "They") - (_ (or (plist-get talk :pronouns) ""))))) - (format "Next, we have \"%s\", by %s%s.%s" + (_ (plist-get talk :pronouns))))) + (format "Next, we have \"%s\",\nby %s%s.%s" (plist-get talk :title) (replace-regexp-in-string ", \\([^,]+\\)$" ", and \\1" @@ -740,19 +767,23 @@ ${bbb-checklist}</li>") (emacsconf-surround " (" (plist-get talk :pronunciation) ")" "") (pcase (plist-get talk :q-and-a) ((or 'nil "") "") - ((rx "after") " You can ask questions via Etherpad and IRC. We'll send them to the speaker, and we'll post the answers on the talk page afterwards.") + ((rx "after") "\nYou can ask questions via Etherpad and IRC.\nWe'll send them to the speaker,\nand we'll post the answers on the talk page afterwards.") ((rx "live") - (format " %s will answer questions via BigBlueButton. You can join using the URL from the talk page or ask questions through Etherpad or IRC." - pronoun - )) + (if pronoun + (format "\n%s will answer questions via web conference.\nYou can join using the URL from the talk page\nor ask questions through Etherpad or IRC." + pronoun) + "\nYou can ask questions via web conference by joining from the talk page\nor ask questions through Etherpad or IRC.")) ((rx "pad") - (format " %s will answer questions via Etherpad." - pronoun - )) + (if pronoun + (format "\n%s will answer questions via Etherpad." pronoun) + "\nYou can ask questions via Etherpad.")) ((rx "IRC") - (format " %s will answer questions via IRC in the #%s channel." - pronoun - (plist-get talk :channel))))))))) + (if pronoun + (format "\n%s will answer questions via IRC in the #%s channel." + pronoun + (plist-get talk :channel)) + (format "\nYou can ask questions via IRC in the #%s channel." + (plist-get talk :channel)))))))))) ;; Related: emacsconf-talk-hyperlist (defun emacsconf-pad-talk-hyperlist (talk &optional do-insert) @@ -770,7 +801,7 @@ ${bbb-checklist}</li>") :media-base emacsconf-media-base-url :mumble (concat emacsconf-id "-" track-id) :next-talk-in-5 (if next-talk (format-time-string "%-l:%M %p" (time-subtract (plist-get next-talk :start-time) (seconds-to-time 300)) emacsconf-timezone) "") - :next-talk-in-1 (if next-talk (format-time-string "%-l:%M %p" (time-subtract (plist-get next-talk :start-time) (seconds-to-time 60)) emacsconf-timezone) "") + :next-talk-in-2 (if next-talk (format-time-string "%-l:%M %p" (time-subtract (plist-get next-talk :start-time) (seconds-to-time 120)) emacsconf-timezone) "") :qa-start (format-time-string "%-l:%M %p" (plist-get talk :qa-time) emacsconf-timezone) :qa-end (if next-talk (format-time-string "%-l:%M %p" (plist-get next-talk :start-time)) "end of shift") @@ -804,17 +835,17 @@ ${bbb-checklist}</li>") (concat (emacsconf-surround "<li><strong>" (plist-get talk :hyperlist-note) "</strong></li>" "") "<li>Recorded intro: <a href=\"${media-base}${year}/backstage/${file-prefix}--intro.webm\">${media-base}${year}/backstage/${file-prefix}--intro.webm</a>" - (if (plist-get talk :video-file) + (if (emacsconf-talk-recorded-p talk) "<li>[ ] [? stream didn't auto-play] ${stream}: <em>handle-session ${slug}</em>; if that doesn't work, <em>play ${slug}</em>; if that still doesn't work, <em>track-mpv ~/current/cache/${conf-id}-${year}-${slug}*--intro.webm</em> and <em>track-mpv ~/current/cache/${conf-id}-${year}-${slug}*--main.webm</em></li>" (concat "<li>Live talk:<ul>" "<li>[ ] [? stream didn't auto-join] ${stream}: <a href=\"${bbb-backstage}\">${bbb-backstage}</a></li>" "<li>[ ] ${host}: Join <a href=\"${bbb-backstage}\">${bbb-backstage}</a> and turn over to speaker.</li></ul></li>")) - (pcase (or (plist-get talk :q-and-a) "") + (pcase (or (plist-get talk :qa-type) "") ((rx "live") (concat "<li>Live Q&A start ${qa-start}, on stream until ${qa-end}<ul> -<li>[ ] ${host}: Join the Q&A room at <a href=\"${bbb-backstage}\">${bbb-backstage}</a> and open the pad at <a href=\"${pad-url}\">${pad-url}</a>; optionally open IRC for ${channel} (<a href=\"${webchat-url}\">${webchat-url}</a>)</li> +<li>[ ] ${host}: Copy the modcode <strong>${bbb-mod-code}</strong> , join the Q&A room at <a href=\"${bbb-backstage}\">${bbb-backstage}</a>, and open the pad at <a href=\"${pad-url}\">${pad-url}</a>; optionally open IRC for ${channel} (<a href=\"${webchat-url}\">${webchat-url}</a>)</li> <li>[ ] [? speaker missing?] ${host}: Let #emacsconf-org know so that we can text or call the speaker</li> <li>[ ] [? stream didn't auto-join?] ${stream}: <em>bbb ${slug}</em> <ul> @@ -823,7 +854,7 @@ ${bbb-checklist}</li>") </ul> </li> <li>[ ] ${stream}: Give the host the go-ahead via Mumble or #emacsconf-org</li> -<li>[ ] ${host}: Start recording and read questions</li> +<li>[ ] ${host}: Announce that people can join using the URL on the talk page or ask questions on the pad or IRC channel. START RECORDING.</li> <li>[ ] ${stream}: Adjust the audio levels as needed: ${ssh-audio}</li> " (if emacsconf-qa-start-open @@ -831,11 +862,10 @@ ${bbb-checklist}</li>") "<li>[ ] ${host}: Decide when to open the Q&A and let ${stream} know</li> <li>[ ] ${stream}: Update the task status (no visible changes): ${ssh-openq}</li>") " -<li>[ ] ${stream}: Confirm BBB redirect at <a href=\"${bbb-redirect}\">${bbb-redirect}</a> goes to BBB room, let host know</li> -<li>[ ] ${host}: Announce that people can join using the URL on the talk page or ask questions on the pad or IRC channel</li> +<li>[ ] ${stream}: Confirm BBB redirect at <a href=\"${bbb-redirect}\">${bbb-redirect}</a> goes to BBB room, let host know; backup: <em>ssh orga@media.emacsconf.org \"~/bin/bbb-open ${slug}\"</em></li> <li>${next-talk-in-5} [? Open Q&A is still going on and it's about five minutes before the next talk] <ul><li>[ ] ${host}: Let the speaker know about the time and that the Q&A can continue off-stream if people want to join</li></ul></li> -<li>${next-talk-in-1} [? Open Q&A is still going on and it's about a minute before the next talk] +<li>${next-talk-in-2} [? Open Q&A is still going on and it's about 2 minutes before the next talk] <ul><li>[ ] ${host}: Announce that the Q&A will continue if people want to join the BBB room from the talk page, and the stream will now move to the next talk</li></ul></li> <li>[? Q&A is done early] <ul> diff --git a/emacsconf-publish.el b/emacsconf-publish.el index c4a3872..31ff72b 100644 --- a/emacsconf-publish.el +++ b/emacsconf-publish.el @@ -46,7 +46,9 @@ (interactive (list (emacsconf-complete-talk-info))) (let ((info (emacsconf-get-talk-info))) (emacsconf-publish-before-page talk info) - (emacsconf-publish-after-page talk info))) + (emacsconf-publish-after-page talk info) + (unless (emacsconf-publish-talk-p talk) + (emacsconf-publish-cancelled-nav-page talk)))) (defun emacsconf-publish-update-talk (talk) "Publish the schedule page and the page for this talk." @@ -65,6 +67,7 @@ (interactive) (emacsconf-publish-talk-page (emacsconf-get-talk-info-for-subtree)) (emacsconf-publish-info-pages) + (emacsconf-publish-schedule) (magit-status-setup-buffer emacsconf-directory)) (defun emacsconf-publish-update-conf-html () @@ -163,7 +166,8 @@ ;; (emacsconf-publish-format-track-as-org (car emacsconf-tracks) "US/Eastern") ;; (emacsconf-get-talk-info) (defun emacsconf-publish-format-track-as-org (track tz &optional info) - (setq info (or info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) + (let ((emacsconf-talk-info-functions (append emacsconf-talk-info-functions (list 'emacsconf-get-abstract-from-wiki)))) + (setq info (or info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))) (concat "** " (plist-get track :name) " :" (plist-get track :id) ":\n:PROPERTIES:\n:CATEGORY: " (plist-get track :id) "\n:END:\n" (mapconcat @@ -208,12 +212,18 @@ (plist-get talk :qa-info)) (plist-get talk :qa-info)) "\n" "") - (emacsconf-surround "\n" (plist-get talk :intro-note) "\n" ""))) + (emacsconf-surround "\n" (plist-get talk :intro-note) "\n" "") + (emacsconf-surround "\nDescription:\n\n" + (when (plist-get talk :org-description) + (with-temp-buffer + (org-paste-subtree 3 (plist-get talk :org-description)) + (buffer-string))) + "\n" ""))) (emacsconf-filter-talks-by-track track info) "\n"))) (defun emacsconf-publish-schedule-org-for-timezone (timezone &optional info) - (interactive (list (completing-read "Timezone: " emacsconf-timezones))) + (interactive (list (completing-read "Time zone: " emacsconf-timezones))) (let ((new-filename (expand-file-name (concat "schedule-" (replace-regexp-in-string @@ -228,7 +238,7 @@ (with-temp-file new-filename (insert "* " emacsconf-name " " emacsconf-year "\n\nTimes are in " - (emacsconf-schedule-rename-etc-timezone timezone) " timezone. You can find this file and other calendars at " + (emacsconf-schedule-rename-etc-timezone timezone) " time zone. You can find this file and other calendars at " emacsconf-media-base-url emacsconf-year "/schedules/ .\n\n" (mapconcat (lambda (track) (emacsconf-publish-format-track-as-org track timezone info)) @@ -237,7 +247,8 @@ (defun emacsconf-publish-schedule-org-files (&optional info) (interactive) - (setq info (or info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) + (let ((emacsconf-talk-info-functions (append emacsconf-talk-info-functions (list 'emacsconf-get-abstract-from-wiki)))) + (setq info (or info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))) (mapc (lambda (tz) (emacsconf-publish-schedule-org-for-timezone tz info)) (append emacsconf-timezones @@ -250,45 +261,54 @@ (mapconcat (lambda (o) (concat - "<tr>" - (format - "<td><a name=\"%s\"></a>" - (plist-get o :slug)) - (plist-get o :qa-link) - "</td>" - "<td>" (if (plist-get o :pad-url) - (format "<a href=\"%s\" target=\"_blank\" rel=\"noreferrer\">Pad</a>" (plist-get o :pad-url)) - "") - "</td>" - "<td>" (format "<a href=\"%s\" target=\"_blank\" rel=\"noreferrer\">Chat</a>" (plist-get o :webchat-url)) - "" - "</td>" + (format "<tr id=\"%s\">" (plist-get o :slug)) "<td>" (format-time-string "%-l:%M" (plist-get o :start-time) emacsconf-timezone) "</td>" + "<td><strong>" + (cond + ((not (emacsconf-talk-recorded-p o)) + (format-time-string "%-l:%M" (plist-get o :start-time) emacsconf-timezone)) + ((string-match "live" (plist-get o :qa-type)) + (format-time-string "%-l:%M" (plist-get o :qa-time) emacsconf-timezone)) + (t "")) + "</strong></td>" "<td>" (format "<a href=\"%s%s/talks/%s\" target=\"_blank\" rel=\"noreferrer\">%s</a>" emacsconf-base-url emacsconf-year (plist-get o :slug) (plist-get o :slug)) "</td>" + "<td>" (if (emacsconf-talk-recorded-p o) (plist-get o :qa-type) "(live talk)") "</td>" + (if (plist-get o :bbb-room) + (format "<td><button class=\"copy\" data-copy=\"%s\" data-label=\"Copy mod code\">Copy mod code</button></td>" + (plist-get o :bbb-mod-code)) + "<td></td>") + (concat "<td>" + (if (plist-get o :bbb-room) + (format "<a href=\"%s\" target=\"_blank\" rel=\"noreferrer\">Join Q&A</a>" (plist-get o :bbb-room) + "")) + "</td>") + "<td>" (if (plist-get o :pad-url) + (format "<a href=\"%s\" target=\"_blank\" rel=\"noreferrer\">Pad</a>" (plist-get o :pad-url)) + "") + "</td>" + "<td>" (format "<a href=\"%s\" target=\"_blank\" rel=\"noreferrer\">Chat</a>" (plist-get o :webchat-url)) + "" + "</td>" + "<td>" (or (plist-get o :title) "") "</td>" + "<td>" (or (plist-get o :speakers) "") (emacsconf-surround " (" (plist-get o :irc) ")" "") "</td>" "</tr>")) info "\n")) -(defun emacsconf-publish-res-index () +(defun emacsconf-publish-backstage-talk-index () "Publish BBB room URLs and pad links for volunteer convenience." (interactive) (let* ((emacsconf-publishing-phase 'conference) (info (mapcar (lambda (o) (append (list :url (concat "#" (plist-get o :slug))) - (if (and (string-match "live" (or (plist-get o :q-and-a) "")) - (plist-get o :bbb-room)) - (append (list - :qa-link - (format "<a href=\"%s\" target=\"_blank\" rel=\"noreferrer\">Join Q&A</a>" (plist-get o :bbb-room))) - o) - o))) + o)) (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))) (mapc (lambda (track) @@ -301,6 +321,7 @@ "<html><head><meta charset=\"utf-8\" /><link rel=\"stylesheet\" href=\"style.css\"></head><body> <div>" (let ((emacsconf-use-absolute-url t) + (emacsconf-schedule-svg-modify-functions '(emacsconf-schedule-svg-color-by-status)) (emacsconf-base-url "")) (with-temp-buffer (svg-print (emacsconf-schedule-svg 800 300 info)) @@ -311,17 +332,19 @@ (mapconcat (lambda (day) (format - "<tr><th colspan=\"6\" style=\"text-align: left\">%s</th></tr> -<tr><th>Q&A</th><th>Pad</th><th>Chat</th><th>Time</th><th>Slug</th><th>Title</th></tr> + "<tr><th colspan=\"7\" style=\"text-align: left\">%s</th></tr> +<tr><th>Talk start</th><th>BBB start</th><th>Talk ID</th><th>Q&A type</th><th>Mod code</th><th>BBB</th><th>Pad</th><th>Chat</th><th>Title</th><th>Speakers</th></tr> %s" (car day) (emacsconf-publish-format-res-talks (cdr day)))) (seq-group-by (lambda (o) (format-time-string "%A, %b %-e" (plist-get o :start-time))) track-talks) "\n") - "</table></body></html>"))) - (with-temp-file (expand-file-name (format "index-%s.html" (plist-get track :id)) emacsconf-res-dir) - (insert result)) + "</table></body>" + (with-temp-buffer + (insert-file-contents (expand-file-name "include-in-index-footer.html" emacsconf-cache-dir)) + (buffer-string)) + "</html>"))) (with-temp-file (expand-file-name (format "index-%s.html" (plist-get track :id)) emacsconf-backstage-dir) (insert result)))) emacsconf-tracks))) @@ -358,9 +381,8 @@ (let ((tracks (emacsconf-video-subtitle-tracks (or (plist-get talk :caption-file) - (concat (replace-regexp-in-string "reencoded\\|original" "main" - video-base) - ".vtt")) + (emacsconf-talk-file talk "--main.vtt") + (emacsconf-talk-file talk "--reencoded.vtt")) (or (plist-get talk :track-base-url) (plist-get talk :base-url)) (plist-get talk :files)))) @@ -501,21 +523,21 @@ ${categories} (defun emacsconf-publish-talk-p (talk) "Return non-nil if the talk is ready to be published. -Talks that are pending review will not be published yet." + Talks that are pending review will not be published yet." (pcase (plist-get talk :status) ('nil nil) ("TODO" nil) ("TO_REVIEW" nil) - ("TO_ACCEPT" nil) + ;; ("TO_ACCEPT" nil) ("CANCELLED" nil) (_ t))) (defun emacsconf-publish-talk-pages (emacsconf-info &optional force) "Populate year/talks/*.md files. -These should include the nav and schedule files, which will be -rewritten as needed. After they are generated, they should be all -right to manually edit to include things like additional -resources." + These should include the nav and schedule files, which will be + rewritten as needed. After they are generated, they should be all + right to manually edit to include things like additional + resources." (interactive (list (emacsconf-get-talk-info) (> (prefix-numeric-value current-prefix-arg) 1))) (mapc (lambda (o) (emacsconf-publish-talk-page o force)) (emacsconf-filter-talks emacsconf-info))) @@ -564,25 +586,29 @@ resources." (defun emacsconf-publish-format-talk-schedule-info (o) "Format schedule information for O." (let ((friendly (concat "/" emacsconf-year "/talks/" (plist-get o :slug) )) - (timestamp (org-timestamp-from-string (plist-get o :scheduled)))) + (timestamp (org-timestamp-from-string (plist-get o :scheduled))) + (talk-p (emacsconf-publish-talk-p o))) (emacsconf-replace-plist-in-string (append o (list :format - (concat (or (plist-get o :video-time) - (plist-get o :time)) - "-min talk; Q&A: " - (pcase (plist-get o :qa-type) - ("none" "ask questions via Etherpad/IRC; we'll e-mail the speaker and post answers on this wiki page after the conference") - ("live" "BigBlueButton conference room") - ("pad" "Etherpad") - ("irc" "IRC") - (_ (plist-get o :qa-type))) - (emacsconf-surround " <" (and (member emacsconf-publishing-phase '(schedule conference)) - (plist-get o :qa-url)) ">" "")) + (if talk-p + (concat (or (plist-get o :video-time) + (plist-get o :time)) + "-min talk ; Q&A: " + (pcase (plist-get o :qa-type) + ("none" "ask questions via Etherpad/IRC; we'll e-mail the speaker and post answers on this wiki page after the conference") + ("live" "BigBlueButton conference room") + ("pad" "Etherpad") + ("irc" "IRC") + (_ (plist-get o :qa-type))) + (emacsconf-surround " <" (and (member emacsconf-publishing-phase '(schedule conference)) + (plist-get o :qa-url)) ">" "")) + (concat (or (plist-get o :video-time) + (plist-get o :time)) "-min talk cancelled")) :pad-info - (if (and emacsconf-publish-include-pads (not (and (member emacsconf-publishing-phase '(schedule conference)) - (string= (plist-get o :qa-type) "etherpad")))) + (if (and talk-p emacsconf-publish-include-pads (not (and (member emacsconf-publishing-phase '(schedule conference)) + (string= (plist-get o :qa-type) "etherpad")))) (format "Etherpad: <https://pad.emacsconf.org/%s-%s> \n" emacsconf-year (plist-get o :slug)) "") :irc-info @@ -608,7 +634,7 @@ resources." (org-timestamp-split-range (org-timestamp-from-string (plist-get o :scheduled)))))) (format - "<div>Times in different timezones:</div><div class=\"times\" start=\"%s\" end=\"%s\"><div class=\"conf-time\">%s</div><div class=\"others\"><div>which is the same as:</div>%s</div></div><div><strong><a href=\"/%s/watch/%s/\">Find out how to watch and participate</a></strong></div>" + "<div>Times in different time zones:</div><div class=\"times\" start=\"%s\" end=\"%s\"><div class=\"conf-time\">%s</div><div class=\"others\"><div>which is the same as:</div>%s</div></div><div><strong><a href=\"/%s/watch/%s/\">Find out how to watch and participate</a></strong></div>" (format-time-string "%Y-%m-%dT%H:%M:%SZ" start t) (format-time-string "%Y-%m-%dT%H:%M:%SZ" end t) (emacsconf-timezone-string o emacsconf-timezone) @@ -659,7 +685,7 @@ ${alternate-apac-info}\n"))) "\nThe following image shows where the talk is in the schedule for " (format-time-string "%a %Y-%m-%d" (plist-get talk :start-time) emacsconf-timezone) ". Solid lines show talks with Q&A via BigBlueButton. Dashed lines show talks with Q&A via IRC or Etherpad." (format "<div class=\"schedule-in-context schedule-svg-container\" data-slug=\"%s\">\n" (plist-get talk :slug)) - (let* ((width 800) (height 150) + (let* ((width 700) (height 150) (talk-date (format-time-string "%Y-%m-%d" (plist-get talk :start-time) emacsconf-timezone)) (start (date-to-time (concat talk-date "T" emacsconf-schedule-start-time emacsconf-timezone-offset))) (end (date-to-time (concat talk-date "T" emacsconf-schedule-end-time emacsconf-timezone-offset))) @@ -699,8 +725,9 @@ This includes the intro note, the schedule, and talk resources." (insert (emacsconf-surround "" (plist-get talk :intro-note) "\n\n" "")) (let ((is-live (emacsconf-talk-live-p talk))) (when is-live (emacsconf-publish-captions-in-wiki talk)) - (when (member emacsconf-publishing-phase '(schedule conference)) - (insert (emacsconf-publish-format-talk-schedule-image talk info))) + (when (emacsconf-publish-talk-p talk) + (when (member emacsconf-publishing-phase '(schedule conference)) + (insert (emacsconf-publish-format-talk-schedule-image talk info)))) (insert (emacsconf-publish-format-talk-schedule-info talk) "\n\n") (insert (if (plist-get talk :public) (emacsconf-wiki-talk-resources talk) "") @@ -834,6 +861,12 @@ This includes captions, contact, and an invitation to participate." (plist-get b :start-time)) t) (t nil))) +(defun emacsconf-publish-cancelled-nav-page (talk) + (with-temp-file (expand-file-name (format "%s-nav.md" (plist-get talk :slug)) + (expand-file-name "info" (expand-file-name emacsconf-year emacsconf-directory))) + (insert "\n<div class=\"talk-nav\"> +Back to the [[talks]] \n</div>"))) + (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. @@ -898,7 +931,10 @@ Back to the [[talks]] \n" (emacsconf-publish-with-wiki-change (mapc (lambda (o) (emacsconf-publish-before-page o info)) - info))) + info) + (mapc (lambda (o) (emacsconf-publish-before-page o info)) + (seq-filter (lambda (o) (string= (plist-get o :status) "CANCELLED")) + (emacsconf-filter-talks info))))) (defun emacsconf-generate-main-schedule-with-tracks (&optional info) (interactive (list nil)) @@ -933,7 +969,7 @@ Back to the [[talks]] \n" Times below are in %{timezone} (GMT${gmt-offset}). If you have Javascript enabled, clicking on talk pages should include times in your computer's local time setting. -You can also get this schedule as iCalendar files: ${icals}. Importing that into your calendar should translate things into your local timezone. Alternatively, you can use these timezone-translated Org files: <${schedule-directory}>"))) +You can also get this schedule as iCalendar files: ${icals}. Importing that into your calendar should translate things into your local time zone. Alternatively, you can use these time-zone-translated Org files: <${schedule-directory}>"))) ;; By track (let* ((by-day (mapcar (lambda (o)) (seq-group-by (lambda (o) @@ -1012,8 +1048,12 @@ You can also get this schedule as iCalendar files: ${icals}. Importing that into "Return a list with the schedule for INFO. Entries are sorted chronologically, with different tracks interleaved." (setq info (or info (emacsconf-get-talk-info))) - (let* ((by-day (emacsconf-by-day (emacsconf-publish-prepare-for-display info))) - (cancelled (seq-filter (lambda (o) (string= (plist-get o :status) "CANCELLED")) info)) + (let* ((by-day (emacsconf-by-day (seq-remove (lambda (o) + (or + (not (plist-get o :scheduled)) + (member (plist-get o :status) '("TODO" "TO_REVIEW" "TO_ACCEPT")))) + (emacsconf-publish-prepare-for-display info)))) + (cancelled (seq-filter (lambda (o) (string= (plist-get o :status) "CANCELLED")) (emacsconf-get-talk-info))) (dates (seq-map (lambda (o) (plist-get (cadr o) :start-time)) by-day)) (links (mapcar (lambda (o) (format "<a href=\"#date-%s\">%s</a>" @@ -1056,61 +1096,59 @@ Entries are sorted chronologically, with different tracks interleaved." "") ))) -(defun emacsconf-publish-schedule (&optional info) - "Generate the schedule or program." - (interactive) - (emacsconf-publish-schedule-svg-snippets) - (setq info (or info (emacsconf-publish-prepare-for-display info))) - (with-temp-file (expand-file-name "schedule-details.md" - (expand-file-name emacsconf-year emacsconf-directory)) - (when (member emacsconf-publishing-phase '(schedule conference)) - (insert - (emacsconf-replace-plist-in-string - (list - :timezone emacsconf-timezone - :gmt-offset emacsconf-timezone-offset - :alternative-timezones - (string-join (emacsconf-timezone-strings - (format "<%s %s-%s>" - emacsconf-date - (plist-get (car emacsconf-tracks) :start) - (plist-get (car emacsconf-tracks) :end)) - nil "~%-l:%M %p") - " / ") - :icals - (concat - (format "<a href=\"%s%s/%s.ics\">%s.ics</a> - " - emacsconf-media-base-url - emacsconf-year - emacsconf-id - emacsconf-id) - (mapconcat (lambda (track) - (format "<a href=\"%s%s/%s-%s.ics\">%s-%s.ics</a>" - emacsconf-media-base-url - emacsconf-year - emacsconf-id - (plist-get track :id) - emacsconf-id - (plist-get track :id))) - emacsconf-tracks " - ")) - :schedule-directory - (concat emacsconf-media-base-url emacsconf-year "/schedules/")) - "The conference is from ${alternative-timezones}. - -Times below are in ${timezone} (GMT${gmt-offset}). If you have Javascript enabled, clicking on talk pages should include times in your computer's local time setting. - -You can also get this schedule as iCalendar files: ${icals}. Importing that into your calendar should translate things into your local timezone. Alternatively, you can use these timezone-translated Org files: <${schedule-directory}> - -"))) - (insert - (if (member emacsconf-publishing-phase '(cfp program)) - (let ((sorted (emacsconf-publish-prepare-for-display (or info (emacsconf-get-talk-info))))) - (mapconcat - (lambda (track) - (concat - "Jump to: " - ;; links to other tracks - (string-join (seq-keep (lambda (track-link) +(defun emacsconf-publish-schedule-with-times (&optional info) + (insert + (emacsconf-replace-plist-in-string + (list + :timezone emacsconf-timezone + :year emacsconf-year + :gmt-offset emacsconf-timezone-offset + :alternative-timezones + (string-join (emacsconf-timezone-strings + (format "<%s %s-%s>" + emacsconf-date + (plist-get (car emacsconf-tracks) :start) + (plist-get (car emacsconf-tracks) :end)) + nil "~%-l:%M %p") + " / ") + :icals + (concat + (format "<a href=\"%s%s/%s.ics\">%s.ics</a> - " + emacsconf-media-base-url + emacsconf-year + emacsconf-id + emacsconf-id) + (mapconcat (lambda (track) + (format "<a href=\"%s%s/%s-%s.ics\">%s-%s.ics</a>" + emacsconf-media-base-url + emacsconf-year + emacsconf-id + (plist-get track :id) + emacsconf-id + (plist-get track :id))) + emacsconf-tracks " - ")) + :schedule-directory + (concat emacsconf-media-base-url emacsconf-year "/schedules/")) + "Times below are in ${timezone} (GMT${gmt-offset}). If you have Javascript enabled, clicking on talk pages should include times in your computer's local time setting. + +[[!inline pages=\"internal(${year}/schedule-image)\" raw=\"yes\"]] + +The conference is from ${alternative-timezones}. + +You can also get this schedule as iCalendar files: ${icals}. Importing that into your calendar should translate things into your local time zone. Alternatively, you can use these time-zone-translated Org files: <${schedule-directory}> + +") + (emacsconf-publish-format-interleaved-schedule info))) + +(defun emacsconf-publish-program-without-times (&optional info) + (insert + (let ((sorted (emacsconf-publish-prepare-for-display (or info (emacsconf-get-talk-info))))) + (mapconcat + (lambda (track) + (concat + "Jump to: " + ;; links to other tracks + (string-join (seq-keep (lambda (track-link) (unless (string= (plist-get track-link :id) (plist-get track :id)) (format "<a href=\"#%s\">%s</a>" @@ -1118,28 +1156,42 @@ You can also get this schedule as iCalendar files: ${icals}. Importing that into (plist-get track-link :name)))) emacsconf-tracks) " | ") - "\n\n" - (let ((track-talks (seq-filter (lambda (o) (string= (plist-get o :track) + "\n\n" + (let ((track-talks (seq-filter (lambda (o) (string= (plist-get o :track) (plist-get track :name))) - sorted))) - (format - "<h1 id=\"%s\" class=\"sched-track %s\">%s (%d talks)</h1>\n%s" - (plist-get track :id) - (plist-get track :name) - (plist-get track :name) - (length track-talks) - (emacsconf-publish-format-main-schedule track-talks))))) - emacsconf-tracks "\n\n")) - (emacsconf-publish-format-interleaved-schedule info)))) - (when (member emacsconf-publishing-phase '(cfp program)) - (with-temp-file (expand-file-name + sorted))) + (format + "<h1 id=\"%s\" class=\"sched-track %s\">%s (%d talk%s)</h1>\n%s" + (plist-get track :id) + (plist-get track :name) + (plist-get track :name) + (length track-talks) + (if (= (length track-talks) 1) "" "s") + (emacsconf-publish-format-main-schedule track-talks))))) + emacsconf-tracks "\n\n")))) + +(defun emacsconf-publish-schedule (&optional info) + "Generate the schedule or program." + (interactive) + (unless (eq emacsconf-publishing-phase 'cfp) (emacsconf-publish-schedule-svg-snippets)) + (setq info (or info (emacsconf-publish-prepare-for-display info))) + (pcase emacsconf-publishing-phase + ((or 'schedule 'conference) + (with-temp-file (expand-file-name "schedule-details.md" + (expand-file-name emacsconf-year emacsconf-directory)) + (emacsconf-publish-schedule-with-times info))) + ((or 'cfp 'program) + (with-temp-file (expand-file-name "schedule-details.md" + (expand-file-name emacsconf-year emacsconf-directory)) + (emacsconf-publish-program-without-times info)) + (with-temp-file (expand-file-name "draft-schedule.md" (expand-file-name emacsconf-year emacsconf-directory)) (insert "[[!sidebar content=\"\"]]\n\n" "This is a *DRAFT* schedule.\n" (let ((emacsconf-publishing-phase 'schedule)) - (emacsconf-publish-format-interleaved-schedule info)))))) + (emacsconf-publish-format-interleaved-schedule info))))))) (defun emacsconf-format-talk-link (talk) (and talk (if (plist-get talk :slug) @@ -1174,7 +1226,8 @@ You can also get this schedule as iCalendar files: ${icals}. Importing that into (pcase emacsconf-publishing-phase ('program (list - :time (plist-get o :time))) + :time (plist-get o :time) + :note (plist-get o :sched-note))) ((or 'schedule 'conference) (list :status (pcase (plist-get o :status) @@ -1283,7 +1336,7 @@ Use FILES as the file list, or look in the cache directory." (assoc-default "TO_ASSIGN" by-status) files "to be captioned, waiting for volunteers" - "<p>You can e-mail <a href=\"mailto:sacha@sachachua.com\">sacha@sachachua.com</a> to call dibs on editing the captions for one of these talks. We use OpenAI Whisper to provide auto-generated VTT that you can use as a starting point, but you can also write the captions from scratch if you like. The starting point for the main video ends in --main.vtt. If you're writing the captions from scratch, you can choose to include timing information, or we can probably figure them out afterwards with a forced alignment tool. More info: <a href=\"https://emacsconf.org/captioning/\">captioning tips</a></p>" + "<p>You can e-mail <a href=\"mailto:sacha@sachachua.com\">sacha@sachachua.com</a> to call dibs on editing the captions for one of these talks. We use OpenAI Whisper to provide auto-generated VTT that you can use as a starting point, but you can also write the captions from scratch if you like. The VTT file has timing information and the TXT file has the plain text; you can work with either. If you're writing the captions from scratch, you can choose to include timing information, or we can figure them out afterwards with a forced alignment tool. More info: <a href=\"https://emacsconf.org/captioning/\">captioning tips</a></p>" (lambda (f) (append f @@ -1353,7 +1406,7 @@ If MODIFY-FUNC is specified, use it to modify the talk." (if (file-exists-p (expand-file-name "include-in-index.html" emacsconf-cache-dir)) (with-temp-buffer (insert-file-contents (expand-file-name "include-in-index.html" emacsconf-cache-dir)) (buffer-string)) "") - "<p>Schedule by status: (gray: waiting, light yellow: processing, yellow: to assign, light green: captioning, green: to check, light blue: captioned and ready)<br />Updated by conf.org and the wiki repository</br />" + "<p>Schedule by status: (gray: waiting, light yellow: processing, yellow: to assign, light blue: captioning, light green: to check, green: captioned and ready)<br />Updated by conf.org and the wiki repository</br />" (let* ((emacsconf-schedule-svg-modify-functions '(emacsconf-schedule-svg-color-by-status)) (img (emacsconf-schedule-svg 800 200 info))) (with-temp-buffer @@ -1380,7 +1433,7 @@ If MODIFY-FUNC is specified, use it to modify the talk." (lambda (status) (format "<li>%s - %d talk(s), %d minutes: %s</li>" (if (string= status "TO_ASSIGN") - "TO_ASSIGN (waiting for captioning volunteers)" + "<strong>TO_ASSIGN (waiting for captioning volunteers)</strong>" status) (length (assoc-default status by-status)) (emacsconf-sum '(:video-time :time) (assoc-default status by-status)) @@ -1392,7 +1445,7 @@ If MODIFY-FUNC is specified, use it to modify the talk." ", "))) (pcase emacsconf-publishing-phase ((or 'program 'schedule 'conference) - '("WAITING_FOR_PREREC" "PROCESSING" "TO_ASSIGN" "TO_CAPTION" "TO_CHECK" "TO_STREAM")) + '("TO_CONFIRM" "WAITING_FOR_PREREC" "PROCESSING" "TO_ASSIGN" "TO_CAPTION" "TO_CHECK" "TO_STREAM")) ((or 'harvest 'resources) '("TO_ARCHIVE" "TO_REVIEW_QA" "TO_INDEX_QA" "TO_CAPTION_QA" "DONE"))) "") @@ -1407,7 +1460,7 @@ If MODIFY-FUNC is specified, use it to modify the talk." ", ") "</div>" (pcase emacsconf-publishing-phase - ((or 'program 'schedule 'conference) + ((or 'program 'schedule 'conference 'cfp) (concat (emacsconf-publish-backstage-list (append @@ -1428,7 +1481,11 @@ If MODIFY-FUNC is specified, use it to modify the talk." files "ready to be streamed") (emacsconf-publish-backstage-list - (assoc-default "WAITING_FOR_PREREC" by-status) files + (seq-sort + #'emacsconf-sort-by-scheduled + (append (assoc-default "WAITING_FOR_PREREC" by-status) + (assoc-default "TO_CONFIRM" by-status))) + files "we're waiting for" "Speakers might submit these, do them live, or cancel the talks."))) ((or 'harvest 'resources) @@ -1517,7 +1574,7 @@ answers without needing to listen to everything again. You can see <a href=\"htt ;; further tests (pcase f ((rx (seq "--" - (or "reencoded" "normalized" "final" "old" "bbb"))) + (or "reencoded" "normalized" "final" "old" "bbb" "backstage"))) nil) ((rx ".diff") nil) ((rx "--original") @@ -1706,22 +1763,24 @@ ${include} (defun emacsconf-video-subtitle-tracks (filename track-base-url &optional files) (setq files (or files (directory-files emacsconf-cache-dir))) - (concat - (if (member (file-name-nondirectory filename) files) - (format "<track label=\"English\" kind=\"captions\" srclang=\"en\" src=\"%s\" default />" - (concat (or track-base-url "") (file-name-nondirectory filename))) - "") - (mapconcat - (lambda (lang) - (let ((lang-file (concat (file-name-sans-extension filename) "_" (car lang) "." (file-name-extension filename)))) - (if (member lang-file files) - (format "<track label=\"%s\" kind=\"captions\" srclang=\"%s\" src=\"%s\" />" - (cdr lang) - (car lang) - (concat (or track-base-url "") (file-name-nondirectory lang-file))) - ""))) - emacsconf-publish-subtitle-languages - ""))) + (if filename + (concat + (if (member (file-name-nondirectory filename) files) + (format "<track label=\"English\" kind=\"captions\" srclang=\"en\" src=\"%s\" default />" + (concat (or track-base-url "") (file-name-nondirectory filename))) + "") + (mapconcat + (lambda (lang) + (let ((lang-file (concat (file-name-sans-extension filename) "_" (car lang) "." (file-name-extension filename)))) + (if (member lang-file files) + (format "<track label=\"%s\" kind=\"captions\" srclang=\"%s\" src=\"%s\" />" + (cdr lang) + (car lang) + (concat (or track-base-url "") (file-name-nondirectory lang-file))) + ""))) + emacsconf-publish-subtitle-languages + "")) + "")) (defun emacsconf-publish-link-file-formats (file-prefix) (string-join (emacsconf-publish-link-file-formats-as-list file-prefix) " ")) @@ -1765,6 +1824,7 @@ ${include} '(:slug :title :speakers :pronouns :pronunciation :url :track :file-prefix :qa-url :qa-type + :live :qa-backstage-url)))) (emacsconf-filter-talks (emacsconf-get-talk-info))) :tracks @@ -2016,6 +2076,7 @@ ${include} (autoload 'subed-parse-file "subed-common") (defun emacsconf-publish-video-description (talk &optional copy skip-title) (interactive (list (emacsconf-complete-talk-info) t)) + (when (stringp talk) (setq talk (emacsconf-resolve-talk talk))) (let ((chapters (subed-parse-file (expand-file-name (concat @@ -2050,15 +2111,70 @@ ${chapters}You can view this and other resources using free/libre software at ${ This video is available under the terms of the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license."))) (if copy (kill-new result)) result)) + +(defun emacsconf-publish-youtube-step-through-publishing () + (interactive) + (catch 'done + (while t + (let ((talk (seq-find (lambda (o) + (and (member (plist-get o :status) '("TO_STREAM" "TO_CHECK")) + (not (plist-get o :youtube)) + (emacsconf-talk-file o "--main.webm"))) + (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))) + (unless talk + (message "All done so far.") + (throw 'done t)) + (kill-new (emacsconf-talk-file talk "--main.webm")) + (message "Video: %s - press any key" (emacsconf-talk-file talk "--main.webm")) + (when (eq (read-char) ?q) (throw 'done t)) + (kill-new (emacsconf-publish-video-description talk t)) + (message "Copied description - press any key") + (when (eq (read-char) ?q) (throw 'done t)) + (when (emacsconf-talk-file talk "--main.vtt") + (kill-new (emacsconf-talk-file talk "--main.vtt")) + (message "Captions: %s - press any key" (emacsconf-talk-file talk "--main.vtt")) + (when (eq (read-char) ?q) (throw 'done t))) + (emacsconf-set-property-from-slug + (plist-get talk :slug) + "YOUTUBE_URL" + (read-string (format "%s - YouTube URL: " (plist-get talk :scheduled)))))))) + +(defun emacsconf-publish-toobnix-step-through-publishing () + (interactive) + (catch 'done + (while t + (let ((talk (seq-find (lambda (o) + (and (not (plist-get o :toobnix-url)) + (emacsconf-talk-file o "--main.webm"))) + (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))) + (unless talk + (message "All done so far.") + (throw 'done t)) + (kill-new (emacsconf-talk-file talk "--main.webm")) + (message "Video: %s - press any key" (emacsconf-talk-file talk "--main.webm")) + (when (eq (read-char) ?q) (throw 'done t)) + (kill-new (emacsconf-publish-video-description talk t)) + (message "Copied description - press any key") + (when (eq (read-char) ?q) (throw 'done t)) + (when (emacsconf-talk-file talk "--main.vtt") + (kill-new (emacsconf-talk-file talk "--main.vtt")) + (message "Captions: %s - press any key" (emacsconf-talk-file talk "--main.vtt")) + (when (eq (read-char) ?q) (throw 'done t))) + (emacsconf-set-property-from-slug + (plist-get talk :slug) + "YOUTUBE" + (read-string (format "%s - Toobnix URL: " (plist-get talk :scheduled)))))))) + + ;; (emacsconf-publish-video-description (emacsconf-find-talk-info "async") t) (defun emacsconf-cache-all-video-data () - (interactive) - (mapc - (lambda (talk) - (when (plist-get talk :file-prefix) - (emacsconf-publish-cache-video-data talk))) - (emacsconf-get-talk-info))) + (interactive) + (mapc + (lambda (talk) + (when (plist-get talk :file-prefix) + (emacsconf-publish-cache-video-data talk))) + (emacsconf-get-talk-info))) ;; (emacsconf-cache-all-video-data t) (defvar emacsconf-cache-dir (expand-file-name "cache" (file-name-directory emacsconf-org-file))) @@ -2204,11 +2320,13 @@ We recommend using a streaming player like mpv to watch the livestreams. Example (concat "mpv " (plist-get track :stream) "\n")) emacsconf-tracks "") - "</pre><table width=\"100%\"><tr><th>Watch page</th><th>IRC channel (libera.chat)</th><th>URL for streaming player (ex: mpv, vlc, ffplay)</th><th>Low res</th></tr>\n" + "</pre><table width=\"100%\"><tr><th>Watch page</th><th>Watch page (low-res)</th><th>IRC channel (libera.chat)</th><th>URL for streaming player (ex: mpv, vlc, ffplay)</th><th>Low res</th></tr>\n" (mapconcat (lambda (track) (emacsconf-replace-plist-in-string - (append (list :year emacsconf-year) track) - "<tr><td><div class=\"sched-track ${name}\"><a href=\"/${year}/watch/${id}/\">${name}</a></div></td><td><a href=\"${webchat-url}\">${channel}</a></td><td><a href=\"${stream}\">${stream}</a></td><td><a href=\"${480p}\">${id}-480p.webm</a></tr>")) + (append (list :year emacsconf-year + :watch-base emacsconf-live-base-url + ) track) + "<tr><td><div class=\"sched-track ${name}\"><a href=\"${watch-base}${year}/watch/${id}/\">${name}</a></div></td><td><a href=\"${watch-base}${year}/watch/${id}-480p/\">${name} (low-res)</a></td><td><a href=\"${webchat-url}\">${channel}</a></td><td><a href=\"${stream}\">${stream}</a></td><td><a href=\"${480p}\">${id}-480p.webm</a></tr>")) emacsconf-tracks "\n") "</table>\n\n" @@ -2891,5 +3009,16 @@ Tends to be quota-limited, though." emacsconf-backstage-dir) t) (emacsconf-publish-update-talk slug))) + +(defun emacsconf-publish-intros-to-backstage () + (interactive) + (dolist (talk (emacsconf-get-talk-info)) + (when (file-exists-p (expand-file-name (concat (plist-get talk :slug) ".webm") + (expand-file-name "intros" emacsconf-stream-asset-dir))) + (copy-file (expand-file-name (concat (plist-get talk :slug) ".webm") + (expand-file-name "intros" emacsconf-stream-asset-dir)) + (expand-file-name (concat (plist-get talk :file-prefix) "--intro.webm") + emacsconf-backstage-dir) + t)))) ;; (provide 'emacsconf-publish) diff --git a/emacsconf-schedule.el b/emacsconf-schedule.el index 9597508..260ef9c 100644 --- a/emacsconf-schedule.el +++ b/emacsconf-schedule.el @@ -25,7 +25,7 @@ ;;; Code: (defvar emacsconf-schedule-strategies - '(emacsconf-schedule-allocate-video-time-rounded-to-five) + '(emacsconf-schedule-allocate-video-time-round-up-to-five) "List of scheduling functions. Each function should take the info and manipulate it as needed, returning the new info.") @@ -267,7 +267,8 @@ Pairs with `emacsconf-schedule-dump-sexp'." (org-entry-put (point) "SCHEDULED" (plist-get talk :scheduled)) (org-entry-put (point) "TRACK" (plist-get talk :track)) (org-entry-put (point) "TIME" (plist-get talk :time))) - (emacsconf-filter-talks info))))) + (emacsconf-filter-talks info)) + (setq emacsconf-schedule-draft nil)))) (defun emacsconf-schedule-save-emailed-times (info &optional field force) (interactive (list (or emacsconf-schedule-draft (emacsconf-get-talk-info)) @@ -435,9 +436,9 @@ Pairs with `emacsconf-schedule-dump-sexp'." "Set talk color based on status. Processing: palegoldenrod, Waiting to be assigned a captioner: yellow, -Captioning in progress: lightgreen, -To check: green, -Ready to stream: blue, +Captioning in progress: lightblue, +To check: lightgreen, +Ready to stream: green, Other status: gray" (unless (plist-get o :invalid) (dom-set-attribute node 'fill @@ -449,11 +450,11 @@ Other status: gray" ("TO_ASSIGN" "yellow") ("TO_CAPTION" - "lightgreen") + "lightblue") ("TO_CHECK" - "green") + "#90ee90") ("TO_STREAM" - "lightblue") + "green") ("TODO" "lightgray") (_ "gray"))))) @@ -662,29 +663,45 @@ Talks with a FIXED_TIME property are not moved." (message "%s" (string-join results "\n")) results))) -(defun emacsconf-schedule-check-time (label o &optional from-time to-time day) +(defun emacsconf-schedule-check-time (label o &rest args) "FROM-TIME and TO-TIME should be nil strings like HH:MM in EST. -DAY should be YYYY-MM-DD if specified. +DAY should be YYYY-MM-DD or Sat/Sun if specified. Both start and end time are tested." (let* ((start-time (format-time-string "%H:%M" (plist-get o :start-time))) (end-time (format-time-string "%H:%M" (plist-get o :end-time))) - result) - (setq result - (or - (and (null o) (format "%s: Not found" label)) - (and from-time (string< start-time from-time) - (format "%s: Starts at %s before %s" label start-time from-time)) - (and to-time (string< to-time end-time) - (format "%s: Ends at %s after %s" label end-time to-time)) - (and day - (not (string= (format-time-string "%Y-%m-%d" (plist-get o :start-time)) - day)) - (format "%s: On %s instead of %s" - label - (format-time-string "%Y-%m-%d" (plist-get o :start-time)) - day)))) - (when result (plist-put o :invalid result)) - result)) + (date (format-time-string "%Y-%m-%d" (plist-get o :start-time))) + (day (format-time-string "%a" (plist-get o :start-time))) + (result t) error) + (if (null o) + (setq error (format "%s: Not found" label)) + (while args + (pcase (car args) + ('or ; skip the rest + (if error + (setq args (cdr args) + error nil) + (setq args nil))) + ('and ; skip the rest if nil + (setq args (if error nil (cdr args)))) + (_ + (let ((from-time (pop args)) + (to-time (pop args)) + (limit-day (pop args))) + (cond + ((and from-time (string< start-time from-time)) + (setq error (format "%s: Starts at %s before %s" label start-time from-time))) + ((and to-time (string< to-time end-time)) + (setq error (format "%s: Ends at %s after %s" label end-time to-time))) + ((and limit-day (string-match "Sat\\|Sun" limit-day)) + (when (not (string= day limit-day)) + (setq error (format "%s: On %s instead of %s" + label day limit-day)))) + (limit-day + (when (not (string= date limit-day)) + (setq error (format "%s: On %s instead of %s" + label date limit-day)))))))))) + (when error (plist-put o :invalid error)) + error)) (defun emacsconf-schedule-q-and-a-p (talk) "Return non-nil if TALK has a Q&A scheduled for the event." @@ -696,16 +713,30 @@ Both start and end time are tested." hours start (pos 0) - (result (list nil nil nil))) - (while (string-match "\\([<>]\\)=? *\\([0-9]+:[0-9]+\\) *EST" avail pos) - (setf (elt result (if (string= (match-string 1 avail) ">") - 0 - 1)) - (match-string 2 avail)) - (setq pos (match-end 0))) - (when (string-match "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]" avail) - (setf (elt result 2) (match-string 0 avail))) - result))) + result) + (with-temp-buffer + (insert avail) + (goto-char (point-min)) + (while (not (eobp)) + (cond + ((looking-at "\\([<>]\\)=? *\\([0-9]+:[0-9]+\\) *EST \\([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]\\|Sat\\|Sun\\)?") + (push (and (string= (match-string 1) ">") ; start time + (match-string 2)) + result) + (push (and (string= (match-string 1) "<") ; end time + (match-string 2)) + result) + (push (match-string 3) result) + (goto-char (match-end 0))) + ((looking-at " or ") + (push 'or result) + (goto-char (match-end 0))) + ((looking-at " and ") + (push 'and result) + (goto-char (match-end 0))) + (t (goto-char (point-max)))))) + (reverse result)))) +;; (emacsconf-schedule-get-time-constraint '(:q_and_a "live" :availability ">= 12:00 EST Sat or <= 12:00 EST Sun - more info")) (defun emacsconf-schedule-rename-etc-timezone (s) "Change Etc/GMT-3 etc. to UTC+3 etc., since Etc uses negative signs and this is confusing." @@ -713,52 +744,62 @@ Both start and end time are tested." ((string-match "Etc/GMT\\+\\(.*\\)" s) (concat "UTC-" (match-string 1 s))) (t s))) +;; (emacsconf-schedule-format-time-constraint (emacsconf-schedule-get-time-constraint '(:q_and_a "live" :availability ">= 12:00 EST Sat or <= 12:00 EST Sun - more info")) t "America/Vancouver") (defun emacsconf-schedule-format-time-constraint (constraints &optional include-offset local-timezone) "Format CONSTRAINTS for display." ;; actually a talk object, extract constraints from it instead - (when (not (= (length constraints) 3)) + (when (plist-get constraints :title) (setq constraints (emacsconf-schedule-get-time-constraint constraints))) - (string-join - (delq nil - (list - (let ((start-time (car constraints)) - (end-time (cadr constraints)) - (start-local (and (car constraints) - local-timezone - (format-time-string - "%H:%M" - (date-to-time (concat emacsconf-date " " (car constraints) ":00 " emacsconf-timezone-offset)) - local-timezone))) - (end-local (and (cadr constraints) - local-timezone - (format-time-string - "%H:%M" - (date-to-time (concat emacsconf-date " " (cadr constraints) ":00 " emacsconf-timezone-offset)) - local-timezone)))) - (cond - ((and start-time end-time) - (concat - (format "between %s-%s" start-time end-time) - (emacsconf-surround " " (and include-offset emacsconf-timezone-offset) "" "") - (if local-timezone - (format " (%s-%s %s)" start-local end-local (emacsconf-schedule-rename-etc-timezone local-timezone)) - ""))) - (start-time - (concat - (format ">= %s" start-time) - (emacsconf-surround " " (and include-offset emacsconf-timezone-offset) "" "") - (if local-timezone - (format " (%s %s)" start-local (emacsconf-schedule-rename-etc-timezone local-timezone)) - ""))) - (end-time - (concat - (format "<= %s" end-time) - (emacsconf-surround " " (and include-offset emacsconf-timezone-offset) "" "") - (if local-timezone - (format " (%s %s)" end-local (emacsconf-schedule-rename-etc-timezone local-timezone)) - ""))))) - (if (elt constraints 2) (format "on %s" (elt constraints 2))))) - " and ")) + (let (results) + (while constraints + (push (pcase (car constraints) + ('or (pop constraints) + "or") + ('and (pop constraints) + "and") + (_ + (let* ((from-time (pop constraints)) + (to-time (pop constraints)) + (from-local (and from-time local-timezone + (format-time-string + "%H:%M" + (date-to-time (concat emacsconf-date " " from-time ":00 " emacsconf-timezone-offset)) + local-timezone))) + (to-local (and to-time local-timezone + (format-time-string + "%H:%M" + (date-to-time (concat emacsconf-date " " to-time ":00 " + emacsconf-timezone-offset)) + local-timezone))) + (limit-day (pop constraints))) + (string-trim + (concat + (cond + ((and from-time to-time) + (concat + (format "between %s-%s" from-time to-time) + (emacsconf-surround " " (and include-offset emacsconf-timezone-offset) "" "") + (if local-timezone + (format "(%s-%s %s) " from-local to-local (emacsconf-schedule-rename-etc-timezone local-timezone)) + ""))) + (from-time + (concat + (format ">= %s" from-time) + (emacsconf-surround " " (and include-offset emacsconf-timezone-offset) "" "") + (if local-timezone + (format " (%s %s)" from-local (emacsconf-schedule-rename-etc-timezone local-timezone)) + ""))) + (to-time + (concat + (format "<= %s " to-time) + (emacsconf-surround "" (and include-offset emacsconf-timezone-offset) "" "") + (if local-timezone + (format " (%s %s)" to-local (emacsconf-schedule-rename-etc-timezone local-timezone)) + ""))) + (t "")) + (if limit-day (concat " " limit-day ""))))))) + results)) + (string-join (reverse results) " "))) (defun emacsconf-schedule-validate-all-talks-present (sched &optional list) (let* ((sched-slugs (mapcar (lambda (o) (plist-get o :slug)) @@ -777,6 +818,12 @@ Both start and end time are tested." (when diff (list (concat "Missing talks: " (string-join diff ", ")))))) +(defun emacsconf-schedule-validate-no-cancelled-talks (sched &optional list) + (let ((cancelled (seq-keep (lambda (o) (when (string= (plist-get o :status) "CANCELLED") (plist-get o :slug))) + sched))) + (when cancelled + (list (concat "Cancelled talks: " (string-join cancelled ", ")))))) + (defun emacsconf-schedule-validate-no-duplicates (sched &optional info) (let* ((sched-slugs (mapcar (lambda (o) (plist-get o :slug)) (emacsconf-filter-talks sched))) diff --git a/emacsconf-stream.el b/emacsconf-stream.el index 211749b..6641392 100644 --- a/emacsconf-stream.el +++ b/emacsconf-stream.el @@ -24,9 +24,9 @@ ;;; Code: -(defvar emacsconf-stream-dir "/data/emacsconf/stream/" +(defvar emacsconf-stream-dir (format "/data/emacsconf/shared/%s/assets/stream/" emacsconf-year) "Directory where the stream versions are. -Files should be in YEAR/file-prefix--main.webm and file-prefix--main.vtt.") +Files should be file-prefix--main.webm and file-prefix--main.vtt.") (defvar emacsconf-stream-host "res.emacsconf.org") (defun emacsconf-stream-track-login (track) @@ -72,7 +72,7 @@ If the element doesn't have a tspan child, use the element itself." (when node (dom-set-attribute node 'style "visibility: hidden") (dom-set-attribute (dom-child-by-tag node 'tspan) 'style "fill: none; stroke: none"))) - (setq text (svg--encode-text text)) + ;; (setq text (svg--encode-text text)) (let ((node (or (dom-child-by-tag (car (dom-by-id dom id)) 'tspan) @@ -261,11 +261,10 @@ especially when two things need to happen close together." (defun emacsconf-stream-get-filename (talk) "Return the local filename for the video file for TALK. -Final files should be stored in /data/emacsconf/stream/YEAR/file-prefix--main.webm." +Final files should be stored in emacsconf-stream-dir/file-prefix--main.webm." (expand-file-name (concat (plist-get talk :file-prefix) "--main.webm") - (expand-file-name emacsconf-year - emacsconf-stream-dir))) + emacsconf-stream-dir)) (defun emacsconf-stream-play-video (talk) "Play just the video for TALK." @@ -414,10 +413,11 @@ With a prefix argument (\\[universal-argument]), clear the overlay." i (substring "123456789 123456789 123456789 123456789 123456789 123456789 " (1+ (length (format "%s %02d" (plist-get talk :slug) i)))))))) - (copy-file - (expand-file-name "template.webm" dir) - (expand-file-name (concat (plist-get talk :file-prefix) "--main.webm") dir) - t)))) + (unless (file-exists-p (expand-file-name (concat (plist-get talk :file-prefix) "--main.webm") dir)) + (copy-file + (expand-file-name "template.webm" dir) + (expand-file-name (concat (plist-get talk :file-prefix) "--main.webm") dir) + t))))) (defun emacsconf-stream-display-talk-info (talk) (interactive (list (emacsconf-complete-talk-info))) @@ -607,7 +607,7 @@ With a prefix argument (\\[universal-argument]), clear the overlay." (shell-quote-argument (expand-file-name (concat (plist-get talk :slug) ".svg") dir))))) (setq prev talk)) - (emacsconf-filter-talks (cdr track))))) + (emacsconf-filter-talks (cdr track))))) by-track))) @@ -818,6 +818,7 @@ ffplay URL (defvar emacsconf-stream-track "General") (defvar emacsconf-stream-clock-buffer "*emacsconf*") (defvar emacsconf-stream-clock-timer nil) +(defvar emacsconf-stream-random-timer nil) (require 'diary-lib) (require 'text-property-search) @@ -861,10 +862,55 @@ ffplay URL " to go" (if message ": " "")) "")))) "") - (or message "")) + (or message "") + "\n\n" + (propertize "Enjoy EmacsConf!" 'emacsconf-random t)) (when (timerp emacsconf-stream-clock-timer) (cancel-timer emacsconf-stream-clock-timer)) (emacsconf-stream-update-time) - (setq emacsconf-stream-clock-timer (run-at-time t 1 #'emacsconf-stream-update-time)))) + (setq emacsconf-stream-clock-timer (run-at-time t 1 #'emacsconf-stream-update-time)) + (setq emacsconf-stream-clock-timer (run-at-time t 10 #'emacsconf-stream-update-random)))) + +(defvar emacsconf-stream-random-file (expand-file-name "fortune.txt" emacsconf-cache-dir)) +(defvar emacsconf-stream-random-data nil) + +(defun emacsconf-stream-shuffle-list (list) + "Shuffle LIST using Fisher-Yates algorithm." + (let ((shuffled (copy-sequence list))) + (dotimes (i (1- (length shuffled))) + (let* ((j (+ i (random (- (length shuffled) i)))) + (temp (nth i shuffled))) + (setf (nth i shuffled) (nth j shuffled)) + (setf (nth j shuffled) temp))) + shuffled)) + +(defun emacsconf-stream-get-random-string () + (when (and (not emacsconf-stream-random-data) + emacsconf-stream-random-file (file-exists-p emacsconf-stream-random-file)) + (setq emacsconf-stream-random-data + (emacsconf-stream-shuffle-list + (with-temp-buffer + (insert-file-contents emacsconf-stream-random-file) + (split-string (string-trim (buffer-string)) "\n%\n"))))) + (when emacsconf-stream-random-data + (pop emacsconf-stream-random-data))) + +(defun emacsconf-stream-update-random () + (if (get-buffer emacsconf-stream-clock-buffer) + (when (get-buffer-window emacsconf-stream-clock-buffer) + (with-current-buffer emacsconf-stream-clock-buffer + (save-excursion + (goto-char (point-min)) + (let (match) + (while (setq match (text-property-search-forward 'emacsconf-random)) + (goto-char (prop-match-beginning match)) + (add-text-properties + (prop-match-beginning match) + (prop-match-end match) + (list 'display + (emacsconf-stream-get-random-string))) + (goto-char (prop-match-end match))))))) + (when (timerp emacsconf-stream-random-timer) + (cancel-timer emacsconf-stream-random-timer)))) (defun emacsconf-stream-update-time () "Update the displayed time." @@ -1193,5 +1239,76 @@ If INFO is non-nil, use that as the schedule instead." (plist-put track :autopilot nil) (emacsconf-stream-track-ssh track "crontab -r"))) +(defun emacsconf-stream-copy-livestream-description (shift) + (interactive (list (completing-read "Shift: " (mapcar (lambda (o) (plist-get o :id)) emacsconf-shifts)))) + (when (stringp shift) (setq shift (seq-find (lambda (o) (string= (plist-get o :id) shift)) emacsconf-shifts))) + (let* ((track-id (when (string-match "-\\([a-z]+?\\)$" (plist-get shift :id)) (match-string 1 (plist-get shift :id)))) + (desc (emacsconf-replace-plist-in-string + (list + :track-name (plist-get shift :track) + :start (format-time-string "%Y-%m-%d %-l:%M %p %Z (UTC %z)" (date-to-time (plist-get shift :start))) + :end (format-time-string "%Y-%m-%d %-l:%M %p %Z (UTC %z)" (date-to-time (plist-get shift :end))) + :start-day (format-time-string "%b %-e %a %p" (date-to-time (plist-get shift :start))) + :year emacsconf-year + :track-id track-id + :irc-channels (concat + (string-join + (seq-keep (lambda (track) + (unless (string= (plist-get track :id) track-id) + (plist-get track :channel))) + emacsconf-tracks) + ",") + "," + (plist-get (emacsconf-get-track track-id) :channel))) + " +${track-name} - ${start-day} EmacsConf ${year} + +This for the ${track-name} track of EmacsConf, the conference about the joy of Emacs and Emacs Lisp. +Start: ${start} +End: ${end} + +Watch using free/open source software: https://live.emacsconf.org/${year}/watch/${track-id}/ +Conference info: https://emacsconf.org/${year}/ +Schedule: https://emacsconf.org/${year}/talks/ +Chat on #emacsconf-${track-id} via https://chat.emacsconf.org/?join=emacsconf,emacsconf-org,emacsconf-accessible,${irc-channels} or irc.libera.chat using your favorite IRC client +Etherpad: Use the Etherpad links from the talk page; general comments in https://pad.emacsconf.org/${year} + +Videos are shared under the terms of the Creative Commons Attribution-ShareAlike 4.0 +International (CC BY-SA 4.0) license. Please observe the guidelines for conduct: https://emacsconf.org/conduct/ +"))) + (when (called-interactively-p 'any) + (kill-new desc)) + desc)) + +(defun emacsconf-stream-populate-random-package-file () + (interactive) + (with-temp-file (expand-file-name "fortune.txt" emacsconf-cache-dir) + (dolist (entry + (emacsconf-stream-shuffle-list + (seq-mapcat (lambda (f) + (let ((base (file-name-base f))) + (mapcar + (lambda (entry) + (list base + (symbol-name (car entry)) + (elt (cdr entry) 2))) + (cdr + (with-temp-buffer + (insert-file-contents + (expand-file-name "archive-contents" f)) + (goto-char (point-min)) + (read (current-buffer))))))) + (directory-files + (expand-file-name "archives" package-user-dir) t + directory-files-no-dot-files-regexp)))) + (unless (bobp) (insert "\n%\n")) + (insert + (format "%s: %s (%s)" + (elt entry 1) + (elt entry 2) + (pcase (elt entry 0) + ("gnu" "GNU ELPA") + ("nongnu" "NonGNU ELPA") + ("melpa" "MELPA"))))))) (provide 'emacsconf-stream) ;;; emacsconf-stream.el ends here diff --git a/emacsconf-subed.el b/emacsconf-subed.el index 37daa1a..fafdf38 100644 --- a/emacsconf-subed.el +++ b/emacsconf-subed.el @@ -357,12 +357,12 @@ Create it if necessary." nil (* i 5000) (1- (* i 5000)) - (format "#+OUTPUT: %s.webm\n[[file:%s]]\n%s" + (emacsconf-pad-expand-intro talk) + (format "#+OUTPUT: %s.webm\n[[file:%s]]" (plist-get talk :slug) (expand-file-name - (concat (plist-get talk :slug) ".svg.png") - (expand-file-name "in-between" emacsconf-stream-asset-dir)) - (emacsconf-pad-expand-intro talk)))) + (concat (plist-get talk :slug) ".png") + (expand-file-name "in-between" emacsconf-stream-asset-dir))))) (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))))) (defvar-local emacsconf-subed-subtitle-source nil "Buffer with the intended subtitles.") diff --git a/emacsconf.el b/emacsconf.el index 1a76a84..0aee6c5 100644 --- a/emacsconf.el +++ b/emacsconf.el @@ -34,20 +34,20 @@ "Name of conference" :group 'emacsconf :type 'string) -(defcustom emacsconf-year "2023" +(defcustom emacsconf-year "2025" "Conference year. String for easy inclusion." :group 'emacsconf :type 'string) -(defcustom emacsconf-cfp-deadline "2023-09-14" "Deadline for proposals." +(defcustom emacsconf-cfp-deadline "2025-09-19" "Target date for proposals." :group 'emacsconf :type 'string) -(defcustom emacsconf-date "2023-12-02" "Starting date of EmacsConf." +(defcustom emacsconf-date "2025-12-06" "Starting date of EmacsConf." :group 'emacsconf :type 'string) -(defcustom emacsconf-video-target-date "2023-11-04" "Target date for receiving talk videos from the speakers." +(defcustom emacsconf-video-target-date "2025-10-31" "Target date for receiving talk videos from the speakers." :group 'emacsconf :type 'string) -(defcustom emacsconf-schedule-announcement-date "2023-10-25" "Date for publishing the schedule." +(defcustom emacsconf-schedule-announcement-date "2025-10-24" "Date for publishing the schedule." :group 'emacsconf :type 'string) (defcustom emacsconf-directory "~/vendor/emacsconf-wiki" @@ -70,7 +70,7 @@ (defcustom emacsconf-base-url "https://emacsconf.org/" "Includes trailing slash" :group 'emacsconf :type 'string) -(defcustom emacsconf-publishing-phase 'program +(defcustom emacsconf-publishing-phase 'resources "Controls what information to include. 'program - don't include times 'schedule - include times; use this leading up to the conference @@ -86,7 +86,7 @@ (const :tag "Harvest: Extracting info" conference) (const :tag "Resources: Don't include status, publish all Q&A" resources))) -(defcustom emacsconf-backstage-phase 'prerec +(defcustom emacsconf-backstage-phase 'harvest "Contros what information to include backstage. 'prerec - focus on captioning 'harvest - focus on Q&A." @@ -125,7 +125,7 @@ (defvar emacsconf-chat-base "https://chat.emacsconf.org/") (defvar emacsconf-backstage-dir "/ssh:orga@media.emacsconf.org:/var/www/media.emacsconf.org/2022/backstage") (defvar emacsconf-upload-dir "/ssh:orga@media.emacsconf.org:/srv/upload") -(defvar emacsconf-res-dir (format "/ssh:orga@res.emacsconf.org:/data/emacsconf/%s" emacsconf-year)) +(defvar emacsconf-res-dir (format "/ssh:orga@res.emacsconf.org:/data/emacsconf/shared/%s" emacsconf-year)) (defvar emacsconf-media-extensions '("webm" "mkv" "mp4" "webm" "mov" "avi" "ts" "ogv" "wav" "ogg" "mp3" )) (defvar emacsconf-ftp-upload-dir "/ssh:orga@media.emacsconf.org:/srv/ftp/anon/upload-here") (defvar emacsconf-backstage-user "emacsconf") @@ -173,6 +173,7 @@ (interactive) (dired emacsconf-backstage-dir "-tl")) (defun emacsconf-res-dired () (interactive) (dired emacsconf-res-dir "-tl")) +(defun emacsconf-res-cache-dired () (interactive) (dired (expand-file-name "cache" emacsconf-res-dir) "-tl")) (defun emacsconf-media-dired () (interactive) (dired emacsconf-public-media-directory "-tl")) (defun emacsconf-cache-dired () (interactive) @@ -330,6 +331,61 @@ Return the list of new filenames." dir) t)))) +(defun emacsconf-upload-scp-from-json (talk key filename) + "Parse PsiTransfer JSON files and copy the uploaded file to the res directory. +The file is associated with TALK. KEY identifies the file in a multi-file upload. +FILENAME specifies an extra string to add to the file prefix if needed." + (interactive (let-alist (json-parse-string (buffer-string) :object-type 'alist) + (list (emacsconf-complete-talk-info) + .metadata.key + (read-string (format "Filename: "))))) + (let* ((source-dir (file-name-directory (buffer-file-name))) + (new-filename (concat (plist-get talk :file-prefix) + (if (string= filename "") + filename + (concat "--" filename)) + "." + (let-alist (json-parse-string (buffer-string) :object-type 'alist) + (file-name-extension .metadata.name)))) + (default-directory (expand-file-name "cache" emacsconf-res-dir)) + (command (emacsconf-upload-scp-command-from-json talk key filename)) + process) + (with-current-buffer (get-buffer-create (format "*scp %s*" (plist-get talk :slug))) + (insert (string-join command " ") "\n") + (set-process-sentinel + (apply #'start-process (concat "scp-" (plist-get talk :slug)) + (current-buffer) nil + command) + (lambda (process event) + (when (string= event "finished") + (message "Finished copying %s" + new-filename))))))) + +(defun emacsconf-upload-scp-command-from-json (talk key filename) + "Parse PsiTransfer JSON files and get the SCP command for copying the uploaded file to the res directory. +The file is associated with TALK. KEY identifies the file in a multi-file upload. +FILENAME specifies an extra string to add to the file prefix if needed." + (interactive (let-alist (json-parse-string (buffer-string) :object-type 'alist) + (list (emacsconf-complete-talk-info) + .metadata.key + (read-string (format "Filename: "))))) + (let* ((source-dir (file-name-directory (buffer-file-name))) + (new-filename (concat (plist-get talk :file-prefix) + (if (string= filename "") + filename + (concat "--" filename)) + "." + (let-alist (json-parse-string (buffer-string) :object-type 'alist) + (file-name-extension .metadata.name)))) + (default-directory (expand-file-name "cache" emacsconf-res-dir)) + (command (list + "scp" + (replace-regexp-in-string "^/ssh:" "" (expand-file-name key source-dir)) + new-filename))) + (when (called-interactively-p 'any) + (kill-new (string-join command " "))) + command)) + (defun emacsconf-upload-copy-from-json (talk key filename) "Parse PsiTransfer JSON files and copy the uploaded file to the res directory. The file is associated with TALK. KEY identifies the file in a multi-file upload. @@ -394,6 +450,14 @@ FILENAME specifies an extra string to add to the file prefix if needed." (setq value (or value (org-read-property-value prop))) (org-entry-put (point) prop value)))) +(defun emacsconf-copy-property (search prop) + (interactive (list (emacsconf-complete-talk) nil)) + (save-window-excursion + (emacsconf-with-talk-heading search + (setq prop (or prop (org-read-property-name))) + (when (called-interactively-p 'any) + (kill-new (org-entry-get (point) prop))) + (org-entry-get (point) prop)))) (defun emacsconf-complete-slug () (emacsconf-get-slug-from-string (emacsconf-complete-talk))) @@ -489,7 +553,9 @@ If INFO is specified, limit it to that list." ,@body)) (defvar emacsconf-status-types - '(("WAITING_FOR_PREREC" . "Waiting for video from speaker") + '(("TO_ACCEPT" . "Waiting for video from speaker") + ("TO_CONFIRM" . "Waiting for video from speaker") + ("WAITING_FOR_PREREC" . "Waiting for video from speaker") ("TO_PROCESS" . "Processing uploaded video") ("PROCESSING" . "Processing uploaded video") ("TO_AUTOCAP" . "Processing uploaded video") @@ -513,6 +579,11 @@ If INFO is specified, limit it to that list." (setq list (cons (match-string-no-properties 0) list))) (plist-put o :categories (reverse list)))) +(defun emacsconf-talk-recorded-p (talk) + "Returns non-nil if TALK will start with a recorded video." + (and (not (plist-get talk :live)) + (plist-get talk :video-file))) + (defun emacsconf-get-talk-info-from-properties (o) (let ((heading (org-heading-components)) (field-props '( @@ -544,6 +615,7 @@ If INFO is specified, limit it to that list." (:prerec-info "PREREC_INFO") ;; Prep (:bbb-room "ROOM") + (:bbb-mod-code "BBB_MOD_CODE") ;; Processing (:file-prefix "FILE_PREFIX") (:video-file "VIDEO_FILE") @@ -574,7 +646,7 @@ If INFO is specified, limit it to that list." (:present "PRESENT") (:talk-id "TALK_ID") ; use slug instead (:qa-public "QA_PUBLIC") ; now tracked by the OPEN_Q and UNSTREAMED_Q status - (:uuid "UUID") ; Pentabarf export + (:uuid "UUID") ; Pentabarf export ))) (apply 'append @@ -685,7 +757,10 @@ The subheading should match `emacsconf-abstract-heading-regexp'." ")"))) (defun emacsconf-get-abstract-from-wiki (o) - (plist-put o :markdown (emacsconf-talk-markdown-from-wiki (plist-get o :slug)))) + (plist-put o :markdown (emacsconf-talk-markdown-from-wiki (plist-get o :slug))) + (when (plist-get o :markdown) + (plist-put o :org-description + (pandoc-convert-stdio (plist-get o :markdown) "markdown" "org")))) (defun emacsconf-add-talk-status (o) @@ -879,10 +954,7 @@ The subheading should match `emacsconf-abstract-heading-regexp'." (plist-put o :qa-link "none") (plist-put o :qa-backstage-url (plist-get o :pad-url)))) (plist-put o :recorded-intro - (let ((filename - (expand-file-name (concat (plist-get o :slug) ".webm") - (expand-file-name "intros" emacsconf-stream-asset-dir)))) - (and (file-exists-p filename) filename))) + (emacsconf-talk-file o "--intro.webm")) o)) (defun emacsconf-search-talk-info (search &optional info) @@ -952,13 +1024,8 @@ The subheading should match `emacsconf-abstract-heading-regexp'." (defun emacsconf-get-talk-info-from-file (&optional filename) - (with-temp-buffer - (insert-file-contents (or filename "conf.org")) - (org-mode) - (org-show-all) - (goto-char (point-min)) - (goto-char (org-find-property "ID" "talks")) - (emacsconf-get-talk-info 'wiki))) + (let ((emacsconf-org-file filename)) + (emacsconf-get-talk-info))) (defun emacsconf-include-next-talks (info number) (let* ((info (emacsconf-publish-prepare-for-display info)) @@ -984,6 +1051,12 @@ The subheading should match `emacsconf-abstract-heading-regexp'." "Return the plist for TALK." (if (stringp talk) (emacsconf-find-talk-info talk info) talk)) +(defun emacsconf-resolve-shift (shift) + "Return the plist for SHIFT." + (if (stringp shift) (seq-find (lambda (o) (string= (plist-get o :id) shift)) + emacsconf-shifts) + shift)) + (defun emacsconf-find-talk-info (filter &optional info) (setq info (or info (emacsconf-filter-talks (emacsconf-get-talk-info)))) (when (stringp filter) (setq filter (list filter))) @@ -1083,6 +1156,11 @@ The subheading should match `emacsconf-abstract-heading-regexp'." (interactive (list (emacsconf-complete-talk))) (insert (plist-get (emacsconf-search-talk-info search) :title))) +(defun emacsconf-insert-talk-schedule (search) + "Insert the schedule for SEARCH." + (interactive (list (emacsconf-complete-talk))) + (insert (emacsconf-mail-format-talk-schedule (emacsconf-search-talk-info search)))) + (defun emacsconf-insert-talk-email (search) "Insert the talk email matching SEARCH." (interactive (list (emacsconf-complete-talk))) @@ -1090,8 +1168,8 @@ The subheading should match `emacsconf-abstract-heading-regexp'." (defun emacsconf-backstage-url (&optional base-url) "Return or insert backstage URL with credentials." - (setq base-url (or base-url (concat emacsconf-media-base-url emacsconf-year "/backstage/"))) (interactive) + (setq base-url (or base-url (concat emacsconf-media-base-url emacsconf-year "/backstage/"))) (let ((url (format "https://%s:%s@%s" emacsconf-backstage-user @@ -1116,10 +1194,12 @@ The subheading should match `emacsconf-abstract-heading-regexp'." "a" #'emacsconf-announce "i e" #'emacsconf-insert-talk-email "i t" #'emacsconf-insert-talk-title + "i s" #'emacsconf-insert-talk-schedule "I" #'emacsconf-message-talk-info "c" #'emacsconf-find-captions-from-slug "d" #'emacsconf-find-caption-directives-from-slug "p" #'emacsconf-set-property-from-slug + "P" #'emacsconf-copy-property "w" #'emacsconf-edit-wiki-page "s" #'emacsconf-set-start-time-for-slug "W" #'emacsconf-browse-wiki-page @@ -1362,6 +1442,7 @@ If TIMEZONES is a string, split it by commas." '((emacsconf-pad-api-key . etherpad_api_key) (emacsconf-pad-base . etherpad_url) (emacsconf-backstage-password . emacsconf_backstage_password)))))) +(defvar emacsconf-live-base-url "https://live.emacsconf.org/") ;; (emacsconf-ansible-load-vars (expand-file-name "prod-vars.yml" emacsconf-ansible-directory)) ;;; Tracks (defvar emacsconf-tracks @@ -1380,21 +1461,21 @@ If TIMEZONES is a string, split it by commas." :vnc-port "5905" :autopilot crontab :status "online") - (:name "Development" :color "skyblue" :id "dev" :channel "emacsconf-dev" - :watch ,(format "https://live.emacsconf.org/%s/watch/dev/" emacsconf-year) - :webchat-url "https://chat.emacsconf.org/?join=emacsconf,emacsconf-org,emacsconf-accessible,emacsconf-gen,emacsconf-dev" - :tramp "/ssh:emacsconf-dev@res.emacsconf.org#46668:" - ;; :toobnix-url "https://toobnix.org/w/w6K77y3bNMo8xsNuqQeCcD" - ;; :youtube-url "https://www.youtube.com/watch?v=PMaoF-xa1b4" - ;; :youtube-studio-url "https://studio.youtube.com/video/PMaoF-xa1b4/livestreaming" - :stream ,(concat emacsconf-stream-base "dev.webm") - :480p ,(concat emacsconf-stream-base "dev-480p.webm") - :uid 2003 - :start "10:00" :end "17:00" - :vnc-display ":6" - :vnc-port "5906" - :autopilot crontab - :status "online"))) + (:name "Development" :color "skyblue" :id "dev" :channel "emacsconf-dev" + :watch ,(format "https://live.emacsconf.org/%s/watch/dev/" emacsconf-year) + :webchat-url "https://chat.emacsconf.org/?join=emacsconf,emacsconf-org,emacsconf-accessible,emacsconf-gen,emacsconf-dev" + :tramp "/ssh:emacsconf-dev@res.emacsconf.org#46668:" + ;; :toobnix-url "https://toobnix.org/w/w6K77y3bNMo8xsNuqQeCcD" + ;; :youtube-url "https://www.youtube.com/watch?v=PMaoF-xa1b4" + ;; :youtube-studio-url "https://studio.youtube.com/video/PMaoF-xa1b4/livestreaming" + :stream ,(concat emacsconf-stream-base "dev.webm") + :480p ,(concat emacsconf-stream-base "dev-480p.webm") + :uid 2003 + :start "10:00" :end "17:00" + :vnc-display ":6" + :vnc-port "5906" + :autopilot crontab + :status "offline"))) (defun emacsconf-get-track (name) "Get the track for NAME. @@ -1458,8 +1539,7 @@ NAME could be a track name, a talk name, or a list." info) info)) -(defvar emacsconf-shifts - (list (list :id "sat-am-gen" :track "General" :start "2023-12-02T08:00:00-0500" :end "2023-12-02T12:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "FlowyCoder" :coord "sachac") (list :id "sat-pm-gen" :track "General" :start "2023-12-02T13:00:00-0500" :end "2023-12-02T18:00:00-0500" :host "zaph" :streamer "sachac" :checkin "FlowyCoder" :coord "sachac") (list :id "sat-am-dev" :track "Development" :start "2023-12-02T08:00:00-0500" :end "2023-12-02T12:00:00-0500" :host "bandali" :streamer "sachac" :checkin "FlowyCoder" :coord "sachac") (list :id "sat-pm-dev" :track "Development" :start "2023-12-02T13:00:00-0500" :end "2023-12-02T18:00:00-0500" :host "bandali" :streamer "sachac" :checkin "FlowyCoder" :coord "sachac") (list :id "sun-am-gen" :track "General" :start "2023-12-03T08:00:00-0500" :end "2023-12-03T12:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "FlowyCoder" :coord "sachac") (list :id "sun-pm-gen" :track "General" :start "2023-12-03T13:00:00-0500" :end "2023-12-03T18:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "FlowyCoder" :coord "sachac") (list :id "sun-am-dev" :track "Development" :start "2023-12-03T08:00:00-0500" :end "2023-12-03T12:00:00-0500" :host "bandali" :streamer "sachac" :checkin "FlowyCoder" :coord "sachac") (list :id "sun-pm-dev" :track "Development" :start "2023-12-03T13:00:00-0500" :end "2023-12-03T18:00:00-0500" :host "bandali" :streamer "sachac" :checkin "FlowyCoder" :coord "sachac"))) +(setq emacsconf-shifts (list (list :id "sat-am-gen" :track "General" :start "2025-12-07T09:00:00-0500" :end "2025-12-07T12:00:00-0500") (list :id "sat-pm-gen" :track "General" :start "2025-12-07T13:00:00-0500" :end "2025-12-07T17:00:00-0500") (list :id "sat-am-dev" :track "Development" :start "2025-12-07T10:00:00-0500" :end "2025-12-07T12:00:00-0500") (list :id "sat-pm-dev" :track "Development" :start "2025-12-07T13:00:00-0500" :end "2025-12-07T17:00:00-0500") (list :id "sun-am-gen" :track "General" :start "2025-12-08T09:00:00-0500" :end "2025-12-08T12:00:00-0500") (list :id "sun-pm-gen" :track "General" :start "2025-12-08T13:00:00-0500" :end "2025-12-08T17:00:00-0500"))) (defun emacsconf-filter-talks-by-time (start-time end-time info) "Return talks that are between START-TIME and END-TIME (inclusive) in INFO." @@ -1531,6 +1611,39 @@ Filter by TRACK if given. Use INFO as the list of talks." (seq-group-by (lambda (o) (plist-get o :speakers)) (or info (emacsconf-active-talks (emacsconf-filter-talks (emacsconf-get-talk-info)))))))) +(defun emacsconf-load-rooms (string) + "Split STRING and load them as ROOM properties. +STRING should be a list of rooms, one room per line, like this: +friendly-id speaker - slugs +friendly-id speaker - slugs +" + (let ((rooms + (mapcar (lambda (row) (when (string-match "^\\(.+?\\) \\(.+\\)" row) + (list (match-string 1 row) (match-string 2 row)))) + (split-string string "\n")))) + (mapc (lambda (talk) + (emacsconf-go-to-talk talk) + (when (plist-get talk :speakers) + (org-entry-put + (point) + "ROOM" + (concat + emacsconf-bbb-base-url + "rooms/" + (car + (seq-find + (lambda (o) + (string-match + (concat + "^" + (regexp-quote + (plist-get talk :speakers)) + " - ") + (cadr o))) + rooms)) + "/join")))) + (emacsconf-active-talks (emacsconf-get-talk-info))))) + (defun emacsconf-surround (before text after &optional alternative) "Concat BEFORE, TEXT, and AFTER if TEXT is specified, or return ALTERNATIVE." (if (and text (not (string= text ""))) @@ -1538,10 +1651,12 @@ Filter by TRACK if given. Use INFO as the list of talks." alternative)) ;;; Volunteer management -(defun emacsconf-get-volunteer-info () +(defun emacsconf-get-volunteer-info (&optional tag) (with-current-buffer (find-file-noselect emacsconf-org-file) (org-map-entries (lambda () (org-entry-properties)) - "volunteer+EMAIL={.}"))) + (format "volunteer+EMAIL={.}%s" + (if tag (concat "+" tag) + ""))))) (defun emacsconf-volunteer-emails-for-completion (&optional info) (mapcar (lambda (o) @@ -1627,19 +1742,21 @@ Filter by TRACK if given. Use INFO as the list of talks." (defun emacsconf-add-org-after-todo-state-change-hook () "Add FUNC to `org-after-todo-stage-change-hook'." (interactive) - (with-current-buffer (find-buffer-visiting emacsconf-org-file) + (with-current-buffer (or (find-buffer-visiting emacsconf-org-file) + (find-file-noselect emacsconf-org-file)) (add-hook 'org-after-todo-state-change-hook #'emacsconf-org-after-todo-state-change nil t))) (defun emacsconf-remove-org-after-todo-state-change-hook () "Remove FUNC from `org-after-todo-stage-change-hook'." (interactive) - (with-current-buffer (find-buffer-visiting emacsconf-org-file) + (with-current-buffer (or (find-buffer-visiting emacsconf-org-file) + (find-file-noselect emacsconf-org-file)) (remove-hook 'org-after-todo-state-change-hook #'emacsconf-org-after-todo-state-change t))) (defvar emacsconf-todo-hooks - '(emacsconf-stream-play-talk-on-change - emacsconf-stream-open-qa-windows-on-change + '(;; emacsconf-stream-play-talk-on-change + ;; emacsconf-stream-open-qa-windows-on-change emacsconf-erc-announce-on-change ;; announce via ERC emacsconf-publish-bbb-redirect emacsconf-stream-update-talk-info-on-change @@ -1704,6 +1821,7 @@ tracks with the ID in the cdr of that list." "Add NOTE as a logbook entry for the current subtree." (move-marker org-log-note-return-to (point)) (move-marker org-log-note-marker (point)) + (setq org-log-note-window-configuration (current-window-configuration)) (with-temp-buffer (insert note) (let ((org-log-note-purpose 'note)) @@ -1712,9 +1830,11 @@ tracks with the ID in the cdr of that list." (defun emacsconf-add-to-talk-logbook (talk note) "Add NOTE as a logbook entry for TALK." (interactive (list (emacsconf-complete-talk) (read-string "Note: "))) - (save-excursion - (emacsconf-with-talk-heading talk - (emacsconf-add-to-logbook note)))) + (save-window-excursion + (save-excursion + (emacsconf-with-talk-heading talk + (emacsconf-add-to-logbook note) + (bury-buffer (current-buffer)))))) (defun emacsconf-reload () "Reload the emacsconf-el modules." @@ -1861,5 +1981,19 @@ tracks with the ID in the cdr of that list." (concat "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 " (shell-quote-argument (expand-file-name filename))))))) +(defun emacsconf-delete-from-all (files) + "Delete FILES from all the directories." + (interactive (list (dired-get-marked-files))) + (dolist (dir (list emacsconf-cache-dir emacsconf-backstage-dir + (expand-file-name "cache" emacsconf-res-dir) + emacsconf-public-media-directory)) + (dolist (file files) + (when (and dir (file-exists-p (expand-file-name (file-name-nondirectory file) + dir))) + (delete-file (expand-file-name (file-name-nondirectory file) + dir)) + (message "Deleting %s from %s" + (file-name-nondirectory file) dir))))) + (provide 'emacsconf) ;;; emacsconf.el ends here |