summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSacha Chua <sacha@sachachua.com>2023-12-11 09:56:55 -0500
committerSacha Chua <sacha@sachachua.com>2023-12-11 09:56:55 -0500
commit104852e78e0750074479f7a8e5ad9f4d5d247294 (patch)
tree63f61ee19961ff9b9dfef7b71c2f0a716e167139
parent281b0f4a7706d2f89e93fb1678f1d76e06f2c39d (diff)
downloademacsconf-el-104852e78e0750074479f7a8e5ad9f4d5d247294.tar.xz
emacsconf-el-104852e78e0750074479f7a8e5ad9f4d5d247294.zip
some toobnix code
-rw-r--r--emacsconf-extract.el385
1 files changed, 311 insertions, 74 deletions
diff --git a/emacsconf-extract.el b/emacsconf-extract.el
index ceca898..52d181b 100644
--- a/emacsconf-extract.el
+++ b/emacsconf-extract.el
@@ -693,14 +693,21 @@ Would you like to help? See [[help_with_chapter_markers]] for more details. You
(when (called-interactively-p 'any) (kill-new s))
s))
-(defun emacsconf-extract-download-published-recordings (source dest)
+(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)
- (format "rsync -avzue ssh %s%s %s\n"
- source
- (match-string 1 (plist-get o :bbb-rec))
- dest)
+ (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))))
@@ -713,6 +720,23 @@ Would you like to help? See [[help_with_chapter_markers]] for more details. You
(setq talk (emacsconf-resolve-talk talk))
(expand-file-name "events.xml" (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-raw-dir)))
+(defun emacsconf-extract-bbb-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.
@@ -957,7 +981,8 @@ Strategies:
(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))
- (stop-ms (or stop-ms (emacsconf-get-file-duration-ms webcam-video)))
+ (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
@@ -971,10 +996,15 @@ Strategies:
(compile-media-output-video-height (cdr final-size)))
(compile-media-get-command spans output))
;; Just webcams
- (compile-media-get-command
- (compile-media-split-tracks
- (list (list :source webcam-video :start-ms start-ms :stop-ms stop-ms)))
- output))))
+ (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))
@@ -1079,19 +1109,27 @@ Strategies:
:headers `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/")))
:as #'json-read))
(setq emacsconf-extract-youtube-api-videos
- (plz 'get (concat "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails,status&forMine=true&order=date&maxResults=50&playlistId="
- (url-hexify-string
- (let-alist (elt (assoc-default 'items emacsconf-extract-youtube-api-channels) 0)
- .contentDetails.relatedPlaylists.uploads)
- ))
- :headers `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/")))
- :as #'json-read)))
+ (emacsconf-extract-youtube-api-paginated-request
+ (concat "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails,status&forMine=true&order=date&maxResults=50&playlistId="
+ (url-hexify-string
+ (let-alist (elt (assoc-default 'items emacsconf-extract-youtube-api-channels) 0)
+ .contentDetails.relatedPlaylists.uploads)
+ ))
+ nil
+ (lambda (item)
+ (string-match (regexp-quote emacsconf-year)
+ (let-alist item .snippet.title))))))
(defvar emacsconf-extract-youtube-tags '("emacs" "emacsconf"))
-(defun emacsconf-extract-youtube-object (video-id talk &optional privacy-status)
- "Format the video object for VIDEO-ID using TALK details."
+(defun emacsconf-extract-youtube-object (video-id talk &optional privacy-status qa)
+ "Format the video object for VIDEO-ID using TALK details.
+If QA is non-nil, treat it as a Q&A video."
(setq privacy-status (or privacy-status "public"))
- (let ((properties (emacsconf-publish-talk-video-properties talk 'youtube)))
+ (let ((properties
+ (funcall (if qa
+ #'emacsconf-publish-answers-video-properties
+ #'emacsconf-publish-talk-video-properties)
+ talk 'youtube)))
`((id . ,video-id)
(kind . "youtube#video")
(snippet
@@ -1099,9 +1137,8 @@ Strategies:
(title . ,(plist-get properties :title))
(tags . ,emacsconf-extract-youtube-tags)
(description . ,(plist-get properties :description))
- ;; Even though I set recordingDetails and status, it doesn't seem to stick.
- ;; I'll leave this in here in case someone else can figure it out.
(recordingDetails (recordingDate . ,(format-time-string "%Y-%m-%dT%TZ" (plist-get talk :start-time) t))))
+ ;; oooh, publishing seems to work now
(status (privacyStatus . ,privacy-status)
(license . "creativeCommon")))))
@@ -1129,16 +1166,53 @@ Strategies:
(when-let ((slug (emacsconf-extract-youtube-get-slug-for-video video-object)))
(emacsconf-resolve-talk slug)))
-(defun emacsconf-extract-youtube-api-update-video (video-object)
- "Update VIDEO-OBJECT."
+(defun emacsconf-extract-youtube-api-videos ()
+ (plz 'get (concat "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails,status&forMine=true&order=date&maxResults=50&playlistId="
+ (url-hexify-string
+ (let-alist (elt (assoc-default 'items emacsconf-extract-youtube-api-channels) 0)
+ .contentDetails.relatedPlaylists.uploads)
+ ))
+ :headers `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/")))
+ :as #'json-read))
+
+(defun emacsconf-extract-youtube-api-paginated-request (url &optional num-pages condition)
+ (let (result current-page (base-url url) current-page-items)
+ (while url
+ (setq current-page
+ (plz 'get url
+ :headers
+ `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/")))
+ :as #'json-read))
+ (setq current-page-items
+ (if condition
+ (seq-filter condition (assoc-default 'items current-page))
+ (assoc-default 'items current-page)))
+ (setq result (append result current-page-items nil))
+ (let-alist current-page
+ (setq url
+ (if .nextPageToken
+ (concat base-url
+ (if (string-match "\\?" base-url) "&" "?")
+ "pageToken=" .nextPageToken)
+ nil)))
+ (when (= (length current-page-items) 0) (setq url nil))
+ (when num-pages
+ (setq num-pages (1- num-pages))
+ (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."
(let-alist video-object
- (let* ((slug (or (emacsconf-extract-youtube-get-slug-for-video video-object)
- (when (string-match (rx (literal emacsconf-id) " " (literal emacsconf-year))
- .snippet.title)
- (completing-read (format "Slug for %s: "
- .snippet.title)
- (seq-map (lambda (o) (plist-get o :slug))
- (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))))))
+ (let* ((slug (or
+ (emacsconf-extract-youtube-get-slug-for-video video-object)
+ (when (string-match (rx (literal emacsconf-id) " " (literal emacsconf-year))
+ .snippet.title)
+ (completing-read (format "Slug for %s: "
+ .snippet.title)
+ (seq-map (lambda (o) (plist-get o :slug))
+ (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))))))
(talk (and slug (emacsconf-resolve-talk slug)))
(video-id .snippet.resourceId.videoId)
(id .id)
@@ -1146,13 +1220,13 @@ Strategies:
(when slug
;; set the YOUTUBE_URL property
(emacsconf-with-talk-heading slug
- (org-entry-put (point) "YOUTUBE_URL" (concat "https://www.youtube.com/watch?v=" video-id))
- (org-entry-put (point) "YOUTUBE_ID" id))
+ (org-entry-put (point) (if qa "QA_YOUTUBE_URL" "YOUTUBE_URL") (concat "https://www.youtube.com/watch?v=" video-id))
+ (org-entry-put (point) (if qa "QA_YOUTUBE_ID" "YOUTUBE_ID") id))
(plz 'put "https://www.googleapis.com/youtube/v3/videos?part=snippet,recordingDetails,status"
:headers `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/"))
("Accept" . "application/json")
("Content-Type" . "application/json"))
- :body (json-encode (emacsconf-extract-youtube-object video-id talk)))))))
+ :body (json-encode (emacsconf-extract-youtube-object video-id talk nil qa)))))))
(defun emacsconf-extract-youtube-rename-videos (&optional videos)
"Rename videos and set the YOUTUBE_URL property in the Org heading."
@@ -1167,53 +1241,216 @@ Strategies:
(assoc-default
'items
(or videos emacsconf-extract-youtube-api-videos
- (plz 'get (concat "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails,status&forMine=true&order=date&maxResults=50&playlistId="
- (url-hexify-string
- (let-alist (elt (assoc-default 'items emacsconf-extract-youtube-api-channels) 0)
- .contentDetails.relatedPlaylists.uploads)
- ))
- :headers `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/")))
- :as #'json-read))))))
+ (emacsconf-extract-youtube-api-videos))))))
+
+(defun emacsconf-extract-youtube-rename-draft-videos-as-qa (&optional videos)
+ "Rename videos and set the QA_YOUTUBE_URL property in the Org heading."
+ (interactive)
+ (let ((info (emacsconf-get-talk-info)))
+ (mapc
+ (lambda (video)
+ (let-alist video
+ (when (and
+ (string= .status.privacyStatus "private")
+ (string-match (rx (literal emacsconf-id) " " (literal emacsconf-year) " ")
+ .snippet.title))
+ (emacsconf-extract-youtube-api-update-video video t))))
+ (assoc-default
+ 'items
+ (or videos emacsconf-extract-youtube-api-videos
+ (emacsconf-extract-youtube-api-videos))))))
;; This still needed some tweaking, so maybe next time we'll try just inserting the items into the playlist
+(defvar emacsconf-extract-youtube-api-playlist nil)
+(defvar emacsconf-extract-youtube-api-playlist-items nil)
(defun emacsconf-extract-youtube-sort-playlist ()
(interactive)
- (let* ((playlist-info
- (seq-find (lambda (o) (let-alist o (string= .snippet.title (concat emacsconf-name " " emacsconf-year))))
- (assoc-default 'items emacsconf-extract-youtube-api-playlists)))
+ (setq emacsconf-extract-youtube-api-playlist (seq-find (lambda (o) (let-alist o (string= .snippet.title (concat emacsconf-name " " emacsconf-year))))
+ (assoc-default 'items emacsconf-extract-youtube-api-playlists)))
+ (setq emacsconf-extract-youtube-api-playlist-items
+ (emacsconf-extract-youtube-api-paginated-request (concat "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails,status&forMine=true&order=date&maxResults=100&playlistId="
+ (url-hexify-string (assoc-default 'id emacsconf-extract-youtube-api-playlist)))))
+ (let* ((playlist-info emacsconf-extract-youtube-api-playlists)
(playlist-items
- (assoc-default 'items
- (plz 'get (concat "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails,status&forMine=true&order=date&maxResults=50&playlistId=" (url-hexify-string (assoc-default 'id playlist-info)))
- :headers `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/")))
- :as #'json-read)))
+ (assoc-default 'items emacsconf-youtube-api-playlist-items))
(info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))
(slugs (seq-map (lambda (o) (plist-get o :slug)) info))
- last-position)
+ (position (1- (length playlist-items))))
;; sort items
- (seq-map-indexed (lambda (talk position)
- (let ((video-object (seq-find (lambda (video) (string= (plist-get talk :slug)
- (emacsconf-extract-youtube-get-slug-for-video video)))
- playlist-items)))
- (let-alist video-object
- (cond
- ((null video-object)
- (message "Could not find video for %s" (plist-get talk :slug)))
- ;; not in the right position, try to move it
- ((> .snippet.position position)
- (let ((video-id .id)
- (playlist-id .snippet.playlistId)
- (resource-id .snippet.resourceId))
- (message "Trying to move %s to %d from %d" (plist-get talk :slug) position .snippet.position)
- (plz 'put "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet"
- :headers `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/"))
- ("Accept" . "application/json")
- ("Content-Type" . "application/json"))
- :body (json-encode
- `((id . ,video-id)
- (snippet
- (playlistId . ,playlist-id)
- (resourceId . ,resource-id)
- (position . ,position)))))))))
- )) info)))
+ (seq-map (lambda (talk)
+ (when (plist-get talk :qa-youtube-id)
+ ;; move the q & a
+ (let ((video-object (seq-find (lambda (video) (string= (assoc-default 'id video)
+ (plist-get talk :qa-youtube-id)))
+ playlist-items)))
+ (let-alist video-object
+ (cond
+ ((null video-object)
+ (message "Could not find video for %s" (plist-get talk :slug)))
+ ;; not in the right position, try to move it
+ ((< .snippet.position position)
+ (let ((video-id .id)
+ (playlist-id .snippet.playlistId)
+ (resource-id .snippet.resourceId))
+ (message "Trying to move %s Q&A to %d from %d" (plist-get talk :slug) position .snippet.position)
+ (plz 'put "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet"
+ :headers `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/"))
+ ("Accept" . "application/json")
+ ("Content-Type" . "application/json"))
+ :body (json-encode
+ `((id . ,video-id)
+ (snippet
+ (playlistId . ,playlist-id)
+ (resourceId . ,resource-id)
+ (position . ,position)))))))))
+ (setq position (1- position)))
+ ;; move the talk if needed
+ (let ((video-object (seq-find (lambda (video) (string-match
+ (regexp-quote (let-alist video .resourceId.videoId))
+ (plist-get talk :youtube-url)))
+ playlist-items)))
+ (let-alist video-object
+ (cond
+ ((null video-object)
+ (message "Could not find video for %s" (plist-get talk :slug)))
+ ;; not in the right position, try to move it
+ ((< .snippet.position position)
+ (let ((video-id .id)
+ (playlist-id .snippet.playlistId)
+ (resource-id .snippet.resourceId))
+ (message "Trying to move %s to %d from %d" (plist-get talk :slug) position .snippet.position)
+ (plz 'put "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet"
+ :headers `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/"))
+ ("Accept" . "application/json")
+ ("Content-Type" . "application/json"))
+ :body (json-encode
+ `((id . ,video-id)
+ (snippet
+ (playlistId . ,playlist-id)
+ (resourceId . ,resource-id)
+ (position . ,position)))))))))
+ (setq position (1- position)))))
+ (nreverse info))))
+
+(defun emacsconf-extract-youtube-get-video-details (&optional videos)
+ (let (result url)
+ (dolist (partition (seq-partition (or videos emacsconf-extract-youtube-api-videos) 50) result)
+ (setq url
+ (format "https://www.googleapis.com/youtube/v3/videos?id=%s&part=snippet,contentDetails,statistics"
+ (mapconcat (lambda (item) (url-hexify-string (let-alist item .contentDetails.videoId))) partition ",")))
+ (message "%s" url)
+ (setq result
+ (append result
+ (assoc-default
+ 'items
+
+ (plz 'get
+ url
+ :headers `(("Authorization" . ,(url-oauth-auth "https://youtube.googleapis.com/youtube/v3/"))
+ ("Accept" . "application/json")
+ ("Content-Type" . "application/json"))
+ :as #'json-read))
+ nil)))))
+
+(defun emacsconf-extract-youtube-duration-msecs (video)
+ (let-alist video
+ (when-let ((duration .contentDetails.duration))
+ (when (string-match "PT\\(?:\\([0-9]+\\)H\\)?\\([0-9]+\\)M\\([0-9]+\\)" duration)
+ (+ (* (string-to-number (or (match-string 1 duration) "0")) 60 60 1000)
+ (* (string-to-number (or (match-string 2 duration) "0")) 60 1000)
+ (* (string-to-number (or (match-string 3 duration) "0")) 1000))))))
+
+(defun emacsconf-extract-youtube-format-duration (s)
+ "Converts a string of the form PT1H9M22S to 1:09:22."
+ (when (and s (string-match "PT\\(?:\\([0-9]+\\)H\\)?\\([0-9]+\\)M\\([0-9]+\\)" s))
+ (concat
+ (emacsconf-surround "" (match-string 1 s) ":")
+ (if (match-string 1 s)
+ (format "%02d:" (string-to-number (match-string 2 s)))
+ (concat (match-string 2 s) ":"))
+ (format "%02d" (string-to-number (match-string 3 s))))))
+
+(defun emacsconf-extract-youtube-find-url-video-in-list (url &optional videos)
+ (seq-find
+ (lambda (o)
+ (let-alist o
+ (or (string-match .id url)
+ (and .contentDetails.videoId (string-match .contentDetails.videoId url)))))
+ (or videos emacsconf-extract-youtube-api-videos)))
+
+(ert-deftest emacsconf-extract-youtube-format-duration ()
+ (expect (emacsconf-extract-youtube-format-duration "PT1H9M22S") :to-equal "1:09:22")
+ (expect (emacsconf-extract-youtube-format-duration "PT9M22S") :to-equal "9:22"))
+
+
+;; (setq emacsconf-extract-youtube-api-video-details (emacsconf-extract-youtube-get-video-details emacsconf-extract-youtube-api-playlist-items))
+
+;;; 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 " 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
+ (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)))
+
+(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)
+ (let ((desc (spookfox-js-injection-eval-in-active-tab
+ "document.querySelector('.video-info-description-html').innerHTML" t)))
+ (when (string-match (rx (literal emacsconf-base-url) (literal emacsconf-year) "/talks/"
+ (group (1+ (not (or " " "/" "\"")))))
+ desc)
+ (let ((slug (match-string 1 desc))
+ (url (spookfox-js-injection-eval-in-active-tab "window.location.href" t)))
+ (save-window-excursion
+ (emacsconf-with-talk-heading slug
+ (org-entry-put (point)
+ (if (string-match "Q&A" desc)
+ "QA_TOOBNIX_URL"
+ "TOOBNIX_URL")
+ url)
+ (message "Updating %s %s %s"
+ slug
+ (if (string-match "Q&A" desc)
+ "QA_TOOBNIX_URL"
+ "TOOBNIX_URL")
+ url))))
+ (shell-command "xdotool key Alt+Tab sleep 1 key Ctrl+w Alt+Tab"))))
+
(provide 'emacsconf-extract)
;;; emacsconf-extract.el ends here