diff options
| -rw-r--r-- | emacsconf-bbb.el | 891 | ||||
| -rw-r--r-- | emacsconf-erc.el | 44 | ||||
| -rw-r--r-- | emacsconf-extract.el | 1023 | ||||
| -rw-r--r-- | emacsconf-hyperlist.el | 4 | ||||
| -rw-r--r-- | emacsconf-mail.el | 634 | ||||
| -rw-r--r-- | emacsconf-pad.el | 100 | ||||
| -rw-r--r-- | emacsconf-publish.el | 779 | ||||
| -rw-r--r-- | emacsconf-schedule.el | 179 | ||||
| -rw-r--r-- | emacsconf-stream.el | 48 | ||||
| -rw-r--r-- | emacsconf-subed.el | 70 | ||||
| -rw-r--r-- | emacsconf-toobnix.el | 323 | ||||
| -rw-r--r-- | emacsconf.el | 255 |
12 files changed, 2913 insertions, 1437 deletions
diff --git a/emacsconf-bbb.el b/emacsconf-bbb.el new file mode 100644 index 0000000..9e0727d --- /dev/null +++ b/emacsconf-bbb.el @@ -0,0 +1,891 @@ +;;; emacsconf-bbb.el --- BigBlueButton -*- lexical-binding: t; -*- + +;; Copyright (C) 2022 Sacha Chua + +;; Author: Sacha Chua <sacha@sachachua.com> + + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; - set BBB_REC to the published presentation URL. If keeping the presentation recording private, don't set BBB_REC; set BBB_MEETING_ID to just the meeting ID. +;; - Use the command from emacsconf-extract-raw-recordings-download-command to download raw data. + +;;; Code: + +;;;###autoload +(defun emacsconf-bbb-status (talk) + (let ((states + '((open . "OPEN_Q UNSTREAMED_Q") + (before . "TODO TO_REVIEW TO_ACCEPT WAITING_FOR_PREREC TO_PROCESS PROCESSING TO_AUTOCAP TO_ASSIGN TO_CAPTION TO_CHECK TO_STREAM PLAYING CLOSED_Q") + (after . "TO_ARCHIVE TO_EXTRACT TO_REVIEW_QA TO_INDEX_QA TO_CAPTION_QA TO_FOLLOW_UP DONE") + (cancelled . "CANCELLED")))) + (if (string-match "live" (or (plist-get talk :q-and-a) "")) + (or (car (seq-find (lambda (state) + (member (plist-get talk :status) (split-string (cdr state)))) + states)) + (throw 'error "Unknown talk BBB state")) + 'irc))) + +(defvar emacsconf-bbb-base-url "https://bbb.emacsverse.org/" "Include trailing slash.") +(defun emacsconf-bbb-room-title-list (&optional info) + (delq nil + (mapcar + (lambda (o) + (when (car o) + (concat "ec" (substring emacsconf-year 2) + "-" (plist-get (emacsconf-get-shift (plist-get (cadr o) :start-time)) :id) + "-" (plist-get (emacsconf-get-track (plist-get (cadr o) :track)) :id) + " " (car o) + " (" + (mapconcat (lambda (talk) (plist-get talk :slug)) (cdr o) ", ") + ")"))) + (seq-group-by (lambda (o) (plist-get o :speakers)) + (or info (emacsconf-active-talks (emacsconf-filter-talks (emacsconf-get-talk-info)))))))) + +(defun emacsconf-bbb-create-rooms () + "Copy the commands needed to create the rooms. +docker exec -it greenlight-v3 /bin/bash -c \"bundle exec rails console\" +user_id = User.find_by_email(\"emacsconf@sachachua.com\").id" + (interactive) + (kill-new + (mapconcat (lambda (group) + (format + "Room.create(user_id: user_id, name: \"%s - %s\")\n" + (plist-get (cadr group) :speakers) + (string-join (mapcar (lambda (talk) (plist-get talk :slug)) + (cdr group)) + ", "))) + (emacsconf-mail-groups (emacsconf-active-talks (emacsconf-get-talk-info))) + "")) + (message "Copied. Run it inside the greenlight-v3 rails console.")) + +(defun emacsconf-bbb-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 + +Print out room IDs with: +Room.all.each { |x| puts x.friendly_id + " " + x.name }; nil +" + (interactive "MInput: ") + (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-bbb-spookfox-set-moderator-codes () + (interactive) + (dolist (talk (seq-filter (lambda (o) + (and (plist-get o :bbb-room) + (not (plist-get o :bbb-mod-code)))) + (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) + (spookfox-js-injection-eval-in-active-tab + (format "window.location.href = \"%s\"" + (replace-regexp-in-string "/join" "" (plist-get talk :bbb-room))) + t) + (sleep-for 3) + (spookfox-js-injection-eval-in-active-tab + "document.querySelector('button[data-rr-ui-event-key=\"settings\"]').click()" t) + (spookfox-js-injection-eval-in-active-tab + "document.querySelector('input#glAnyoneCanStart').checked = true") + (spookfox-js-injection-eval-in-active-tab + "document.querySelector('input#muteOnStart').checked = true") + (spookfox-js-injection-eval-in-active-tab + "document.querySelectorAll('.border-end button')[2].click()" t) + (let ((code (spookfox-js-injection-eval-in-active-tab + "document.querySelector('.access-code-input input').value" t))) + (message "Setting %s to %s" (plist-get talk :slug) code) + (emacsconf-set-property-from-slug + talk "BBB_MOD_CODE" + code) + (sit-for 2)))) + +(defun emacsconf-bbb-spookfox-confirm-settings () + (interactive) + (dolist (talk (seq-filter (lambda (o) + (plist-get o :bbb-room)) + (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) + (spookfox-js-injection-eval-in-active-tab + (format "window.location.href = \"%s\"" + (replace-regexp-in-string "/join" "" (plist-get talk :bbb-room))) + t) + (sleep-for 3) + (spookfox-js-injection-eval-in-active-tab + "document.querySelector('button[data-rr-ui-event-key=\"settings\"]').click()" t) + (sleep-for 3))) + +(defvar emacsconf-extract-bbb-raw-dir (format "~/proj/emacsconf/%s/bbb/" emacsconf-year) "End with a \"/\".") +(defvar emacsconf-extract-bbb-published-dir (format "~/proj/emacsconf/%s/bbb/published/" emacsconf-year) "End with a \"/\".") +(defvar emacsconf-extract-bbb-path "/ssh:bbb@bbb.emacsverse.org:/var/bigbluebutton/") + +(defun emacsconf-extract-raw-recordings-download-command () + "Copy the command for downloading raw recordings." + (interactive) + (let ((s (mapconcat (lambda (o) (if (plist-get o :bbb-meeting-id) + (format "rsync -avzue ssh %s %s\n" + (expand-file-name (plist-get o :bbb-meeting-id) emacsconf-extract-bbb-path) + emacsconf-extract-bbb-raw-dir) + "")) + (emacsconf-get-talk-info)))) + (when (called-interactively-p 'any) (kill-new s)) + s)) + +(defun emacsconf-extract-download-published-recordings-command () + "Copy the command for downloading published recordings from SOURCE to DEST." + (interactive) + (kill-new + (mapconcat (lambda (o) (if (plist-get o :bbb-meeting-id) + (replace-regexp-in-string + "/ssh:" "" + (format "rsync -avzue ssh %s %s # %s\n" + (expand-file-name + (plist-get o :bbb-meeting-id) + (expand-file-name "published/presentation" + emacsconf-extract-bbb-path + )) + emacsconf-extract-bbb-published-dir + (plist-get o :slug))) + "")) + (emacsconf-get-talk-info)))) + +(defun emacsconf-extract-bbb-parse-events-dir (&optional dir) + (mapcar (lambda (file) + (emacsconf-extract-bbb-parse-events file)) + (directory-files-recursively (or dir emacsconf-extract-bbb-raw-dir) "events.xml"))) + +(defun emacsconf-extract-bbb-raw-events-file-name (talk) + (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 (&optional event-xml-files) + (let* ((max 0) + (participant-count 0) + (meeting-count 0) + (max-meetings 0) + (max-participants 0) + meeting-participants + (meeting-events + (sort + (seq-mapcat + (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))) + ("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) + ("CreatePresentationPodEvent" (cl-incf meeting-count) (when (> meeting-count max-meetings) (setq max-meetings meeting-count))) + ("ParticipantJoinEvent" (cl-incf participant-count) (when (> participant-count max-participants) (setq max-participants participant-count))) + ("ParticipantLeftEvent" (cl-decf participant-count)) + ("EndAndKickAllEvent" (cl-decf meeting-count)))) + `((,(length meeting-participants) "Number of meetings analyzed") + (,max-participants "Max number of simultaneous users") + (,max-meetings "Max number of simultaneous meetings") + (,(apply 'max (mapcar (lambda (o) (length (plist-get o :participants))) meeting-participants)) "Max number of people in one meeting") + (,(length (seq-uniq (seq-mapcat (lambda (o) (mapcar #'cdr (plist-get o :participants))) meeting-participants))) "Total unique users") + (,(length (seq-uniq (seq-mapcat (lambda (o) (plist-get o :talking)) meeting-participants))) "Total unique talking")))) + +(defun emacsconf-extract-bbb-dired-raw (talk) + (interactive (list (emacsconf-complete-talk-info))) + (setq talk (emacsconf-resolve-talk talk)) + (dired (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-raw-dir))) + +(defun emacsconf-extract-bbb-dired-published (talk) + (interactive (list (emacsconf-complete-talk-info))) + (setq talk (emacsconf-resolve-talk talk)) + (dired (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir))) + + +(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))))) + (conf-joined-time + (and conf-joined + (string-to-number (dom-text (dom-by-tag (dom-parent dom conf-joined) 'timestampUTC))))) + recording-start + recording-stop + recording-spans + stream-start + chat + talking + participants + talking-starts + deskshare-info + (meeting-date (dom-text (dom-by-tag (dom-parent dom (car conf-joined)) 'date)))) + (dolist (event (dom-by-tag dom 'event)) + (let ((timestamp (string-to-number (dom-text (dom-by-tag event 'timestampUTC))))) + (pcase (dom-attr event 'eventname) + ("ParticipantJoinEvent" + (push (cons (dom-text (dom-by-tag event 'userId)) + (dom-text (dom-by-tag event 'name))) + participants)) + ("StartRecordingEvent" + (setq recording-start timestamp + recording-stop nil + recording-file + (file-name-nondirectory (dom-text (dom-by-tag event 'filename))))) + ("StopRecordingEvent" + (setq recording-stop timestamp) + (push (cons recording-start recording-stop) recording-spans)) + ("PublicChatEvent" + ;; only include events in the public recording + (when (and recording-start + (null recording-stop) + (>= timestamp recording-start)) + (push (list + timestamp + (dom-text (dom-by-tag event 'sender)) + (with-temp-buffer + (insert + (replace-regexp-in-string + "'" "'" + (replace-regexp-in-string "<.*?>" "" + (dom-text (dom-by-tag event 'message))))) + (mm-url-decode-entities) + (buffer-string))) + chat))) + ("ParticipantTalkingEvent" + (let* ((speaker (assoc-default + (dom-text (dom-by-tag event 'participant)) + participants))) + (if (string= (dom-text (dom-by-tag event 'talking)) "true") + ;; started talking + (if (assoc-default speaker talking-starts) + (setcdr (assoc speaker talking-starts) + timestamp) + (push (cons speaker timestamp) talking-starts)) + (when (and recording-start + (>= timestamp recording-start) + (assoc-default speaker talking-starts) + (or (null recording-stop) + (<= (assoc-default speaker talking-starts) + recording-stop))) + (push (list speaker + (- (max (assoc-default speaker talking-starts) recording-start) + recording-start) + (- (if recording-stop (min recording-stop timestamp) timestamp) + recording-start) + recording-file) + talking))))) + ("StartWebRTCDesktopShareEvent" + (setq deskshare-info (cons (dom-text (dom-by-tag event 'filename)) + timestamp))) + + ))) + `((name . ,meeting-name) + (id . ,meeting-id) + (conf-joined . ,conf-joined-time) + (recording-start . ,recording-start) + (meeting-date . ,meeting-date) + (participants . ,participants) + (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) + (format "- %s (%s)\n%s" + (assoc-default 'name events) + (assoc-default 'id events) + (mapconcat + (lambda (message) + (format " - %s: %s\n" + (elt message 1) + (with-temp-buffer + (insert + (replace-regexp-in-string + "'" "'" + (replace-regexp-in-string "<.*?>" "" + (elt message 2)))) + (mm-url-decode-entities) + (buffer-string)))) + (assoc-default 'chat events) + ""))) + (emacsconf-extract-bbb-parse-events-dir) + "")) + +(defun emacsconf-extract-spookfox-update-bbb-rec () + (interactive) + (let* ((data + (spookfox-js-injection-eval-in-active-tab + "row = [...document.querySelectorAll('.dropdown-toggle')].find((o) => o.textContent.match('Unlisted')).closest('tr'); [row.querySelector('#recording-text').getAttribute('title'), row.querySelector('a.btn-primary').getAttribute('href')]" t) + ) + (slug + (when (and data (string-match "^\\([^(]+\\) (" (elt data 0))) + (split-string + (match-string 1 (elt data 0)) + ", ")))) + (when data + (if (> (length slug) 1) + (setq slug (completing-read "Talk: " slug)) + (setq slug (car slug))) + (emacsconf-with-talk-heading slug + (if (org-entry-get (point) "BBB_REC") + (progn + (kill-new (elt data 1)) + (error "%s already has BBB_REC?" slug)) + (org-entry-put (point) "BBB_REC" (elt data 1)))) + (message "Updated BBB_REC for %s to %s" slug (elt data 1)) + (spookfox-js-injection-eval-in-active-tab + "row = [...document.querySelectorAll('.dropdown-toggle')].find((o) => o.textContent.match('Unlisted')).closest('tr'); row.querySelector('.button_to .dropdown-item .fa-globe').closest('button').click();" t)))) + +(defun emacsconf-make-webcams-deskshare-spans (talk &optional start-ms stop-ms strategy) + (let* ((start-ms (or start-ms 0)) + (source-dir (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir)) + (secs (/ start-ms 1000.0)) + (deskshare (xml-parse-file (expand-file-name "deskshare.xml" source-dir))) + (webcam-video (expand-file-name "video/webcams.webm" source-dir)) + (deskshare-video (expand-file-name "deskshare/deskshare.webm" source-dir)) + (stop-ms (or stop-ms (emacsconf-get-file-duration-ms deskshare-video))) + spans) + (mapc (lambda (o) + (unless (or (= secs (string-to-number (dom-attr o 'start_timestamp))) + (= (string-to-number (dom-attr o 'start_timestamp)) 0) + (> secs (/ stop-ms 1000.0))) + (setq spans (cons (list :source webcam-video + :start-ms (* secs 1000) + :stop-ms + (* 1000 + (if (eq strategy 'test) + (+ secs 3) + (max secs (string-to-number (dom-attr o 'start_timestamp)))))) + spans))) + (when (and (<= (string-to-number (dom-attr o 'start_timestamp)) + (/ stop-ms 1000.0)) + (>= (string-to-number (dom-attr o 'stop_timestamp)) + (/ start-ms 1000.0))) + (setq spans (cons (list :source deskshare-video + :start-ms (max (* 1000 (string-to-number (dom-attr o 'start_timestamp))) + start-ms) + :stop-ms + (if (eq strategy 'test) + (* 1000 (+ (string-to-number (dom-attr o 'start_timestamp)) 3)) + (min (* 1000 (string-to-number (dom-attr o 'stop_timestamp))) + stop-ms))) + spans)) + (setq secs (string-to-number (dom-attr o 'stop_timestamp))))) + (dom-by-tag deskshare 'event)) + (unless (>= (floor (* secs 1000)) stop-ms) + (setq spans (cons (list :source webcam-video + :start-ms (* 1000 secs) + :stop-ms (if (eq strategy 'test) + (* 1000 (+ secs 3)) + stop-ms)) + spans))) + (if (eq strategy 'test) + `((video ,@(nreverse spans)) + (audio ,@(mapcar (lambda (o) + (list :source webcam-video + :start-ms (plist-get o :start-ms) + :stop-ms (plist-get o :stop-ms))) + (reverse spans)))) + `((video ,@(nreverse spans)) + (audio (:source ,webcam-video :start-ms ,start-ms :stop-ms ,stop-ms)))))) + +(defun emacsconf-get-ffmpeg-to-splice-webcam-and-recording (talk &optional start-ms stop-ms info strategy) + "Return FFMPEG command for slicing. +Strategies: +- 'fast-cut-start-keyframe - find the keyframe before the start ms and cut from there, doing a fast copy. +- 'start-keyframe-and-reencode - find the keyframe before the start ms and cut from there, reencoding. +- 'cut-and-concat - seek to the keyframe before, slowly find the start-ms, reencode the snippet, and then do a fast copy of the remaining. May have encoding errors. +- default: copy from start-ms to stop-ms, reencoding. +" + (interactive (list (emacsconf-complete-talk-info))) + (setq talk (emacsconf-resolve-talk talk)) + (let* ((slug (plist-get talk :slug)) + (start-ms (or start-ms 0)) + (source-dir (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir)) + (video-slug (plist-get (seq-find (lambda (o) (string= (plist-get o :slug) slug)) info) :video-slug)) + (output (expand-file-name (concat (plist-get talk :file-prefix) "--answers.webm") emacsconf-cache-dir)) + (webcam-video (expand-file-name "video/webcams.webm" source-dir)) + (deskshare-video (expand-file-name "deskshare/deskshare.webm" source-dir)) + (webcam-duration (emacsconf-get-file-duration-ms webcam-video)) + (stop-ms (or stop-ms webcam-duration)) + (command + (if (file-exists-p deskshare-video) + ;; Has deskshare + (let* ((deskshare (xml-parse-file (expand-file-name "deskshare.xml" source-dir))) + (final-size (compile-media-max-dimensions + deskshare-video + webcam-video)) + (duration (compile-media-get-file-duration-ms webcam-video)) + (spans (emacsconf-make-webcams-deskshare-spans talk start-ms stop-ms strategy)) + (compile-media-output-video-width (car final-size)) + (compile-media-output-video-height (cdr final-size))) + (compile-media-get-command spans output)) + ;; Just webcams + (if (and (= start-ms 0) + (= stop-ms webcam-duration)) + (format "cp %s %s" + webcam-video + output) + (compile-media-get-command + (compile-media-split-tracks + (list (list :source webcam-video :start-ms start-ms :stop-ms stop-ms))) + output))))) + (when (called-interactively-p 'any) + (kill-new command)) + command)) + +(defun emacsconf-extract-chat (filename) + (when (file-exists-p filename) + (message "%s" filename) + (mapcar + (lambda (node) + (when (string= (dom-attr node 'target) "chat") + (let ((message + (replace-regexp-in-string + "\\[<u>\\([^<]+\\)?</u>\\](\"\\([^<]+\\)\")" + "<\\2>" + (condition-case nil + (html-to-markdown-string (dom-attr node 'message)) + (error + (replace-regexp-in-string + "<a href=\"\\(.+?\\)\" rel=\"nofollow\"><u>\\(.+?\\)</u></a>" + "<\\2>" + (dom-attr node 'message))))))) + (list (string-to-number (dom-attr node 'in)) (dom-attr node 'name) message)))) + (dom-by-tag (xml-parse-file filename) 'chattimeline)))) + +(defvar emacsconf-extract-bbb-chat-use-wall-clock-time nil + "Non-nil means use wall clock time for logs.") +(defun emacsconf-extract-chats () + (interactive) + (mapc (lambda (o) + (let* ((playback-dir (expand-file-name (plist-get o :slug) + (expand-file-name "bbb-playbacks" emacsconf-cache-dir))) + (chat + (emacsconf-extract-extract-chat + (expand-file-name + "slides_new.xml" + playback-dir))) + metadata) + (when chat + (setq metadata (xml-parse-file (expand-file-name "metadata.xml" + playback-dir))) + (let ((recording-start (/ (string-to-number (dom-text + (dom-by-tag metadata 'start_time))) + 1000))) + (with-temp-file (expand-file-name (concat (plist-get o :file-prefix) "--extract.txt") + emacsconf-cache-dir) + (insert + (mapconcat + (lambda (line) + (format "`%s` _%s_ %s \n" + (if emacsconf-extract-bbb-chat-use-wall-clock-time + (format-time-string "%H:%M:%S" + (seconds-to-time + (+ recording-start + (elt line 0)))) + (format-seconds "%h:%.2m:%.2s" + (elt line 0))) + (elt line 1) + (elt line 2))) + chat + ""))))))) + (emacsconf-prepare-for-display (emacsconf-get-talk-info)))) + +(defun emacsconf-extract-bbb-copy-files (&optional info) + (interactive) + (mapc + (lambda (o) + (let ((playback-dir (expand-file-name (plist-get o :slug) + (expand-file-name "bbb-playbacks" emacsconf-cache-dir)))) + (mapc (lambda (file) + (when (and (file-exists-p (expand-file-name file playback-dir)) + (not (file-exists-p (expand-file-name (concat (plist-get o :file-prefix) "--bbb-" file) emacsconf-cache-dir)))) + (copy-file (expand-file-name file playback-dir) + (expand-file-name (concat (plist-get o :file-prefix) "--bbb-" file) emacsconf-cache-dir) + t))) + '("webcams.webm" "metadata.xml" "deskshare.webm" "deskshare.xml" "slides_new.xml" "webcams.opus")))) + (or info (emacsconf-prepare-for-display (emacsconf-get-talk-info))))) + +(defvar emacsconf-extract-dump-dir "/ssh:orga@res.emacsconf.org#46668:~/current/live0-streams/") + +(defun emacsconf-extract-wget-bbb (o) + (when (plist-get o :bbb-playback) + (let ((meeting-id (when (string-match "meetingId=\\(.+\\)" + (plist-get o :bbb-playback)) + (match-string 1 (plist-get o :bbb-playback))))) + (concat "mkdir " (plist-get o :slug) "\n" + "cd " (plist-get o :slug) "\n" + (mapconcat + (lambda (file) + (concat + "wget https://bbb.emacsverse.org/presentation/" + meeting-id "/" file "\n")) + '("video/webcams.webm" "metadata.xml" "deskshare/deskshare.webm" "panzooms.xml" "cursor.xml" "deskshare.xml" "captions.json" "presentation_text.json" "slides_new.xml") + "") + "cd ..\n")))) + +(defun emacsconf-extract-bbb-events-xml (o) + "Copy the events.xml from the raw BBB directory copied from bbb@bbb.emacsverse.org." + (if (plist-get o :bbb-playback) + (let ((meeting-id (when (string-match "meetingId=\\(.+\\)" + (plist-get o :bbb-playback)) + (match-string 1 (plist-get o :bbb-playback))))) + (format "scp ~/current/bbb-raw/%s/events.xml orga@media.emacsconf.org:~/backstage/%s--bbb-events.xml\n" + meeting-id + (plist-get o :file-prefix))) + "")) + +(defun emacsconf-extract-bbb-voice-events (file) + "Return a list of voice events. +(:name participant :start-clock start-time :start-ms ... :stop-clock stop-time :stop-ms)." + (let ((dom (xml-parse-file file)) + start-recording + stop-recording + start-ms stop-ms + participants results) + (setq start-recording + (date-to-time + (dom-text + (dom-by-tag + (dom-elements dom 'eventname "StartRecordingEvent") + 'date)))) + (setq stop-recording + (date-to-time + (dom-text + (dom-by-tag + (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))) + ;; get the participant names and put them in an alist + (setq participants + (mapcar (lambda (o) (list + (dom-text (dom-by-tag o 'userId)) + :name (dom-text (dom-by-tag o 'name)))) + (seq-filter + (lambda (node) (string= (dom-attr node 'eventname) + "ParticipantJoinEvent")) + (dom-by-tag dom 'event)))) + ;; get the voice events + (mapc (lambda (o) + (let ((participant (assoc-default + (dom-text (dom-by-tag o 'participant)) + participants)) + (time (date-to-time (dom-text (dom-by-tag o 'date)))) + o-start o-stop) + (if (string= (dom-text (dom-by-tag o 'talking)) + "true") + ;; start talking + (plist-put participant + ;; although maybe timestampUTC will be useful somehow + :start time) + ;; clamp it to start-recording and stop-recording + (when (and (time-less-p (plist-get participant :start) + stop-recording) + (time-less-p start-recording time)) + (setq o-start + (- (max (* 1000 (time-to-seconds (plist-get participant :start))) + start-ms) + start-ms) + o-stop + (- (min (* 1000 (time-to-seconds time)) + stop-ms) + start-ms)) + (setq results + (cons (list + :name + (plist-get participant :name) + :start-ms + o-start + :stop-ms + o-stop + :start-clock + (plist-get participant :start) + :stop-clock + time + :duration-ms + (- o-stop o-start)) + results)))))) + (seq-filter + (lambda (node) (string= (dom-attr node 'eventname) + "ParticipantTalkingEvent")) + (dom-by-tag dom 'event))) + (nreverse results))) + +;; (emacsconf-extract-bbb-voice-events "~/proj/emacsconf/cache/emacsconf-2022-sqlite--using-sqlite-as-a-data-source-a-framework-and-an-example--andrew-hyatt--bbb-events.xml") +;; Okay, now that we have voice events, what can we do with them? +;; We can insert notes into the VTT for now to try to guess the speaker, when the speaker changes +;; The audio is not split up by speaker, so the transcript is also not very split up +;; Some speech-to-text systems can do speaker diarization, which also tries to identify speakers +;; huh, is the StartRecordingEvent timestamp reliable? Am I misreading it? + + +(defun emacsconf-private-qa (&optional info) + (seq-remove (lambda (o) + (or (null (emacsconf-talk-file o "--bbb-webcams.webm")) + (plist-get o :qa-public))) + (or info (emacsconf-get-talk-info)))) +;; sqlite detached localizing +(defun emacsconf-extract-review-qa (talk) + (interactive (list (emacsconf-complete-talk-info (emacsconf-private-qa)))) + (find-file (emacsconf-talk-file talk "--bbb-webcams.vtt"))) + +(defun emacsconf-extract-publish-qa (talk &optional time) + (interactive (list (emacsconf-complete-talk-info (unless current-prefix-arg (emacsconf-private-qa))) + (when current-prefix-arg + (read-string "Time: ")))) + (when (stringp talk) (setq talk (emacsconf-resolve-talk talk))) + (let ((buff (get-buffer-create "*ffmpeg*")) + (large-file-warning-threshold nil)) + (cond + ((emacsconf-talk-file talk "--bbb-deskshare.webm") + (apply 'call-process "ffmpeg" nil buff nil + (append + (list + "-y" + "-i" + (expand-file-name + (concat + (plist-get talk :file-prefix) + "--bbb-deskshare.webm") + emacsconf-cache-dir)) + (when time (list "-to" time)) + (list + "-i" + (expand-file-name + (concat + (plist-get talk :file-prefix) + "--bbb-webcams.opus") + emacsconf-cache-dir)) + (when time (list "-to" time)) + (list + "-c" + "copy" + (expand-file-name + (concat + (plist-get talk :file-prefix) + "--answers.webm") + emacsconf-cache-dir))))) + (time + (apply 'call-process "ffmpeg" nil buff nil + (append + (list + "-y" + "-i" + (expand-file-name + (concat + (plist-get talk :file-prefix) + "--bbb-webcams.webm") + emacsconf-cache-dir)) + (when time (list "-to" time)) + (list + "-c" + "copy" + (expand-file-name + (concat + (plist-get talk :file-prefix) + "--answers.webm") + emacsconf-cache-dir))))) + (t + (copy-file + (expand-file-name + (concat + (plist-get talk :file-prefix) + "--bbb-webcams.webm") + emacsconf-cache-dir) + (expand-file-name + (concat + (plist-get talk :file-prefix) + "--answers.webm") + emacsconf-cache-dir) + t))) + (call-process "ffmpeg" nil buff nil "-y" "-i" + (emacsconf-talk-file talk "--answers.webm") + "-c" "copy" + (emacsconf-talk-file talk "--answers.opus" t)) + (dolist (suffix '("opus" "webm")) + (copy-file + (expand-file-name + (concat + (plist-get talk :file-prefix) + "--answers." suffix) + emacsconf-cache-dir) + (expand-file-name + (concat + (plist-get talk :file-prefix) + "--answers." suffix) + emacsconf-backstage-dir) + t) + (copy-file + (expand-file-name + (concat + (plist-get talk :file-prefix) + "--answers." suffix) + emacsconf-backstage-dir) + (expand-file-name + (concat + (plist-get talk :file-prefix) + "--answers." suffix) + emacsconf-public-media-directory) + t)) + (save-window-excursion + (emacsconf-go-to-talk talk) + (org-entry-put + (point) + "QA_PUBLIC" "t") + (unless (string-match "Q&A posted publicly." (or (org-entry-get (point) "QA_NOTE") "")) + (org-entry-put + (point) + "QA_NOTE" + (concat "Q&A posted publicly." + (emacsconf-surround " " + (org-entry-get (point) "QA_NOTE") + "" ""))))))) + +(defun emacsconf-extract-waveform-published-webcam-video (talk) + (interactive (list (emacsconf-complete-talk-info))) + (setq talk (emacsconf-resolve-talk talk)) + (waveform-show (expand-file-name "video/webcams.webm" + (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir)))) + +(defun emacsconf-extract-chat (slug speaker) + (interactive (list + (emacsconf-complete-talk) + (completing-read "Speaker: " + (seq-uniq + (mapcar (lambda (node) (dom-attr node 'name)) + (dom-by-tag (xml-parse-region (point-min) (point-max)) 'chattimeline))) + ))) + (let ((text + (mapconcat (lambda (node) + (when (string= (dom-attr node 'target) "chat") + (let ((message + (replace-regexp-in-string + "\\(^[^ +]?\\): " "" + (replace-regexp-in-string "<a href=\"\\(.+?\\)\" rel=\"nofollow\"><u>\\(.+?\\)</u></a>" + "<\\1>" (dom-attr node 'message))))) + (if (string-match speaker (dom-attr node 'name)) + (format "- %s: %s\n" speaker message) + (format "- %s\n" message))))) + (dom-by-tag (xml-parse-region (point-min) (point-max)) 'chattimeline) + ""))) + (emacsconf-edit-wiki-page slug) + (if (re-search-forward "# Discussion" nil t) + (progn + (goto-char (match-end 0)) + (insert "\n\n")) + (goto-char (point-max))) + (kill-new text))) + + + + +;; TODO: Combine lines from same nick, or identify speakers with anon1/2/etc. +(defun emacsconf-extract-chat-from-dired () + (interactive) + (find-file (expand-file-name "slides_new.xml" (dired-get-file-for-visit))) + (call-interactively 'emacsconf-extract-chat)) + +(defvar emacsconf-extract-conference-username "emacsconf" "Name of the streaming user.") + +(provide 'emacsconf-bbb) +;;; emacsconf-bbb.el ends here diff --git a/emacsconf-erc.el b/emacsconf-erc.el index b01e3f6..bde801b 100644 --- a/emacsconf-erc.el +++ b/emacsconf-erc.el @@ -68,12 +68,12 @@ (defcustom emacsconf-erc-org "#emacsconf-org" "Channel for organizers") (defcustom emacsconf-topic-templates - '(("#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")) + '(("#emacsconf" "Welcome to EmacsConf 2025 | 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/2025/watch/gen/ | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") + ("#emacsconf-dev" "Development track | https://emacsconf.org/2025/watch/dev/ | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") + ("#emacsconf-accessible" "EmacsConf 2025 accessibility - help by describing what's happening | Subscribe to https://lists.gnu.org/mailman/listinfo/emacsconf-discuss for updates") + ("#emacsconf-org" "EmacsConf 2025 | 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 2025 | 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") @@ -229,6 +229,9 @@ If MESSAGE is not specified, reset the topic to the template." (plist-get talk :title) (emacsconf-surround " - " (plist-get talk :speakers-with-pronouns) "" ""))) (erc-send-message + (concat "Talk page: " + (plist-get talk :absolute-url))) + (erc-send-message (concat "Add your notes/questions to the pad: " (plist-get talk :pad-url))) (cond ((string-match "live" (or (plist-get talk :q-and-a) "")) @@ -547,5 +550,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..798d543 100644 --- a/emacsconf-extract.el +++ b/emacsconf-extract.el @@ -25,79 +25,8 @@ ;;; Code: -(defun emacsconf-extract-chat (filename) - (when (file-exists-p filename) - (message "%s" filename) - (mapcar - (lambda (node) - (when (string= (dom-attr node 'target) "chat") - (let ((message - (replace-regexp-in-string - "\\[<u>\\([^<]+\\)?</u>\\](\"\\([^<]+\\)\")" - "<\\2>" - (condition-case nil - (html-to-markdown-string (dom-attr node 'message)) - (error - (replace-regexp-in-string - "<a href=\"\\(.+?\\)\" rel=\"nofollow\"><u>\\(.+?\\)</u></a>" - "<\\2>" - (dom-attr node 'message))))))) - (list (string-to-number (dom-attr node 'in)) (dom-attr node 'name) message)))) - (dom-by-tag (xml-parse-file filename) 'chattimeline)))) -;; (emacsconf-extract-extract-chat (expand-file-name "bbb-playbacks/haskell/slides_new.xml" emacsconf-cache-dir)) -(defvar emacsconf-extract-bbb-chat-use-wall-clock-time nil - "Non-nil means use wall clock time for logs.") -(defun emacsconf-extract-chats () - (interactive) - (mapc (lambda (o) - (let* ((playback-dir (expand-file-name (plist-get o :slug) - (expand-file-name "bbb-playbacks" emacsconf-cache-dir))) - (chat - (emacsconf-extract-extract-chat - (expand-file-name - "slides_new.xml" - playback-dir))) - metadata) - (when chat - (setq metadata (xml-parse-file (expand-file-name "metadata.xml" - playback-dir))) - (let ((recording-start (/ (string-to-number (dom-text - (dom-by-tag metadata 'start_time))) - 1000))) - (with-temp-file (expand-file-name (concat (plist-get o :file-prefix) "--extract.txt") - emacsconf-cache-dir) - (insert - (mapconcat - (lambda (line) - (format "`%s` _%s_ %s \n" - (if emacsconf-extract-bbb-chat-use-wall-clock-time - (format-time-string "%H:%M:%S" - (seconds-to-time - (+ recording-start - (elt line 0)))) - (format-seconds "%h:%.2m:%.2s" - (elt line 0))) - (elt line 1) - (elt line 2))) - chat - ""))))))) - (emacsconf-prepare-for-display (emacsconf-get-talk-info)))) - -(defun emacsconf-extract-bbb-copy-files (&optional info) - (interactive) - (mapc - (lambda (o) - (let ((playback-dir (expand-file-name (plist-get o :slug) - (expand-file-name "bbb-playbacks" emacsconf-cache-dir)))) - (mapc (lambda (file) - (when (and (file-exists-p (expand-file-name file playback-dir)) - (not (file-exists-p (expand-file-name (concat (plist-get o :file-prefix) "--bbb-" file) emacsconf-cache-dir)))) - (copy-file (expand-file-name file playback-dir) - (expand-file-name (concat (plist-get o :file-prefix) "--bbb-" file) emacsconf-cache-dir) - t))) - '("webcams.webm" "metadata.xml" "deskshare.webm" "deskshare.xml" "slides_new.xml" "webcams.opus")))) - (or info (emacsconf-prepare-for-display (emacsconf-get-talk-info))))) +;; (emacsconf-extract-extract-chat (expand-file-name "bbb-playbacks/haskell/slides_new.xml" emacsconf-cache-dir)) (defvar emacsconf-extract-dump-dir "/ssh:orga@res.emacsconf.org#46668:~/current/live0-streams/") (defun emacsconf-extract-dump-time-from-filename (f) @@ -247,6 +176,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 +202,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,131 +242,49 @@ "Question: " (emacsconf-extract-question-headings (emacsconf-get-slug-from-string (file-name-base (buffer-file-name))))))) - (insert "NOTE " question "\n\n")) - -(defun emacsconf-extract-wget-bbb (o) - (when (plist-get o :bbb-playback) - (let ((meeting-id (when (string-match "meetingId=\\(.+\\)" - (plist-get o :bbb-playback)) - (match-string 1 (plist-get o :bbb-playback))))) - (concat "mkdir " (plist-get o :slug) "\n" - "cd " (plist-get o :slug) "\n" - (mapconcat - (lambda (file) - (concat - "wget https://bbb.emacsverse.org/presentation/" - meeting-id "/" file "\n")) - '("video/webcams.webm" "metadata.xml" "deskshare/deskshare.webm" "panzooms.xml" "cursor.xml" "deskshare.xml" "captions.json" "presentation_text.json" "slides_new.xml") - "") - "cd ..\n")))) - -(defun emacsconf-extract-bbb-events-xml (o) - "Copy the events.xml from the raw BBB directory copied from bbb@bbb.emacsverse.org." - (if (plist-get o :bbb-playback) - (let ((meeting-id (when (string-match "meetingId=\\(.+\\)" - (plist-get o :bbb-playback)) - (match-string 1 (plist-get o :bbb-playback))))) - (format "scp ~/current/bbb-raw/%s/events.xml orga@media.emacsconf.org:~/backstage/%s--bbb-events.xml\n" - meeting-id - (plist-get o :file-prefix))) - "")) - -(defun emacsconf-extract-bbb-voice-events (file) - "Return a list of voice events. -(:name participant :start-clock start-time :start-ms ... :stop-clock stop-time :stop-ms)." - (let ((dom (xml-parse-file file)) - start-recording - stop-recording - start-ms stop-ms - participants results) - (setq start-recording - (date-to-time - (dom-text - (dom-by-tag - (dom-elements dom 'eventname "StartRecordingEvent") - 'date)))) - (setq stop-recording - (date-to-time - (dom-text - (dom-by-tag - (dom-elements dom 'eventname "StopRecordingEvent") - 'date)))) - (setq start-ms (* 1000 (time-to-seconds start-recording)) - stop-ms (* 1000 (time-to-seconds stop-recording))) - ;; get the participant names and put them in an alist - (setq participants - (mapcar (lambda (o) (list - (dom-text (dom-by-tag o 'userId)) - :name (dom-text (dom-by-tag o 'name)))) - (seq-filter - (lambda (node) (string= (dom-attr node 'eventname) - "ParticipantJoinEvent")) - (dom-by-tag dom 'event)))) - ;; get the voice events - (mapc (lambda (o) - (let ((participant (assoc-default - (dom-text (dom-by-tag o 'participant)) - participants)) - (time (date-to-time (dom-text (dom-by-tag o 'date)))) - o-start o-stop) - (if (string= (dom-text (dom-by-tag o 'talking)) - "true") - ;; start talking - (plist-put participant - ;; although maybe timestampUTC will be useful somehow - :start time) - ;; clamp it to start-recording and stop-recording - (when (and (time-less-p (plist-get participant :start) - stop-recording) - (time-less-p start-recording time)) - (setq o-start - (- (max (* 1000 (time-to-seconds (plist-get participant :start))) - start-ms) - start-ms) - o-stop - (- (min (* 1000 (time-to-seconds time)) - stop-ms) - start-ms)) - (setq results - (cons (list - :name - (plist-get participant :name) - :start-ms - o-start - :stop-ms - o-stop - :start-clock - (plist-get participant :start) - :stop-clock - time - :duration-ms - (- o-stop o-start)) - results)))))) - (seq-filter - (lambda (node) (string= (dom-attr node 'eventname) - "ParticipantTalkingEvent")) - (dom-by-tag dom 'event))) - (nreverse results))) -;; (emacsconf-extract-bbb-voice-events "~/proj/emacsconf/cache/emacsconf-2022-sqlite--using-sqlite-as-a-data-source-a-framework-and-an-example--andrew-hyatt--bbb-events.xml") -;; Okay, now that we have voice events, what can we do with them? -;; We can insert notes into the VTT for now to try to guess the speaker, when the speaker changes -;; The audio is not split up by speaker, so the transcript is also not very split up -;; Some speech-to-text systems can do speaker diarization, which also tries to identify speakers -;; huh, is the StartRecordingEvent timestamp reliable? Am I misreading it? + (subed-set-subtitle-comment (concat "Q: " question))) + (defvar emacsconf-extract-irc-speaker-nick nil "*Nick for the speaker.") +(defun emacsconf-extract-pad-clean-up () + (interactive) + (goto-char (point-min)) + (while (re-search-forward "\\\\" nil t) + (replace-match "")) + (goto-char (point-min)) + (while (re-search-forward "{rel=\"noreferrer noopener\"}" nil t) + (replace-match ""))) + (defun emacsconf-extract-selected-irc () "Copy the lines that start with -." (interactive) (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 + "q" #'emacsconf-extract-irc-copy-line-to-other-window-as-question) + +(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)) @@ -455,6 +307,8 @@ (match-string 1)) "A: ") ""))) + (when (string= (match-string 2) "https") + (setq line (concat (match-string 2) line))) (setq line (concat (if (or (string= prefix "A: ") indent) " " "") @@ -480,6 +334,31 @@ (emacsconf-edit-wiki-page talk)) (require 'hydra) + +(defvar-keymap emacsconf-extract-irc-keymap + :doc "Make it easy to extract lines from IRC" + "c" #'emacsconf-extract-irc-copy-line-to-other-window-as-list-item + "q" (lambda () (interactive) (emacsconf-extract-irc-copy-line-to-other-window-as-list-item "Q: ")) + "o" #'other-window + "t" #'emacsconf-extract-irc-open-talk-in-other-window + "n" #'next-line + "p" #'previous-line + "N" #'move-line-down + "P" #'move-line-up + "<up>" #'previous-line + "<down>" #'next-line + "<right>" (lambda () (interactive) (goto-char (line-beginning-position)) (insert " ")) + "<left>" (lambda () (interactive) (goto-char (line-beginning-position)) (delete-char 2)) + "<prior>" #'scroll-down-command + "<next>" #'scroll-up-command + "a" (lambda () (interactive) (emacsconf-extract-irc-copy-line-to-other-window-as-list-item "A: ")) + "l" (lambda () (interactive) (save-window-excursion (other-window 1) (consult-line))) + "<spc>" #'pop-to-mark-command) + +(defun emacsconf-extract-irc () + (interactive) + (set-transient-map emacsconf-extract-irc-keymap t)) + (defhydra emacsconf-extract-irc () "Make it easy to extract lines from IRC" ("c" emacsconf-extract-irc-copy-line-to-other-window-as-list-item "copy") @@ -490,6 +369,8 @@ ("p" previous-line "previous") ("N" move-line-down "move down") ("P" move-line-up "move up") + ("<up>" previous-line) + ("<down>" next-line) ("<right>" (progn (goto-char (line-beginning-position)) (insert " ")) "indent") ("<left>" (progn (goto-char (line-beginning-position)) (delete-char 2)) "dedent") ("<prior>" scroll-down-command) @@ -503,151 +384,14 @@ (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)))) - -(defun emacsconf-private-qa (&optional info) - (seq-remove (lambda (o) - (or (null (emacsconf-talk-file o "--bbb-webcams.webm")) - (plist-get o :qa-public))) - (or info (emacsconf-get-talk-info)))) -;; sqlite detached localizing -(defun emacsconf-extract-review-qa (talk) - (interactive (list (emacsconf-complete-talk-info (emacsconf-private-qa)))) - (find-file (emacsconf-talk-file talk "--bbb-webcams.vtt"))) - -(defun emacsconf-extract-publish-qa (talk &optional time) - (interactive (list (emacsconf-complete-talk-info (unless current-prefix-arg (emacsconf-private-qa))) - (when current-prefix-arg - (read-string "Time: ")))) - (when (stringp talk) (setq talk (emacsconf-resolve-talk talk))) - (let ((buff (get-buffer-create "*ffmpeg*")) - (large-file-warning-threshold nil)) - (cond - ((emacsconf-talk-file talk "--bbb-deskshare.webm") - (apply 'call-process "ffmpeg" nil buff nil - (append - (list - "-y" - "-i" - (expand-file-name - (concat - (plist-get talk :file-prefix) - "--bbb-deskshare.webm") - emacsconf-cache-dir)) - (when time (list "-to" time)) - (list - "-i" - (expand-file-name - (concat - (plist-get talk :file-prefix) - "--bbb-webcams.opus") - emacsconf-cache-dir)) - (when time (list "-to" time)) - (list - "-c" - "copy" - (expand-file-name - (concat - (plist-get talk :file-prefix) - "--answers.webm") - emacsconf-cache-dir))))) - (time - (apply 'call-process "ffmpeg" nil buff nil - (append - (list - "-y" - "-i" - (expand-file-name - (concat - (plist-get talk :file-prefix) - "--bbb-webcams.webm") - emacsconf-cache-dir)) - (when time (list "-to" time)) - (list - "-c" - "copy" - (expand-file-name - (concat - (plist-get talk :file-prefix) - "--answers.webm") - emacsconf-cache-dir))))) - (t - (copy-file - (expand-file-name - (concat - (plist-get talk :file-prefix) - "--bbb-webcams.webm") - emacsconf-cache-dir) - (expand-file-name - (concat - (plist-get talk :file-prefix) - "--answers.webm") - emacsconf-cache-dir) - t))) - (call-process "ffmpeg" nil buff nil "-y" "-i" - (emacsconf-talk-file talk "--answers.webm") - "-c" "copy" - (emacsconf-talk-file talk "--answers.opus" t)) - (dolist (suffix '("opus" "webm")) - (copy-file - (expand-file-name - (concat - (plist-get talk :file-prefix) - "--answers." suffix) - emacsconf-cache-dir) - (expand-file-name - (concat - (plist-get talk :file-prefix) - "--answers." suffix) - emacsconf-backstage-dir) - t) - (copy-file - (expand-file-name - (concat - (plist-get talk :file-prefix) - "--answers." suffix) - emacsconf-backstage-dir) - (expand-file-name - (concat - (plist-get talk :file-prefix) - "--answers." suffix) - emacsconf-public-media-directory) - t)) - (save-window-excursion - (emacsconf-go-to-talk talk) - (org-entry-put - (point) - "QA_PUBLIC" "t") - (unless (string-match "Q&A posted publicly." (or (org-entry-get (point) "QA_NOTE") "")) - (org-entry-put - (point) - "QA_NOTE" - (concat "Q&A posted publicly." - (emacsconf-surround " " - (org-entry-get (point) "QA_NOTE") - "" ""))))))) + (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))))) + + ;; (emacsconf-extract-publish-qa "workflows" "13:56.000") (emacsconf-extract-publish-qa "journalism") (emacsconf-extract-publish-qa "handwritten" "28:36.240") ;; (kill-new (mapconcat #'emacsconf-extract-bbb-events-xml (emacsconf-get-talk-info) "")) ;; (dolist (slug '("haskell" "hyperorg" "health" "jupyter" "workflows" "wayland" "mail" "meetups" "orgsuperlinks" "rde" "science")) @@ -676,396 +420,12 @@ Would you like to help? See [[help_with_chapter_markers]] for more details. You ;; (mapc #'emacsconf-extract-add-help-index-qa (emacsconf-prepare-for-display (emacsconf-get-talk-info))) ;; (emacsconf-extract-download-published-recordings "bbb:/var/bigbluebutton/published/presentation/" "" ~/proj/emacsconf/2023/bbb-published/"") -(defvar emacsconf-extract-bbb-raw-dir "~/proj/emacsconf/2023/bbb/" "End with a \"/\".") -(defvar emacsconf-extract-bbb-published-dir "~/proj/emacsconf/2023/bbb-published/" "End with a \"/\".") -(defvar emacsconf-extract-conference-username "emacsconf" "Name of the streaming user.") -(defvar emacsconf-extract-bbb-path "/ssh:bbb@bbb.emacsverse.org:/var/bigbluebutton/") -(defun emacsconf-extract-raw-recordings-download-command () - "Copy the command for downloading raw recordings." - (interactive) - (let ((s (mapconcat (lambda (o) (if (plist-get o :bbb-meeting-id) - (format "rsync -avzue ssh %s %s\n" - (expand-file-name (plist-get o :bbb-meeting-id) emacsconf-extract-bbb-path) - emacsconf-extract-bbb-raw-dir) - "")) - (emacsconf-get-talk-info)))) - (when (called-interactively-p 'any) (kill-new s)) - s)) - -(defun emacsconf-extract-download-published-recordings-command () - "Copy the command for downloading published recordings from SOURCE to DEST." - (interactive) - (kill-new - (mapconcat (lambda (o) (if (plist-get o :bbb-meeting-id) - (replace-regexp-in-string - "/ssh:" "" - (format "rsync -avzue ssh %s %s # %s\n" - (expand-file-name - (plist-get o :bbb-meeting-id) - (expand-file-name "published/presentation" - emacsconf-extract-bbb-path - )) - emacsconf-extract-bbb-published-dir - (plist-get o :slug))) - "")) - (emacsconf-get-talk-info)))) - -(defun emacsconf-extract-bbb-parse-events-dir (&optional dir) - (mapcar (lambda (file) - (emacsconf-extract-bbb-parse-events file)) - (directory-files-recursively (or dir emacsconf-extract-bbb-raw-dir) "events.xml"))) - -(defun emacsconf-extract-bbb-raw-events-file-name (talk) - (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 () - (let* ((max 0) - (participant-count 0) - (meeting-count 0) - (max-meetings 0) - (max-participants 0) - meeting-participants - (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") - (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)) - (lambda (a b) (< (car a) (car b)))))) - (dolist (event meeting-events) - (pcase (cdr event) - ("CreatePresentationPodEvent" (cl-incf meeting-count) (when (> meeting-count max-meetings) (setq max-meetings meeting-count))) - ("ParticipantJoinEvent" (cl-incf participant-count) (when (> participant-count max-participants) (setq max-participants participant-count))) - ("ParticipantLeftEvent" (cl-decf participant-count)) - ("EndAndKickAllEvent" (cl-decf meeting-count)))) - `((,(length meeting-participants) "Number of meetings analyzed") - (,max-participants "Max number of simultaneous users") - (,max-meetings "Max number of simultaneous meetings") - (,(apply 'max (mapcar (lambda (o) (length (plist-get o :participants))) meeting-participants)) "Max number of people in one meeting") - (,(length (seq-uniq (seq-mapcat (lambda (o) (mapcar #'cdr (plist-get o :participants))) meeting-participants))) "Total unique users") - (,(length (seq-uniq (seq-mapcat (lambda (o) (plist-get o :talking)) meeting-participants))) "Total unique talking")))) - -(defun emacsconf-extract-bbb-dired-raw (talk) - (interactive (list (emacsconf-complete-talk-info))) - (setq talk (emacsconf-resolve-talk talk)) - (dired (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-raw-dir))) -(defun emacsconf-extract-bbb-dired-published (talk) - (interactive (list (emacsconf-complete-talk-info))) - (setq talk (emacsconf-resolve-talk talk)) - (dired (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir))) -(defun emacsconf-extract-waveform-published-webcam-video (talk) - (interactive (list (emacsconf-complete-talk-info))) - (setq talk (emacsconf-resolve-talk talk)) - (waveform-show (expand-file-name "video/webcams.webm" - (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)) - (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))))) - (conf-joined-time - (and conf-joined - (string-to-number (dom-text (dom-by-tag (dom-parent dom conf-joined) 'timestampUTC))))) - recording-start - recording-stop - recording-spans - stream-start - chat - talking - participants - talking-starts - deskshare-info - (meeting-date (dom-text (dom-by-tag (dom-parent dom (car conf-joined)) 'date)))) - (dolist (event (dom-by-tag dom 'event)) - (let ((timestamp (string-to-number (dom-text (dom-by-tag event 'timestampUTC))))) - (pcase (dom-attr event 'eventname) - ("ParticipantJoinEvent" - (push (cons (dom-text (dom-by-tag event 'userId)) - (dom-text (dom-by-tag event 'name))) - participants)) - ("StartRecordingEvent" - (setq recording-start timestamp - recording-stop nil - recording-file - (file-name-nondirectory (dom-text (dom-by-tag event 'filename))))) - ("StopRecordingEvent" - (setq recording-stop timestamp) - (push (cons recording-start recording-stop) recording-spans)) - ("PublicChatEvent" - ;; only include events in the public recording - (when (and recording-start - (null recording-stop) - (>= timestamp recording-start)) - (push (list - timestamp - (dom-text (dom-by-tag event 'sender)) - (with-temp-buffer - (insert - (replace-regexp-in-string - "'" "'" - (replace-regexp-in-string "<.*?>" "" - (dom-text (dom-by-tag event 'message))))) - (mm-url-decode-entities) - (buffer-string))) - chat))) - ("ParticipantTalkingEvent" - (let* ((speaker (assoc-default - (dom-text (dom-by-tag event 'participant)) - participants))) - (if (string= (dom-text (dom-by-tag event 'talking)) "true") - ;; started talking - (if (assoc-default speaker talking-starts) - (setcdr (assoc speaker talking-starts) - timestamp) - (push (cons speaker timestamp) talking-starts)) - (when (and recording-start - (>= timestamp recording-start) - (assoc-default speaker talking-starts) - (or (null recording-stop) - (<= (assoc-default speaker talking-starts) - recording-stop))) - (push (list speaker - (- (max (assoc-default speaker talking-starts) recording-start) - recording-start) - (- (if recording-stop (min recording-stop timestamp) timestamp) - recording-start) - recording-file) - talking))))) - ("StartWebRTCDesktopShareEvent" - (setq deskshare-info (cons (dom-text (dom-by-tag event 'filename)) - timestamp))) - - ))) - `((name . ,meeting-name) - (id . ,meeting-id) - (conf-joined . ,conf-joined-time) - (recording-start . ,recording-start) - (meeting-date . ,meeting-date) - (participants . ,participants) - (talking . ,(nreverse talking)) - (chat . ,(nreverse chat))))) - -(defun emacsconf-extract-bbb-format-chat () - (mapconcat - (lambda (events) - (format "- %s (%s)\n%s" - (assoc-default 'name events) - (assoc-default 'id events) - (mapconcat - (lambda (message) - (format " - %s: %s\n" - (elt message 1) - (with-temp-buffer - (insert - (replace-regexp-in-string - "'" "'" - (replace-regexp-in-string "<.*?>" "" - (elt message 2)))) - (mm-url-decode-entities) - (buffer-string)))) - (assoc-default 'chat events) - ""))) - (emacsconf-extract-bbb-parse-events-dir) - "")) - -(defun emacsconf-extract-spookfox-update-bbb-rec () - (interactive) - (let* ((data - (spookfox-js-injection-eval-in-active-tab - "row = [...document.querySelectorAll('.dropdown-toggle')].find((o) => o.textContent.match('Unlisted')).closest('tr'); [row.querySelector('#recording-text').getAttribute('title'), row.querySelector('a.btn-primary').getAttribute('href')]" t) - ) - (slug - (when (and data (string-match "^\\([^(]+\\) (" (elt data 0))) - (split-string - (match-string 1 (elt data 0)) - ", ")))) - (when data - (if (> (length slug) 1) - (setq slug (completing-read "Talk: " slug)) - (setq slug (car slug))) - (emacsconf-with-talk-heading slug - (if (org-entry-get (point) "BBB_REC") - (progn - (kill-new (elt data 1)) - (error "%s already has BBB_REC?" slug)) - (org-entry-put (point) "BBB_REC" (elt data 1)))) - (message "Updated BBB_REC for %s to %s" slug (elt data 1)) - (spookfox-js-injection-eval-in-active-tab - "row = [...document.querySelectorAll('.dropdown-toggle')].find((o) => o.textContent.match('Unlisted')).closest('tr'); row.querySelector('.button_to .dropdown-item .fa-globe').closest('button').click();" t)))) - - -(defun emacsconf-extract-chat (slug speaker) - (interactive (list - (emacsconf-complete-talk) - (completing-read "Speaker: " - (seq-uniq - (mapcar (lambda (node) (dom-attr node 'name)) - (dom-by-tag (xml-parse-region (point-min) (point-max)) 'chattimeline))) - ))) - (let ((text - (mapconcat (lambda (node) - (when (string= (dom-attr node 'target) "chat") - (let ((message - (replace-regexp-in-string - "\\(^[^ +]?\\): " "" - (replace-regexp-in-string "<a href=\"\\(.+?\\)\" rel=\"nofollow\"><u>\\(.+?\\)</u></a>" - "<\\1>" (dom-attr node 'message))))) - (if (string-match speaker (dom-attr node 'name)) - (format "- %s: %s\n" speaker message) - (format "- %s\n" message))))) - (dom-by-tag (xml-parse-region (point-min) (point-max)) 'chattimeline) - ""))) - (emacsconf-edit-wiki-page slug) - (if (re-search-forward "# Discussion" nil t) - (progn - (goto-char (match-end 0)) - (insert "\n\n")) - (goto-char (point-max))) - (kill-new text))) -;; TODO: Combine lines from same nick, or identify speakers with anon1/2/etc. -(defun emacsconf-extract-chat-from-dired () - (interactive) - (find-file (expand-file-name "slides_new.xml" (dired-get-file-for-visit))) - (call-interactively 'emacsconf-extract-chat)) - -(defun emacsconf-make-webcams-deskshare-spans (talk &optional start-ms stop-ms strategy) - (let* ((start-ms (or start-ms 0)) - (source-dir (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir)) - (secs (/ start-ms 1000.0)) - (deskshare (xml-parse-file (expand-file-name "deskshare.xml" source-dir))) - (webcam-video (expand-file-name "video/webcams.webm" source-dir)) - (deskshare-video (expand-file-name "deskshare/deskshare.webm" source-dir)) - (stop-ms (or stop-ms (emacsconf-get-file-duration-ms deskshare-video))) - spans) - (mapc (lambda (o) - (unless (or (= secs (string-to-number (dom-attr o 'start_timestamp))) - (= (string-to-number (dom-attr o 'start_timestamp)) 0) - (> secs (/ stop-ms 1000.0))) - (setq spans (cons (list :source webcam-video - :start-ms (* secs 1000) - :stop-ms - (* 1000 - (if (eq strategy 'test) - (+ secs 3) - (max secs (string-to-number (dom-attr o 'start_timestamp)))))) - spans))) - (when (and (<= (string-to-number (dom-attr o 'start_timestamp)) - (/ stop-ms 1000.0)) - (>= (string-to-number (dom-attr o 'stop_timestamp)) - (/ start-ms 1000.0))) - (setq spans (cons (list :source deskshare-video - :start-ms (max (* 1000 (string-to-number (dom-attr o 'start_timestamp))) - start-ms) - :stop-ms - (if (eq strategy 'test) - (* 1000 (+ (string-to-number (dom-attr o 'start_timestamp)) 3)) - (min (* 1000 (string-to-number (dom-attr o 'stop_timestamp))) - stop-ms))) - spans)) - (setq secs (string-to-number (dom-attr o 'stop_timestamp))))) - (dom-by-tag deskshare 'event)) - (unless (>= (floor (* secs 1000)) stop-ms) - (setq spans (cons (list :source webcam-video - :start-ms (* 1000 secs) - :stop-ms (if (eq strategy 'test) - (* 1000 (+ secs 3)) - stop-ms)) - spans))) - (if (eq strategy 'test) - `((video ,@(nreverse spans)) - (audio ,@(mapcar (lambda (o) - (list :source webcam-video - :start-ms (plist-get o :start-ms) - :stop-ms (plist-get o :stop-ms))) - (reverse spans)))) - `((video ,@(nreverse spans)) - (audio (:source ,webcam-video :start-ms ,start-ms :stop-ms ,stop-ms)))))) - -(defun emacsconf-get-ffmpeg-to-splice-webcam-and-recording (talk &optional start-ms stop-ms info strategy) - "Return FFMPEG command for slicing. -Strategies: -- 'fast-cut-start-keyframe - find the keyframe before the start ms and cut from there, doing a fast copy. -- 'start-keyframe-and-reencode - find the keyframe before the start ms and cut from there, reencoding. -- 'cut-and-concat - seek to the keyframe before, slowly find the start-ms, reencode the snippet, and then do a fast copy of the remaining. May have encoding errors. -- default: copy from start-ms to stop-ms, reencoding. -" - (interactive (list (emacsconf-complete-talk-info))) - (setq talk (emacsconf-resolve-talk talk)) - (let* ((slug (plist-get talk :slug)) - (start-ms (or start-ms 0)) - (source-dir (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir)) - (video-slug (plist-get (seq-find (lambda (o) (string= (plist-get o :slug) slug)) info) :video-slug)) - (output (expand-file-name (concat (plist-get talk :file-prefix) "--answers.webm") emacsconf-cache-dir)) - (webcam-video (expand-file-name "video/webcams.webm" source-dir)) - (deskshare-video (expand-file-name "deskshare/deskshare.webm" source-dir)) - (webcam-duration (emacsconf-get-file-duration-ms webcam-video)) - (stop-ms (or stop-ms webcam-duration)) - (command - (if (file-exists-p deskshare-video) - ;; Has deskshare - (let* ((deskshare (xml-parse-file (expand-file-name "deskshare.xml" source-dir))) - (final-size (compile-media-max-dimensions - deskshare-video - webcam-video)) - (duration (compile-media-get-file-duration-ms webcam-video)) - (spans (emacsconf-make-webcams-deskshare-spans talk start-ms stop-ms strategy)) - (compile-media-output-video-width (car final-size)) - (compile-media-output-video-height (cdr final-size))) - (compile-media-get-command spans output)) - ;; Just webcams - (if (and (= start-ms 0) - (= stop-ms webcam-duration)) - (format "cp %s %s" - webcam-video - output) - (compile-media-get-command - (compile-media-split-tracks - (list (list :source webcam-video :start-ms start-ms :stop-ms stop-ms))) - output))))) - (when (called-interactively-p 'any) - (kill-new command)) - command)) + + + ;; (kill-new ;; (emacsconf-extract-replace-strings @@ -1127,6 +487,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 +518,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 +544,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 +670,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 +733,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 +892,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)) @@ -1503,90 +978,6 @@ If QA is non-nil, treat it as a Q&A video." ;;; PeerTube -(defvar emacsconf-extract-toobnix-api-client nil) -(defvar emacsconf-extract-toobnix-api-bearer-token nil) -(defvar emacsconf-extract-toobnix-api-username "bandali") -(defvar emacsconf-extract-toobnix-api-channel-handle "emacsconf") - -(defun emacsconf-extract-toobnix-api-header () - `(("Authorization" . ,(concat "Bearer " - (if (stringp emacsconf-extract-toobnix-api-bearer-token) - emacsconf-extract-toobnix-api-bearer-token - (assoc-default 'access_token emacsconf-extract-toobnix-api-bearer-token)))))) - -(defun emacsconf-extract-toobnix-api-setup () - (interactive) - (require 'plz) - (require 'url-http-oauth) - (setq emacsconf-extract-toobnix-api-client - (plz 'get "https://toobnix.org/api/v1/oauth-clients/local" :as #'json-read)) - (setq emacsconf-extract-toobnix-api-bearer-token - (assoc-default - 'access_token - (plz 'post "https://toobnix.org/api/v1/users/token" - :body (mm-url-encode-www-form-urlencoded - `(("client_id" . ,(assoc-default 'client_id emacsconf-extract-toobnix-api-client)) - ("client_secret" . ,(assoc-default 'client_secret emacsconf-extract-toobnix-api-client)) - ("grant_type" . "password") - ("username" . ,emacsconf-extract-toobnix-api-username) - ("password" . ,(auth-info-password (car (auth-source-search :host "https://toobnix.org")))))) - :as #'json-read) - ) -) - (setq emacsconf-extract-toobnix-api-channels - (plz 'get (format "https://toobnix.org/api/v1/accounts/%s/video-channels" - emacsconf-extract-toobnix-api-username) - :headers - :as #'json-read)) - (setq emacsconf-extract-toobnix-api-videos - (plz 'get - (format "https://toobnix.org/api/v1/accounts/%s/videos?count=100&sort=-createdAt" - emacsconf-extract-toobnix-api-username) - :headers (emacsconf-extract-toobnix-api-header) - :as #'json-read)) - (setq emacsconf-extract-toobnix-api-playlists - (append - (assoc-default 'data - (plz 'get - (format "https://toobnix.org/api/v1/video-channels/%s/video-playlists?sort=-createdAt" - emacsconf-extract-toobnix-api-channel-handle) - :headers (emacsconf-extract-toobnix-api-header) - :as #'json-read)) - nil))) - -(defun emacsconf-extract-toobnix-publish-video-from-edit-page () - "Messy hack to set a video to public and store the URL." - (interactive) - (spookfox-js-injection-eval-in-active-tab "document.querySelector('label[for=privacy]').scrollIntoView(); document.querySelector('label[for=privacy]').closest('.form-group').querySelector('input').dispatchEvent(new Event('input'));" t) - (sit-for 1) - (spookfox-js-injection-eval-in-active-tab "document.querySelector('span[title=\"Anyone can see this video\"]').click()" t) - (sit-for 1) - (spookfox-js-injection-eval-in-active-tab "document.querySelector('button.orange-button').click()" t)(sit-for 3) - (emacsconf-extract-store-url) - (shell-command "xdotool key Alt+Tab sleep 1 key Ctrl+w Alt+Tab")) - -(defun emacsconf-extract-toobnix-set-up-playlist () - (interactive) - (mapcar - (lambda (o) - (when (plist-get o :toobnix-url) - (browse-url (plist-get o :toobnix-url)) - (read-key "press a key when page is loaded") - (spookfox-js-injection-eval-in-active-tab "document.querySelector('.action-button-save').click()" t) - (spookfox-js-injection-eval-in-active-tab "document.querySelector('my-peertube-checkbox').click()" t) - (read-key "press a key when saved to playlist")) - (when (plist-get o :qa-toobnix-url) - (browse-url (plist-get o :qa-toobnix-url)) - (read-key "press a key when page is loaded") - (spookfox-js-injection-eval-in-active-tab "document.querySelector('.action-button-save').click()" t) - (spookfox-js-injection-eval-in-active-tab "document.querySelector('my-peertube-checkbox').click()" t) - (read-key "press a key when saved to playlist"))) - (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) - -(defun emacsconf-extract-toobnix-view-qa (talk) - (interactive (list (emacsconf-complete-talk-info))) - (browse-url (plist-get (emacsconf-resolve-talk talk) :qa-toobnix-url))) - (defun emacsconf-extract-youtube-spookfox-add-playlist-numbers () "Number the playlist for easier checking. Related: `emacsconf-extract-check-playlists'." diff --git a/emacsconf-hyperlist.el b/emacsconf-hyperlist.el index df1ed02..bbc3f41 100644 --- a/emacsconf-hyperlist.el +++ b/emacsconf-hyperlist.el @@ -138,7 +138,7 @@ (defun emacsconf-hyperlist-day-events (day &optional track info) (let* ((talks - (emacsconf-prepare-for-display + (emacsconf-publish-prepare-for-display (emacsconf-filter-talks-by-time (concat day "T00:00:00" emacsconf-timezone-offset) (concat day "T23:59:59" emacsconf-timezone-offset) @@ -163,7 +163,7 @@ (time-less-p (car a) (car b)))))) (defun emacsconf-hyperlist-format-streamer-day (day &optional track info) - (setq info (emacsconf-prepare-for-display + (setq info (emacsconf-publish-prepare-for-display (if info (mapcar #'emacsconf-resolve-talk info) (emacsconf-get-talk-info)))) (when track (setq info (emacsconf-filter-talks-by-track track info))) diff --git a/emacsconf-mail.el b/emacsconf-mail.el index 23743e8..40536de 100644 --- a/emacsconf-mail.el +++ b/emacsconf-mail.el @@ -20,7 +20,18 @@ ;;; Commentary: -;; +;; The typical communication flow for speakers is: + +;; - emacsconf-mail-review +;; - emacsconf-mail-accept-talk +;; - emacsconf-mail-upload-and-backstage-info +;; - emacsconf-mail-acknowledge-upload +;; - emacsconf-mail-captions-for-review +;; - emacsconf-mail-draft-schedule +;; - emacsconf-mail-intro-to-all +;; - emacsconf-mail-checkin-instructions-to-all +;; - emacsconf-mail-template-speakers-thanks-after-conference + ;;; Code: @@ -253,7 +264,9 @@ insert into the current buffer instead of drafting e-mails." (add-hook 'message-sent-hook `(lambda () (save-window-excursion - (emacsconf-add-to-talk-logbook ,(plist-get o :slug) ,message))) + (emacsconf-add-to-talk-logbook ,(plist-get o :slug) ,message)) + (when (match-buffers "*unsent") + (switch-to-buffer (car (match-buffers "*unsent"))))) nil t)) (defun emacsconf-mail-group-by-email (&optional info) @@ -361,8 +374,8 @@ 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))) + (while (and (listp body) (plist-get (car body) :content)) + (setq body (plist-get (car body) :content))) (let* ((data (list :body body)) (fields '((:title "^[* ]*Talk title") (:description "^[* ]*Talk description") @@ -581,6 +594,22 @@ Include some other things, too, such as emacsconf-year, title, name, email, url, "Volunteers: " (emacsconf-volunteer-emails-for-completion)))) (compose-mail (string-join volunteers ", "))) +(defun emacsconf-mail-core () + (interactive) + (let ((people + (seq-remove + (lambda (o) + (string= user-mail-address (assoc-default "EMAIL" o 'string=))) + (emacsconf-get-volunteer-info "core")))) + (compose-mail + (mapconcat (lambda (o) (assoc-default "EMAIL" o 'string=)) people ", ")) + (message-goto-body) + (insert + "Hello, " + (string-join (sort (mapcar (lambda (o) (assoc-default "NAME_SHORT" o 'string=)) people)) ", ") + "!\n\n") + (message-goto-subject))) + (defun emacsconf-mail-notmuch-search-for-volunteer (volunteer) (interactive (list @@ -622,7 +651,7 @@ This includes NAME_SHORT and EMAIL_NOTES." :body " Hi, ${speakers-short}! -Thanks for submitting your proposal! (ZZZ TODO: feedback) +Thanks for submitting your proposal! We'll wait a week (~ ${notification-date}) in case the other volunteers want to chime in regarding your talk. =) @@ -782,7 +811,7 @@ ${signature}" (defvar emacsconf-mail-bcc-email "*Extra e-mail address to Bcc for delivery confirmation.") -(defun emacsconf-mail-format-talk-schedule (o) +(defun emacsconf-mail-format-talk-schedule (o &optional old-schedule) "Format the schedule for O for inclusion in mail messages etc." (interactive (list (emacsconf-complete-talk))) (when (stringp o) @@ -797,6 +826,8 @@ ${signature}" (plist-get o :start-time) (plist-get o :timezone) "%b %-e %a %-I:%M %#p %Z")))) + (when old-schedule + (setq result (format "%s\n(Previous: %s)" result old-schedule))) (when (called-interactively-p 'any) (insert result)) result)) @@ -829,9 +860,9 @@ comments or requests. 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} -will be highlighted with a black border in the schedule, and your -talk ID${plural} (${slugs}) will be highlighted with a yellow -background in the notes that follow.${wrap} +will be highlighted with a thicker black border in the schedule. If I've mentioned your +talk ID${plural} (${slugs}) in the notes that follow, they'll be highlighted with a yellow +background.${wrap} As of the time I write this e-mail, your tentative schedule is: @@ -872,8 +903,9 @@ ${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} -will have a black border in the schedule and a yellow background -in the notes that follow. +will be highlighted with a thicker black border in the schedule. If I've mentioned your +talk ID${plural} (${slugs}) in the notes that follow, they'll be highlighted with a yellow +background.${wrap} 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, @@ -938,9 +970,8 @@ ${file-list} 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 +volunteer will work 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 one.${fill} @@ -1012,11 +1043,19 @@ ${signature}") (and (emacsconf-talk-file talk "--main.vtt") (file-exists-p (emacsconf-talk-file talk "--main.vtt")))) (emacsconf-get-talk-info))))) + (unless (plist-get talk :captioner) + (message "No captioner set for talk.") + (save-excursion + (emacsconf-with-talk-heading talk + (org-set-property "CAPTIONER" (assoc-default "CUSTOM_ID" (emacsconf-complete-volunteer) + #'string=))) + (setq talk (emacsconf-search-talk-info (plist-get talk :slug))))) (let ((captions (expand-file-name (concat (plist-get talk :file-prefix) "--main.vtt") emacsconf-cache-dir)) (captioner-info (with-current-buffer (find-file-noselect emacsconf-org-file) (org-entry-properties (org-find-property "CUSTOM_ID" (plist-get talk :captioner)))))) + (emacsconf-mail-prepare (list :subject "${conf-name} ${year}: Captions for ${title}" @@ -1074,6 +1113,60 @@ ${captions} :captions (mapconcat (lambda (sub) (concat (emacsconf-surround "\nNOTE " (elt sub 4) "\n\n" "") (elt sub 3))) (subed-parse-file captions) "\n"))) (mml-attach-file captions "text/vtt" "Subtitles" "attachment"))) +(defun emacsconf-mail-answers-for-review (talk) + "E-mail Q&A session for TALK so that the speakers can review them." + (interactive (list (emacsconf-complete-talk-info + (seq-filter + (lambda (talk) + (and (emacsconf-talk-file talk "--answers.vtt") + (file-exists-p (emacsconf-talk-file talk "--answers.vtt")) + (not (plist-get talk :qa-public)))) + (emacsconf-get-talk-info))))) + (let ((captions (expand-file-name (concat (plist-get talk :file-prefix) "--answers.vtt") + emacsconf-cache-dir)) + (captioner-info + (with-current-buffer (find-file-noselect emacsconf-org-file) + (org-entry-properties (org-find-property "CUSTOM_ID" (plist-get talk :captioner)))))) + (emacsconf-mail-prepare + (list + :subject "${conf-name} ${year}: Q&A for ${title}" + :to "${email}" + :log-note "sent q&a for review" + :body "${email-notes}Hi ${speakers-short}! + +Thank you for speaking at ${conf-name} ${year}! We're working on getting +the Q&A recordings out the door. We noticed you had a long Q&A session +that continued off-stream. Was there anything that would need to be +removed before we can publish the recording? You can review it at ${url} +(video is in --answers.webm, captions are in --answers.vtt). I've also +attached the automatic captions for easy skimming. In the interests of +getting stuff out the door quickly, we haven't edited the Q&A captions +much; it's mostly there so you can remember the conversation and let us +know if we need to trim anything.${wrap} + +${signature} + +${captions} +") + (plist-get talk :email) + (list + :email-notes (emacsconf-surround "ZZZ: " (plist-get talk :email-notes) "\n\n" "") + :conf-name emacsconf-name + :speakers-short (plist-get talk :speakers-short) + :year emacsconf-year + :email (plist-get talk :email) + :title (plist-get talk :title) + :signature user-full-name + :url + (format "https://%s:%s@media.emacsconf.org/%s/backstage/#%s" + emacsconf-backstage-user + emacsconf-backstage-password + emacsconf-year + (plist-get talk :slug)) + :password emacsconf-backstage-password + :captions (mapconcat (lambda (sub) (concat (emacsconf-surround "\nNOTE " (elt sub 4) "\n\n" "") (elt sub 3))) (subed-parse-file captions) "\n"))) + (mml-attach-file captions "text/vtt" "Subtitles" "attachment"))) + (defun emacsconf-mail-upload-and-backstage-info (group) "E-mail upload and backstage access information to GROUP." (interactive (list (emacsconf-mail-complete-email-group))) @@ -1082,15 +1175,17 @@ ${captions} :subject "${conf-name} ${year}: Upload instructions, backstage info" :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" - :log-note "sent backstage and upload information" + :log-note "emacsconf-mail-upload-and-backstage-info" :body "${email-notes}Hi ${name}! -Hope things are going well for you! I got the upload service up -and running so you can upload your video${plural} and other talk -resources (ex: speaker notes, Org files, slides, etc.). You can -access it at ${upload-url} with the password -\"${upload-password}\". Please let me know if you run into technical issues.${fill} +Hope things are going well for you! I got the upload service up and +running so you can upload your video${plural} and other talk +resources (ex: speaker notes, Org files, slides, etc.). You can access +it at ${upload-url} with the password \"${upload-password}\". After you +upload your file(s), please e-mail me so that I can grab it and start +the conversion/captioning process. Please let me know if you run into +technical issues.${fill} If you can get your file(s) uploaded by ${video-target-date}, that would give us plenty of time to reencode it, edit captions, @@ -1124,7 +1219,9 @@ ${signature}") :backstage-user emacsconf-backstage-user :backstage-password emacsconf-backstage-password :upload-url - (concat "https://ftp-upload.emacsconf.org/?sid=" + (concat "https://upload.emacsconf.org/?sid=" + emacsconf-year + "-" emacsconf-upload-password "-" (mapconcat (lambda (o) (plist-get o :slug)) (cdr group) "-")) @@ -1226,6 +1323,81 @@ people's talks too.")) (or (assoc-default "NAME" volunteer) (assoc-default "NAME_SHORT" volunteer))))))) +(defun emacsconf-mail-last-minute-activation () + "E-mail backstage info to captioning volunteers." + (interactive) + (dolist (volunteer (emacsconf-get-volunteer-info "lastmin")) + (emacsconf-mail-prepare + (list + :subject "${conf-name} ${conf-year}: Finishing touches! =)" + :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" + :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" + :body + "Hello, ${name-short}! + +Home stretch! ${conf-name} ${conf-year} is coming up this weekend. +It looks like we're in pretty good shape, and I'm excited about how things will go. + +If you happen to find yourself with some extra time, we'd love it if you +could check out the backstage area for whichever talks you're interested +in. Here's the URL: + + ${backstage-url} + +You could watch a talk or two (or more!), see if there are any +video/audio/caption glitches, add a couple of questions or notes to the +Etherpad to get the ball rolling... Whatever you think might help make +${conf-name} smoother and more fun! + +No worries if you're busy. =) + +Thanks for being part of ${conf-name} ${conf-year}! + +${user-signature}") + (assoc-default "EMAIL" volunteer) + (list + :backstage-url (emacsconf-backstage-url) + :conf-name emacsconf-name + :conf-year emacsconf-year + :email (assoc-default "EMAIL" volunteer) + :user-email user-mail-address + :user-signature user-full-name + :name-short (or (assoc-default "NAME" volunteer) + (assoc-default "NAME_SHORT" volunteer)))))) + +(defun emacsconf-mail-template-volunteers-thanks-after-conference () + (interactive) + (let ((groups + (with-current-buffer (find-file-noselect emacsconf-org-file) + (org-map-entries (lambda () + (list :name (or (org-entry-get (point) "NAME_SHORT") + (org-entry-get (point) "NAME")) + :email (org-entry-get (point) "EMAIL") + :thanks (org-entry-get (point) "THANKS") + :tags (string-join (org-get-tags) ", "))) + "THANKS={.}")))) + (dolist (volunteer groups) + (emacsconf-mail-prepare + (list + :subject "${conf-name} ${conf-year}: Thank you for volunteering! =)" + :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" + :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" + :body + "Hello, ${name}! + +${thanks} Hope you can join us again next year! + +Best regards, + +${user-signature}") + (plist-get volunteer :email) + (append volunteer + (list + :conf-name emacsconf-name + :conf-year emacsconf-year + :user-email user-mail-address + :user-signature user-full-name)))))) + (defun emacsconf-mail-backstage-info-to-speakers-and-captioners () (interactive) (let ((template (emacsconf-mail-merge-get-template "backstage")) @@ -1275,7 +1447,9 @@ quality of the reencoded video.")) (talk-groups (seq-group-by (lambda (talk) (cond - ((string= (plist-get talk :status) "WAITING_FOR_PREREC") + ((plist-get talk :live) 'live-talk) + ((member (plist-get talk :status) + (list "WAITING_FOR_PREREC" "TO_CONFIRM")) 'waiting-for-prerec) ((string= (plist-get talk :qa-type) "live") 'live) @@ -1297,16 +1471,48 @@ quality of the reencoded video.")) (emacsconf-mail-prepare (append base - (pcase type - ('waiting-for-prerec - (list - :subject "${conf-name} ${year}: options, please check intro" - :body - "${email-notes}Hi, ${name}! + (pcase type + ('live-talk + (list + :subject "${conf-name} ${year}: confirming with you, also please check intro pronunciation" + :body + "${email-notes}Hi, ${name}! + +How are you feeling about ${conf-name}? It's coming up soon - just two weeks away. +I wanted to check in with all the speakers in case things have come up. +I have you penciled in for a live talk. In case life +has become unexpectedly busy for you and you're not sure if +you want to do it, let me know and I can adjust the schedule. +No stress. =) ${fill} + +Since you're planning to do a live talk, I'll set up a BigBlueButton web +conference room. I plan to bring the web conference server up on Friday +night before the conference, and I'll e-mail you the check-in +information then so that you can test things if you like. If you want to +test things beforehand, let me know what time and date might work for +you and I can set that up.${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} , +and you can also find it under \"--intro.webm\" in your talk's section +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. -${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. +Best regards, + +${signature}")) + ('waiting-for-prerec + (list + :subject "${conf-name} ${year}: gently checking in with you, also please check intro" + :body + "${email-notes}Hi, ${name}! + +How are you feeling about ${conf-name}? It's coming up soon - just two weeks away. +I wanted to check in with all the speakers in case things have come up. +In case life has become unexpectedly busy for you and you're not sure if +you want to go through with your presentation, let me know and I can adjust the schedule. +No stress. =) ${fill} Option A: You can upload your presentation before the conference @@ -1325,17 +1531,20 @@ 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. On Thursday or Friday, I'll send you the BigBlueButton -information so you can check in and try things out.${fill} +BigBlueButton. I plan to bring the web conference server up on Friday +night before the conference, and I'll e-mail you the check-in +information then so that you can test things if you like. If you want to +test things beforehand, let me know what time and date might work for +you and I can set that up.${fill} Option C: It's okay, you can cancel -Sometimes an interesting talk idea turns out to be more challenging -than you'd like. Sometimes life happens. If you're stressing out and -you don't think you can make it, no worries, no need to feel -embarrassed or guilty or anything like that. Let me know and I can -update the schedule so that other speakers have extra time for Q&A. We -hope you'll consider proposing a talk for another EmacsConf! +Sometimes an interesting talk idea turns out to be more challenging than +you'd like. Sometimes life happens. If you're stressing out and you +don't think you can make it, no worries, no need to feel embarrassed or +guilty or anything like that. Let me know and I can update the +schedule. We hope you'll consider proposing a talk for another +EmacsConf! ---------------- If you're planning to go through with the talk (yay!), could you @@ -1349,17 +1558,25 @@ e-mail me the corrections.${fill} Best regards, ${signature}")) - ('live - (list - :subject "${conf-name} ${year}: please check intro pronunciation" - :body - "${email-notes}Hi, ${name}! - -Thanks again for uploading your presentation early! + ('live + (list + :subject "${conf-name} ${year}: thanks again for your video, also please check intro pronunciation" + :body + "${email-notes}Hi, ${name}! + +Thanks again for uploading your presentation early! How are you feeling +about ${conf-name}? It's coming up soon - just two weeks away. I wanted +to check in with all the speakers in case things have come up. In case +life has become unexpectedly busy for you and you're not sure if you +want to go through with your presentation & live Q&A, let me know and I +can adjust the schedule. No stress. =) ${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} +BigBlueButton web conference room. I plan to bring the web conference +server up on Friday night before the conference, and I'll e-mail you the +check-in information then so that you can test things if you like. If +you want to test things beforehand, let me know what time and date might +work for you and I can set that up.${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} , @@ -1371,14 +1588,20 @@ e-mail me the corrections. Best regards, ${signature}")) - ('pad-or-irc - (list - :subject "${conf-name} ${year}: please check intro pronunciation" - :body - "${email-notes}Hi, ${name}! + ('pad-or-irc + (list + :subject "${conf-name} ${year}: thanks again for your video, also please check intro pronunciation" + :body + "${email-notes}Hi, ${name}! Thanks again for uploading your presentation early! +How are you feeling about ${conf-name}? It's coming up soon - just two +weeks away. I wanted to check in with all the speakers in case things +have come up. In case life has become unexpectedly busy for you and +you're not sure if you want to go through with your presentation & +Q&A, let me know and I can adjust the schedule. No stress. =)${fill} + 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} , and you can also find it under \"--intro.webm\" in your talk's section @@ -1386,17 +1609,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 on Thursday or Friday so that -you'll be all set. +We'll send you check-in instructions on the Friday before the conference +so that you'll be all set. Best regards, ${signature}")) - ('after - (list - :subject "${conf-name} ${year}: please check intro pronunciation" - :body - "${email-notes}Hi, ${name}! + ('after + (list + :subject "${conf-name} ${year}: thanks again for your video, please check intro pronunciation" + :body + "${email-notes}Hi, ${name}! Thanks again for uploading your presentation early! @@ -1417,7 +1640,7 @@ us! Best regards, ${signature}")) - (_ (error "Unknown type %s" (symbol-name type))))) + (_ (error "Unknown type %s" (symbol-name type))))) (car group) (list :email-notes (or (plist-get (car group) :email-notes) "") @@ -1436,7 +1659,7 @@ ${signature}")) (plist-get talk :slug))) (cdr group) " , ") - :upload-url (concat "https://ftp-upload.emacsconf.org/?sid=" + :upload-url (concat "https://upload.emacsconf.org/?sid=" emacsconf-upload-password "-" (mapconcat (lambda (o) (plist-get o :slug)) (cdr group) "-")) @@ -1487,55 +1710,87 @@ ${signature}")) (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) +(defun emacsconf-mail-schedule-updates () + "E-mail general schedule updates." + (interactive) + (let* ((log-note (concat "sent schedule as of " (format-time-string "%Y-%m-%d"))) + (talks (emacsconf-filter-talks-by-logbook + log-note + (emacsconf-rescheduled-talks))) + (groups (emacsconf-mail-groups talks))) + (dolist (group groups) + (emacsconf-mail-interim-schedule-update group log-note)))) + +(defun emacsconf-mail-interim-schedule-update (group &optional log-note) "E-mail a quick update about the schedule." (interactive (list (emacsconf-complete-talk-info))) - (emacsconf-mail-prepare + (emacsconf-mail-prepare (list - :subject "${conf-name} ${year}: Schedule update ${sched-one-line}" + :subject "${conf-name} ${year}: Schedule update as of ${date}: ${summary}" :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" - :log-note "sent updated schedule" + :log-note log-note :body "Hello, ${speakers-short}! -We tweaked the schedule a bit. Your new schedule is: +We tweaked the schedule a bit so that it's based on the current video lengths. +Your new schedule is: ${schedule} Let us know if you need to reschedule! ${signature}") - (plist-get talk :email) + (or (plist-get group :email) + (plist-get (car (cdr group)) :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) + :date (format-time-string "%Y-%m-%d") + :email (or (plist-get group :email) + (plist-get (car (cdr group)) :email)) + :speakers-short + (or + (plist-get group :speakers-short) + (plist-get (car (cdr group)) :speakers-short)) :signature user-full-name + :summary + (mapconcat + (lambda (talk) + (let ((minutes (emacsconf-schedule-difference-from-emailed talk))) + (if (> (abs minutes) 0) + (format "%s: %s min %s" + (plist-get talk :slug) + (abs minutes) + (if (< minutes 0) + "earlier" + "later")) + "same time, different length"))) + (if (plist-get group :slug) + (list group) + (cdr group)) + "; ") :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 () + (mapconcat (lambda (talk) + (emacsconf-indent-string (emacsconf-mail-format-talk-schedule talk (plist-get talk :emailed-schedule)) 2)) + (if (plist-get group :slug) + (list group) + (cdr group)) + "\n\n")))) + +(defun emacsconf-mail-check-in-update (talk-or-group) "E-mail day-of schedule updates" - (interactive) + (interactive (list (emacsconf-complete-talk-info))) (let ((groups - (emacsconf-mail-groups - (seq-remove (lambda (talk) - (string= (replace-regexp-in-string "[<>]" "" (plist-get talk :scheduled)) - (plist-get talk :checkin-schedule-sent))) - (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))))) + (emacsconf-mail-groups (if (plist-get talk-or-group :slug) + (list talk-or-group) + talk-or-group)))) (dolist (group groups) (emacsconf-mail-prepare (list - :subject "${conf-name} ${year}: Schedule update - new check-in time ${checkin-time}" + :subject "${conf-name} ${year}: SCHEDULE UPDATE - new check-in time ${checkin-time}" :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" :log-note "sent updated schedule" @@ -1551,8 +1806,7 @@ 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} -If something comes up, please let us know as soon as you can. Here's -my emergency contact information: ${emergency}${wrap} +If something comes up, please let us know as soon as you can. Thank you for sharing your time and energy with the ${conf-name} community! @@ -1650,8 +1904,7 @@ 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} -${waiting}If something comes up, please let us know as soon as you can. Here's -my emergency contact information: ${emergency}${wrap} +${waiting}If something comes up, please let us know as soon as you can.${wrap} Thank you for sharing your time and energy with the ${conf-name} community! @@ -1673,8 +1926,8 @@ ${signature}") :waiting (let ((waiting (seq-remove (lambda (o) (plist-get o :video-file)) (cdr group)))) (cond ((= (length waiting) 0) "") - ((= (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"))) + ((= (length waiting) 1) "If you happen to be able to get a pre-recorded video together, 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, 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) @@ -1682,7 +1935,7 @@ ${signature}") (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}" + "\n\nIt might be a good idea to do a tech check to make sure I didn't mess up BigBlueButton. =) 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. The server is up at the moment and should continue to be up until the conference, so you can do your tech check early if you want.${wrap}" "") :checkin-info (mapconcat @@ -1701,21 +1954,21 @@ ${signature}") ((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)) - " (moderator code: " - (plist-get o :bbb-mod-code) - ")")) + (concat + "Use the moderator code " + (plist-get o :bbb-mod-code) + "\n for this talk & Q&A BigBlueButton room: " + (emacsconf-backstage-url (plist-get o :bbb-backstage)))) ((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)) - " (moderator code: " - (plist-get o :bbb-mod-code) - ")")) + (concat + "Use the moderator code " + (plist-get o :bbb-mod-code) + "\n for this Q&A BigBlueButton room: " + (emacsconf-backstage-url (plist-get o :bbb-backstage)))) ((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") @@ -1775,7 +2028,7 @@ Sacha") (cdr group) " , ") :email-notes (emacsconf-surround "ZZZ: " (plist-get (cadr group) :email-notes) "\n\n" "")))) -(defun emacsconf-mail-template-speakers-thanks-after-conferences () +(defun emacsconf-mail-template-speakers-thanks-after-conference () (interactive) (let* ((log-note "sent thanks to speaker after conference") (groups @@ -1788,28 +2041,30 @@ Sacha") (emacsconf-mail-prepare (list :subject "Thanks for speaking at ${conf-name} ${year}!" - :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 log-note :body "${email-notes}Hi, ${speakers-short}! -Thank you so much for being part of ${conf-name} ${year}! Hundreds of people +Thank you so much for being part of ${conf-name} ${year}! Lots of people enjoyed it, and I'm sure even more will come across the videos in the days to follow. -Your videos are available on the talk page at ${talk-urls} , and -we've added the questions and comments that we've collected from -IRC/BBB/Etherpad. For your convenience, I've also included them below. +We've added the questions and comments that we've collected from +IRC/BBB/Etherpad to ${talk-urls} . For your convenience, I've also +included them below. You can edit the wiki directly or e-mail me +anything you'd like me to add.${wrap} -Your videos are also available on YouTube and Toobnix at: +Videos are up at: ${video-urls} -If you want to reupload the video to your own channel, feel free -to do so. If you let me know where you've uploaded it, I can -switch our playlist to include your version of the video -instead. That way, it might be easier for you to respond to +Please feel free to check for comments.${wrap} + +If you want to reupload the video to your own channel, feel free to do +so. If you like, I can switch our playlist to include your version of +the video instead. That way, it might be easier for you to respond to comments on videos. If you would like to share more resources or add more answers to @@ -1830,6 +2085,7 @@ ${feedback} :speakers-short (plist-get (cadr group) :speakers-short) :conf-name emacsconf-name :year emacsconf-year + :wiki (concat emacsconf-base-url emacsconf-year "/talks/") :talk-urls (mapconcat (lambda (talk) @@ -2014,6 +2270,147 @@ ${signature} (emacsconf-mail-template-mailing-address group)) (message "Drafted %d messages" (length groups)))) +(defun emacsconf-mail-template-ask-volunteer-for-mailing-address (volunteer) + (interactive (list (emacsconf-complete-volunteer))) + (emacsconf-mail-prepare + (list + :subject "${conf-name} ${year}: Thank you! 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}" + :body + "Hi, ${name-short}! + +${email-notes} + +We have swag 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} +") + (assoc-default "EMAIL" volunteer) + (list + :email-notes + (emacsconf-surround + "ZZZ: " + (replace-regexp-in-string + ":volunteer" "" + (assoc-default "ALLTAGS" volunteer)) + "\n\n" "") + :name-short (assoc-default "NAME_SHORT" volunteer) + :conf-name emacsconf-name + :year emacsconf-year + :email (assoc-default "EMAIL" volunteer) + :base-url emacsconf-base-url + :signature user-full-name + :user-email user-mail-address + :sticker-mailer emacsconf-sticker-mailer))) + +(defun emacsconf-mail-template-mail-youtube-comments (group) + "Send more YouTube comments." + (interactive (list (emacsconf-mail-complete-email-group + (seq-filter + (lambda (o) + (and + (or + (emacsconf-talk-file o "--answers--original.vtt") + (emacsconf-talk-file o "--original.vtt")) + (not (string-match "Asked for permission regarding the rest of the Q&A" + (plist-get o :logbook))))) + (emacsconf-get-talk-info))))) + (emacsconf-mail-prepare + (list + :subject "${conf-name} ${year}: May we post the rest of the Q&A?" + :reply-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" + :mail-followup-to "emacsconf-submit@gnu.org, ${email}, ${user-email}" + :log-note "Asked for permission regarding the rest of the Q&A" + :body + "${email-notes}Hi, ${speakers-short}! + +We're experimenting with a new harvesting workflow for live and +Q&A videos this year to make things more predictable for speakers +and participants. Sometimes people have so much fun chatting +after the talk that they might forget that the recording for the +session Q&A will be posted for other people to learn from. + +I've trimmed your online videos to roughly when the host left the +BigBlueButton room. There was lots of great discussion +afterwards, though, so I'd love to include the rest of it if +that's okay with you. To make it easier for you to review that +part or reuse what you shared in the Q&A session, I've included +an automatically-generated transcript for the whole Q&A +session. I've indicated the section that got trimmed out of the +published recording with \"NOTE Start of section to review\" in +the transcript. You can watch the session at ${bbb-recording-url} . + +- Option A: We could post the rest of the Q&A as is, which lets + people listen to the conversation and learn from it + +- Option B: We can keep the published Q&A video to just the part + that the host was in, and either you or I can go over the + transcript to pull out interesting notes for the summary or for + other posts + +What do you think? + +${signature} +---- +${transcript} +") + (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 + :bbb-recording-url + (mapconcat + (lambda (talk) + (plist-get talk :bbb-rec)) + (cdr group) + " , ") + :signature user-full-name + :email (car group) + :transcript + (mapconcat + (lambda (talk) + (concat + (plist-get talk :title) "\n\n" + (mapconcat + (lambda (sub) + (concat (emacsconf-surround "\nNOTE " (elt sub 4) "\n\n" "") + (elt sub 3))) + (subed-parse-file (or (emacsconf-talk-file talk "--answers--original.vtt") + (emacsconf-talk-file talk "--original.vtt"))) "\n"))) + (cdr group) + "----") + :user-email user-mail-address)) + (dolist (talk (cdr group)) + (mml-attach-file (or (emacsconf-talk-file talk "--answers--original.vtt") + (emacsconf-talk-file talk "--original.vtt")) + "text/vtt" + (concat "Automatic captions for " (plist-get talk :title)) + "attachment"))) + ;;; Other mail functions (defun emacsconf-mail-verify-delivery (subject &optional groups) @@ -2175,10 +2572,17 @@ This minimizes the risk of mail delivery issues and radio silence." part (expand-file-name new-filename emacsconf-backstage-dir))))) (mm-dissect-buffer)))) +;;;###autoload (defun emacsconf-notmuch-submissions () "Search for recent submissions." (interactive) - (notmuch-search emacsconf-submit-email)) + (notmuch-search (format "to:%s and not subject:\"requires approval\" and not subject:\"moderator request(s) waiting\" and not from:no-reply@netdata.cloud" emacsconf-submit-email))) + +;;;###autoload +(defun emacsconf-notmuch-new-submissions () + "Search for recent submissions." + (interactive) + (notmuch-search (format "to:%s and not subject:\"requires approval\" and not subject:\"moderator request(s) waiting\" and not from:no-reply@netdata.cloud and not tag:replied and not tag:sent" emacsconf-submit-email))) (defun emacsconf-notmuch-check-sent (query &optional groups) (interactive "MSubject: ") diff --git a/emacsconf-pad.el b/emacsconf-pad.el index 6a79d45..fe52fd1 100644 --- a/emacsconf-pad.el +++ b/emacsconf-pad.el @@ -177,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 @@ -188,7 +190,7 @@ You can find it in $ETHERPAD_PATH/APIKEY.txt" :questions (string-join (make-list 6 "<li>Q: <ul><li>A: </li></ul></li>")) :conf-pad-url - (concat "https://pad.emacsconf.org/" emacsconf-year) + (concat "https://pad.emacsconf.org/emacsconf") :irc-nick-details (if (plist-get o :irc) (concat "Speaker nick: " (plist-get o :irc) " - ") @@ -198,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> @@ -262,9 +264,10 @@ ${next-talk-list} (emacsconf-pad-create-pad pad-id) (when (or emacsconf-pad-force-all (not (emacsconf-pad-modified-p pad-id)) - (progn - (browse-url (emacsconf-pad-url o)) - (y-or-n-p (format "%s might have been modified. Reset? " (plist-get o :slug))))) + (and (called-interactively-p) + (progn + (browse-url (emacsconf-pad-url o)) + (y-or-n-p (format "%s might have been modified. Reset? " (plist-get o :slug)))))) (emacsconf-pad-set-html pad-id (emacsconf-pad-initial-content o)) @@ -366,7 +369,7 @@ ${next-talk-list} (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 "checkin-" (downcase (format-time-string "%a" (date-to-time (plist-get shift :start))))))) + :checkin-pad (concat emacsconf-pad-base "private-" emacsconf-private-pad-prefix "-" emacsconf-year "-checkin-" (downcase (format-time-string "%a" (date-to-time (plist-get shift :start))))))) (shift-talks (mapcar (lambda (o) (append prefixed o)) (seq-filter @@ -486,7 +489,7 @@ ${next-talk-list} (plist-get talk :timezone) "%-l:%M %p")) "<ul> -<li>Message for the speaker: Thanks for checking in! Your BigBlueButton web conference room is at ${backstage-url}. If you don't have the backstage username and password saved, let me know and I can send you a direct message with the info. Please join me there so that I can set you as a moderator and go through the preflight checklist with you. +<li>Message for the speaker: Thanks for checking in! Your BigBlueButton web conference room is at ${backstage-url}. If you don't have the backstage username and password saved, let me know and I can send you a direct message with the info. Please join me there so that I can set you as a moderator and go through the preflight checklist with you.</li> <li>Direct message for the speaker if needed: Your BigBlueButton web conference room is at ${backstage-url-with-password}, or username \"${backstage-user}\" and password \"${backstage-password}\".</li> <li>Pronunciation: ${pronunciation}</li> <li>Checklist<ul> @@ -495,9 +498,10 @@ ${next-talk-list} <li>[ ] Speaker can hear others</li> <li>[ ] No audio feedback issues (may need headphones or earphones)</li> <li>[ ] Screen sharing: (optional) -<ul><li>[ ] Window or screen can be shared -<li>[ ] Text is readable</li></ul> -<li>[ ] Webcam sharing (optional)</li></ul></li> +<ul><li>[ ] Window or screen can be shared</li> +<li>[ ] Text is readable</li></ul></li> +<li>[ ] Webcam sharing (optional)</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> @@ -524,7 +528,8 @@ ${bbb-checklist}</li>") (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))) (mapc (lambda (day) - (let ((pad-id (concat "private-" emacsconf-private-pad-prefix "-checkin-" (downcase (format-time-string "%a" (plist-get (cadr day) :checkin-time)))))) + (let ((base-pad (concat "private-" emacsconf-private-pad-prefix "-" emacsconf-year)) + (pad-id (concat "private-" emacsconf-private-pad-prefix "-" emacsconf-year "-checkin-" (downcase (format-time-string "%a" (plist-get (cadr day) :checkin-time)))))) (emacsconf-pad-create-pad pad-id) (emacsconf-pad-set-html pad-id @@ -532,17 +537,20 @@ ${bbb-checklist}</li>") "<em>${checkin}:</em> " "" (concat (car day) + "\n" + emacsconf-pad-base base-pad "\n" "<p>If anyone's still missing by the specified time, please let us know in #emacsconf-org so we can call them.</p>" "<ul>" - (mapconcat - (lambda (talk) - (emacsconf-pad-format-checkin-hyperlist talk)) - (seq-sort - (lambda (a b) - (time-less-p (plist-get a :checkin-time) - (plist-get b :checkin-time))) - (seq-filter (lambda (talk) (plist-get talk :checkin-time)) day)) - "\n") + (let ((body (mapconcat + (lambda (talk) + (emacsconf-pad-format-checkin-hyperlist talk)) + (seq-sort + (lambda (a b) + (time-less-p (plist-get a :checkin-time) + (plist-get b :checkin-time))) + (seq-filter (lambda (talk) (plist-get talk :checkin-time)) day)) + "\n"))) + body) "</ul>"))))) (seq-group-by (lambda (talk) (format-time-string "%A, %b %-e, %Y" (plist-get talk :checkin-time))) @@ -687,6 +695,7 @@ ${bbb-checklist}</li>") (format-time-string "%-l:%M %p" (plist-get talk :start-time) emacsconf-timezone)) (plist-get talk :hyperlist-note) "</li>" "") :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-2 (if next-talk (format-time-string "%-l:%M %p" (time-subtract (plist-get next-talk :start-time) (seconds-to-time 120)) 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) "")) talk) (concat @@ -694,7 +703,7 @@ ${bbb-checklist}</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>") + "<li><strong>${start-hhmm} ${slug} live talk</strong>: it should play a prerecorded intro, but if it doesn't, use mod code <strong>${bbb-mod-code}</strong> to join ${bbb-backstage} and introduce talk, then turn it over to speaker for <strong>live talk</strong>: ${expanded-intro} (pronunciation: ${pronunciation})</li>") ;; Q&A (if (and (not (emacsconf-talk-recorded-p talk)) (not (string= (or (plist-get talk :qa-type) "none") "none"))) @@ -712,7 +721,7 @@ ${bbb-checklist}</li>") "<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} (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> + "<li>[ ] <strong>${qa-hhmm} ${slug} Q&A live</strong> (on stream until ${end-of-qa}): Use mod code <strong>${bbb-mod-code}</strong> to join ${bbb-backstage} . START RECORDING. Invite people to put their questions in the Etherpad, and read questions from there. ${pad-url}</li> ${open-qa} " (if next-talk @@ -739,7 +748,8 @@ ${bbb-checklist}</li>") (interactive) (emacsconf-pad-prepopulate-shift-hyperlists) (emacsconf-pad-prepopulate-checkins) - (emacsconf-pad-prepopulate-host-hyperlists)) + (emacsconf-pad-prepopulate-host-hyperlists) + (emacsconf-pad-prepopulate-index)) (defun emacsconf-pad-expand-intro (talk) "Make an intro for TALK." @@ -762,19 +772,19 @@ ${bbb-checklist}</li>") ", and \\1" (plist-get talk :speakers)) (emacsconf-surround " (" (plist-get talk :pronunciation) ")" "") - (pcase (plist-get talk :q-and-a) + (pcase (plist-get talk :qa-type) ((or 'nil "") "") - ((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") + ("none" "\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.") + ("live" (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") + ("pad" (if pronoun (format "\n%s will answer questions via Etherpad." pronoun) "\nYou can ask questions via Etherpad.")) - ((rx "IRC") + ("irc" (if pronoun (format "\n%s will answer questions via IRC in the #%s channel." pronoun @@ -819,7 +829,7 @@ ${bbb-checklist}</li>") (setq result (emacsconf-replace-plist-in-string modified-talk - (format "<li><strong>%s %s (intro: %s, talk: %s, Q&A: %s) %s <a href=\"%s\">%s</a></strong><ul>%s</ul>\n</li>" + (format "<li><strong>%s %s (intro: %s, talk: %s, Q&A: %s) %s <a href=\"%s\">%s</a></strong><br /><ul><li>%s%s%s</li>%s</ul>\n</li>" (format-time-string "%-l:%M %p" (plist-get talk :start-time) emacsconf-timezone) (plist-get talk :slug) (if (plist-get talk :recorded-intro) "recorded" "live") @@ -828,23 +838,25 @@ ${bbb-checklist}</li>") (plist-get talk :title) (plist-get talk :absolute-url) (plist-get talk :absolute-url) - + (emacsconf-surround "" (plist-get talk :speakers) "" "No speakers") + (emacsconf-surround " Pron: " (plist-get talk :pronunciation) "" "") + (emacsconf-surround " (" (plist-get talk :pronouns) ")" "") (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 (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>" + "<li>[? stream didn't auto-play] ${stream}: <em>handle-session ${slug}</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>")) + "<li>[? stream didn't auto-join] ${stream}: mod code ${bbb-mod-code} <a href=\"${bbb-backstage}\">${bbb-backstage}</a></li>" + "<li>[ ] ${host}: mod code ${bbb-mod-code} , join <a href=\"${bbb-backstage}\">${bbb-backstage}</a> and turn over to speaker.</li></ul></li>")) (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}: 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> +<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> <li>Backup URL for BBB: <a href=\"${bbb-backstage}\">${bbb-backstage}</a></li> <li>Backup URL for pad: <a href=\"${pad-url}\">${pad-url}</a></li> @@ -852,12 +864,10 @@ ${bbb-checklist}</li>") </li> <li>[ ] ${stream}: Give the host the go-ahead via Mumble or #emacsconf-org</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 "" - "<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>[ ] ${host}: Decide when to open the Q&A and let ${stream} know</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] @@ -872,30 +882,24 @@ ${bbb-checklist}</li>") </ul></li></ul></li>")) ((rx "irc") " -<li>[ ] ${stream}: Update the task status, which should open the pad and IRC; arrange windows: ${ssh-closedq} -<ul><li>Backup link to pad: <a href=\"${pad-url}\">${pad-url}</a></li> -<li>Backup link to #${channel}: <a href=\"${webchat-url}\">${webchat-url}</a></li></ul></li> -<li>[ ] ${stream}: Update the task status (no visible changes): ${ssh-openq}</li> -<li>[ ] ${host}: Announce that people can ask questions in the ${channel} IRC channel.</li> +<li>Backup link to pad: <a href=\"${pad-url}\">${pad-url}</a></li> +<li>Backup link to #${channel}: <a href=\"${webchat-url}\">${webchat-url}</a></li> ") ((rx "Mumble") " <li>[ ] ${stream}: Bring the speaker's Mumble login over to the ${channel} channel in Mumble. Confirm that Mumble is audible and adjust audio as needed: ssh emacsconf-${track-id}@res.emacsconf.org -p 46668 \"mum-vol 85%%\" (or mum-louder, mum-quieter)</li> <li>[ ] ${stream}: Mark the Q&A as closed: ${ssh-closedq} . This should display the QA slide (backup: ${ssh-track} and run <em>firefox ${qa-slide-url} &</em>)</li> -<li>[ ] ${stream}: Update the task status (no visible changes): ${ssh-openq}</li> - <li>[ ] ${host}: Announce that people can ask questions in the pad or on the ${channel} IRC channel.</li> ") ((rx "after") " -<li>[ ] ${stream}: Update the task status: ${ssh-closedq} # this should open the pad and IRC; arrange the windows <ul><li>Backup link to pad: ${pad-url}</li> <li>Backup link to #${channel}: ${webchat-url}</li></ul></li> <li>[ ] ${host}: Announce that people can ask questions in the pad or on the ${channel} IRC channel, and that the speaker will follow up later.</li> -<li>[ ] ${stream}: Update the task status: ${ssh-openq} # this should not make any visible changes, just update the task status</li>" +" ) ((rx "pad") - "<li>[ ] [? pad didn't auto-open] ${stream}: ${pad-url}</li>") + "<li>[? pad didn't auto-open] ${stream}: ${pad-url}</li>") (_ "<li>[ ] ${stream}: Open the IRC channel (${channel}) and the pad, and arrange the windows: ${ssh-closedq}</li> ")))))) diff --git a/emacsconf-publish.el b/emacsconf-publish.el index 31ff72b..0af176d 100644 --- a/emacsconf-publish.el +++ b/emacsconf-publish.el @@ -30,7 +30,7 @@ :type 'string :group 'emacsconf) -(defcustom emacsconf-main-extensions '("--main.webm" "--main.opus" "--main.org" ".org" ".odp" ".pdf" ".pptx" ".el" "--compressed56.webm" "--main.vtt" "--main_fr.vtt" "--main_ja.vtt" "--main_es.vtt" "--main--chapters.vtt" "--script.fountain" "--main.pdf" "--slides.pdf") +(defcustom emacsconf-main-extensions '("--main.webm" "--main.opus" "--main.org" ".org" ".odp" ".pdf" ".pptx" ".el" "--compressed56.webm" "--main.vtt" "--main_fr.vtt" "--main_ja.vtt" "--main_es.vtt" "--main--chapters.vtt" "--script.fountain" "--main.pdf" "--slides.pdf" "--answers.vtt" "--answers.webm") "Extensions to list on public pages." :type '(repeat string) :group 'emacsconf) @@ -65,6 +65,7 @@ (defun emacsconf-publish-add-talk () "Add the current talk to the wiki." (interactive) + (emacsconf-current-org-notebook-refresh-schedule) (emacsconf-publish-talk-page (emacsconf-get-talk-info-for-subtree)) (emacsconf-publish-info-pages) (emacsconf-publish-schedule) @@ -145,23 +146,34 @@ (video (and file-prefix (emacsconf-publish-index-card-video (or (plist-get talk :video-id) - (concat (plist-get talk :slug) "-mainVideo")) + (concat "mainVideo-" (plist-get talk :slug))) video-file talk)))) ;; Add extra information to the talk (setq talk (append talk (list + :video-type (or (plist-get talk :video-type) "mainVideo") :time-info (emacsconf-surround "Duration: " (plist-get talk :video-duration) " minutes" "") :video-html (or (plist-get video :video) "") :audio-html (or (plist-get video :audio) "") :chapter-list (or (plist-get video :chapter-list) "") :resources (or (plist-get video :resources) "") - :extra (or (plist-get talk :extra) "") + :extra + (concat + (if (plist-get talk :backstage) + ;; include schedule + (concat + "Starts: " + (format-time-string "%-I:%M %#p" + (plist-get talk :start-time)) + " - Q&A: " (plist-get talk :q-and-a) "\n") + "") + (or (plist-get talk :extra) "")) :speaker-info (or (plist-get talk :speakers-with-pronouns) "")))) (emacsconf-replace-plist-in-string talk - "<div class=\"vid\">${video-html}${audio-html}<div>${extra}</div>${time-info}${resources}${chapter-list}</div>"))) + "<div class=\"vid ${video-type}\">${video-html}${audio-html}<div>${extra}</div>${time-info}${resources}${chapter-list}</div>"))) ;; (emacsconf-publish-format-track-as-org (car emacsconf-tracks) "US/Eastern") ;; (emacsconf-get-talk-info) @@ -172,53 +184,107 @@ "** " (plist-get track :name) " :" (plist-get track :id) ":\n:PROPERTIES:\n:CATEGORY: " (plist-get track :id) "\n:END:\n" (mapconcat (lambda (talk) - (concat - "*** " (plist-get talk :title) "\n" - "<" - (format-time-string - (cdr org-time-stamp-formats) - (plist-get talk :start-time) - tz) - ">--<" - (format-time-string - (cdr org-time-stamp-formats) - (plist-get talk :end-time) - tz) - ">\n" - (emacsconf-surround "- " (plist-get talk :speakers-with-pronouns) "\n" "") - (emacsconf-surround "- " (plist-get talk :absolute-url) "\n" "") - "- Watch live: " - (mapconcat (lambda (player) - (org-link-make-string - (concat "shell:" player " " (plist-get track :stream) " &") - player)) - '("mpv" "vlc" "ffplay") - " or ") - " or " - (org-link-make-string - (plist-get track :watch) - "web-based player") - "\n" - (emacsconf-surround "- Etherpad: " (plist-get talk :pad-url) "\n" "") - (emacsconf-surround "- Chat: " - (org-link-make-string - (plist-get talk :webchat-url) - (concat "#" (plist-get talk :channel))) - "\n" "") - (emacsconf-surround "- Q&A: " - (if (plist-get talk :qa-url) - (org-make-link-string - (plist-get talk :qa-url) - (plist-get talk :qa-info)) - (plist-get talk :qa-info)) - "\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" ""))) + (with-temp-buffer + (org-mode) + + (concat + "*** " (plist-get talk :title) "\n" + "<" + (format-time-string + (cdr org-time-stamp-formats) + (plist-get talk :start-time) + tz) + ">--<" + (format-time-string + (cdr org-time-stamp-formats) + (plist-get talk :end-time) + tz) + ">\n" + (emacsconf-surround "- " (plist-get talk :speakers-with-pronouns) "\n" "") + (emacsconf-surround "- " (plist-get talk :absolute-url) "\n" "") + "- Watch live: " + (mapconcat (lambda (player) + (org-link-make-string + (concat "shell:" player " " (plist-get track :stream) " &") + player)) + '("mpv" "vlc" "ffplay") + " or ") + " or " + (org-link-make-string + (plist-get track :watch) + "web-based player") + "\n" + "- You can also watch it in VLC by choosing menu - Media - Open Network Stream and putting in " (plist-get track :stream) "\n" + (emacsconf-surround "- Q&A: " + (if (plist-get talk :qa-url) + (org-make-link-string + (plist-get talk :qa-url) + (plist-get talk :qa-info)) + (plist-get talk :qa-info)) + "\n" "") + (emacsconf-surround "- Etherpad: " (plist-get talk :pad-url) "\n" "") + (emacsconf-surround " - Text version (no JS): " (plist-get talk :pad-url) "/export/txt\n" "") + (emacsconf-surround " - HTML version (no JS): " (plist-get talk :pad-url) "/export/html\n" "") + (emacsconf-surround "- Chat: " + (org-link-make-string + (plist-get talk :webchat-url) + (concat "#" (plist-get talk :channel))) + "\n" "") + "- " (org-link-make-string + (plist-get talk :video-url) + (if (plist-get talk :video-time) + + "Talk recording (posted soon after the talk starts)" + "Recording for live talk (posted in a week or two)")) "\n" + "- " + (org-link-make-string + (plist-get talk :captions-url) + (if (plist-get talk :video-time) + "Captions (posted soon after the talk starts)" + "Captions for live talk (posted in a week or two)")) + "\n" + "- Email for questions: " + (let ((email (or (plist-get talk :public-email) + emacsconf-fallback-email))) + (org-link-make-string + (concat "mailto:" + email + "?body=&subject=" + (url-hexify-string + (format "%s %s: %s" + emacsconf-name + emacsconf-year + (plist-get talk :title)))) + email)) + "\n" + (let ((other-files + (seq-remove (lambda (o) + (string-match "--main\\.\\(vtt\\|webm\\)")) + (emacsconf-publish-filter-public-files talk)))) + (if other-files + (concat "- Other files:\n" + (mapconcat + (lambda (file) + (concat " - " + (org-link-make-string + (format "%s%s/%s" + emacsconf-media-base-url + (file-name-nondirectory file)) + (replace-regexp-in-string + (concat "^" (regexp-quote (plist-get talk :file-prefix))) "" + (file-name-nondirectory file))))) + other-files "\n") + "\n") + "")) + (emacsconf-surround "\n" (plist-get talk :intro-note) "\n" "") + (emacsconf-surround "\nDescription:\n\n" + (when (plist-get talk :org-description) + (with-temp-buffer + (if (org-kill-is-subtree-p (plist-get talk :org-description)) + (org-paste-subtree 3 (plist-get talk :org-description)) + (insert (plist-get talk :org-description))) + (buffer-string))) + "\n" "")))) (emacsconf-filter-talks-by-track track info) "\n"))) @@ -364,10 +430,10 @@ (list :source-src (when (stringp video-file) - (if (plist-get talk :public) - (format "%s%s/%s" emacsconf-media-base-url (plist-get talk :conf-year) - (file-name-nondirectory video-file)) - (file-name-nondirectory video-file))) + (if (plist-get talk (if (string-match "--answers" video-file) :qa-public :public)) + (format "%s%s/%s" emacsconf-media-base-url (plist-get talk :conf-year) + (file-name-nondirectory video-file)) + (file-name-nondirectory video-file))) :captions (or (and (stringp video-file) @@ -380,9 +446,7 @@ )) (let ((tracks (emacsconf-video-subtitle-tracks - (or (plist-get talk :caption-file) - (emacsconf-talk-file talk "--main.vtt") - (emacsconf-talk-file talk "--reencoded.vtt")) + (plist-get talk :caption-file) (or (plist-get talk :track-base-url) (plist-get talk :base-url)) (plist-get talk :files)))) @@ -401,8 +465,10 @@ (plist-get chapter-info :html)) "") :video-id video-id - :video-file-size (if (and (stringp video-file) (file-exists-p video-file)) - (file-size-human-readable (file-attribute-size (file-attributes video-file)))) + :video-file-size (if (and (stringp video-file) + (file-exists-p + (expand-file-name video-file emacsconf-cache-dir))) + (file-size-human-readable (file-attribute-size (file-attributes (expand-file-name video-file emacsconf-cache-dir))))) :links (concat (emacsconf-surround "<li><a href=\"" @@ -415,6 +481,10 @@ (and (plist-get talk :backstage) (plist-get talk :bbb-backstage)) "\">Open backstage BigBlueButton</a></li>" "") + (emacsconf-surround "<li>BBB mod code: " + (and (plist-get talk :backstage) + (plist-get talk :bbb-mod-code)) + "</li>" "") (emacsconf-surround "<li><a href=\"" (and (member emacsconf-publishing-phase '(schedule conference)) (plist-get talk :qa-url)) @@ -426,8 +496,16 @@ (mapconcat (lambda (s) (concat "<li>" s "</li>")) - (emacsconf-publish-link-file-formats-as-list talk) + (emacsconf-publish-link-file-formats-as-list + talk + (seq-filter (lambda (o) (string-match "--answers" o)) + (emacsconf-publish-filter-public-files talk))) "") + :youtube-info (if (plist-get talk :youtube-url) + (format + "<li><a href=\"%s\">View on Youtube</a></li>" + (plist-get talk :youtube-url)) + "") :toobnix-info (if (plist-get talk :toobnix-url) (format "<li><a href=\"%s\">View on Toobnix</a></li>" @@ -442,7 +520,7 @@ :video (emacsconf-replace-plist-in-string info - (if (and (stringp video-file) (string-match "webm$" video-file)) + (if (stringp video-file) "<video controls preload=\"none\" id=\"${video-id}\"><source src=\"${source-src}\" />${captions}${chapter-track}<p><em>Your browser does not support the video tag. Please download the video instead.</em></p></video>${chapter-list}" (or (plist-get talk :video-note) ""))) :audio @@ -457,7 +535,7 @@ :resources (emacsconf-replace-plist-in-string info - "<div class=\"files resources\"><ul>${links}${other-files}${toobnix-info}</ul></div>")))) + "<div class=\"files resources\"><ul>${links}${other-files}${toobnix-info}${youtube-info}</ul></div>")))) (defun emacsconf-publish-format-public-email (o &optional email) (format "[%s](mailto:%s?subject=%s)" @@ -469,9 +547,15 @@ (let ((extra-info (mapconcat #'identity (delq nil (list (unless (string= (plist-get o :pronunciation) "nil") - (emacsconf-surround "Pronunciation: " (plist-get o :pronunciation) "")) + (emacsconf-surround "Pronunciation: " + (if (string-match "\\[" (or (plist-get o :pronunciation) "")) + (org-export-string-as (plist-get o :pronunciation) 'md t) + (plist-get o :pronunciation)) + "")) (when (plist-get o :irc) (format "IRC: %s" (plist-get o :irc))) - (plist-get o :public-contact) + (if (string-match "\\[" (or (plist-get o :public-contact) "")) + (org-export-string-as (plist-get o :public-contact) 'md t) + (plist-get o :public-contact)) (when (plist-get o :public-email) (format "<mailto:%s>" (plist-get o :public-email))))) ", "))) (concat (plist-get o :speakers-with-pronouns) @@ -563,11 +647,13 @@ ${categories} (emacsconf-publish-index-card (append (list :public 1 - :video-id (concat (plist-get o :slug) "-qanda") - :toobnix-url nil + :video-type "qanda" + :video-id (concat "qanda-" (plist-get o :slug)) + :youtube-url (plist-get o :qa-youtube-url) + :toobnix-url (plist-get o :qa-toobnix-url) :captions-edited (plist-get o :qa-captions-edited) :caption-file (emacsconf-talk-file o "--answers.vtt") - :video-file (emacsconf-talk-file o "--answers.webm") + :video-file (plist-get o :qa-video-file) :video-duration (plist-get o :qa-video-duration) :audio-file (emacsconf-talk-file o "--answers.opus") :chapter-file (emacsconf-talk-file o "--answers--chapters.vtt") @@ -581,8 +667,6 @@ ${categories} (plist-get track :webchat-url) (plist-get track :channel)))) -(defvar emacsconf-publish-include-pads nil "When non-nil, include Etherpad info.") - (defun emacsconf-publish-format-talk-schedule-info (o) "Format schedule information for O." (let ((friendly (concat "/" emacsconf-year "/talks/" (plist-get o :slug) )) @@ -603,9 +687,18 @@ ${categories} ("irc" "IRC") (_ (plist-get o :qa-type))) (emacsconf-surround " <" (and (member emacsconf-publishing-phase '(schedule conference)) - (plist-get o :qa-url)) ">" "")) + (plist-get o :qa-url)) ">" "") + (if (string= (plist-get o :qa-type) "pad") + "" + (format " Etherpad: <%s>" + (plist-get o :pad-url) + ) + )) (concat (or (plist-get o :video-time) - (plist-get o :time)) "-min talk cancelled")) + (plist-get o :time)) + (if (string= (plist-get o :status) "CANCELLED") + "-min talk cancelled" + "-min talk"))) :pad-info (if (and talk-p emacsconf-publish-include-pads (not (and (member emacsconf-publishing-phase '(schedule conference)) (string= (plist-get o :qa-type) "etherpad")))) @@ -657,7 +750,7 @@ ${alternate-apac-info}\n"))) (and (string= (plist-get talk :public-email) "t") (plist-get talk :email)) (plist-get talk :public-email) - "emacsconf-org-private@gnu.org")))) + emacsconf-fallback-email)))) (defun emacsconf-publish-captions-in-wiki (talk) "Copy the captions file." @@ -720,7 +813,7 @@ This includes the intro note, the schedule, and talk resources." (setq info (or info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) (with-temp-file (expand-file-name (format "%s-before.md" (plist-get talk :slug)) (expand-file-name "info" (expand-file-name emacsconf-year emacsconf-directory))) - + (hack-dir-local-variables-non-file-buffer) (insert "<!-- Automatically generated by emacsconf-publish-before-page -->\n") (insert (emacsconf-surround "" (plist-get talk :intro-note) "\n\n" "")) (let ((is-live (emacsconf-talk-live-p talk))) @@ -737,14 +830,15 @@ This includes the intro note, the schedule, and talk resources." (defun emacsconf-format-transcript-from-list (subtitles video-id &optional lang) "Return subtitle directives for SUBTITLES." - (when (stringp subtitles) (setq subtitles (subed-parse-file subtitles))) + (let ((subed-sanitize-functions nil)) + (when (stringp subtitles) (setq subtitles (subed-parse-file subtitles)))) (mapconcat (lambda (sub) (let ((msecs (elt sub 1))) (concat (if (and (elt sub 4) (not (string= (elt sub 4) ""))) - (format "\n[[!template new=\"1\" text=\"\"\"%s\"\"\" start=\"%s\" video=\"%s\" id=\"subtitle\"%s]]\n\n" - (string-trim (replace-regexp-in-string "^NOTE[ \n]" "" (elt sub 4))) + (format "\n<div class=\"transcript-heading\">[[!template new=\"1\" text=\"\"\"%s\"\"\" start=\"%s\" video=\"%s\" id=\"subtitle\"%s]]</div>" + (replace-regexp-in-string "\\." "\\\\." (string-trim (replace-regexp-in-string "^NOTE[ \n]" "" (elt sub 4)))) (concat (format-seconds "%02h:%02m:%02s" (/ (floor msecs) 1000)) "." (format "%03d" (mod (floor msecs) 1000))) video-id @@ -752,8 +846,11 @@ This includes the intro note, the schedule, and talk resources." "") (format "[[!template text=\"\"\"%s\"\"\" start=\"%s\" video=\"%s\" id=\"subtitle\"%s]]" - (replace-regexp-in-string "^#" "\\\\#" - (replace-regexp-in-string "\"" """ (elt sub 3))) + (replace-regexp-in-string + "^#" "\\\\#" + (replace-regexp-in-string + "*" "\\\\*" + (replace-regexp-in-string "\"" """ (elt sub 3)))) (concat (format-seconds "%02h:%02m:%02s" (/ (floor msecs) 1000)) "." (format "%03d" (mod (floor msecs) 1000))) video-id @@ -763,7 +860,9 @@ This includes the intro note, the schedule, and talk resources." (defun emacsconf-publish-format-transcript (talk &optional video-id lang title) "Format the transcript for TALK, adding paragraph markers when possible." (require 'subed) - (let* ((subtitles + (setq video-id (or video-id "mainVideo")) + (let* ((subed-sanitize-functions nil) + (subtitles (subed-parse-file (if lang (format "%s_%s.vtt" (file-name-sans-extension @@ -771,14 +870,14 @@ This includes the intro note, the schedule, and talk resources." lang) (plist-get talk :caption-file))))) (if subtitles - (format "<a name=\"%s-%s-transcript%s\"></a> -# %s%s + (format "<div class=\"transcript%s\"><a name=\"%s-%s-transcript%s\"></a><h1>%s%s</h1> %s -" +</div>" + (if video-id (concat " transcript-" video-id) "") (plist-get talk :slug) - (or video-id "mainVideo") + video-id (emacsconf-surround "-" lang "" "") (if lang (assoc-default lang emacsconf-publish-subtitle-languages) (or title "Transcript")) (if (emacsconf-captions-edited-p (plist-get talk :caption-file)) @@ -827,12 +926,13 @@ This includes captions, contact, and an invitation to participate." ;; Contact information (with-temp-file (expand-file-name (format "%s-after.md" (plist-get talk :slug)) (expand-file-name "info" (expand-file-name emacsconf-year emacsconf-directory))) + (hack-dir-local-variables-non-file-buffer) (insert "<!-- Automatically generated by emacsconf-publish-after-page -->\n" "\n\n" ;; main transcript (if (plist-get talk :public) (emacsconf-publish-format-captions talk) "") - (if (emacsconf-talk-file talk "--answers.vtt") + (if (and (plist-get talk :qa-public) (emacsconf-talk-file talk "--answers.vtt")) (emacsconf-publish-format-transcript (append (list :chapter-file (emacsconf-talk-file talk "--answers--chapters.vtt") @@ -1180,7 +1280,7 @@ You can also get this schedule as iCalendar files: ${icals}. Importing that into (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) + ((or 'cfp 'program 'harvest 'resources) (with-temp-file (expand-file-name "schedule-details.md" (expand-file-name emacsconf-year emacsconf-directory)) (emacsconf-publish-program-without-times info)) @@ -1239,7 +1339,9 @@ You can also get this schedule as iCalendar files: ${icals}. Importing that into :time (plist-get o :time) :q-and-a (plist-get o :qa-link) :note (plist-get o :sched-note) - :pad (and emacsconf-publish-include-pads (plist-get o :pad-url)) + :pad (and emacsconf-publish-include-pads + (not (string= "pad" (plist-get o :qa-type))) + (plist-get o :pad-url)) :startutc (format-time-string "%FT%T%z" (plist-get o :start-time) t) :endutc (format-time-string "%FT%T%z" (plist-get o :end-time) t) :start (format-time-string "%-l:%M" (plist-get o :start-time) emacsconf-timezone) @@ -1278,6 +1380,8 @@ You can also get this schedule as iCalendar files: ${icals}. Importing that into (or (plist-get o :toobnix-url) (plist-get o :video-file))) "video posted") + (when (plist-get o :qa-public) + "Q&A posted") (emacsconf-surround "video: " (plist-get o :video-duration) "" nil) (emacsconf-surround "answers: " (and (plist-get o :qa-public) (plist-get o :qa-video-duration)) @@ -1387,10 +1491,15 @@ If MODIFY-FUNC is specified, use it to modify the talk." f)))) talks "\n"))) -(defun emacsconf-publish-backstage-index (&optional filename) +(defun emacsconf-publish-backstage-index (&optional filename dest) "Render the backstage index to FILENAME." - (interactive) - (setq filename (or filename (expand-file-name "index.html" emacsconf-backstage-dir))) + (interactive (list nil (if current-prefix-arg + emacsconf-cache-dir + emacsconf-backstage-dir))) + (setq dest (or dest + emacsconf-cache-dir)) + (emacsconf-current-org-notebook-refresh-schedule) + (setq filename (or filename (expand-file-name "index.html" dest))) (let ((info (or emacsconf-schedule-draft (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))) (with-temp-file filename (let* ((talks @@ -1398,9 +1507,9 @@ If MODIFY-FUNC is specified, use it to modify the talk." (lambda (o) (append (list :captions-edited t :backstage t) o)) - (emacsconf-filter-talks info))) + (emacsconf-filter-talks info))) (by-status (seq-group-by (lambda (o) (plist-get o :status)) talks)) - (files (directory-files emacsconf-backstage-dir))) + (files (directory-files dest))) ;emacsconf-backstage-dir very sislow right now (insert "<html><head><meta charset=\"UTF-8\"><link rel=\"stylesheet\" href=\"/style.css\" /></head><body>" (if (file-exists-p (expand-file-name "include-in-index.html" emacsconf-cache-dir)) @@ -1445,7 +1554,7 @@ If MODIFY-FUNC is specified, use it to modify the talk." ", "))) (pcase emacsconf-publishing-phase ((or 'program 'schedule 'conference) - '("TO_CONFIRM" "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" "TO_ARCHIVE" "TO_REVIEW_QA")) ((or 'harvest 'resources) '("TO_ARCHIVE" "TO_REVIEW_QA" "TO_INDEX_QA" "TO_CAPTION_QA" "DONE"))) "") @@ -1560,7 +1669,10 @@ answers without needing to listen to everything again. You can see <a href=\"htt (if (file-exists-p (expand-file-name "include-in-index-footer.html" emacsconf-cache-dir)) (with-temp-buffer (insert-file-contents (expand-file-name "include-in-index-footer.html" emacsconf-cache-dir)) (buffer-string)) "") - "</body></html>"))))) + "</body></html>")))) + (when (and (called-interactively-p 'any) + (eq (window-system) 'x)) + (emacsconf-backstage-web))) (defun emacsconf-publish-filter-public-files (talk &optional selector files) "Return files that are okay to post publicly for TALK." @@ -1574,7 +1686,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" "backstage"))) + (or "reencoded" "normalized" "final" "old" "bbb" "backstage" "pad" "silences"))) nil) ((rx ".diff") nil) ((rx "--original") @@ -1598,15 +1710,17 @@ answers without needing to listen to everything again. You can see <a href=\"htt files))) (defun emacsconf-publish-public-index-for-talk (o files) - (format "<li><div class=\"title\"><a name=\"%s\" href=\"%s\">%s</a></div></div><div class=\"speakers\">%s</div>%s</li>%s" + (format "<li><div class=\"title\"><a name=\"%s\" href=\"%s\">%s</a></div></div>%s%s</li>%s" (plist-get o :slug) (plist-get o :absolute-url) (plist-get o :title) - (plist-get o :speakers) + (emacsconf-surround "<div class=\"speakers\">" (plist-get o :speakers) "</div>" "") (emacsconf-publish-index-card (append (list :files (seq-remove (lambda (f) (string-match "--answers" f)) (emacsconf-publish-filter-public-files o)) + :caption-file + (emacsconf-talk-file o "--main.vtt") :audio-file (emacsconf-talk-file o "--main.opus") :links @@ -1627,7 +1741,8 @@ answers without needing to listen to everything again. You can see <a href=\"htt "\">Play recording from BigBlueButton</a></li>" "")))) o)) (if (or (emacsconf-talk-file o "--answers.webm") - (emacsconf-talk-file o "--answers.opus")) + (emacsconf-talk-file o "--answers.opus") + (emacsconf-get-preferred-video (concat (plist-get o :file-prefix) "--answers"))) (format "<li><div class=\"title\"><a href=\"%s\">Q&A for %s</a></div>%s</li>" (plist-get o :absolute-url) (plist-get o :title) @@ -1641,13 +1756,16 @@ answers without needing to listen to everything again. You can see <a href=\"htt :video-file-size (plist-get o :qa-video-file-size) :video-file (plist-get o :qa-video-file) :audio-file (emacsconf-talk-file o "--answers.opus") + :caption-file (emacsconf-talk-file o "--answers.vtt") + :youtube-url (plist-get o :qa-youtube-url) + :toobnix-url (plist-get o :qa-toobnix-url) :files (emacsconf-publish-filter-public-files o "--answers" files)) - o))) + o nil))) ""))) -;; (emacsconf-publish-public-index-for-talk (emacsconf-resolve-talk "rms") (directory-files emacsconf-cache-dir)) +;; (emacsconf-publish-public-index-for-talk (emacsconf-resolve-talk "gmail") (directory-files emacsconf-public-media-directory)) (defun emacsconf-publish-public-index (&optional filename) (interactive (list (expand-file-name "index.html" emacsconf-public-media-directory))) @@ -1674,7 +1792,7 @@ answers without needing to listen to everything again. You can see <a href=\"htt "")) "<html><head><meta charset=\"utf-8\" /></head><link rel=\"stylesheet\" href=\"/style.css\" /><body> <h1>${conf-name} ${year}</h1> -<div class=\"m3u\"><a href=\"index.m3u\">M3U playlist for playing in MPV and other players</a></div> +<div class=\"m3u\"><a href=\"index.m3u\">M3U playlist for playing in MPV and other players</a> | <a href=\"schedules/\">Schedules</a></div> <div>Quick links: ${quick-links}</div> <ol class=\"videos\">${videos}</ol> ${include} @@ -1698,12 +1816,13 @@ ${include} (emacsconf-publish-index-card (append (list :base-url (concat emacsconf-media-base-url (plist-get f :conf-year) "/") + :caption-file (emacsconf-talk-file f "--main.vtt") :track-base-url (format "/%s/captions/" (plist-get f :conf-year)) :links (unless (eq emacsconf-publishing-phase 'resources) (emacsconf-surround "<li><a href=\"" - (plist-get o :bbb-rec) + (plist-get f :bbb-rec) "\">Play recording from BigBlueButton</a></li>" "")) :files @@ -1714,6 +1833,7 @@ ${include} (if (plist-get f :qa-public) (emacsconf-publish-index-card (append + f (list :public 1 :base-url (concat emacsconf-media-base-url (plist-get f :conf-year) "/") @@ -1721,10 +1841,12 @@ ${include} :track-base-url (format "/%s/captions/" (plist-get f :conf-year)) :video-file - (emacsconf-talk-file f "--answers.webm") + (plist-get f :qa-video-file) + :caption-file (emacsconf-talk-file f "--answers.vtt") + :youtube-url (plist-get f :qa-youtube-url) :files (emacsconf-publish-filter-public-files f files "--answers")) - f)) + )) ""))) info "\n")) "</ol>"))) @@ -1785,26 +1907,43 @@ ${include} (defun emacsconf-publish-link-file-formats (file-prefix) (string-join (emacsconf-publish-link-file-formats-as-list file-prefix) " ")) -(defun emacsconf-publish-link-file-formats-as-list (talk) - (seq-map - (lambda (file) - (let ((cache-file (expand-file-name (file-name-nondirectory file) emacsconf-cache-dir))) - (format "<a href=\"%s%s\">Download %s%s</a>%s" - (or (plist-get talk :base-url) "") - (file-name-nondirectory file) - (replace-regexp-in-string (concat "^" (regexp-quote (plist-get talk :file-prefix))) "" (file-name-nondirectory file)) - (if (and (file-exists-p cache-file) - (> (file-attribute-size (file-attributes cache-file)) 1000000)) - (format " (%sB)" (file-size-human-readable (file-attribute-size (file-attributes cache-file)))) - "") - (if (and (string-match "--\\(main\\|answers\\)\\.vtt" file) - (not (emacsconf-captions-edited-p (expand-file-name file emacsconf-cache-dir)))) - " (unedited)" - "")))) - (or (plist-get talk :files) - (if (plist-get talk :backstage) - (emacsconf-publish-talk-files talk) - (emacsconf-publish-filter-public-files talk))))) +(defun emacsconf-publish-file-description (talk file) + (let ((cache-file (expand-file-name (file-name-nondirectory file) emacsconf-cache-dir))) + (concat + (replace-regexp-in-string (concat "^" (regexp-quote (plist-get talk :file-prefix))) "" (file-name-nondirectory file)) + (if + (and + (file-exists-p cache-file) + (> (file-attribute-size (file-attributes cache-file)) 1000000)) + (format " (%sB)" (file-size-human-readable (file-attribute-size (file-attributes cache-file)))) + "")))) + +(defun emacsconf-publish-link-file-formats-as-list (talk &optional files) + (let ((public-files (or files (emacsconf-publish-filter-public-files talk)))) + (seq-map + (lambda (file) + (let ((cache-file (expand-file-name (file-name-nondirectory file) emacsconf-cache-dir))) + (format "<a href=\"%s%s\">Download %s%s</a>%s%s" + (or (plist-get talk :base-url) "") + (file-name-nondirectory file) + (replace-regexp-in-string (concat "^" (regexp-quote (plist-get talk :file-prefix))) "" (file-name-nondirectory file)) + (if (and (file-exists-p cache-file) + (> (file-attribute-size (file-attributes cache-file)) 1000000)) + (format " (%sB)" (file-size-human-readable (file-attribute-size (file-attributes cache-file)))) + "") + (if (and (string-match "--\\(main\\|answers\\)\\.vtt" file) + (not (emacsconf-captions-edited-p (expand-file-name file emacsconf-cache-dir)))) + " (unedited)" + "") + (if (and (plist-get talk :backstage) + (not (member file public-files))) + " (backstage)" + "") + ))) + (or (plist-get talk :files) + (if (plist-get talk :backstage) + (emacsconf-publish-talk-files talk) + public-files))))) (defun emacsconf-publish-talks-json () "Return JSON format with a subset of talk information." @@ -1902,8 +2041,8 @@ ${include} (if (file-exists-p qa-video) (string-trim (shell-command-to-string (concat "sha1sum -b " (shell-quote-argument qa-video) " | cut -d ' ' -f 1"))) "") - (or (plist-get o :qa-youtube) "") - (or (plist-get o :qa-toobnix) ""))))))) + (or (plist-get o :qa-youtube-url) "") + (or (plist-get o :qa-toobnix-url) ""))))))) (emacsconf-public-talks (emacsconf-get-talk-info)))))) (insert (orgtbl-to-csv (cons '("Conference" "Slug" "Title" "Speakers" "Talk page URL" "Video URL" "Date" "Duration" "SHA" "Youtube URL" "Toobnix URL") @@ -2036,8 +2175,11 @@ ${include} (concat (regexp-quote (if suffix (concat file-prefix "--" suffix) file-prefix)) - "\\." (regexp-opt emacsconf-media-extensions)) s)) files)) - '("main" "captioned" "normalized" "reencoded" "compressed" "original" nil))) + "\\.\\(" (regexp-opt emacsconf-media-extensions) + "\\)") + s)) + files)) + '("main" "captioned" "normalized" "reencoded" "compressed" "original"))) (seq-find 'file-exists-p (seq-map (lambda (suffix) @@ -2112,58 +2254,39 @@ This video is available under the terms of the Creative Commons Attribution-Shar (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 () +(defun emacsconf-publish-youtube-step-through-publishing-talk (talk) + (interactive (list (emacsconf-complete-talk-info + (seq-remove + (lambda (talk) + (or (not (plist-get talk :video-file)) + (plist-get talk :youtube-url))) + (emacsconf-get-talk-info))))) + (kill-new (plist-get talk :video-file)) + (y-or-n-p (format "Video: %s - create video and upload this filename. Done?" (plist-get talk :video-file))) + (kill-new (emacsconf-publish-video-description talk t)) + (y-or-n-p "Copied description. Paste into description, move first line to title, add to playlist. Done?") + (when (emacsconf-talk-file talk "--main.vtt") + (kill-new (emacsconf-talk-file talk "--main.vtt")) + (y-or-n-p (format "Captions: %s. Add to video elements. Done?" (emacsconf-talk-file talk "--main.vtt")))) + (emacsconf-set-property-from-slug + (plist-get talk :slug) + "YOUTUBE_URL" + (read-string (format "%s - YouTube URL: " (plist-get talk :scheduled))))) + +(defun emacsconf-publish-youtube-step-through-publishing-all () (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))))) + (and (member (plist-get o :status) '("TO_STREAM" "TO_CHECK" "PLAYING" "TO_ARCHIVE")) + (not (plist-get o :youtube-url)) + (plist-get o :video-file))) + (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-youtube-step-through-publishing-talk talk))))) + ;; (emacsconf-publish-video-description (emacsconf-find-talk-info "async") t) @@ -2185,7 +2308,9 @@ This video is available under the terms of the Creative Commons Attribution-Shar (emacsconf-with-talk-heading talk (let* ((video-file-name (emacsconf-get-preferred-video (plist-get talk :file-prefix))) (video-file (and video-file-name (expand-file-name video-file-name emacsconf-cache-dir))) - (qa-file (emacsconf-talk-file talk "--answers.webm")) + (qa-file-name (or (emacsconf-talk-file talk "--answers.webm") + (emacsconf-get-preferred-video (concat (plist-get talk :file-prefix) "--answers")))) + (qa-file (and qa-file-name (expand-file-name qa-file-name emacsconf-cache-dir))) (intro-file (expand-file-name (concat (plist-get talk :slug) ".webm") (expand-file-name "intros" emacsconf-stream-asset-dir))) duration) @@ -2243,16 +2368,7 @@ This video is available under the terms of the Creative Commons Attribution-Shar (browse-url (format "https://studio.youtube.com/video/%s/edit" (match-string 1 url)))) (browse-url (concat "https://studio.youtube.com/channel/" emacsconf-youtube-channel-id))))) -(defun emacsconf-toobnix-edit () - (interactive) - (let ((url (org-entry-get (point) "TOOBNIX_URL"))) - (if url - (when (string-match "/w/\\([A-Za-z0-9]+\\)" url) - (browse-url (format "https://toobnix.org/videos/update/%s" (match-string 1 url)))) - (when (> (length (org-entry-get (point) "FILE_PREFIX")) 80) - (copy-file (expand-file-name (concat (org-entry-get (point) "FILE_PREFIX") "--main.webm") emacsconf-cache-dir) - (expand-file-name (concat "emacsconf-" emacsconf-year "-" (org-entry-get (point) "SLUG") ".webm") emacsconf-cache-dir) t)) - (browse-url "https://toobnix.org/videos/upload#upload")))) + (defun emacsconf-publish-files () (interactive) @@ -2434,6 +2550,7 @@ For better performance, we recommend watching <a href=\"${stream-hires}\">${stre <li>mpv ${stream-hires}</li> <li>vlc ${stream-hires}</li> <li>ffplay ${stream-hires}</li> +<li>You can also watch it in VLC by choosing menu - Media - Open Network Stream and putting in " (plist-get track :stream) "</li> </ul> If you have limited bandwidth, you can watch the low-res stream <a href=\"${480p}\">${480p}</a>. @@ -2537,6 +2654,8 @@ vlc https://live0.emacsconf.org/gen.webm ffplay https://live0.emacsconf.org/gen.webm </pre> +<p>You can also watch it in VLC by using menu - Media - Open Network Stream and putting in <code>https://live0.emacsconf.org/gen.webm</code></p>. + <p>If you experience any disruptions, try reloading the page you're using to watch the video. If that still doesn't work, please check our status page at <a href=\"https://status.emacsconf.org\">https://status.emacsconf.org</a> for updates on the @@ -2653,17 +2772,19 @@ The Q&A room for ${title} has finished. You can find more information about the There is no live Q&A room for ${title}. You can find more information about the talk at <a href=\"${base-url}${url}\">${base-url}${url}</a>.</body></html>" ))))))) -(defun emacsconf-publish-media-files-on-change (talk) +(defun emacsconf-publish-media-files-on-change (talk &optional always-update) "Publish the files and update the index." - (interactive (list (emacsconf-complete-talk-info))) + (interactive (list (emacsconf-complete-talk-info) current-prefix-arg)) (let ((org-state (if (boundp 'org-state) org-state (plist-get talk :status)))) (if (plist-get talk :public) ;; Copy main files from backstage to public (let ((public-files (emacsconf-publish-filter-public-files talk))) (mapc (lambda (file) - (when (not (file-exists-p (expand-file-name file emacsconf-public-media-directory))) - (copy-file (expand-file-name file emacsconf-backstage-dir) - (expand-file-name file emacsconf-public-media-directory) t))) + (when (or always-update (not (file-exists-p (expand-file-name file emacsconf-public-media-directory)))) + (copy-file (if (file-exists-p (expand-file-name file emacsconf-backstage-dir)) + (expand-file-name file emacsconf-backstage-dir) + (expand-file-name file emacsconf-cache-dir)) + (expand-file-name file emacsconf-public-media-directory) t))) public-files)) ;; Remove files from public (let ((files (directory-files emacsconf-public-media-directory nil @@ -2722,43 +2843,7 @@ This video is available under the terms of the Creative Commons Attribution-Shar (emacsconf-with-talk-heading talk)) result)) -(defvar emacsconf-publish-toobnix-upload-command "peertube-cli") -(defvar emacsconf-publish-toobnix-channel "EmacsConf") -;; (defun emacsconf-publish-get-toobnix-token () -;; (let ((secrets (plz 'get "https://toobnix.org/api/v1/oauth-clients/local" :as #'json-read))) - -;; ) - -;; ) -(defun emacsconf-publish-upload-to-toobnix (properties) - "Uses peertube-cli: https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/tools.md" - (with-temp-buffer - (let ((arguments - (append - (list "upload" "-f" (plist-get properties :file)) - (when (plist-get properties :title) - (list "-n" (plist-get properties :title))) - (when (plist-get properties :description) - (list "-d" (plist-get properties :description))) - (list "-L" "en" - "-C" emacsconf-publish-toobnix-channel - "-l" "2" - "-c" "15" - "-P" (if (string= (plist-get properties :privacy) "public") "1" "2") "-t" - (cond - ((stringp (plist-get properties :tags)) - (plist-get properties :tags)) - ((listp (plist-get properties :tags)) - (string-join (plist-get properties :tags) ",")) - (t "emacs")))))) - (kill-new (mapconcat - #'shell-quote-argument - (append (list emacsconf-publish-toobnix-upload-command) arguments) - " ")) - (apply #'call-process - emacsconf-publish-toobnix-upload-command - nil t t arguments) - (buffer-string)))) + ;; YouTube (defun emacsconf-publish-spookfox-update-youtube-video () @@ -2814,6 +2899,9 @@ Tends to be quota-limited, though." arguments) " ")) (with-current-buffer (get-buffer-create "*YouTube*") (erase-buffer) + (kill-new (concat (car emacsconf-publish-youtube-upload-command) + " " + (mapconcat #'shell-quote-argument arguments " "))) (apply #'call-process (car emacsconf-publish-youtube-upload-command) nil t t @@ -2851,29 +2939,37 @@ Tends to be quota-limited, though." "This video is available under the terms of the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.\n"))) (defun emacsconf-publish-answers-description (talk platform) - (let ((title (concat emacsconf-name " " emacsconf-year " Q&A: " (plist-get talk :title)))) - (concat - (if (< (length title) 100) "" (concat title "\n")) - (plist-get talk :speakers-with-pronouns) "\n\n" - "This is the Q&A for the talk at " - (plist-get talk - (if (eq platform 'toobnix) :toobnix-url :youtube-url)) " .\n\n" - (if (emacsconf-talk-file talk "--answers--chapters.vtt") - (let ((chapters (subed-parse-file (emacsconf-talk-file talk "--answers--chapters.vtt")))) - (concat - (mapconcat - (lambda (chapter) - (concat - (if (= (elt chapter 1) 0) - "00:00" - (format-seconds "%.2h:%z%.2m:%.2s" (floor (/ (elt chapter 1) 1000)))) - " " (elt chapter 3) "\n")) - chapters - "") - "\n")) - "") - "You can view this and other resources using free/libre software at " (plist-get talk :absolute-url) " .\n" - "This video is available under the terms of the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.\n"))) + (interactive (list (emacsconf-complete-talk-info) 'youtube)) + (let ((title (concat emacsconf-name " " emacsconf-year " Q&A: " (plist-get talk :title))) + s) + (setq s + (concat + (if (and (not (called-interactively-p 'any)) (< (length title) 100)) + "" + (concat title "\n")) + (plist-get talk :speakers-with-pronouns) "\n\n" + "This is the Q&A for the talk at " + (plist-get talk + (if (eq platform 'toobnix) :toobnix-url :youtube-url)) " .\n\n" + (if (emacsconf-talk-file talk "--answers--chapters.vtt") + (let ((chapters (subed-parse-file (emacsconf-talk-file talk "--answers--chapters.vtt")))) + (concat + (mapconcat + (lambda (chapter) + (concat + (if (= (elt chapter 1) 0) + "00:00" + (format-seconds "%.2h:%z%.2m:%.2s" (floor (/ (elt chapter 1) 1000)))) + " " (elt chapter 3) "\n")) + chapters + "") + "\n")) + "") + "You can view this and other resources using free/libre software at " (plist-get talk :absolute-url) " .\n" + "This video is available under the terms of the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.\n")) + (when (called-interactively-p 'any) + (kill-new s)) + s)) ;; (emacsconf-publish-answers-description (emacsconf-resolve-talk "async") 'toobnix) (defvar emacsconf-publish-talk-video-tags (format "emacs,%s,%s%s" emacsconf-id emacsconf-id emacsconf-year) @@ -2899,7 +2995,7 @@ Tends to be quota-limited, though." :tags emacsconf-publish-talk-video-tags :playlist (concat emacsconf-name " " emacsconf-year) :date (plist-get talk :start-time) - :privacy (if (plist-get talk :public) "public" "unlisted") + :privacy (if (plist-get talk :qa-public) "public" "unlisted") :title (if (< (length title) 100) title (concat (substring title 0 97) "...")) :description (emacsconf-publish-answers-description talk platform)))) @@ -2908,19 +3004,32 @@ Tends to be quota-limited, though." (list (emacsconf-complete-talk-info) (intern (completing-read "Platform: " '("youtube" "toobnix"))))) (let ((file (emacsconf-talk-file talk "--main.webm")) + (props (emacsconf-publish-talk-video-properties talk platform)) + url output) (when (and file (not (plist-get talk (if (eq platform 'toobnix) :toobnix-url :youtube-url)))) (setq output (funcall (if (eq platform 'toobnix) - #'emacsconf-publish-upload-to-toobnix + #'emacsconf-toobnix-upload #'emacsconf-publish-upload-to-youtube) - (emacsconf-publish-talk-video-properties talk platform))) - (when (and (string-match "Video URL: \\(.*+\\)" output) (eq platform 'youtube)) - (setq output (match-string 1 output)) - (save-window-excursion - (emacsconf-go-to-talk talk) - (org-entry-put (point) "YOUTUBE_URL" output))) + props)) + (pcase platform + ('youtube + (when (string-match "Video URL: \\(.*+\\)" output) + (setq url (match-string 1 output)) + (save-window-excursion + (emacsconf-go-to-talk talk) + (org-entry-put (point) "YOUTUBE_URL" url)))) + ('toobnix + (setq url (emacsconf-toobnix-latest-video-url props)) + (save-window-excursion + (emacsconf-go-to-talk talk) + (org-entry-put + (point) + "TOOBNIX_URL" + url)))) + (message "Uploaded talk for %s: %s" (plist-get talk :slug) url) output))) (defun emacsconf-publish-upload-answers (talk platform) @@ -2928,32 +3037,40 @@ Tends to be quota-limited, though." (intern (completing-read "Platform: " '("youtube" "toobnix"))))) (let ((file (emacsconf-talk-file talk "--answers.webm")) (title (concat emacsconf-name " " emacsconf-year " Q&A: " (plist-get talk :title))) - output) - (when (and file (not (plist-get talk (if (eq platform 'toobnix) :qa-toobnix :qa-youtube)))) + output + (props (emacsconf-publish-answers-video-properties talk platform)) + url) + (when (and file (not (plist-get talk (if (eq platform 'toobnix) :qa-toobnix-url :qa-youtube-url)))) (setq output (funcall (if (eq platform 'toobnix) - #'emacsconf-publish-upload-to-toobnix + #'emacsconf-toobnix-upload #'emacsconf-publish-upload-to-youtube) - (list - :file file - :tags "emacs,emacsconf,answers" - :playlist (concat emacsconf-name " " emacsconf-year) - :date (plist-get talk :start-time) - :title (if (< (length title) 100) title (concat (substring title 0 97) "...")) - :description (emacsconf-publish-answers-description talk platform)))) - (when (string-match "Video URL: \\(.*+\\)" output) - (setq output (match-string 1 output)) - (save-window-excursion - (emacsconf-go-to-talk talk) - (org-entry-put (point) "QA_YOUTUBE" output))) + props)) + (pcase platform + ('youtube + (when (string-match "Video URL: \\(.*+\\)" output) + (setq url (match-string 1 output)) + (save-window-excursion + (emacsconf-go-to-talk talk) + (org-entry-put (point) "QA_YOUTUBE_URL" url)))) + ('toobnix + (setq url (emacsconf-toobnix-latest-video-url props)) + (save-window-excursion + (emacsconf-go-to-talk talk) + (org-entry-put + (point) + "QA_TOOBNIX_URL" + url)))) + (message "Uploaded Q&A for %s: %s" (plist-get talk :slug) url) output))) (defun emacsconf-publish-upload-answers-to-youtube (talk) - (let ((file (emacsconf-talk-file talk "--answers.webm")) + (interactive (list (emacsconf-complete-talk-info))) + (let ((file (plist-get talk :qa-video-file)) (title (concat emacsconf-name " " emacsconf-year " Q&A: " (plist-get talk :title))) output) - (when (and file (not (plist-get talk :qa-youtube))) + (when (and file (not (plist-get talk :qa-youtube-url))) (setq output (emacsconf-publish-upload-to-youtube (list @@ -3020,5 +3137,119 @@ Tends to be quota-limited, though." (expand-file-name (concat (plist-get talk :file-prefix) "--intro.webm") emacsconf-backstage-dir) t)))) + +(defun emacsconf-publish-update-transcript () + (interactive) + (emacsconf-subed-make-chapter-file-based-on-comments) + (let ((talk (emacsconf-resolve-talk (emacsconf-get-slug-from-string (buffer-file-name))))) + (emacsconf-publish-media-files-on-change talk) + (emacsconf-publish-with-wiki-change + (emacsconf-publish-captions-in-wiki talk) + (emacsconf-publish-info-pages-for-talk talk)))) + +;; for emacs.tv +(defun emacsconf-publish-insert-video-entries (&optional info tags) + (interactive) + (setq tags (or tags (format ":emacsconf:emacsconf%s:" emacsconf-year))) + (dolist (talk (emacsconf-publish-prepare-for-display (or info (emacsconf-get-talk-info)))) + (when (emacsconf-talk-file talk "--main.webm") + (let ((new-entry (emacsconf-replace-plist-in-string + (append + (list + :conf-name emacsconf-name + :conf-year emacsconf-year + :media-url (format "https://media.emacsconf.org/%s/%s--main.webm" + emacsconf-year + (plist-get talk :file-prefix)) + :transcript-url + (if (emacsconf-talk-file "--main.vtt" + (format "https://media.emacsconf.org/%s/%s--main.vtt" + emacsconf-year + (plist-get talk :file-prefix))) + "") + :duration (or (plist-get talk :qa-video-duration) + (emacsconf-format-seconds + (/ (compile-media-get-file-duration-ms (emacsconf-talk-file talk "--main.webm")) + 1000))) + :url (concat emacsconf-base-url (plist-get talk :url)) + :tags (if (plist-get talk :tags) (concat tags (substring (plist-get talk :tags) 1)) tags) + :date (format-time-string "%FT%T%z" (plist-get talk :start-time) t)) + talk + (list :youtube-url "" :toobnix-url "" :speakers "")) + "* ${title} ${tags} +:PROPERTIES: +:DATE: ${date} +:URL: ${url} +:DURATION: ${duration} +:MEDIA_URL: ${media-url} +:YOUTUBE_URL: ${youtube-url} +:TOOBNIX_URL: ${toobnix-url} +:TRANSCRIPT_URL: ${transcript-url} +:SPEAKERS: ${speakers} +:SERIES: ${conf-name} ${conf-year} +:END: +" + ))) + (with-current-buffer (find-file-noselect emacstv-index-org) + (if (and (plist-get talk :youtube-url) (emacstv-find-by-youtube-url (plist-get talk :youtube-url))) + (org-entry-put (point) "DATE" (format-time-string "%FT%T%z" (plist-get talk :start-time) t)) + (goto-char (point-max)) + (insert new-entry))))))) + +(defun emacsconf-publish-insert-video-entries-for-answers (&optional info tags) + (interactive) + (setq tags (or tags (format ":answers:emacsconf:emacsconf%s:" emacsconf-year))) + (dolist (talk (emacsconf-publish-prepare-for-display (or info (emacsconf-get-talk-info)))) + (when (emacsconf-talk-file talk "--answers.webm") + (let ((new-entry (emacsconf-replace-plist-in-string + (append + (list + :conf-name emacsconf-name + :conf-year emacsconf-year + :youtube-url (plist-get talk :qa-youtube-url) + :toobnix-url (plist-get talk :qa-toobnix-url) + :media-url (format "https://media.emacsconf.org/%s/%s--answers.webm" + emacsconf-year + (plist-get talk :file-prefix)) + :transcript-url (if (emacsconf-talk-file talk "--answers.vtt") + (format "https://media.emacsconf.org/%s/%s--answers.vtt" + emacsconf-year + (plist-get talk :file-prefix)) + "") + :url (concat emacsconf-base-url (plist-get talk :url)) + :duration (or (plist-get talk :qa-video-duration) + (emacsconf-format-seconds + (/ (compile-media-get-file-duration-ms (emacsconf-talk-file talk "--answers.webm")) + 1000))) + :tags (if (plist-get talk :tags) (concat tags (substring (plist-get talk :tags) 1)) tags) + :date (format-time-string "%FT%T%z" (plist-get talk :start-time) t)) + talk + (list :youtube-url "" :toobnix-url "" :speakers "")) + "* Q&A: ${title} ${tags} +:PROPERTIES: +:DATE: ${date} +:URL: ${url} +:DURATION: ${duration} +:MEDIA_URL: ${media-url} +:YOUTUBE_URL: ${youtube-url} +:TOOBNIX_URL: ${toobnix-url} +:TRANSCRIPT_URL: ${transcript-url} +:SPEAKERS: ${speakers} +:SERIES: ${conf-name} ${conf-year} +:END: +" + ))) + (with-current-buffer (find-file-noselect emacstv-index-org) + (if (and (plist-get talk :qa-youtube-url) (emacstv-find-by-youtube-url (plist-get talk :qa-youtube-url))) + (progn + (org-entry-put (point) "DATE" (format-time-string "%FT%T%z" (plist-get talk :start-time) t)) + (org-entry-put (point) + "TRANSCRIPT_URL" + (if (emacsconf-talk-file talk "--answers.vtt") + (format "https://media.emacsconf.org/%s/%s--answers.vtt" + emacsconf-year + (plist-get talk :file-prefix)) + ""))) + (insert new-entry))))))) ;; (provide 'emacsconf-publish) diff --git a/emacsconf-schedule.el b/emacsconf-schedule.el index 260ef9c..328fabb 100644 --- a/emacsconf-schedule.el +++ b/emacsconf-schedule.el @@ -29,16 +29,16 @@ "List of scheduling functions. Each function should take the info and manipulate it as needed, returning the new info.") -(defvar emacsconf-schedule-max-time 30) +(defvar emacsconf-schedule-max-time 40) (defun emacsconf-schedule-allocate-at-most (info) "Allocate at most `emacsconf-schedule-max-time' to the talks." (mapcar (lambda (o) - (when (plist-get o :max-time) - (plist-put o :time + (plist-put o :time (number-to-string (min - (string-to-number (plist-get o :max-time)) - emacsconf-max-time)))) + (string-to-number (or (plist-get o :time) + (plist-get o :max-time))) + emacsconf-schedule-max-time))) o) info)) @@ -109,6 +109,21 @@ Each function should take the info and manipulate it as needed, returning the ne emacsconf-schedule-tweak-allocations))) (emacsconf-schedule-prepare info))) +(defun emacsconf-schedule-prepare-test-schedule (start &optional info) + (interactive (list (org-read-date t t nil "Start time: "))) + (let* ((emacsconf-schedule-break-time 2) + (emacsconf-schedule-lunch-time 2) + (emacsconf-schedule-max-time 1) + (emacsconf-schedule-default-buffer-minutes 1) + (emacsconf-schedule-default-buffer-minutes-for-live-q-and-a 1) + (emacsconf-schedule-strategies '(emacsconf-schedule-allocate-buffer-time + emacsconf-schedule-override-breaks + emacsconf-schedule-allocate-buffer-time-at-most-max-time + emacsconf-schedule-allocate-max-time + emacsconf-schedule-allocate-at-most + emacsconf-schedule-tweak-allocations))) + (emacsconf-schedule-prepare info))) + (defun emacsconf-schedule-copy-previous-track (info) "Use :set-track to update INFO." (cl-loop with track = (plist-get (car info) :set-track) @@ -124,7 +139,8 @@ Each function should take the info and manipulate it as needed, returning the ne (plist-put o :buffer (number-to-string (if (string-match "live" (or (plist-get o :q-and-a) "")) - (min (string-to-number (plist-get o :max-time)) + (min (string-to-number (or (plist-get o :max-time) + (plist-get o :time))) emacsconf-schedule-default-buffer-minutes-for-live-q-and-a) emacsconf-schedule-default-buffer-minutes)))) o) @@ -258,8 +274,9 @@ Pairs with `emacsconf-schedule-dump-sexp'." (list "Status" "Slug" "Schedule" "Time" "Buffer" "Title" "Name" "Q&A" "Availability")) (mapcar #'emacsconf-schedule-format-summary-row (or info (emacsconf-get-talk-info))))) -(defun emacsconf-schedule-update-from-info (info) - (interactive (list (or emacsconf-schedule-draft (emacsconf-get-talk-info)))) +(defun emacsconf-schedule-update-from-info (&optional info) + (interactive) + (setq info (or info emacsconf-schedule-draft (emacsconf-get-talk-info))) (save-window-excursion (save-excursion (mapc (lambda (talk) @@ -270,40 +287,38 @@ Pairs with `emacsconf-schedule-dump-sexp'." (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)) - (read-string "Field: ") current-prefix-arg)) +(defun emacsconf-schedule-save-emailed-times (info &optional field) + (interactive (list (or emacsconf-schedule-draft (emacsconf-get-talk-info)))) + (setq field (or field "EMAILED_SCHEDULE")) (save-window-excursion (save-excursion (mapc (lambda (talk) (emacsconf-go-to-talk (plist-get talk :slug)) - (when (and (plist-get talk :scheduled) - (or force (null (org-entry-get (point) - (or field "ORIGINAL_SCHEDULE"))))) + (when (plist-get talk :scheduled) (org-entry-put (point) - (or field "ORIGINAL_SCHEDULE") + field (replace-regexp-in-string "[<>]" "" (plist-get talk :scheduled))))) (emacsconf-filter-talks info))))) (defvar emacsconf-schedule-svg-modify-functions '(emacsconf-schedule-svg-color-by-track) "Functions to run to modify the display of each item.") (defvar emacsconf-use-absolute-url nil "Non-nil means try to use absolute URLs.") -(defun emacsconf-schedule-svg-track (svg base-x base-y width height start-time end-time info) +(defun emacsconf-schedule-svg-track (svg base-x base-y width height start-time end-time info &optional direction) "Draw the actual rectangles and text for the talks." - (let ((scale (/ width (float-time (time-subtract end-time start-time))))) + (let ((scale (/ (if (eq direction 'vertical) height width) (float-time (time-subtract end-time start-time))))) (mapc (lambda (o) (let* ((offset (floor (* scale (float-time (time-subtract (plist-get o :start-time) start-time))))) (size (floor (* scale (float-time (time-subtract (plist-get o :end-time) (plist-get o :start-time)))))) - (x (+ base-x offset)) - (y base-y) + (x (if (eq direction 'vertical) base-x (+ base-x offset))) + (y (if (eq direction 'vertical) (+ base-y offset) base-y)) (node (dom-node 'rect (list (cons 'x x) (cons 'y y) (cons 'opacity "0.8") - (cons 'width size) - (cons 'height (1- height)) + (cons 'width (if (eq direction 'vertical) (1- width) size)) + (cons 'height (if (eq direction 'vertical) size (1- height))) (cons 'stroke "black") (cons 'stroke-dasharray (if (string-match "live" (or (plist-get o :q-and-a) "live")) @@ -336,7 +351,8 @@ Pairs with `emacsconf-schedule-dump-sexp'." (dom-node 'g `((transform . ,(format "translate(%d,%d)" - (+ x size -2) (+ y height -2)))) + (if (eq direction 'vertical) x (+ x size -2)) + (if (eq direction 'vertical) (+ y size -2) (+ y height -2))))) (dom-node 'text (list @@ -344,7 +360,7 @@ Pairs with `emacsconf-schedule-dump-sexp'." (cons 'x 0) (cons 'y 0) (cons 'font-size 10) - (cons 'transform "rotate(-90)")) + (cons 'transform (if (eq direction 'vertical) nil "rotate(-90)"))) (svg--encode-text (or (plist-get o :slug) (plist-get o :title)))))))) (run-hook-with-args 'emacsconf-schedule-svg-modify-functions @@ -354,44 +370,73 @@ Pairs with `emacsconf-schedule-dump-sexp'." parent))) info))) -(defun emacsconf-schedule-svg-day (elem label width height start end tracks) +(defun emacsconf-schedule-svg-day (elem label width height start end tracks &optional direction) "Add the time scale and the talks on a given day." - (let* ((label-margin 15) - (track-height (/ (- height (* 2 label-margin)) (length tracks))) - (x 0) (y label-margin) - (scale (/ width (float-time (time-subtract end start)))) + (let* ((label-margin (if (eq direction 'vertical) 40 15)) + (x (if (eq direction 'vertical) label-margin 0)) + (y (if (eq direction 'vertical) label-margin label-margin)) + (track-size (if (eq direction 'vertical) + (/ (- width (* 2 label-margin)) (length tracks)) + (/ (- height (* 2 label-margin)) (length tracks)))) + (scale (/ (if (eq direction 'vertical) height width) (float-time (time-subtract end start)))) (time start)) (dom-append-child elem (dom-node 'title nil (concat "Schedule for " label))) (svg-rectangle elem 0 0 width height :fill "white") - (svg-text elem label :x 3 :y (- label-margin 3) :fill "black" :font-size "10") + (if (eq direction 'vertical) + (svg-text elem label :x 3 :y (- label-margin 10) :fill "black" :font-size "10") + (svg-text elem label :x 3 :y (- label-margin 3) :fill "black" :font-size "10")) (mapc (lambda (track) (emacsconf-schedule-svg-track - elem x y width track-height - start end track) - (setq y (+ y track-height))) + elem x y + (if (eq direction 'vertical) track-size width) + (if (eq direction 'vertical) height track-size) + start end track + direction) + (if (eq direction 'vertical) + (setq x (+ x track-size)) + (setq y (+ y track-size)))) tracks) ;; draw grid (while (time-less-p time end) - (let ((x (* (float-time (time-subtract time start)) scale))) + (let ((x (if (eq direction 'vertical) + 3 + (* (float-time (time-subtract time start)) scale))) + (y (if (eq direction 'vertical) + (+ (* (float-time (time-subtract time start)) scale) label-margin) + 3))) (dom-append-child elem (dom-node 'g - `((transform . ,(format "translate(%d,%d)" x label-margin))) - (dom-node - 'line - `((stroke . "darkgray") - (x1 . 0) - (y1 . 0) - (x2 . 0) - (y2 . ,(- height label-margin label-margin)))) + `((transform . ,(format "translate(%d,%d)" x y))) + (if (eq direction 'vertical) + (dom-node + 'line + `((stroke . "darkgray") + (x1 . ,label-margin) + (y1 . 0) + (x2 . ,(- width label-margin)) + (y2 . 0))) + (dom-node + 'line + `((stroke . "darkgray") + (x1 . 0) + (y1 . 0) + (x2 . 0) + (y2 . ,(- height label-margin label-margin))))) (dom-node 'text - `((fill . "black") - (x . 0) - (y . ,(- height 2 label-margin)) - (font-size . 10) - (text-anchor . "left")) + (if (eq direction 'vertical) + `((fill . "black") + (x . 0) + (y . 0) + (font-size . 10) + (dy . ".4em")) + `((fill . "black") + (x . 0) + (y . ,(- height label-margin -5)) + (font-size . 10) + (text-anchor . "left"))) (svg--encode-text (format-time-string "%-l %p" time emacsconf-timezone))))) (setq time (time-add time (seconds-to-time 3600))))) elem)) @@ -411,7 +456,7 @@ Pairs with `emacsconf-schedule-dump-sexp'." "peachpuff") (t "gray")))) -(defun emacsconf-schedule-svg (width height &optional info) +(defun emacsconf-schedule-svg (width height &optional info direction) "Make the schedule SVG for INFO." (setq info (or info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) (let ((days (seq-group-by (lambda (o) @@ -430,7 +475,8 @@ Pairs with `emacsconf-schedule-dump-sexp'." :start start :end end :tracks (emacsconf-by-track (cdr o))))) - days)))) + days) + direction))) (defun emacsconf-schedule-svg-color-by-status (o node &optional _) "Set talk color based on status. @@ -459,23 +505,28 @@ Other status: gray" "lightgray") (_ "gray"))))) -(defun emacsconf-schedule-svg-days (width height days) +(defun emacsconf-schedule-svg-days (width height days &optional direction) "Display multiple days." (let ((svg (svg-create width height)) - (day-height (/ height (length days))) - (y 0)) + (day-height (if (eq direction 'vertical) height (/ height (length days)))) + (day-width (if (eq direction 'vertical) (/ width (length days)) width)) + (y 0) + (x 0)) (dom-append-child svg (dom-node 'title nil "Graphical view of the schedule")) (mapc (lambda (day) - (let ((group (dom-node 'g `((transform . ,(format "translate(0,%d)" y)))))) + (let ((group (dom-node 'g `((transform . ,(format "translate(%d,%d)" x y)))))) (dom-append-child svg group) (emacsconf-schedule-svg-day group (plist-get day :label) - width day-height + day-width day-height (date-to-time (plist-get day :start)) (date-to-time (plist-get day :end)) - (plist-get day :tracks))) - (setq y (+ y day-height))) + (plist-get day :tracks) + direction)) + (if (eq direction 'vertical) + (setq x (+ x day-width)) + (setq y (+ y day-height)))) days) svg)) @@ -705,7 +756,7 @@ Both start and end time are tested." (defun emacsconf-schedule-q-and-a-p (talk) "Return non-nil if TALK has a Q&A scheduled for the event." - (not (string-match "after the event" (or (plist-get talk :q-and-a) "")))) + (not (string-match "none\\|email\\|after the event" (or (plist-get talk :qa-type) "")))) (defun emacsconf-schedule-get-time-constraint (o) (when (emacsconf-schedule-q-and-a-p o) @@ -719,14 +770,14 @@ Both start and end time are tested." (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\\)?") + ((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) + (push (and (match-string 3) (string-trim (match-string 3))) result) (goto-char (match-end 0))) ((looking-at " or ") (push 'or result) @@ -832,6 +883,18 @@ Both start and end time are tested." (when dupes (list (concat "Duplicate talks: " (mapconcat 'car dupes ", ")))))) +(defun emacsconf-schedule-validate-videos-fit-in-time (sched &optional info) + "If there are prerecorded videos, make sure they fit in the time allocated." + (seq-keep + (lambda (o) + (when (and (plist-get o :video-time) + (> (string-to-number (plist-get o :video-time)) + (string-to-number (plist-get o :time)))) + (format "%s: video: %s, allocated %s" + (plist-get o :slug) + (plist-get o :video-time) + (plist-get o :time)))) + sched)) (defvar emacsconf-schedule-validation-functions '(emacsconf-schedule-validate-time-constraints emacsconf-schedule-validate-live-q-and-a-sessions-are-staggered emacsconf-schedule-validate-all-talks-present diff --git a/emacsconf-stream.el b/emacsconf-stream.el index 6641392..d526367 100644 --- a/emacsconf-stream.el +++ b/emacsconf-stream.el @@ -1210,10 +1210,13 @@ XDG_RUNTIME_DIR=\"/run/user/%d\" (defun emacsconf-stream-crontabs (&optional test-mode info) "Write the streaming users' crontab files. If TEST-MODE is non-nil, use the videos in the test directory. +If TEST-MODE is a date, use that as the starting date. If INFO is non-nil, use that as the schedule instead." - (interactive) + (interactive (list (when current-prefix-arg (org-read-date t t nil "Start time: ")))) (let ((emacsconf-publishing-phase 'conference)) - (setq info (or info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) + (setq info (or info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) + (when (and test-mode (listp test-mode)) + (setq info (emacsconf-schedule-prepare-test-schedule test-mode info))) (dolist (track emacsconf-tracks) (let ((talks (seq-filter (lambda (talk) (string= (plist-get talk :track) @@ -1280,6 +1283,47 @@ International (CC BY-SA 4.0) license. Please observe the guidelines for conduct: (kill-new desc)) desc)) +(defun emacsconf-stream-toobnix-copy-livestream-description (track) + (interactive (list (emacsconf-complete-track))) + (when (stringp track) (setq track (emacsconf-get-track track))) + (let* ((track-id (plist-get track :id)) + (desc (emacsconf-replace-plist-in-string + (list + :track-name (plist-get track :name) + :dates emacsconf-dates + :conf-name emacsconf-name + :year emacsconf-year + :track-id track-id + :base-url emacsconf-base-url + :channel (plist-get track :channel) + :chat-url (plist-get track :webchat-url) + :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 track :channel))) + " +${track-name} - ${conf-name} ${year} + +This for the ${track-name} track of ${conf-name} (${dates}) + +Watch using free/open source software: https://live.emacsconf.org/${year}/watch/${track-id}/ +Conference info: ${base-url}${year}/ +Schedule: ${base-url}${year}/talks/ +Chat on #${channel} via ${chat-url} 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) diff --git a/emacsconf-subed.el b/emacsconf-subed.el index fafdf38..e600dd2 100644 --- a/emacsconf-subed.el +++ b/emacsconf-subed.el @@ -128,7 +128,7 @@ TYPE can be 'end if you want the match end instead of the beginning." (subtitles (mapconcat (lambda (sub) - (string-join + (string-join (emacsconf-split-text-based-on-heuristics (elt sub 3) subtitle-text-limit) "\n")) (emacsconf-combine-close-subtitles (subed-subtitle-list)) @@ -147,13 +147,11 @@ TYPE can be 'end if you want the match end instead of the beginning." (let ((new-filename (concat (file-name-sans-extension (buffer-file-name)) "--chapters.vtt")) (subtitles (subed-subtitle-list)) (subed-auto-play-media nil)) - (when (or (not (file-exists-p new-filename)) - (yes-or-no-p (format "%s exists. Overwrite? " new-filename))) - (subed-create-file - new-filename - (emacsconf-subed-list-chapter-markers-based-on-comments - subtitles) - t)))) + (subed-create-file + new-filename + (emacsconf-subed-list-chapter-markers-based-on-comments + subtitles) + t))) (defun emacsconf-subed-list-chapter-markers-based-on-comments (subtitles) "Make a list of subtitles based on which SUBTITLES have comments." @@ -171,7 +169,34 @@ TYPE can be 'end if you want the match end instead of the beginning." subtitles) (nreverse (cons current result)))) -(defun emacsconf-subed-mark-chapter (chapter-name) +(defalias 'emacsconf-extract-subed-copy-section-text #'emacsconf-subed-copy-current-chapter-text) + +(defun emacsconf-subed-copy-current-chapter-text (&optional only-from-point) + "Copy text between NOTE and NOTE chapter comments." + (interactive (list current-prefix-arg)) + (let* ((start (cond + (only-from-point (save-excursion (subed-jump-to-subtitle-start-pos))) + ((save-excursion (re-search-backward "^NOTE[ \n]" nil t)) + (match-beginning 0)) + (t + (point-min)))) + (end (cond + ((looking-at "^NOTE[ \n]") (match-beginning 0)) + ((save-excursion (re-search-forward "^NOTE[ \n]" nil t)) + (match-beginning 0)) + (t + (point-max)))) + (s (string-trim + (replace-regexp-in-string + "\n" " " + (subed-subtitle-list-text (subed-subtitle-list start end)))))) + (message "%s" s) + (kill-new s) + s)) + +(defun emacsconf-subed-set-chapter (chapter-name) + "I think this adds a chapter heading for the region. +Use `subed-set-subtitle-comment' and `emacsconf-subed-make-chapter-file-based-on-comments' or `subed-section-comments-as-chapters' instead." (interactive "MChapter: ") (let ((start (subed-subtitle-msecs-start))) (save-excursion @@ -204,7 +229,7 @@ TYPE can be 'end if you want the match end instead of the beginning." "new=\"1\" " "") (replace-regexp-in-string - "\n" " " + "\n" " " (replace-regexp-in-string "\"" "" ;" (replace-regexp-in-string "[][]" "" (subed-subtitle-text)))) @@ -299,14 +324,20 @@ Create it if necessary." (interactive "e") (goto-char (posn-point (event-start event))) (skip-syntax-backward "w") - (subed-split-subtitle) + (if (derived-mode-p 'subed-mode) + (subed-split-subtitle) + (insert "\n")) (recenter)) (defun emacsconf-subed-merge-and-unfill () "Merge this subtitle with the next one." (interactive) - (subed-merge-with-next) - (emacsconf-subed-unfill-paragraph)) + (if (derived-mode-p 'subed-mode) + (progn + (subed-merge-with-next) + (emacsconf-subed-unfill-paragraph)) + (goto-char (line-end-position)) + (join-line))) (defun emacsconf-subed-unfill-paragraph () "Sometimes the regular fill doesn't work." @@ -358,8 +389,8 @@ Create it if necessary." (* i 5000) (1- (* i 5000)) (emacsconf-pad-expand-intro talk) - (format "#+OUTPUT: %s.webm\n[[file:%s]]" - (plist-get talk :slug) + (format "#+OUTPUT: %s--intro.webm\n[[file:%s]]" + (plist-get talk :file-prefix) (expand-file-name (concat (plist-get talk :slug) ".png") (expand-file-name "in-between" emacsconf-stream-asset-dir))))) @@ -431,5 +462,14 @@ Create it if necessary." map) t)) +(defun emacsconf-subed-insert-question-heading-from-other-window () + (interactive) + (insert + (with-selected-window + (other-window) + (replace-regexp-in-string + "^- +" "NOTE " + (buffer-substring (line-beginning-position) (line-end-position)))) + "\n\n")) (provide 'emacsconf-subed) ;;; emacsconf-subed.el ends here diff --git a/emacsconf-toobnix.el b/emacsconf-toobnix.el new file mode 100644 index 0000000..c82a2d6 --- /dev/null +++ b/emacsconf-toobnix.el @@ -0,0 +1,323 @@ +;;; emacsconf-toobnix.el --- Use the Toobnix REST API -*- lexical-binding: t; -*- + +;; Copyright (C) 2025 Sacha Chua + +;; Author: Sacha Chua <sacha@sachachua.com> +;; Keywords: multimedia + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; https://docs.joinpeertube.org/api/rest-getting-started +;; https://docs.joinpeertube.org/api-rest-reference.html +;; +;; Get the access token with M-x `emacsconf-toobnix-api-setup'. + +;;; Code: + +(defvar emacsconf-toobnix-upload-command "peertube-cli") +(defvar emacsconf-toobnix-channel "EmacsConf") + +(defun emacsconf-toobnix-update-video-description (talk &optional type) + "Update the description for TALK. +TYPE is 'talk or 'answers." + (interactive + (let ((talk (emacsconf-complete-talk-info))) + (list + talk + (if (plist-get talk :qa-toobnix-url) + (intern (completing-read "Type: " '("talk" "answers"))) + 'talk)))) + (setq type (or type 'talk)) + (let* ((properties + (pcase type + ('answers (emacsconf-publish-answers-video-properties talk 'toobnix)) + (_ (emacsconf-publish-talk-video-properties talk 'toobnix)))) + (id + (emacsconf-toobnix-id-from-url + (plist-get talk (pcase type + ('answers :qa-toobnix-url) + (_ :toobnix-url))))) + (boundary (format "%s%d" (make-string 20 ?-) + (time-to-seconds))) + (url-request-method "PUT") + (url-request-extra-headers + (cons (cons "Content-Type" + (concat "multipart/form-data; boundary=" boundary)) + (emacsconf-toobnix-api-header))) + (url-request-data + (mm-url-encode-multipart-form-data + `(("description" . + ,(encode-coding-string + (plist-get properties :description) + 'utf-8-dos))) + boundary)) + (url (concat "https://toobnix.org/api/v1/videos/" id))) + (with-current-buffer (url-retrieve-synchronously url) + (prog1 (buffer-string) + (kill-buffer (current-buffer)))))) + +(defun emacsconf-toobnix-upload-all () + (interactive) + (dolist (talk (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))) + (unless (or (plist-get talk :toobnix-url) + (null (plist-get talk :video-file))) + (message "Uploading %s" (plist-get talk :slug)) + (emacsconf-publish-upload-talk talk 'toobnix)) + (unless (or (plist-get talk :qa-toobnix) + (null (plist-get talk :qa-video-file))) + (message "Uploading %s answers" (plist-get talk :slug)) + (emacsconf-publish-upload-answers talk 'toobnix)))) + +(defun emacsconf-toobnix-upload (properties) + "Uses peertube-cli: https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/tools.md" + (with-temp-buffer + (let ((arguments + (append + (list "upload" "-f" (plist-get properties :file)) + (when (plist-get properties :title) + (list "-n" (plist-get properties :title))) + (when (plist-get properties :description) + (list "-d" (plist-get properties :description))) + (list "-L" "en" + "-C" emacsconf-toobnix-channel + "-l" "2" + "--verbose" "debug" + "-c" "15" + "-P" (if (string= (plist-get properties :privacy) "public") "1" "2") "-t" + (cond + ((stringp (plist-get properties :tags)) + (plist-get properties :tags)) + ((listp (plist-get properties :tags)) + (string-join (plist-get properties :tags) ",")) + (t "emacs")))))) + (kill-new (mapconcat + #'shell-quote-argument + (append (list emacsconf-toobnix-upload-command) arguments) + " ")) + (apply #'call-process + emacsconf-toobnix-upload-command + nil t t arguments) + (buffer-string)))) + +(defun emacsconf-toobnix-step-through-publishing-talk (talk) + (interactive (list (emacsconf-complete-talk-info + (seq-remove + (lambda (talk) + (or (not (plist-get talk :video-file)) + (plist-get talk :toobnix-url))) + (emacsconf-get-talk-info))))) + (kill-new (plist-get talk :video-file)) + (y-or-n-p + (format "Video: %s - create video and upload this filename. Done?" + (plist-get talk :video-file))) + (kill-new (emacsconf-publish-video-description talk t)) + (y-or-n-p "Copied description. Paste into description, move first line to title, add to playlist. Done?") + (when (emacsconf-talk-file talk "--main.vtt") + (kill-new (emacsconf-talk-file talk "--main.vtt")) + (y-or-n-p (format "Captions: %s. Add to video elements. Done?" + (emacsconf-talk-file talk "--main.vtt")))) + (emacsconf-set-property-from-slug + (plist-get talk :slug) + "TOOBNIX_URL" + (read-string (format "%s - Toobnix URL: " (plist-get talk :scheduled))))) + +(defun emacsconf-toobnix-step-through-publishing-all () + (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)) + (emacsconf-toobnix-step-through-publishing-talk talk))))) + +(defun emacsconf-toobnix-id-from-url (url) + (when (string-match "https://toobnix.org/\\(?:w\\|videos/watch\\)/\\(.+\\)" url) + (match-string 1 url))) + +(defun emacsconf-toobnix-add-captions (talk type) + (interactive (list (emacsconf-complete-talk-info) + (intern (completing-read "Type of talk: " '("talk" "answers"))))) + (let* ((url (format "https://toobnix.org/api/v1/videos/%s/captions/en" + (emacsconf-toobnix-id-from-url (plist-get talk (pcase type + ('talk :toobnix-url) + ('answers :qa-toobnix-url)))))) + (filename (expand-file-name + (concat (plist-get talk :file-prefix) + (pcase type + ('talk "--main.vtt") + ('answers "--answers.vtt"))) + emacsconf-cache-dir)) + (file-arg (concat "captionfile=@" filename)) + (auth-header (emacsconf-toobnix-api-header))) + (with-temp-buffer + (call-process "curl" nil t nil + "-s" + "-i" + "--request" "PUT" + "--header" (concat (caar auth-header) ": " (cdar auth-header)) + "--form" file-arg + url) + (buffer-string)))) + +(defun emacsconf-toobnix-upload-all-captions () + (interactive) + (dolist (talk (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))) + (when (plist-get talk :toobnix-url) + (message "Uploading captions for %s" (plist-get talk :slug)) + (emacsconf-toobnix-add-captions talk 'talk)) + (when (plist-get talk :qa-toobnix-url) + (message "Uploading captions for %s Q&A" (plist-get talk :slug)) + (emacsconf-toobnix-add-captions talk 'answers)))) + +(defun emacsconf-toobnix-edit () + (interactive) + (let ((url (org-entry-get (point) "TOOBNIX_URL"))) + (if url + (when (string-match "/w/\\([A-Za-z0-9]+\\)" url) + (browse-url (format "https://toobnix.org/videos/update/%s" (match-string 1 url)))) + (when (> (length (org-entry-get (point) "FILE_PREFIX")) 80) + (copy-file (expand-file-name (concat (org-entry-get (point) "FILE_PREFIX") "--main.webm") emacsconf-cache-dir) + (expand-file-name (concat "emacsconf-" emacsconf-year "-" (org-entry-get (point) "SLUG") ".webm") emacsconf-cache-dir) t)) + (browse-url "https://toobnix.org/videos/upload#upload")))) + + +(defvar emacsconf-toobnix-api-client nil) +(defvar emacsconf-toobnix-api-bearer-token nil) +(defvar emacsconf-toobnix-api-username "bandali") +(defvar emacsconf-toobnix-api-channel-handle "emacsconf") + +(defun emacsconf-toobnix-api-header () + `(("Authorization" . ,(concat "Bearer " + (if (stringp emacsconf-toobnix-api-bearer-token) + emacsconf-toobnix-api-bearer-token + (assoc-default 'access_token emacsconf-toobnix-api-bearer-token)))))) + +(defun emacsconf-toobnix-api-setup () + (interactive) + (require 'plz) + (require 'url-http-oauth) + (setq emacsconf-toobnix-api-client + (plz 'get "https://toobnix.org/api/v1/oauth-clients/local" :as #'json-read)) + (setq emacsconf-toobnix-api-bearer-token + (assoc-default + 'access_token + (plz 'post "https://toobnix.org/api/v1/users/token" + :body (mm-url-encode-www-form-urlencoded + `(("client_id" . ,(assoc-default 'client_id emacsconf-toobnix-api-client)) + ("client_secret" . ,(assoc-default 'client_secret emacsconf-toobnix-api-client)) + ("grant_type" . "password") + ("username" . ,emacsconf-toobnix-api-username) + ("password" . ,(auth-info-password (car (auth-source-search :host "https://toobnix.org")))))) + :as #'json-read) + ) +) + (setq emacsconf-toobnix-api-channels + (plz 'get (format "https://toobnix.org/api/v1/accounts/%s/video-channels" + emacsconf-toobnix-api-username) + :headers (emacsconf-toobnix-api-header) + :as #'json-read)) + (setq emacsconf-toobnix-api-videos + (plz 'get + (format "https://toobnix.org/api/v1/accounts/%s/videos?count=100&sort=-createdAt" + emacsconf-toobnix-api-username) + :headers (emacsconf-toobnix-api-header) + :as #'json-read)) + (setq emacsconf-toobnix-api-playlists + (append + (assoc-default 'data + (plz 'get + (format "https://toobnix.org/api/v1/video-channels/%s/video-playlists?sort=-createdAt" + emacsconf-toobnix-api-channel-handle) + :headers (emacsconf-toobnix-api-header) + :as #'json-read)) + nil))) + +(defun emacsconf-toobnix-latest-video-url (&optional props) + (if props + (alist-get 'url + (seq-find (lambda (o) + (string= (alist-get 'name o) + (plist-get props :title))) + (alist-get 'data + (plz 'get + (format "https://toobnix.org/api/v1/accounts/%s/videos?count=100&sort=-createdAt" + emacsconf-toobnix-api-username) + :headers (emacsconf-toobnix-api-header) + :as #'json-read) + ))) + (alist-get 'url + (car (alist-get 'data + (plz 'get + (format "https://toobnix.org/api/v1/accounts/%s/videos?count=1&sort=-createdAt" + emacsconf-toobnix-api-username) + :headers (emacsconf-toobnix-api-header) + :as #'json-read)))))) + + + +(defun emacsconf-toobnix-video-captions (url) + (when (string-match "https://toobnix.org/\\(?:w\\|videos/watch\\)/\\(.+\\)" url) + (let ((id (match-string 1 url))) + (alist-get 'data (plz 'get + (format "https://toobnix.org/api/v1/videos/%s/captions" + id) + :headers (emacsconf-toobnix-api-header) + :as #'json-read))))) + +(defun emacsconf-toobnix-publish-video-from-edit-page () + "Messy hack to set a video to public and store the URL." + (interactive) + (spookfox-js-injection-eval-in-active-tab "document.querySelector('label[for=privacy]').scrollIntoView(); document.querySelector('label[for=privacy]').closest('.form-group').querySelector('input').dispatchEvent(new Event('input'));" t) + (sit-for 1) + (spookfox-js-injection-eval-in-active-tab "document.querySelector('span[title=\"Anyone can see this video\"]').click()" t) + (sit-for 1) + (spookfox-js-injection-eval-in-active-tab "document.querySelector('button.orange-button').click()" t)(sit-for 3) + (emacsconf-extract-store-url) + (shell-command "xdotool key Alt+Tab sleep 1 key Ctrl+w Alt+Tab")) + +(defun emacsconf-toobnix-set-up-playlist () + (interactive) + (mapcar + (lambda (o) + (when (plist-get o :toobnix-url) + (browse-url (plist-get o :toobnix-url)) + (read-key "press a key when page is loaded") + (spookfox-js-injection-eval-in-active-tab "document.querySelector('.action-button-save').click()" t) + (spookfox-js-injection-eval-in-active-tab "document.querySelector('my-peertube-checkbox').click()" t) + (read-key "press a key when saved to playlist")) + (when (plist-get o :qa-toobnix-url) + (browse-url (plist-get o :qa-toobnix-url)) + (read-key "press a key when page is loaded") + (spookfox-js-injection-eval-in-active-tab "document.querySelector('.action-button-save').click()" t) + (spookfox-js-injection-eval-in-active-tab "document.querySelector('my-peertube-checkbox').click()" t) + (read-key "press a key when saved to playlist"))) + (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))) + +(defun emacsconf-toobnix-view (talk) + (interactive (list (emacsconf-complete-talk-info))) + (browse-url (plist-get (emacsconf-resolve-talk talk) :toobnix-url))) + +(defun emacsconf-toobnix-view-qa (talk) + (interactive (list (emacsconf-complete-talk-info))) + (browse-url (plist-get (emacsconf-resolve-talk talk) :qa-toobnix-url))) + + +(provide 'emacsconf-toobnix) +;;; emacsconf-toobnix.el ends here diff --git a/emacsconf.el b/emacsconf.el index fe2a30c..cd82d54 100644 --- a/emacsconf.el +++ b/emacsconf.el @@ -34,20 +34,23 @@ "Name of conference" :group 'emacsconf :type 'string) -(defcustom emacsconf-year "2024" +(defcustom emacsconf-year "2025" "Conference year. String for easy inclusion." :group 'emacsconf :type 'string) -(defcustom emacsconf-cfp-deadline "2024-09-20" "Deadline for proposals." +(defcustom emacsconf-cfp-deadline "2025-09-19" "Target date for proposals." :group 'emacsconf :type 'string) -(defcustom emacsconf-date "2024-12-07" "Starting date of EmacsConf." +(defcustom emacsconf-date "2025-12-06" "Starting date of EmacsConf." :group 'emacsconf :type 'string) -(defcustom emacsconf-video-target-date "2024-11-08" "Target date for receiving talk videos from the speakers." +(defcustom emacsconf-dates "2025-12-06 to 2025-12-07" "Conference dates." :group 'emacsconf :type 'string) -(defcustom emacsconf-schedule-announcement-date "2024-10-25" "Date for publishing the schedule." +(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 "2025-10-24" "Date for publishing the schedule." :group 'emacsconf :type 'string) (defcustom emacsconf-directory "~/vendor/emacsconf-wiki" @@ -70,7 +73,7 @@ (defcustom emacsconf-base-url "https://emacsconf.org/" "Includes trailing slash" :group 'emacsconf :type 'string) -(defcustom emacsconf-publishing-phase 'conference +(defcustom emacsconf-publishing-phase 'harvest "Controls what information to include. 'program - don't include times 'schedule - include times; use this leading up to the conference @@ -83,10 +86,10 @@ (const :tag "Program: Don't include times" program) (const :tag "Schedule: Include detailed times" schedule) (const :tag "Conference: Show IRC and watching info" conference) - (const :tag "Harvest: Extracting info" conference) + (const :tag "Harvest: Extracting info" harvest) (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." @@ -95,11 +98,20 @@ (const tag "Prerec" 'prerec) (const tag "Q&A harvesting" 'harvest))) +(defcustom emacsconf-publish-include-pads t "When non-nil, include Etherpad info." + :group 'emacsconf + :type 'boolean) + (defcustom emacsconf-org-file nil "Path to the Org file with emacsconference information." :type 'file :group 'emacsconf) +(defcustom emacsconf-fallback-email "emacsconf-org-private@gnu.org" + "E-mail for public wiki pages if the speaker doesn't have a public email address." + :type 'string + :group 'emacsconf) + (defcustom emacsconf-upcoming-file nil "Path to the Org file with upcoming talks." :type 'file @@ -126,7 +138,7 @@ (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/shared/%s" emacsconf-year)) -(defvar emacsconf-media-extensions '("webm" "mkv" "mp4" "webm" "mov" "avi" "ts" "ogv" "wav" "ogg" "mp3" )) +(defvar emacsconf-media-extensions '("webm" "mkv" "mp4" "m4v" "mov" "avi" "mpv" "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") (defvar emacsconf-backstage-password nil "Password for backstage area.") @@ -172,6 +184,11 @@ (defun emacsconf-backstage-dired () (interactive) (dired emacsconf-backstage-dir "-tl")) + +(defun emacsconf-backstage-web () + (interactive) + (browse-url (emacsconf-backstage-url))) + (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")) @@ -474,10 +491,16 @@ FILENAME specifies an extra string to add to the file prefix if needed." (format "[%s](%s \"%s\")" desc path (plist-get talk :title))) (_ path)))) +(defun emacsconf-org-insert-description (link desc) + (unless desc + (when (string-match "emacsconf:\\(.+\\)" link) + (plist-get (emacsconf-search-talk-info (match-string 1 link)) :title)))) + (with-eval-after-load 'org (org-link-set-parameters "emacsconf" :follow #'emacsconf-go-to-talk + :insert-description #'emacsconf-org-insert-description :complete (lambda () (concat "emacsconf:" (emacsconf-complete-slug))) :export #'emacsconf-export-slug)) @@ -616,6 +639,7 @@ If INFO is specified, limit it to that list." ;; Prep (:bbb-room "ROOM") (:bbb-mod-code "BBB_MOD_CODE") + (:emailed-schedule "EMAILED_SCHEDULE") ;; Processing (:file-prefix "FILE_PREFIX") (:video-file "VIDEO_FILE") @@ -631,14 +655,16 @@ If INFO is specified, limit it to that list." (:caption-note "CAPTION_NOTE") (:captions-edited "CAPTIONS_EDITED") ;; Conference + (:live "LIVE") (:check-in "CHECK_IN") (:public "PUBLIC") (:intro-note "INTRO_NOTE") (:sched-note "SCHED_NOTE") (:hyperlist-note "HYPERLIST_NOTE") ;; Extraction - (:qa-youtube "QA_YOUTUBE") - (:qa-toobnix "QA_TOOBNIX") + (:qa-youtube-url "QA_YOUTUBE_URL") + (:qa-toobnix-url "QA_TOOBNIX_URL") + (:qa-video-file "QA_VIDEO_FILE") (:bbb-playback "BBB_PLAYBACK") ;; Old (:alternate-apac "ALTERNATE_APAC") @@ -775,7 +801,7 @@ The subheading should match `emacsconf-abstract-heading-regexp'." (time-less-p (plist-get o :start-time) (current-time))) (plist-put o :public t)) - (when (eq emacsconf-publishing-phase 'resource) + (when (eq emacsconf-publishing-phase 'resources) (plist-put o :qa-public t)) o) @@ -826,7 +852,8 @@ The subheading should match `emacsconf-abstract-heading-regexp'." emacsconf-add-timezone-conversions emacsconf-add-speakers-with-pronouns emacsconf-add-live-info - emacsconf-add-video-info) + emacsconf-add-video-info + emacsconf-add-media-info) "Functions to collect information.") (defun emacsconf-add-speakers-with-pronouns (o) @@ -885,6 +912,15 @@ The subheading should match `emacsconf-abstract-heading-regexp'." (list "youtube" "qa-youtube" "toobnix" "qa-toobnix")) o) +(defun emacsconf-add-media-info (o) + (plist-put o + :video-url (format "%s%s/%s--main.webm" emacsconf-media-base-url emacsconf-year (plist-get o :file-prefix))) + (plist-put o + :captions-url (format "%s%s/%s--main.vtt" emacsconf-media-base-url emacsconf-year (plist-get o :file-prefix))) + (plist-put o + :audio-url (format "%s%s/%s--main.opus" emacsconf-media-base-url emacsconf-year (plist-get o :file-prefix))) + o) + (defun emacsconf-add-live-info (o) (plist-put o :absolute-url (concat emacsconf-base-url (plist-get o :url))) (plist-put o :in-between-url (format "%s%s/in-between/%s.png" @@ -1024,13 +1060,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)) @@ -1171,6 +1202,11 @@ The subheading should match `emacsconf-abstract-heading-regexp'." (interactive (list (emacsconf-complete-talk))) (insert (plist-get (emacsconf-search-talk-info search) :email))) +(defun emacsconf-insert-talk-link (search) + "Insert the talk link matching SEARCH." + (interactive (list (emacsconf-complete-talk))) + (insert (concat emacsconf-base-url "/" (plist-get (emacsconf-search-talk-info search) :url)))) + (defun emacsconf-backstage-url (&optional base-url) "Return or insert backstage URL with credentials." (interactive) @@ -1198,6 +1234,7 @@ The subheading should match `emacsconf-abstract-heading-regexp'." :doc "Keymap for emacsconf-related things" "a" #'emacsconf-announce "i e" #'emacsconf-insert-talk-email + "i l" #'emacsconf-insert-talk-link "i t" #'emacsconf-insert-talk-title "i s" #'emacsconf-insert-talk-schedule "I" #'emacsconf-message-talk-info @@ -1457,22 +1494,22 @@ If TIMEZONES is a string, split it by commas." :webchat-url "https://chat.emacsconf.org/?join=emacsconf,emacsconf-org,emacsconf-accessible,emacsconf-dev,emacsconf-gen" :stream ,(concat emacsconf-stream-base "gen.webm") :480p ,(concat emacsconf-stream-base "gen-480p.webm") - ;; :youtube-url "https://www.youtube.com/watch?v=UEJ88c7MJq0" - ;; :youtube-studio-url "https://studio.youtube.com/video/UEJ88c7MJq0/livestreaming" - ;; :toobnix-url "https://toobnix.org/w/7t9X8eXuSby8YpyEKTb4aj" + :youtube-url "https://www.youtube.com/live/FI3eGeGCyQM" + :youtube-studio-url "https://studio.youtube.com/video/FI3eGeGCyQM/livestreaming" + :toobnix-url "https://toobnix.org/w/oLwaPU7MgMFDAWPhaFdW1t" :start "09:00" :end "17:00" :uid 2002 :vnc-display ":5" :vnc-port "5905" :autopilot crontab - :status "offline") + :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" + :webchat-url "https://chat.emacsconf.org/?join=emacsconf,emacsconf-org,emacsconf-accessible,emacsconf-gen,emacsconf-de" :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" + :toobnix-url "https://toobnix.org/w/uXGmcRigZD82UWr5nehKeL" + :youtube-url "https://youtube.com/live/KCZthyBhHtg" + :youtube-studio-url "https://studio.youtube.com/video/KCZthyBhHtg/livestreaming" :stream ,(concat emacsconf-stream-base "dev.webm") :480p ,(concat emacsconf-stream-base "dev-480p.webm") :uid 2003 @@ -1544,8 +1581,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 "2024-12-07T09:00:00-0500" :end "2024-12-07T12:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "sachac" :coord "sachac") (list :id "sat-pm-gen" :track "General" :start "2024-12-07T13:00:00-0500" :end "2024-12-07T17:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "sachac" :coord "sachac") (list :id "sat-am-dev" :track "Development" :start "2024-12-07T10:00:00-0500" :end "2024-12-07T12:00:00-0500" :host "corwin" :streamer "sachac" :checkin "sachac" :coord "sachac") (list :id "sat-pm-dev" :track "Development" :start "2024-12-07T13:00:00-0500" :end "2024-12-07T17:00:00-0500" :host "corwin" :streamer "sachac" :checkin "sachac" :coord "sachac") (list :id "sun-am-gen" :track "General" :start "2024-12-08T09:00:00-0500" :end "2024-12-08T12:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "corwin" :coord "sachac") (list :id "sun-pm-gen" :track "General" :start "2024-12-08T13:00:00-0500" :end "2024-12-08T17:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "corwin" :coord "sachac"))) +(setq emacsconf-shifts (list (list :id "sat-am-gen" :track "General" :start "2025-12-06T09:00:00-0500" :end "2025-12-06T12:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "sachac" :coord "sachac") (list :id "sat-pm-gen" :track "General" :start "2025-12-06T13:00:00-0500" :end "2025-12-06T17:00:00-0500" :host "zaeph" :streamer "sachac" :checkin "sachac" :coord "sachac") (list :id "sat-am-dev" :track "Development" :start "2025-12-06T10:00:00-0500" :end "2025-12-06T12:00:00-0500" :host "corwin" :streamer "sachac" :checkin "sachac" :coord "sachac") (list :id "sat-pm-dev" :track "Development" :start "2025-12-06T13:00:00-0500" :end "2025-12-06T17:00:00-0500" :host "corwin" :streamer "sachac" :checkin "sachac" :coord "sachac") (list :id "sun-am-gen" :track "General" :start "2025-12-07T09:00:00-0500" :end "2025-12-07T12:00:00-0500" :host "zaeph/corwin" :streamer "sachac" :checkin "sachac" :coord "sachac") (list :id "sun-pm-gen" :track "General" :start "2025-12-07T13:00:00-0500" :end "2025-12-07T17:00:00-0500" :host "zaeph/corwin" :streamer "sachac" :checkin "sachac" :coord "sachac"))) (defun emacsconf-filter-talks-by-time (start-time end-time info) "Return talks that are between START-TIME and END-TIME (inclusive) in INFO." @@ -1578,19 +1614,6 @@ Filter by TRACK if given. Use INFO as the list of talks." (defun emacsconf-talk-all-done-p (talk) (member (plist-get talk :status) (split-string "TO_ARCHIVE TO_EXTRACT TO_FOLLOW_UP DONE"))) -(defun emacsconf-bbb-status (talk) - (let ((states - '((open . "OPEN_Q UNSTREAMED_Q") - (before . "TODO TO_REVIEW TO_ACCEPT WAITING_FOR_PREREC TO_PROCESS PROCESSING TO_AUTOCAP TO_ASSIGN TO_CAPTION TO_CHECK TO_STREAM PLAYING CLOSED_Q") - (after . "TO_ARCHIVE TO_EXTRACT TO_REVIEW_QA TO_INDEX_QA TO_CAPTION_QA TO_FOLLOW_UP DONE") - (cancelled . "CANCELLED")))) - (if (string-match "live" (or (plist-get talk :q-and-a) "")) - (or (car (seq-find (lambda (state) - (member (plist-get talk :status) (split-string (cdr state)))) - states)) - (throw 'error "Unknown talk BBB state")) - 'irc))) - (defun emacsconf-captions-edited-p (filename) "Return non-nil if FILENAME has been edited and is okay for inclusion." (and @@ -1601,54 +1624,7 @@ Filter by TRACK if given. Use INFO as the list of talks." (goto-char (point-min)) (re-search-forward "captioned by" (line-end-position) t)))) -(defvar emacsconf-bbb-base-url "https://bbb.emacsverse.org/" "Include trailing slash.") -(defun emacsconf-bbb-room-title-list (&optional info) - (delq nil - (mapcar - (lambda (o) - (when (car o) - (concat "ec" (substring emacsconf-year 2) - "-" (plist-get (emacsconf-get-shift (plist-get (cadr o) :start-time)) :id) - "-" (plist-get (emacsconf-get-track (plist-get (cadr o) :track)) :id) - " " (car o) - " (" - (mapconcat (lambda (talk) (plist-get talk :slug)) (cdr o) ", ") - ")"))) - (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." @@ -1676,16 +1652,8 @@ friendly-id speaker - slugs (defun emacsconf-volunteer-insert-email (&optional info) (interactive) - (insert (completing-read - (mapcar - (lambda (o) - (emacsconf-surround - (if (assoc-default "ITEM" o) - (concat (assoc-default "ITEM" o) " <") - "<") - (assoc-default "EMAIL" o) - ">" "")) - (or info (emacsconf-get-volunteer-info)))))) + (insert (assoc-default "EMAIL" (emacsconf-complete-volunteer info) #'string=))) + (defun emacsconf-complete-volunteer (&optional info) (setq info (or info (emacsconf-get-volunteer-info))) (let* ((choices @@ -2001,5 +1969,90 @@ tracks with the ID in the cdr of that list." (message "Deleting %s from %s" (file-name-nondirectory file) dir))))) +(defun emacsconf-current-org-notebook-filename () + "Return the filename for the current year's public organizers notebook." + (expand-file-name "organizers-notebook/index.org" (expand-file-name emacsconf-year emacsconf-directory))) + +(defun emacsconf-current-org-notebook-open (&optional common) + "Open the current year's public organizers notebook. +With a prefix argument (\\[universal-argument]), open the general organizers notebook." + (interactive (list current-prefix-arg)) + (find-file (if common + (expand-file-name "organizers-notebook/index.org" + emacsconf-directory) + (emacsconf-current-org-notebook-filename)))) + +(defun emacsconf-current-org-notebook-heading (&optional common) + "Open the current year's public organizers notebook and jump to a heading. +With a prefix argument (\\[universal-argument]), open the general organizers notebook." + (interactive (list current-prefix-arg)) + (emacsconf-current-org-notebook-open common) + (cond + ((fboundp 'consult-org-heading) + (call-interactively #'consult-org-heading)) + (t (call-interactively #'org-goto)))) + +(defun emacsconf-main-org-notebook-heading (&optional other) + "Open the main public organizers notebook and jump to a heading. +With a prefix argument (\\[universal-argument]), open this year's notebook." + (interactive (list current-prefix-arg)) + (emacsconf-current-org-notebook-heading (not other))) + +(defvar emacsconf-refresh-schedule-from-org nil "Non-nil means refresh the schedule from the organizer notebook.") +(defun emacsconf-current-org-notebook-refresh-schedule () + "Refresh info from draft schedule." + (interactive) + (when emacsconf-refresh-schedule-from-org + (save-window-excursion + (with-current-buffer (find-file-noselect (emacsconf-current-org-notebook-filename)) + (save-restriction + (widen) + (goto-char (org-find-property "CUSTOM_ID" "draft-schedule")) + (org-babel-execute-subtree)))))) + +(defun emacsconf-insert-availability-comment (talk) + (interactive (list (or (emacsconf-search-talk-info (thing-at-point 'symbol)) + (emacsconf-complete-talk)))) + (save-excursion + (goto-char (line-end-position)) + (insert " ; " (plist-get talk :availability)))) + +(defun emacsconf-cancel-talk (talk) + "Cancel TALK. Assume that the schedule has already been updated." + (interactive (list (emacsconf-complete-talk))) + (emacsconf-with-talk-heading talk + (org-todo "CANCELLED") + (emacsconf-current-org-notebook-refresh-schedule) + (emacsconf-schedule-update-from-info) + (emacsconf-update-schedule) + (emacsconf-publish-watch-pages) + (emacsconf-publish-talks-json-to-files) + (emacsconf-publish-info-pages-for-talk talk) + (emacsconf-publish-info-pages) + (emacsconf-stream-generate-in-between-pages) + ;; Regenerate intros + (find-file (emacsconf-latest-file (expand-file-name "../assets/intros" emacsconf-cache-dir) "^\\(intro\\|script\\).*.vtt")) + (message "Remember to regenerate the intro videos."))) + +(defun emacsconf-rescheduled-talks (&optional info) + "See `emacsconf-schedule-save-emailed-times' and `emacsconf-mail-schedule-updates'." + (seq-filter + (lambda (o) + (and (plist-get o :email) + (plist-get o :qa-type) + (not (string= (plist-get o :emailed-schedule) + (replace-regexp-in-string "[<>]" "" (plist-get o :scheduled)))) + (not (string= (plist-get o :qa-type) "none")) + (not (= (emacsconf-schedule-difference-from-emailed o) 0)))) + (emacsconf-publish-prepare-for-display (emacsconf-filter-talks (or info (emacsconf-get-talk-info)))))) + +(defun emacsconf-schedule-difference-from-emailed (talk) + "Return the number of minutes. Negative means earlier." + (let ((start (plist-get talk :start-time)) + (emailed (org-timestamp-to-time (org-timestamp-split-range + (org-timestamp-from-string + (format "<%s>" (plist-get talk :emailed-schedule))))))) + (round (/ (float-time (time-subtract start emailed)) 60.0)))) + (provide 'emacsconf) ;;; emacsconf.el ends here |
