#EmacsConf backstage: file prefixes
| emacs, org, emacsconfSometimes it makes sense to dynamically generate information related to a talk and then save it as an Org property so that I can manually edit it. For example, we like to name all the talk files using this pattern: "emacsconf-year-slug--title--speakers". That's a lot to type consistently! We can generate most of these prefixes automatically, but some might need tweaking, like when the talk title or speaker names have special characters.
Calculating the file prefix for a talk
First we need something that turns a string into an ID.
emacsconf-slugify: Turn S into an ID.
(defun emacsconf-slugify (s) "Turn S into an ID. Replace spaces with dashes, remove non-alphanumeric characters, and downcase the string." (replace-regexp-in-string " +" "-" (replace-regexp-in-string "[^a-z0-9 ]" "" (downcase s))))
Then we can use that to calculate the file prefix for a given talk.
emacsconf-file-prefix: Create the file prefix for TALK
(defun emacsconf-file-prefix (talk) "Create the file prefix for TALK." (concat emacsconf-id "-" emacsconf-year "-" (plist-get talk :slug) "--" (emacsconf-slugify (plist-get talk :title)) (if (plist-get talk :speakers) (concat "--" (emacsconf-slugify (plist-get talk :speakers))) "")))
Then we can map over all the talk entries that don't have FILE_PREFIX
defined:
emacsconf-set-file-prefixes: Set the FILE_PREFIX property for each talk entry that needs it.
(defun emacsconf-set-file-prefixes () "Set the FILE_PREFIX property for each talk entry that needs it." (interactive) (org-map-entries (lambda () (org-entry-put (point) "FILE_PREFIX" (emacsconf-file-prefix (emacsconf-get-talk-info-for-subtree)))) "SLUG={.}-FILE_PREFIX={.}"))
That stores the file prefix in an Org property, so we can edit it if it needs tweaking.
Renaming files to match the file prefix
Now that we have that, how can we use it? One way is to rename files
from within Emacs. I can mark multiple files with Dired's m
command
or work on them one at a time. If there are several files with the
same extension, I can specify something to add to the filename to tell
them apart.
emacsconf-rename-files: Rename the marked files or the current file to match TALK.
(defun emacsconf-rename-files (talk &optional filename) "Rename the marked files or the current file to match TALK. If FILENAME is specified, use that as the extra part of the filename after the prefix. This is useful for distinguishing files with the same extension. Return the list of new filenames." (interactive (list (emacsconf-complete-talk-info))) (prog1 (mapcar (lambda (file) (let* ((extra (or filename (read-string (format "Filename (%s): " (file-name-base file))))) (new-filename (expand-file-name (concat (plist-get talk :file-prefix) (if (string= extra "") "" (concat "--" extra)) "." (file-name-extension file)) (file-name-directory file)))) (rename-file file new-filename t) new-filename)) (or (dired-get-marked-files) (list (buffer-file-name)))) (when (derived-mode-p 'dired-mode) (revert-buffer))))
Working with files on other computers
Because Dired works over TRAMP, I can use that to rename files on a remote server without changing anything about the code. I can open the remote directory with Dired and everything just works.
TRAMP also makes it easy to copy a file to the backstage directory after it's renamed, which saves me having to do that as a separate step.
emacsconf-rename-and-upload-to-backstage: Rename marked files or the current file, then upload to backstage.
(defun emacsconf-rename-and-upload-to-backstage (talk &optional filename) "Rename marked files or the current file, then upload to backstage." (interactive (list (emacsconf-complete-talk-info))) (mapc (lambda (file) (copy-file file (expand-file-name (file-name-nondirectory file) emacsconf-backstage-dir) t)) (emacsconf-rename-files talk)))
So if my emacsconf-backstage-dir
is set to
/ssh:orga@res:/var/www/res.emacsconf.org/2023/backstage
, then it
looks up the details for res
in my ~/.ssh/config
and copies the
file there.
Renaming files using information from a JSON
What if I don't want to rename the files from Emacs? If I use Emacs's JSON support to export some information from the talks as a JSON file, then I can easily use that data from the command line.
Here's how I export the talk information:
emacsconf-talks-json: Return JSON format with a subset of talk information.
(defun emacsconf-publish-talks-json () "Return JSON format with a subset of talk information." (json-encode (list :talks (mapcar (lambda (o) (apply 'list (cons :start-time (format-time-string "%FT%T%z" (plist-get o :start-time) t)) (cons :end-time (format-time-string "%FT%T%z" (plist-get o :end-time) t)) (mapcar (lambda (field) (cons field (plist-get o field))) '(:slug :title :speakers :pronouns :pronunciation :url :track :file-prefix)))) (emacsconf-filter-talks (emacsconf-get-talk-info))))))
emacsconf-publish-talks-json-to-files
(defun emacsconf-publish-talks-json-to-files () "Export talk information as JSON so that we can use it in shell scripts." (interactive) (mapc (lambda (dir) (when (and dir (file-directory-p dir)) (with-temp-file (expand-file-name "talks.json" dir) (insert (emacsconf-talks-json))))) (list emacsconf-res-dir emacsconf-ansible-directory)))
Then I can use jq
to extract the information with
jq -r '.talks[] | select(.slug=="'$SLUG'")["file-prefix"]' < $TALKS_JSON
Here it is in the context of a shell script that renames the given file to match a talk's prefix.
#!/bin/bash # # Usage: rename-original.sh $slug $file [$extra] [$talks-json] SLUG=$1 FILE=$2 TALKS_JSON=${4:-~/current/talks.json} EXTRA="" if [ -z ${3-unset} ]; then EXTRA="" elif [ -n "$3" ]; then EXTRA="--$3" elif echo "$FILE" | grep -e '\(webm\|mp4\|mov\)'; then EXTRA="--original" fi filename=$(basename -- "$FILE") extension="${filename##*.}" filename="${filename%.*}" FILE_PREFIX=$(jq -r '.talks[] | select(.slug=="'$SLUG'")["file-prefix"]' < $TALKS_JSON) mv "$FILE" $FILE_PREFIX$EXTRA.$extension echo $FILE_PREFIX$EXTRA.$extension # Copy to original if needed if [ -f $FILE_PREFIX--original.webm ] && [ ! -f $FILE_PREFIX--main.$extension ]; then cp $FILE_PREFIX--original.$extension $FILE_PREFIX--main.webm fi
Then I can use something like rename-original.sh emacsconf
video.webm
to
emacsconf-2023-emacsconf--emacsconforg-how-we-use-org-mode-and-tramp-to-organize-and-run-a-multitrack-conference--sacha-chua--original.webm
.
Working with PsiTransfer-uploaded files
JSON support is useful for getting files into our system, too. For EmacsConf 2022, we used PsiTransfer as a password-protected web-based file upload service. That was much easier for speakers to deal with than FTP, especially for large files. PsiTransfer makes a JSON file for each batch of uploads, which is handy because the uploaded files are named based on the key instead of keeping their filenames and extensions. I wrote a function to copy an uploaded file from the PsiTransfer directory to the backstage directory, renaming it along the way. That meant that I could open the JSON for the uploaded files via TRAMP and then copy a file between two remote directories without manually downloading it to my computer.
emacsconf-upload-copy-from-json: Parse PsiTransfer JSON files and copy the uploaded file to the backstage directory.
(defun emacsconf-upload-copy-from-json (talk key filename) "Parse PsiTransfer JSON files and copy the uploaded file to the backstage directory. The file is associated with TALK. KEY identifies the file in a multi-file upload. FILENAME specifies an extra string to add to the file prefix if needed." (interactive (let-alist (json-parse-string (buffer-string) :object-type 'alist) (list (emacsconf-complete-talk-info) .metadata.key (read-string (format "Filename: "))))) (let ((new-filename (concat (plist-get talk :file-prefix) (if (string= filename "") filename (concat "--" filename)) "." (let-alist (json-parse-string (buffer-string) :object-type 'alist) (file-name-extension .metadata.name))))) (copy-file key (expand-file-name new-filename emacsconf-backstage-dir) t)))
So that's how I can handle things that can be mostly automated but that might need a little human intervention: use Emacs Lisp to make a starting point, tweak it a little if needed, and then make it easy to use that value elsewhere. Renaming files can be tricky, so it's good to reduce the chance for typos!