<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/atom.xsl" type="text/xsl"?><feed
	xmlns="http://www.w3.org/2005/Atom"
	xmlns:thr="http://purl.org/syndication/thread/1.0"
	xml:lang="en-US"
	><title>Sacha Chua - category - youtube</title>
	<subtitle>Emacs, sketches, and life</subtitle>
	<link rel="self" type="application/atom+xml" href="https://sachachua.com/blog/category/youtube/feed/atom/index.xml" />
  <link rel="alternate" type="text/html" href="https://sachachua.com/blog/category/youtube" />
  <id>https://sachachua.com/blog/category/youtube/feed/atom/index.xml</id>
  <generator uri="https://11ty.dev">11ty</generator>
	<updated>2024-01-04T15:57:25Z</updated>
<entry>
		<title type="html">Quick notes on livestreaming to YouTube with FFmpeg on a Lenovo X230T</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2024/01/quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2024-01-04T15:57:25Z</updated>
    <published>2024-01-04T15:57:25Z</published>
    <category term="video" />
<category term="youtube" />
<category term="streaming" />
<category term="ffmpeg" />
<category term="yay-emacs" />
		<id>https://sachachua.com/blog/2024/01/quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t/</id>
		<content type="html"><![CDATA[<div class="update" id="org78e2751">
<p>
<span class="timestamp-wrapper"><span class="timestamp">[2024-01-05 Fri]</span></span>: Updated scripts
</p>

</div>

<p>
</p><div class="sketch-full"><a class="photoswipe" href="https://sketches.sachachua.com/filename/2024-01-03-03%20Quick%20thoughts%20on%20livestreaming%20%23sharing%20%23video%20%23streaming%20%23community.png" data-src="https://sketches.sachachua.com/static/2024-01-03-03%20Quick%20thoughts%20on%20livestreaming%20%23sharing%20%23video%20%23streaming%20%23community.png" data-title="2024-01-03-03 Quick thoughts on livestreaming #sharing #video #streaming #community.png" data-w="2808" data-h="3744"><picture>
      <img src="https://sketches.sachachua.com/static/2024-01-03-03%20Quick%20thoughts%20on%20livestreaming%20%23sharing%20%23video%20%23streaming%20%23community.png" width="2808" height="3744" alt="2024-01-03-03 Quick thoughts on livestreaming #sharing #video #streaming #community.png" loading="lazy" style="max-height: 90vw; height: auto; width: auto" decoding="async">
      <figcaption>2024-01-03-03 Quick thoughts on livestreaming #sharing #video #streaming #community.png</figcaption>
    </picture></a></div>
<p></p>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Text from the sketch</strong></summary>
<p>
Quick thoughts on livestreaming
</p>

<p>
Why:
</p>

<ul class="org-ul">
<li>work out loud</li>
<li>share tips</li>
<li>share more</li>
<li>spark conversations</li>
<li>(also get questions about things)</li>
</ul>

<p>
Doable with ffmpeg on my X230T:
</p>

<ul class="org-ul">
<li>streaming from my laptop</li>
<li>lapel mic + system audio,</li>
<li>second screen for monitoring</li>
</ul>

<p>
Ideas for next time:
</p>

<ul class="org-ul">
<li>Overall notes in Emacs with outline, org-timer timestamped notes; capture to this file</li>
<li>Elisp to start/stop the stream → find old code</li>
<li>Use the Yeti? Better sound</li>
<li>tee to a local recording</li>
<li>grab screenshot from SuperNote mirror?</li>
</ul>

<p>
Live streaming info density:
</p>

<ul class="org-ul">
<li>High: Emacs News review, package/workflow demo</li>
<li>Narrating a blog post to make it a video</li>
<li>Categorizing Emacs News, exploring packages</li>
<li>Low: Figuring things out</li>
</ul>

<p>
YouTube can do closed captions for livestreams, although accuracy is
low. Videos take a while to be ready to download.
</p>


</details>
<div id="outline-container-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-experimenting-with-working-out-loud" class="outline-2">
<h3 id="quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-experimenting-with-working-out-loud">Experimenting with working out loud</h3>
<div class="outline-text-2" id="text-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-experimenting-with-working-out-loud">
<p>
I wanted to write <a href="https://emacsconf.org/2023/report">a report on EmacsConf 2023</a> so that we could share it
with speakers, volunteers, participants, donors, related organizations
like the Free Software Foundation, and other communities. I
experimented with livestreaming via YouTube while I worked on the
conference highlights.
</p>

<p>
It's a little over an hour long and probably very boring, but it was
nice of people to drop by and say hello.
</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/KXKA-3JFb14?si=2V5XQmQSUte0ZWyF" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>

<p>
The main parts are:
</p>

<ul class="org-ul">
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h0m00s">0:00</a>: reading through other conference reports for inspiration</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h6m54s">6:54</a>: writing an overview of the talks</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h13m10s">13:10</a>: adding quotes for specific talks</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h25m00s">25:00</a>: writing about the overall conference</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h32m00s">32:00</a>: squeezing in more highlights</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h49m00s">49:00</a>: fiddling with the formatting and the export</li>
</ul>

<p>
It mostly worked out, aside from a brief moment of "uhhh, I'm
looking at our private conf.org file on stream". Fortunately, the
e-mail addresses that were showed were the public ones.
</p>
</div>
</div>
<div id="outline-container-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-technical-details" class="outline-2">
<h3 id="quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-technical-details">Technical details</h3>
<div class="outline-text-2" id="text-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-technical-details">
<p>
Setup:
</p>

<ul class="org-ul">
<li><p>
I set up environment variables and screen resolution:
</p>

<div class="org-src-container">
<pre class="src src-sh" id="org8d1d9a1">  <span class="org-comment-delimiter"># </span><span class="org-comment">From pacmd list-sources | egrep '^\s+name'</span>
  <span class="org-variable-name">LAPEL</span>=alsa_input.usb-Jieli_Technology_USB_Composite_Device_433035383239312E-00.mono-fallback <span class="org-comment-delimiter">#</span>
  <span class="org-variable-name">YETI</span>=alsa_input.usb-Blue_Microphones_Yeti_Stereo_Microphone_REV8-00.analog-stereo
  <span class="org-variable-name">SYSTEM</span>=alsa_output.pci-0000_00_1b.0.analog-stereo.monitor
  <span class="org-comment-delimiter"># </span><span class="org-comment">MIC=$LAPEL</span>
  <span class="org-comment-delimiter"># </span><span class="org-comment">AUDIO_WEIGHTS="1 1"</span>
  <span class="org-variable-name">MIC</span>=$<span class="org-variable-name">YETI</span>
  <span class="org-variable-name">AUDIO_WEIGHTS</span>=<span class="org-string">"0.5 0.5"</span>
  <span class="org-variable-name">OFFSET</span>=+1920,430
  <span class="org-variable-name">SIZE</span>=1280x720
  <span class="org-variable-name">SCREEN</span>=LVDS-1  <span class="org-comment-delimiter"># </span><span class="org-comment">from xrandr</span>
  xrandr &#45;&#45;output $<span class="org-variable-name">SCREEN</span> &#45;&#45;mode 1280x720
</pre>
</div>
</li>
<li>I switch to a larger size and a light theme. I also turn <a href="https://github.com/minad/consult">consult</a> previews off to minimize the risk of leaking data through buffer previews.
<details open=""><summary>my-emacsconf-prepare-for-screenshots: Set the resolution, change to a light theme, and make the text bigger.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-emacsconf-prepare-for-screenshots</span> ()
  (<span class="org-keyword">interactive</span>)
  (shell-command <span class="org-string">"xrandr &#45;&#45;output LVDS-1 &#45;&#45;mode 1280x720"</span>)
  (modus-themes-load-theme <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">modus-operandi</span>)
  (my-hl-sexp-update-overlay)
  (set-face-attribute <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">default</span> nil <span class="org-builtin">:height</span> 170)
  (keycast-mode))
</pre></div></details></li>
</ul>

<p>
Testing:
</p>


<div class="org-src-container">
<pre class="src src-sh">ffmpeg -f x11grab -video_size $<span class="org-variable-name">SIZE</span> -i :0.0$<span class="org-variable-name">OFFSET</span> -y /tmp/test.png; display /tmp/test.png
ffmpeg -f pulse -i $<span class="org-variable-name">MIC</span> -f pulse -i $<span class="org-variable-name">SYSTEM</span> -filter_complex <span class="org-variable-name">amix</span>=<span class="org-variable-name">inputs</span>=2:<span class="org-variable-name">weights</span>=$<span class="org-variable-name">AUDIO_WEIGHTS</span>:<span class="org-variable-name">duration</span>=longest:<span class="org-variable-name">normalize</span>=0 -y /tmp/test.mp3; mpv /tmp/test.mp3
<span class="org-variable-name">DATE</span>=$(date <span class="org-string">"+%Y-%m-%d-%H-%M-%S"</span>)
ffmpeg -f x11grab -framerate 30 -video_size $<span class="org-variable-name">SIZE</span> -i :0.0$<span class="org-variable-name">OFFSET</span> -f pulse -i $<span class="org-variable-name">MIC</span> -f pulse -i $<span class="org-variable-name">SYSTEM</span> -filter_complex <span class="org-string">"amix=inputs=2:weights=$AUDIO_WEIGHTS:duration=longest:normalize=0"</span> -c:v libx264 -preset fast -maxrate 690k -bufsize 2000k -g 60 -vf <span class="org-variable-name">format</span>=yuv420p -c:a aac -b:a 96k -y -flags +global_header <span class="org-string">"/home/sacha/recordings/$DATE.flv"</span> -f flv
</pre>
</div>


<p>
Streaming:
</p>


<div class="org-src-container">
<pre class="src src-sh"><span class="org-variable-name">DATE</span>=$(date <span class="org-string">"+%Y-%m-%d-%H-%M-%S"</span>)
ffmpeg -f x11grab -framerate 30 -video_size $<span class="org-variable-name">SIZE</span> -i :0.0$<span class="org-variable-name">OFFSET</span> -f pulse -i $<span class="org-variable-name">MIC</span> -f pulse -i $<span class="org-variable-name">SYSTEM</span> -filter_complex <span class="org-string">"amix=inputs=2:weights=$AUDIO_WEIGHTS:duration=longest:normalize=0[audio]"</span> -c:v libx264 -preset fast -maxrate 690k -bufsize 2000k -g 60 -vf <span class="org-variable-name">format</span>=yuv420p -c:a aac -b:a 96k -y -f tee -map 0:v -map <span class="org-string">'[audio]'</span> -flags +global_header  <span class="org-string">"/home/sacha/recordings/$DATE.flv|[f=flv]rtmp://a.rtmp.youtube.com/live2/$YOUTUBE_KEY"</span>
</pre>
</div>


<p>
To restore my previous setup:
</p>

<p>
</p><details open=""><summary>my-emacsconf-back-to-normal: Go back to a more regular setup.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-emacsconf-back-to-normal</span> ()
  (<span class="org-keyword">interactive</span>)
  (shell-command <span class="org-string">"xrandr &#45;&#45;output LVDS-1 &#45;&#45;mode 1366x768"</span>)
  (modus-themes-load-theme <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">modus-vivendi</span>)
  (my-hl-sexp-update-overlay)
  (set-face-attribute <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">default</span> nil <span class="org-builtin">:height</span> 115)
  (keycast-mode -1))
</pre></div></details>
<p></p>
</div>
</div>
<div id="outline-container-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-ideas-for-next-steps" class="outline-2">
<h3 id="quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-ideas-for-next-steps">Ideas for next steps</h3>
<div class="outline-text-2" id="text-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-ideas-for-next-steps">
<p>
I can think of a few workflow tweaks that might be fun:
</p>

<ul class="org-ul">
<li>a stream notes buffer on the right side of the screen for context
information, timestamped notes to make editing/review easier (maybe
using <code>org-timer</code>), etc. I experimented with some streaming-related
code in my config, so I can dust that off and see what that's like.
I also want to have an <code>org-capture</code> template for it so that I can add
notes from anywhere.</li>
<li>a quick way to <a href="https://sachachua.com/blog/2024/01/using-puppeteer-to-grab-an-image-from-the-supernote-s-screen-mirror/">add a screenshot from my Supernote</a> to my Org files</li>
</ul>

<p>
I think I'll try going through an informal presentation or Emacs News as my next livestream experiment, since that's probably higher information density.
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2024/01/quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t/index.org">View org source for this post</a></div>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2024%2F01%2Fquick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">EmacsConf backstage: Trimming the BigBlueButton recordings based on YouTube duration</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2023/12/emacsconf-backstage-trimming-the-bigbluebutton-recordings/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2023-12-28T20:04:29Z</updated>
    <published>2023-12-28T20:04:29Z</published>
    <category term="emacsconf" />
<category term="emacs" />
<category term="youtube" />
<category term="video" />
		<id>https://sachachua.com/blog/2023/12/emacsconf-backstage-trimming-the-bigbluebutton-recordings/</id>
		<content type="html"><![CDATA[<p>
I wanted to get the Q&amp;A sessions up quickly after the conference, so I
uploaded them to YouTube and added them to the <a href="https://www.youtube.com/playlist?list=PLomc4HLgvuCUdrW3JkugtKv8xPelUoOyP">EmacsConf 2023
playlist</a>. I used YouTube's video editor to roughly guess where to
trim them based on the waveforms. I needed to actually trim the source
videos, though, so that our copies would be up to date and I could use
those for the Toobnix uploads.
</p>

<p>
My first task was to figure out which videos needed to be trimmed to
match the YouTube edits. First, I retrieved the video details using
the API and the code that I added to <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-extract.el">emacsconf-extract.el</a>.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-video-details (emacsconf-extract-youtube-get-video-details emacsconf-extract-youtube-api-playlist-items))
</pre>
</div>


<p>
Then I made a table comparing the file duration with the YouTube duration, showing rows only if the difference was more than 3 minutes.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp" id="orgfb43101">(append
 <span class="org-highlight-quoted-quote">'</span>((<span class="org-string">"type"</span> <span class="org-string">"slug"</span> <span class="org-string">"file duration"</span> <span class="org-string">"youtube duration"</span> <span class="org-string">"diff"</span>))
 (<span class="org-keyword">let</span> ((threshold-secs (* 3 60))) <span class="org-comment-delimiter">; </span><span class="org-comment">don't sweat small differences</span>
   (seq-mapcat
    (<span class="org-keyword">lambda</span> (talk)
      (seq-keep
       (<span class="org-keyword">lambda</span> (row)
         (<span class="org-keyword">when</span> (plist-get talk (cadr row))
           (<span class="org-keyword">let*</span> ((video (emacsconf-extract-youtube-find-url-video-in-list
                          (plist-get talk (cadr row))
                          emacsconf-extract-youtube-api-video-details))
                  (video-duration (<span class="org-keyword">if</span> (<span class="org-keyword">and</span> video (emacsconf-extract-youtube-duration-msecs video))
                                      (/ (emacsconf-extract-youtube-duration-msecs video) 1000.0)))
                  (file-duration (ceiling
                                  (/ (compile-media-get-file-duration-ms (emacsconf-talk-file talk (format <span class="org-string">"&#45;&#45;%s.webm"</span> (car row))))
                                     1000.0))))
             (<span class="org-keyword">when</span> (<span class="org-keyword">and</span> video-duration (&gt; (abs (- file-duration video-duration)) threshold-secs))
               (list (car row)
                     (plist-get talk <span class="org-builtin">:slug</span>)
                     (<span class="org-keyword">and</span> file-duration (format-seconds <span class="org-string">"%h:%z%.2m:%.2s"</span> file-duration))
                     (<span class="org-keyword">and</span> video-duration (format-seconds <span class="org-string">"%h:%z%.2m:%.2s"</span> video-duration))
                     (emacsconf-format-seconds
                      (abs (- file-duration video-duration))))))))
       <span class="org-highlight-quoted-quote">'</span>((<span class="org-string">"main"</span> <span class="org-builtin">:youtube-url</span>)
         (<span class="org-string">"answers"</span> <span class="org-builtin">:qa-youtube-url</span>))))
    (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))))
</pre>
</div>


<p>
Then I got the commands to trim the videos.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"> (mapconcat (<span class="org-keyword">lambda</span> (row)
              (<span class="org-keyword">let</span> ((talk (emacsconf-resolve-talk (elt row 1))))
                (format <span class="org-string">"ffmpeg -y -i %s&#45;&#45;%s.webm -t %s -c copy %s&#45;&#45;%s&#45;&#45;trimmed.webm"</span>
                        (plist-get talk <span class="org-builtin">:file-prefix</span>)
                        (car row)
                        (concat (elt row 3) <span class="org-string">".000"</span>)
                        (plist-get talk <span class="org-builtin">:file-prefix</span>)
                        (car row))))
            (cdr to-trim)
            <span class="org-string">"\n"</span>))
</pre>
</div>


<p>
After quickly checking the results, I copied them over to the original videos, updated the video data in my conf.org, and republished the info pages in the wiki.
</p>

<p>
The time I spent on figuring out how to talk to the YouTube API feels like it's paying off.
</p>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2023%2F12%2Femacsconf-backstage-trimming-the-bigbluebutton-recordings%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">EmacsConf backstage: Using Spookfox to publish YouTube and Toobnix video drafts</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2023/12/emacsconf-backstage-using-spookfox-to-publish-youtube-and-toobnix-video-drafts/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2023-12-12T20:09:03Z</updated>
    <published>2023-12-12T20:09:03Z</published>
    <category term="emacsconf" />
<category term="emacs" />
<category term="spookfox" />
<category term="youtube" />
<category term="video" />
		<id>https://sachachua.com/blog/2023/12/emacsconf-backstage-using-spookfox-to-publish-youtube-and-toobnix-video-drafts/</id>
		<content type="html"><![CDATA[<p>
I ran into quota limits when uploading videos to YouTube with a command-line tool, so I uploaded videos by selecting up to 15 videos at a time using the web-based interface. Each video was a draft, though, and I was having a hard time updating its visibility through the API. I think it eventually worked, but in the meantime, I used this very hacky hack to look for the "Edit Draft" button and click through the screens to publish them.
</p>

<p>
</p><details open=""><summary>emacsconf-extract-youtube-publish-video-drafts-with-spookfox: Look for drafts and publish them.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-publish-video-drafts-with-spookfox</span> ()
  <span class="org-doc">"Look for drafts and publish them."</span>
  (<span class="org-keyword">while</span> (not (eq (spookfox-js-injection-eval-in-active-tab
                   <span class="org-string">"document.querySelector('.edit-draft-button div') != null"</span> t) <span class="org-builtin"><span class="org-warning">:false</span></span><span class="org-warning">))</span>
    (<span class="org-keyword">progn</span>
      (spookfox-js-injection-eval-in-active-tab
       <span class="org-string">"document.querySelector('.edit-draft-button div').click()"</span> t)
      (sleep-for 2)
      (spookfox-js-injection-eval-in-active-tab
       <span class="org-string">"document.querySelector('#step-title-3').click()"</span> t)
      (<span class="org-keyword">when</span> (spookfox-js-injection-eval-in-active-tab
             <span class="org-string">"document.querySelector('tp-yt-paper-radio-button[name=\"PUBLIC\"] #radioLabel').click()"</span> t)
        (spookfox-js-injection-eval-in-active-tab
         <span class="org-string">"document.querySelector('#done-button').click()"</span> t)
        (<span class="org-keyword">while</span> (not (eq  (spookfox-js-injection-eval-in-active-tab
                          <span class="org-string">"document.querySelector('#close-button .label') == null"</span> t)
                         <span class="org-builtin">:false</span>))
          (sleep-for 1))

        (spookfox-js-injection-eval-in-active-tab
         <span class="org-string">"document.querySelector('#close-button .label').click()"</span> t)
        (sleep-for 1)))))
</pre></div></details>
<p></p>

<p>
Another example of a hacky Spookfox workaround was publishing the
unlisted videos. I couldn't figure out how to properly authenticate
with the Toobnix (Peertube) API to change the visibility of videos.
Peertube uses AngularJS components in the front end, so using
<code>.click()</code> on the input elements didn't seem to trigger anything. I
found out that I needed to use <code>.dispatchEvent(new Event('input'))</code> to
tell the dropdown for the visibility to display the options. <a href="https://stackoverflow.com/questions/67633828/angular-and-vanila-js-set-input-value">source</a>
</p>

<p>
</p><details open=""><summary>emacsconf-extract-toobnix-publish-video-from-edit-page: Messy hack to set a video to public and store the URL.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-toobnix-publish-video-from-edit-page</span> ()
  <span class="org-doc">"Messy hack to set a video to public and store the URL."</span>
  (<span class="org-keyword">interactive</span>)
  (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('label[for=privacy]').scrollIntoView(); document.querySelector('label[for=privacy]').closest('</span><span class="org-string"><span class="org-constant">.form-group</span></span><span class="org-string">').querySelector('</span><span class="org-string"><span class="org-constant">input</span></span><span class="org-string">').dispatchEvent(new Event('</span><span class="org-string"><span class="org-constant">input</span></span><span class="org-string">'));"</span> t)
  (sit-for 1)
  (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('span[title=\"Anyone can see this video\"]').click()"</span> t)
  (sit-for 1)
  (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('</span><span class="org-string"><span class="org-constant">button.orange-button</span></span><span class="org-string">').click()"</span> t)(sit-for 3)
  (emacsconf-extract-store-url)
  (shell-command <span class="org-string">"xdotool key Alt+Tab sleep 1 key Ctrl+w Alt+Tab"</span>))
</pre></div></details>
<p></p>

<p>
It's a little nicer using Spookfox to automate browser interactions
than using xdotool, since I can get data out of it too. I could also
have used Puppeteer from either Python or NodeJS, but it's nice
staying with Emacs Lisp. Spookfox has some Javascript limitations
(can't close windows, etc.), so I might still use bits of xdotool or
Puppeteer to work around that. Still, it's nice to now have an idea of
how to talk to AngularJS components.
</p>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2023%2F12%2Femacsconf-backstage-using-spookfox-to-publish-youtube-and-toobnix-video-drafts%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">EmacsConf backstage: Making a (play)list, checking it twice</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2023/12/emacsconf-backstage-making-a-play-list-checking-it-twice/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2023-12-12T15:50:37Z</updated>
    <published>2023-12-12T15:50:37Z</published>
    <category term="emacs" />
<category term="emacsconf" />
<category term="spookfox" />
<category term="youtube" />
<category term="video" />
		<id>https://sachachua.com/blog/2023/12/emacsconf-backstage-making-a-play-list-checking-it-twice/</id>
		<content type="html"><![CDATA[<p>
I wanted the <a href="https://www.youtube.com/playlist?list=PLomc4HLgvuCUdrW3JkugtKv8xPelUoOyP">EmacsConf 2023 Youtube</a> and
<a href="https://toobnix.org/w/p/nMXCCJ25wxKUtbuQiwkakA">Toobnix playlists</a>
to mostly reflect the schedule of the conference by track, with talks
followed by their Q&amp;A sessions (if recorded).
</p>
<div id="outline-container-org8e7129c" class="outline-2">
<h3 id="org8e7129c">The list</h3>
<div class="outline-text-2" id="text-org8e7129c">
<p>
I used Emacs Lisp to generate a list of videos in the order I wanted.
That Sunday closing remarks aren't actually in the playlists because
they're combined with the Q&amp;A for my session on how we run Emacsconf.
</p>

<p>
</p><details><summary>emacsconf-extract-check-playlists: Return a table for checking playlist order.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-check-playlists</span> ()
  <span class="org-doc">"Return a table for checking playlist order."</span>
  (<span class="org-keyword">let</span> ((pos 0))
    (seq-mapcat (<span class="org-keyword">lambda</span> (o)
                  (delq
                   nil
                   (list
                    (<span class="org-keyword">when</span> (emacsconf-talk-file o <span class="org-string">"&#45;&#45;main.webm"</span>)
                      (<span class="org-keyword">cl-incf</span> pos)
                      (list pos
                            (plist-get o <span class="org-builtin">:title</span>)
                            (org-link-make-string
                             (plist-get o <span class="org-builtin">:youtube-url</span>)
                             <span class="org-string">"YouTube"</span>)
                            (org-link-make-string
                             (plist-get o <span class="org-builtin">:toobnix-url</span>)
                             <span class="org-string">"Toobnix"</span>)))
                    (<span class="org-keyword">when</span> (emacsconf-talk-file o <span class="org-string">"&#45;&#45;answers.webm"</span>)
                      (<span class="org-keyword">cl-incf</span> pos)
                      (list pos (concat <span class="org-string">"Q&amp;A: "</span> (plist-get o <span class="org-builtin">:title</span>))
                            (org-link-make-string
                             (plist-get o <span class="org-builtin">:qa-youtube-url</span>)
                             <span class="org-string">"YouTube"</span>)
                            (org-link-make-string
                             (plist-get o <span class="org-builtin">:qa-toobnix-url</span>)
                             <span class="org-string">"Toobnix"</span>))))))
                (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))))
</pre></div></details>
<p></p>

<div class="bordered" id="org2e50b18">
<table>


<colgroup>
<col class="org-right">

<col class="org-left">

<col class="org-left">

<col class="org-left">
</colgroup>
<tbody>
<tr>
<td class="org-right">1</td>
<td class="org-left">An Org-Mode based text adventure game for learning the basics of Emacs, inside Emacs, written in Emacs Lisp</td>
<td class="org-left"><a href="https://youtu.be/7R0yA0R1jsk">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/2oqbPJB8Wm3QSo4HCKAyVn">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">2</td>
<td class="org-left">Authoring and presenting university courses with Emacs and a full libre software stack</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=cklJ58i-HUY">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/mAnNW7jnPq5qhUPH2dzVQf">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">3</td>
<td class="org-left">Q&amp;A: Authoring and presenting university courses with Emacs and a full libre software stack</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=d3Q1BgLhlj0">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/tjHQVNVaTa8dd9cfAXXQ3W">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">4</td>
<td class="org-left">Teaching computer and data science with literate programming tools</td>
<td class="org-left"><a href="https://youtu.be/U15zUNBz2CU">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/b4eLjcLo9vcewVTzrv95L8">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">5</td>
<td class="org-left">Q&amp;A: Teaching computer and data science with literate programming tools</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=eP4CIw_L4Mw">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/fCxBagZrbR5QrgD9YcrtTJ">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">6</td>
<td class="org-left">Who needs Excel? Managing your students qualifications with org-table</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=UzDqOrFGWbw">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/p8K8mtayv2HYtw1gK3zUwR">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">7</td>
<td class="org-left">one.el: the static site generator for Emacs Lisp Programmers</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=AaRCuN0flRE">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/x2yYYWLHQe75FTV8sWiDmy">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">8</td>
<td class="org-left">Q&amp;A: one.el: the static site generator for Emacs Lisp Programmers</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=Uq5IbkI7G7A">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/2b6Jdhxd2PBekPRCNv9c21">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">9</td>
<td class="org-left">Emacs turbo-charges my writing</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=HxlEK6W7RyA">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/ke3UCJaJSLyQr7Emv8VxST">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">10</td>
<td class="org-left">Q&amp;A: Emacs turbo-charges my writing</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=CEflOQRvLiw">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/dC3RnBATHdZwgfjD9QKgZY">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">11</td>
<td class="org-left">Why Nabokov would use Org-Mode if he were writing today</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=-E_uNxwL2_I">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/bDou9TDETryMt18KcdB56A">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">12</td>
<td class="org-left">Q&amp;A: Why Nabokov would use Org-Mode if he were writing today</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=uPemMZV1r-0">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/7BVMF29fRn3JAeRsuCnKnz">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">13</td>
<td class="org-left">Collaborative data processing and documenting using org-babel</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=fz7-Kd83IjM">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/7AAwoawr5MXNSrqiHJQoak">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">14</td>
<td class="org-left">How I play TTRPGs in Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=KUMkj9HWiEY">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/oNkcCHdWCKXRv6KnUTAeEC">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">15</td>
<td class="org-left">Q&amp;A: How I play TTRPGs in Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=bC6KWTR1Zz4">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/rzufpMjdQyGnqNBKC7jKXp">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">16</td>
<td class="org-left">Org-Mode workflow: informal reference tracking</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=qx1yeJ1Exrw">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/cYpEatASFWXLzDfKH4Fhec">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">17</td>
<td class="org-left">(Un)entangling projects and repos</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=o9j4IwJsvPI">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/wLxyZBoFAad575Lp4PGyoF">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">18</td>
<td class="org-left">Emacs development updates</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=SPSoRZVJUf8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/57HSebb9a9JZynh2B3ehze">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">19</td>
<td class="org-left">Emacs core development: how it works</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=2izQJiuL0vA">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/m4XmrmE9Geat54AKT1RQaH">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">20</td>
<td class="org-left">Top 10 ways Hyperbole amps up Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=BysjfL25Nlc">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/4Cpb89zHKgQjob3gHUs73C">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">21</td>
<td class="org-left">Using Koutline for stream of thought journaling</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=dO-gv898Vmg">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/vV7qtK176DVE6RLXrZ18Ee">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">22</td>
<td class="org-left">Parallel text replacement</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=fUbBIWOJFh4">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/t3G5zo35epS6HvVot9MdZv">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">23</td>
<td class="org-left">Q&amp;A: Parallel text replacement</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=lKpvJqRXu-E">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/hQtZZfYvzoUsDxSySigK33">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">24</td>
<td class="org-left">Eat and Eat powered Eshell, fast featureful terminal inside Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=KQ5Jt-63G9U">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/t4pPDtbXiZdHHEyWJVUtNs">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">25</td>
<td class="org-left">The browser in a buffer</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=mp6gaVjmKIU">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/1quXfJqC9bh9VxkA9UC21x">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">26</td>
<td class="org-left">Speedcubing in Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=Q5HPmyaiu4g">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/2DYX2o8kB1Rv8Mqaj7H1Dx">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">27</td>
<td class="org-left">Emacs MultiMedia System (EMMS)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=kII413hkyis">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/ppdF62LysvxpXgZVaeF9wk">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">28</td>
<td class="org-left">Q&amp;A: Emacs MultiMedia System (EMMS)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=KJGBASdI2JI">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/b5xiqdxWCGyCo2cdsK3v9h">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">29</td>
<td class="org-left">Programming with steno</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=McHurKmk-rQ">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/1xodScC6DPkfbnqG5FmbB3">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">30</td>
<td class="org-left">Mentoring VS-Coders as an Emacsian (or How to show not tell people about the wonders of Emacs)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=44rt1f1llhQ">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/sV9eKtGiPYZi5urxjoqerv">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">31</td>
<td class="org-left">Q&amp;A: Mentoring VS-Coders as an Emacsian (or How to show not tell people about the wonders of Emacs)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=Bpv9JxUO4GQ">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/fwef53HxTKFe8ox4rz6fv8">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">32</td>
<td class="org-left">Emacs saves the Web (maybe)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=kqOZwsylo48">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/fvzGU4cQQ2meZVKNGEHMht">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">33</td>
<td class="org-left">Q&amp;A: Emacs saves the Web (maybe)</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=p5dsKRWrF3s">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/9XrHVFwLeTVNQnkerSLvUt">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">34</td>
<td class="org-left">Sharing Emacs is Caring Emacs: Emacs education and why I embraced video</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=-tx72HJNfOc">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/3b5XfkceUaRjJuN5Pumgee">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">35</td>
<td class="org-left">Q&amp;A: Sharing Emacs is Caring Emacs: Emacs education and why I embraced video</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=GbFFhj0pvCE">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/w5vRTCwyMHJM3WHAgnMpBZ">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">36</td>
<td class="org-left">MatplotLLM, iterative natural language data visualization in org-babel</td>
<td class="org-left"><a href="https://youtu.be/LhhFA5i_Os4">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/7bwq1vAqYzY24iEMYAdcB1">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">37</td>
<td class="org-left">Enhancing productivity with voice computing</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=Z7l1ImjXOWM">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/vYHj7iSYhUbTxDv93NvzzY">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">38</td>
<td class="org-left">Q&amp;A: Enhancing productivity with voice computing</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=7E6gtxlbk3I">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/dNTjvu2RDXBckTu8ZVsWvC">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">39</td>
<td class="org-left">LLM clients in Emacs, functionality and standardization</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=HN3Y75D4tEs">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/ck1LWXvRiAGNLWFA8s4Ymi">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">40</td>
<td class="org-left">Q&amp;A: LLM clients in Emacs, functionality and standardization</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=jLvuR3xjoOs">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/54Dvx1e93HvpyeHULcYo5Z">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">41</td>
<td class="org-left">Improving compiler diagnostics with overlays</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=g7mwN5QtcmA">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/5fJkawU4R9b1dJq5BcDykx">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">42</td>
<td class="org-left">Q&amp;A: Improving compiler diagnostics with overlays</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=pjnil_gZpf8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/teTgghXfTj1cwwPhTzqfve">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">43</td>
<td class="org-left">Editor Integrated REPL Driven Development for all languages</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=1bk0pqpMCfQ">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/74srjNx1cgMr5MsJ9NWNNi">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">44</td>
<td class="org-left">REPLs in strange places: Lua, LaTeX, LPeg, LPegRex, TikZ</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=lGjfzfC1CH0">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/oAjqkLNfo9B63EE1G6cJJV">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">45</td>
<td class="org-left">Literate Documentation with Emacs and Org Mode</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=BAFZ-vTnfSo">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/8ak16Qy1tjeFEqmcnan6MQ">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">46</td>
<td class="org-left">Q&amp;A: Literate Documentation with Emacs and Org Mode</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=2g8Px71GEq8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/6VJjyqjPnCw2Pd4hVZGYvB">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">47</td>
<td class="org-left">Windows into Freedom</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=7aVgVd2_HTs">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/4DeRkvJyKFdCBLWnHtsZW2">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">48</td>
<td class="org-left">Bringing joy to Scheme programming</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=F-H3YQywr-4">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/4moUfTEo2G8we5JuLGArWx">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">49</td>
<td class="org-left">Q&amp;A: Bringing joy to Scheme programming</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=GQY64ngbiF8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/1t3hLzUmaNqtjE9aQXFbad">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">50</td>
<td class="org-left">GNU Emacs: A World of Possibilities</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=T5yZZK18w5w">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/jFaSuNYt2FqibtcAvmVdbF">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">51</td>
<td class="org-left">Q&amp;A: GNU Emacs: A World of Possibilities</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=bxBfbd4ezU8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/bBtwrYpKWdffVRakQCf27F">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">52</td>
<td class="org-left">A modern Emacs look-and-feel without pain</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=E1u6DcHis9M">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/1DRDY8vZK3SW5M8zAPJQSp">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">53</td>
<td class="org-left">The Emacsen family, the design of an Emacs and the importance of Lisp</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=7SGcLpjC5CE">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/qgJ84RLV2FZYyeSusDskwU">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">54</td>
<td class="org-left">Q&amp;A: The Emacsen family, the design of an Emacs and the importance of Lisp</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=wHhqq30bR60">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/97CWyEHugrureSjCSWHGMy">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">55</td>
<td class="org-left">emacs-gc-stats: Does garbage collection actually slow down Emacs?</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=YA1RJxH4xfQ">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/ngenUPBLDDkZGmsxK8vimJ">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">56</td>
<td class="org-left">Q&amp;A: emacs-gc-stats: Does garbage collection actually slow down Emacs?</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=X2rsy-WXUFI">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/mkgAZPPGB8yLmNns3W193K">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">57</td>
<td class="org-left">hyperdrive.el: Peer-to-peer filesystem in Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=7tcpmZrvz9w">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/9wLA55XACiGnS3nNBNwsV5">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">58</td>
<td class="org-left">Q&amp;A: hyperdrive.el: Peer-to-peer filesystem in Emacs</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=C7IikdsdXtg">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/dTxH3GjrJi4hXVz7i72G34">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">59</td>
<td class="org-left">Writing a language server in OCaml for Emacs, fun, and profit</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=VhUIS55UbQs">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/jgMzmGyx4H1YDwc5n1eRZu">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">60</td>
<td class="org-left">Q&amp;A: Writing a language server in OCaml for Emacs, fun, and profit</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=L0w2_63c05A">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/umt4tcX8ufvM7xEVoe7KgC">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">61</td>
<td class="org-left">What I learned by writing test cases for GNU Hyperbole</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=maNQSKxXIzI">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/4XmcGSe3TQrJJNUqQXqK2B">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">62</td>
<td class="org-left">Q&amp;A: What I learned by writing test cases for GNU Hyperbole</td>
<td class="org-left"><a href="https://youtube.com/watch?v=EQFpZQoqtYI">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/qBmDg6Dm5ppCEdnKVEm7ih">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">63</td>
<td class="org-left">EmacsConf.org: How we use Org Mode and TRAMP to organize and run a multi-track conference</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=uTregv3rNl0">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/eX2dXG3xMtUHuuBz4fssGT">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">64</td>
<td class="org-left">Q&amp;A: EmacsConf.org: How we use Org Mode and TRAMP to organize and run a multi-track conference</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=nhCe5k_smZA">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/18zetSzZEs6cfARaKLqiPa">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">65</td>
<td class="org-left">Saturday opening remarks</td>
<td class="org-left"><a href="https://youtu.be/piEHmLVtG6A">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/wEZX2JkDFpFqNFXnYeQTyb">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">66</td>
<td class="org-left">Saturday closing remarks</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=6OOUfBF6t7k">YouTube</a></td>
<td class="org-left"><a href="https://www.youtube.com/playlist?list=PLomc4HLgvuCUdrW3JkugtKv8xPelUoOyP">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">67</td>
<td class="org-left">Sunday opening remarks</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=B3NKI5Mviq8">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/9zjMPEZz1nYokiY7rk4wYv">Toobnix</a></td>
</tr>

<tr>
<td class="org-right">68</td>
<td class="org-left">Sunday closing remarks</td>
<td class="org-left"><a href="https://www.youtube.com/watch?v=qqvelKB5v0c">YouTube</a></td>
<td class="org-left"><a href="https://toobnix.org/w/p/nMXCCJ25wxKUtbuQiwkakA">Toobnix</a></td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<div id="outline-container-org8a4082e" class="outline-2">
<h3 id="org8a4082e">YouTube</h3>
<div class="outline-text-2" id="text-org8a4082e">
<p>
 I bulk-added the Youtube videos to the playlist. The videos were not
in order because I uploaded some late submissions and forgotten
videos, which then got added to the end of the list.
</p>

<p>
I tried using the API to sort the playlist. This got it most of the
way there, and then I sorted the rest by hand. 
</p>

<p>
</p><details><summary>emacsconf-extract-youtube-api-sort-playlist: Try to roughly sort the playlist.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-api-sort-playlist</span> (<span class="org-type">&amp;optional</span> dry-run-only)
  <span class="org-doc">"Try to roughly sort the playlist."</span>
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-playlist (seq-find (<span class="org-keyword">lambda</span> (o) (<span class="org-keyword">let-alist</span> o (string= .snippet.title (concat emacsconf-name <span class="org-string">" "</span> emacsconf-year))))
                                        (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">items</span> emacsconf-extract-youtube-api-playlists)))
  (<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-playlist-items
        (emacsconf-extract-youtube-api-paginated-request (concat <span class="org-string">"https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails,status&amp;forMine=true&amp;order=date&amp;maxResults=100&amp;playlistId="</span>
                                                (url-hexify-string (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span> emacsconf-extract-youtube-api-playlist)))))
  (<span class="org-keyword">let*</span> ((playlist-info emacsconf-extract-youtube-api-playlists)
         (playlist-items emacsconf-extract-youtube-api-playlist-items)
         (info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info)))
         (slugs (seq-map (<span class="org-keyword">lambda</span> (o) (plist-get o <span class="org-builtin">:slug</span>)) info))
         (position (1- (length playlist-items)))
         result)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">sort items</span>
    (mapc (<span class="org-keyword">lambda</span> (talk)
            (<span class="org-keyword">when</span> (plist-get talk <span class="org-builtin">:qa-youtube-id</span>)
              <span class="org-comment-delimiter">;; </span><span class="org-comment">move the q &amp; a</span>
              (<span class="org-keyword">let</span> ((video-object (emacsconf-extract-youtube-find-url-video-in-list
                                   (plist-get talk <span class="org-builtin">:qa-youtube-url</span>)
                                   playlist-items)))
                (<span class="org-keyword">let-alist</span> video-object
                  (<span class="org-keyword">cond</span>
                   ((null video-object)
                    (message <span class="org-string">"Could not find video for %s"</span> (plist-get talk <span class="org-builtin">:slug</span>)))
                   <span class="org-comment-delimiter">;; </span><span class="org-comment">not in the right position, try to move it</span>
                   ((&lt; .snippet.position position)
                    (<span class="org-keyword">let</span> ((video-id .id)
                          (playlist-id .snippet.playlistId)
                          (resource-id .snippet.resourceId))
                      (message <span class="org-string">"Trying to move %s Q&amp;A to %d from %d"</span> (plist-get talk <span class="org-builtin">:slug</span>) position .snippet.position)
                      (add-to-list <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">result</span> (list (plist-get talk <span class="org-builtin">:slug</span>) <span class="org-string">"answers"</span> .snippet.position position))
                      (<span class="org-keyword">unless</span> dry-run-only
                        (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">put</span> <span class="org-string">"https://www.googleapis.com/youtube/v3/playlistItems?part=snippet"</span>
                          <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>))
                                     (<span class="org-string">"Accept"</span> . <span class="org-string">"application/json"</span>)
                                     (<span class="org-string">"Content-Type"</span> . <span class="org-string">"application/json"</span>))
                          <span class="org-builtin">:body</span> (json-encode
                                 <span class="org-highlight-quoted-quote">`</span>((id . ,video-id)
                                   (snippet
                                    (playlistId . ,playlist-id)
                                    (resourceId . ,resource-id)
                                    (position . ,position))))))))))
                (<span class="org-keyword">setq</span> position (1- position))))
            <span class="org-comment-delimiter">;; </span><span class="org-comment">move the talk if needed</span>
            (<span class="org-keyword">let</span> ((video-object
                   (emacsconf-extract-youtube-find-url-video-in-list
                    (plist-get talk <span class="org-builtin">:youtube-url</span>)
                    playlist-items)))
              (<span class="org-keyword">let-alist</span> video-object
                (<span class="org-keyword">cond</span>
                 ((null video-object)
                  (message <span class="org-string">"Could not find video for %s"</span> (plist-get talk <span class="org-builtin">:slug</span>)))
                 <span class="org-comment-delimiter">;; </span><span class="org-comment">not in the right position, try to move it</span>
                 ((&lt; .snippet.position position)
                  (<span class="org-keyword">let</span> ((video-id .id)
                        (playlist-id .snippet.playlistId)
                        (resource-id .snippet.resourceId))
                    (message <span class="org-string">"Trying to move %s to %d from %d"</span> (plist-get talk <span class="org-builtin">:slug</span>) position .snippet.position)
                    (add-to-list <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">result</span> (list (plist-get talk <span class="org-builtin">:slug</span>) <span class="org-string">"main"</span> .snippet.position position))
                    (<span class="org-keyword">unless</span> dry-run-only
                      (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">put</span> <span class="org-string">"https://www.googleapis.com/youtube/v3/playlistItems?part=snippet"</span>
                        <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>))
                                   (<span class="org-string">"Accept"</span> . <span class="org-string">"application/json"</span>)
                                   (<span class="org-string">"Content-Type"</span> . <span class="org-string">"application/json"</span>))
                        <span class="org-builtin">:body</span> (json-encode
                               <span class="org-highlight-quoted-quote">`</span>((id . ,video-id)
                                 (snippet
                                  (playlistId . ,playlist-id)
                                  (resourceId . ,resource-id)
                                  (position . ,position))))))
                    ))))
              (<span class="org-keyword">setq</span> position (1- position))))
          (nreverse info))
    result))
</pre></div></details>
<p></p>

<p>
 I needed to sort some of the videos manually. Trying to scroll by
dragging items to the top of the currently-displayed section of the
list was slow, and dropping the item near the top of the list so that
I could pick it up again after paging up was a little disorienting.
Fortunately, keyboard scrolling with page-up and page-down worked even
while dragging an item, so that was what I ended up doing: select the
item and then page-up while dragging.
</p>

<p>
YouTube doesn't display numbers for the playlist positions, but this
will add them. The numbers don't dynamically update when the list is
reordered, so I just re-ran the code after moving things around.
</p>

<p>
</p><details><summary>emacsconf-extract-youtube-spookfox-add-playlist-numbers: Number the playlist for easier checking.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-spookfox-add-playlist-numbers</span> ()
  <span class="org-doc">"Number the playlist for easier checking.</span>
<span class="org-doc">Related: `</span><span class="org-doc"><span class="org-constant">emacsconf-extract-check-playlists</span></span><span class="org-doc">'."</span>
  (<span class="org-keyword">interactive</span>)
  (spookfox-js-injection-eval-in-active-tab <span class="org-string">"[...document.querySelectorAll('</span><span class="org-string"><span class="org-constant">ytd-playlist-video-renderer</span></span><span class="org-string">')].forEach((o, i) =&gt; { o.querySelector('</span><span class="org-string"><span class="org-constant">.number</span></span><span class="org-string">')?.remove(); let div = document.createElement('</span><span class="org-string"><span class="org-constant">div</span></span><span class="org-string">'); div.classList.add('</span><span class="org-string"><span class="org-constant">number</span></span><span class="org-string">'); div.textContent = i; o.prepend(div) }))"</span> t))
</pre></div></details>
<p></p>


<figure id="org82934b4">
<img src="https://sachachua.com/blog/2023/12/emacsconf-backstage-making-a-play-list-checking-it-twice/2023-12-11_12-57-25.png" alt="2023-12-11_12-57-25.png">

<figcaption><span class="figure-number">Figure 1: </span>Adding numbers to the Youtube playlist</figcaption>
</figure>

<p>
In retrospect, I could
probably have just cleared the playlist and then added the videos using the
in the right order instead of fiddling with inserting things.
</p>
</div>
</div>
<div id="outline-container-org9a7ad62" class="outline-2">
<h3 id="org9a7ad62">Toobnix (Peertube)</h3>
<div class="outline-text-2" id="text-org9a7ad62">
<p>
Toobnix (Peertube) doesn't seem to have a way to bulk-add videos to a
playlist (or even to bulk-set their visibility). I started trying to
figure out how to use the API, but I got stuck because my token didn't
seem to let me access unlisted videos or do other things that required
proper authentication. Anyway, I came up with this messy hack to open
the videos in sequence and add them to the playlist using Spookfox.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-toobnix-set-up-playlist</span> ()
  (<span class="org-keyword">interactive</span>)
  (mapcar
   (<span class="org-keyword">lambda</span> (o)
     (<span class="org-keyword">when</span> (plist-get o <span class="org-builtin">:toobnix-url</span>)
       (browse-url (plist-get o <span class="org-builtin">:toobnix-url</span>))
       (read-key <span class="org-string">"press a key when page is loaded"</span>)
       (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('</span><span class="org-string"><span class="org-constant">.action-button-save</span></span><span class="org-string">').click()"</span> t)
       (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('</span><span class="org-string"><span class="org-constant">my-peertube-checkbox</span></span><span class="org-string">').click()"</span> t)
       (read-key <span class="org-string">"press a key when saved to playlist"</span>))
     (<span class="org-keyword">when</span> (plist-get o <span class="org-builtin">:qa-toobnix-url</span>)
       (browse-url (plist-get o <span class="org-builtin">:qa-toobnix-url</span>))
       (read-key <span class="org-string">"press a key when page is loaded"</span>)
       (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('</span><span class="org-string"><span class="org-constant">.action-button-save</span></span><span class="org-string">').click()"</span> t)
       (spookfox-js-injection-eval-in-active-tab <span class="org-string">"document.querySelector('</span><span class="org-string"><span class="org-constant">my-peertube-checkbox</span></span><span class="org-string">').click()"</span> t)
       (read-key <span class="org-string">"press a key when saved to playlist"</span>)))
   (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))
</pre>
</div>

<p>
Maybe next year, I might be able to figure out how to use the APIs to
do this stuff automatically.
</p>

<p>
This code is in <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-extract.el">emacsconf-extract.el</a>.
</p>
</div>
</div>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2023%2F12%2Femacsconf-backstage-making-a-play-list-checking-it-twice%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Updating YouTube videos via the YouTube Data API using Emacs Lisp and url-http-oauth</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2023/12/updating-youtube-videos-via-the-youtube-data-api-using-emacs-lisp-and-url-http-oauth/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2023-12-09T16:23:48Z</updated>
    <published>2023-12-09T16:23:48Z</published>
    <category term="elisp" />
<category term="emacs" />
<category term="emacsconf" />
<category term="youtube" />
<category term="video" />
		<id>https://sachachua.com/blog/2023/12/updating-youtube-videos-via-the-youtube-data-api-using-emacs-lisp-and-url-http-oauth/</id>
		<content type="html"><![CDATA[<p>
We upload EmacsConf videos to both YouTube and Toobnix, which is a
PeerTube instance. This makes it easier for people to come across them
after the conference.
</p>

<p>
I can upload to Toobnix and set titles and descriptions using the
peertube-cli tool. I tried a Python script for uploading to YouTube,
but it was a bit annoying due to quota restrictions. Instead, I
uploaded the videos by dragging and dropping them into YouTube Studio.
This allowed me to upload 15 at a time.
</p>

<p>
The videos on YouTube had just the filenames. I wanted to rename the
videos and set the descriptions. In 2022, I used xdotool, simulating
mouse clicks and pasting in text for larger text blocks.
</p>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Xdotool script</strong></summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-xdotool-insert-mouse-location</span>
    (interactive)
  (<span class="org-keyword">let</span> ((pos (shell-command-to-string <span class="org-string">"xdotool getmouselocation"</span>)))
    (<span class="org-keyword">when</span> (string-match <span class="org-string">"x:</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">[0-9]+</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string"> y:</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">[0-9]+</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">"</span> pos)
      (insert (format <span class="org-string">"(shell-command \"xdotool mousemove %s %s click 1\")\n"</span> (match-string 1 pos) (match-string 2 pos))))))

(<span class="org-keyword">setq</span> list (seq-filter (<span class="org-keyword">lambda</span> (o)
                         (<span class="org-keyword">and</span>
                          (file-exists-p
                           (expand-file-name
                            (concat (plist-get o <span class="org-builtin">:video-slug</span>) <span class="org-string">"&#45;&#45;final.webm"</span>)
                            emacsconf-cache-dir))
                          (null (plist-get o <span class="org-builtin">:youtube-url</span>))))
            (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))

(<span class="org-keyword">while</span> list
  (<span class="org-keyword">progn</span>
    (shell-command <span class="org-string">"xdotool mousemove 707 812 click 1 sleep 2"</span>)

    (<span class="org-keyword">setq</span> talk (<span class="org-keyword">pop</span> list))
    <span class="org-comment-delimiter">;; </span><span class="org-comment">click create</span>
    (shell-command <span class="org-string">"xdotool mousemove 843 187 click 1 sleep 1"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">video</span>
    (shell-command <span class="org-string">"xdotool mousemove 833 217 click 1 sleep 1"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">select files</span>
    (shell-command (concat <span class="org-string">"xdotool mousemove 491 760 click 1 sleep 4 type "</span>
                           (shell-quote-argument (concat (plist-get talk <span class="org-builtin">:video-slug</span>) <span class="org-string">"&#45;&#45;final.webm"</span>))))
    <span class="org-comment-delimiter">;; </span><span class="org-comment">open</span>
    (shell-command <span class="org-string">"xdotool mousemove 1318 847 click 1 sleep 5"</span>)

    (kill-new (concat
               emacsconf-name <span class="org-string">" "</span>
               emacsconf-year <span class="org-string">": "</span>
               (plist-get talk <span class="org-builtin">:title</span>)
               <span class="org-string">" - "</span>
               (plist-get talk <span class="org-builtin">:speakers-with-pronouns</span>)))
    (shell-command <span class="org-string">"xdotool sleep 1 mousemove 331 440 click :1 key Ctrl+a Delete sleep 1 key Ctrl+Shift+v sleep 2"</span>)

    (kill-new (emacsconf-publish-video-description talk t))
    (shell-command <span class="org-string">"xdotool mousemove 474 632 click 1 sleep 1 key Ctrl+a sleep 1 key Delete sleep 1 key Ctrl+Shift+v"</span>))
  (read-string <span class="org-string">"Press a key once you've pasted in the description"</span>)

  <span class="org-comment-delimiter">;; </span><span class="org-comment">next</span>
  (<span class="org-keyword">when</span> (emacsconf-captions-edited-p (expand-file-name (concat (plist-get talk <span class="org-builtin">:video-slug</span>) <span class="org-string">"&#45;&#45;main.vtt"</span>) emacsconf-cache-dir))
    (shell-command <span class="org-string">"xdotool mousemove 352 285 click 1 sleep 1"</span>)

    <span class="org-comment-delimiter">;; </span><span class="org-comment">add captions</span>
    (shell-command <span class="org-string">"xdotool mousemove 877 474 click 1 sleep 3"</span>)
    (shell-command <span class="org-string">"xdotool mousemove 165 408 click 1 sleep 1"</span>)
    (shell-command <span class="org-string">"xdotool mousemove 633 740 click 1 sleep 2"</span>)
    (shell-command (concat <span class="org-string">"xdotool mousemove 914 755  click 1 sleep 4 type "</span>
                           (shell-quote-argument (concat (plist-get talk <span class="org-builtin">:video-slug</span>) <span class="org-string">"&#45;&#45;main.vtt"</span>))))
    (read-string <span class="org-string">"Press a key once you've loaded the VTT"</span>)
    (shell-command <span class="org-string">"xdotool mousemove 910 1037 sleep 1 click 1 sleep 4"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">done</span>
    (shell-command <span class="org-string">"xdotool mousemove 890 297 click 1 sleep 3"</span>)
    )


  (<span class="org-keyword">progn</span>
    <span class="org-comment-delimiter">;; </span><span class="org-comment">visibility</span>
    (shell-command <span class="org-string">"xdotool mousemove 810 303 click 1 sleep 2"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">public</span>
    (shell-command <span class="org-string">"xdotool mousemove 119 614 click 1 sleep 2"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">copy</span>
    (shell-command <span class="org-string">"xdotool mousemove 882 669 click 1 sleep 1"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">done</span>
    (shell-command <span class="org-string">"xdotool mousemove 908 1089 click 1 sleep 5 key Alt+Tab"</span>)

    (<span class="org-keyword">emacsconf-with-talk-heading</span> talk
      (org-entry-put (point) <span class="org-string">"YOUTUBE_URL"</span> (read-string <span class="org-string">"URL: "</span>))
      ))
  )
</pre>
</div>


</details>

<p>
Using xdotool wasn't very elegant, since I needed to figure out the
coordinates for each click. I tried using Spookfox to control Mozilla
Firefox from Emacs, but Youtube's editing interface didn't seem to
have any textboxes that I could set. I decided to use EmacsConf 2023
as an excuse to learn how to talk to the Youtube Data API, which
required figuring out OAuth. Even though it was easy to find examples
in Python and NodeJS, I wanted to see if I could stick with using
Emacs Lisp so that I could add the code to the <a href="https://git.emacsconf.org/emacsconf-el/">emacsconf-el</a> repository.
</p>

<p>
After a quick search, I picked <a href="https://elpa.gnu.org/packages/url-http-oauth.html">url-http-oauth</a> as the library that I'd
try first. I used the url-http-oauth-demo.el included in the package
to figure out what to set for the YouTube Data API. I wrote a function
to make getting the redirect URL easier
(<code>emacsconf-extract-oauth-browse-and-prompt</code>). Once I authenticated
successfully, I explored using alphapapa's plz library. It can handle
finding the JSON object and parsing it out for me. With it, I updated
videos to include titles and descriptions from my Emacs code, and I
copied the video IDs into my Org properties.
</p>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>emacsconf-extract.el code for Youtube renaming</strong></summary>
<p>
</p><div class="org-src-container">
<pre class="src src-emacs-lisp"><span class="org-comment-delimiter">;;; </span><span class="org-comment">YouTube</span>

<span class="org-comment-delimiter">;; </span><span class="org-comment">When the token needs refreshing, delete the associated lines from</span>
<span class="org-comment-delimiter">;; </span><span class="org-comment">~/.authinfo This code just sets the title and description. Still</span>
<span class="org-comment-delimiter">;; </span><span class="org-comment">need to figure out how to properly set the license, visibility,</span>
<span class="org-comment-delimiter">;; </span><span class="org-comment">recording date, and captions.</span>
<span class="org-comment-delimiter">;;</span>
<span class="org-comment-delimiter">;; </span><span class="org-comment">To avoid being prompted for the client secret, it's helpful to have a line in ~/.authinfo or ~/.authinfo.gpg with</span>
<span class="org-comment-delimiter">;; </span><span class="org-comment">machine https://oauth2.googleapis.com/token username CLIENT_ID password CLIENT_SECRET</span>

(<span class="org-keyword">defvar</span> <span class="org-variable-name">emacsconf-extract-google-client-identifier</span> nil)
(<span class="org-keyword">defvar</span> <span class="org-variable-name">emacsconf-extract-youtube-api-channels</span> nil)
(<span class="org-keyword">defvar</span> <span class="org-variable-name">emacsconf-extract-youtube-api-categories</span> nil)

(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-oauth-browse-and-prompt</span> (url)
  <span class="org-doc">"Open URL and wait for the redirected code URL."</span>
  (browse-url url)
  (read-from-minibuffer <span class="org-string">"Paste the redirected code URL: "</span>))

(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-api-setup</span> ()
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">plz</span>)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">url-http-oauth</span>)
  (<span class="org-keyword">when</span> (getenv <span class="org-string">"GOOGLE_APPLICATION_CREDENTIALS"</span>)
    (<span class="org-keyword">let-alist</span> (json-read-file (getenv <span class="org-string">"GOOGLE_APPLICATION_CREDENTIALS"</span>))
      (<span class="org-keyword">setq</span> emacsconf-extract-google-client-identifier .web.client_id)))
  (<span class="org-keyword">unless</span> (url-http-oauth-interposed-p <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>)
    (url-http-oauth-interpose
     <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"client-identifier"</span> . ,emacsconf-extract-google-client-identifier)
       (<span class="org-string">"resource-url"</span> . <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>)
       (<span class="org-string">"authorization-code-function"</span> . emacsconf-extract-oauth-browse-and-prompt)
       (<span class="org-string">"authorization-endpoint"</span> . <span class="org-string">"https://accounts.google.com/o/oauth2/v2/auth"</span>)
       (<span class="org-string">"authorization-extra-arguments"</span> .
        ((<span class="org-string">"redirect_uri"</span> . <span class="org-string">"http://localhost:8080"</span>)))
       (<span class="org-string">"access-token-endpoint"</span> . <span class="org-string">"https://oauth2.googleapis.com/token"</span>)
       (<span class="org-string">"scope"</span> . <span class="org-string">"https://www.googleapis.com/auth/youtube"</span>)
       (<span class="org-string">"client-secret-method"</span> . prompt))))
  (<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-channels
        (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">get</span> <span class="org-string">"https://youtube.googleapis.com/youtube/v3/channels?part=contentDetails&amp;mine=true"</span>
          <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>)))
          <span class="org-builtin">:as</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">json-read</span>))
  (<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-categories
        (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">get</span> <span class="org-string">"https://youtube.googleapis.com/youtube/v3/videoCategories?part=snippet&amp;regionCode=CA"</span>
          <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>)))
          <span class="org-builtin">:as</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">json-read</span>))
  (<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-videos
        (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">get</span> (concat <span class="org-string">"https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails,status&amp;forMine=true&amp;order=date&amp;maxResults=50&amp;playlistId="</span>
                          (url-hexify-string
                           (<span class="org-keyword">let-alist</span> (elt (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">items</span> emacsconf-extract-youtube-api-channels) 0)
                             .contentDetails.relatedPlaylists.uploads)
                           ))
          <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>)))
          <span class="org-builtin">:as</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">json-read</span>)))

(<span class="org-keyword">defvar</span> <span class="org-variable-name">emacsconf-extract-youtube-tags</span> <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"emacs"</span> <span class="org-string">"emacsconf"</span>))
(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-object</span> (video-id talk <span class="org-type">&amp;optional</span> privacy-status)
  <span class="org-doc">"Format the video object for VIDEO-ID using TALK details."</span>
  (<span class="org-keyword">setq</span> privacy-status (<span class="org-keyword">or</span> privacy-status <span class="org-string">"unlisted"</span>))
  (<span class="org-keyword">let</span> ((properties (emacsconf-publish-talk-video-properties talk <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">youtube</span>)))
    <span class="org-highlight-quoted-quote">`</span>((id . ,video-id)
      (kind . <span class="org-string">"youtube#video"</span>)
      (snippet
       (categoryId . <span class="org-string">"28"</span>)
       (title . ,(plist-get properties <span class="org-builtin">:title</span>))
       (tags . ,emacsconf-extract-youtube-tags)
       (description . ,(plist-get properties <span class="org-builtin">:description</span>))
       <span class="org-comment-delimiter">;; </span><span class="org-comment">Even though I set recordingDetails and status, it doesn't seem to stick.</span>
       <span class="org-comment-delimiter">;; </span><span class="org-comment">I'll leave this in here in case someone else can figure it out.</span>
       (recordingDetails (recordingDate . ,(format-time-string <span class="org-string">"%Y-%m-%dT%TZ"</span> (plist-get talk <span class="org-builtin">:start-time</span>) t))))
      (status (privacyStatus . <span class="org-string">"unlisted"</span>)
              (license . <span class="org-string">"creativeCommon"</span>)))))

(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-api-update-video</span> (video-object)
  <span class="org-doc">"Update VIDEO-OBJECT."</span>
  (<span class="org-keyword">let-alist</span> video-object
    (<span class="org-keyword">let*</span> ((slug (<span class="org-keyword">cond</span>
                  <span class="org-comment-delimiter">;; </span><span class="org-comment">not yet renamed</span>
                  ((string-match (<span class="org-keyword">rx</span> (literal emacsconf-id) <span class="org-string">" "</span> (literal emacsconf-year) <span class="org-string">" "</span>
                                     (group (1+ (<span class="org-keyword">or</span> (syntax word) <span class="org-string">"-"</span>)))
                                     <span class="org-string">"  "</span>)
                                 .snippet.title)
                   (match-string 1 .snippet.title))
                  <span class="org-comment-delimiter">;; </span><span class="org-comment">renamed, match the description instead</span>
                  ((string-match (<span class="org-keyword">rx</span> (literal emacsconf-base-url) (literal emacsconf-year) <span class="org-string">"/talks/"</span>
                                     (group (1+ (<span class="org-keyword">or</span> (syntax word) <span class="org-string">"-"</span>))))
                                 .snippet.description)
                   (match-string 1 .snippet.description))
                  <span class="org-comment-delimiter">;; </span><span class="org-comment">can't find, prompt</span>
                  (t
                   (<span class="org-keyword">when</span> (string-match (<span class="org-keyword">rx</span> (literal emacsconf-id) <span class="org-string">" "</span> (literal emacsconf-year))
                                       .snippet.title)
                     (completing-read (format <span class="org-string">"Slug for %s: "</span>
                                              .snippet.title)
                                      (seq-map (<span class="org-keyword">lambda</span> (o) (plist-get o <span class="org-builtin">:slug</span>))
                                               (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))))))
           (video-id .snippet.resourceId.videoId)
           (id .id)
           result)
      (<span class="org-keyword">when</span> slug
        <span class="org-comment-delimiter">;; </span><span class="org-comment">set the YOUTUBE_URL property</span>
        (<span class="org-keyword">emacsconf-with-talk-heading</span> slug
          (org-entry-put (point) <span class="org-string">"YOUTUBE_URL"</span> (concat <span class="org-string">"https://www.youtube.com/watch?v="</span> video-id))
          (org-entry-put (point) <span class="org-string">"YOUTUBE_ID"</span> id))
        (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">put</span> <span class="org-string">"https://www.googleapis.com/youtube/v3/videos?part=snippet,recordingDetails,status"</span>
          <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>))
                     (<span class="org-string">"Accept"</span> . <span class="org-string">"application/json"</span>)
                     (<span class="org-string">"Content-Type"</span> . <span class="org-string">"application/json"</span>))
          <span class="org-builtin">:body</span> (json-encode (emacsconf-extract-youtube-object video-id (emacsconf-resolve-talk slug))))))))

(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-rename-videos</span> (<span class="org-type">&amp;optional</span> videos)
  <span class="org-doc">"Rename videos and set the YOUTUBE_URL property in the Org heading."</span>
  (<span class="org-keyword">let</span> ((info (emacsconf-get-talk-info)))
    (mapc
     (<span class="org-keyword">lambda</span> (video)
       (<span class="org-keyword">when</span> (string-match (<span class="org-keyword">rx</span> (literal emacsconf-id) <span class="org-string">" "</span> (literal emacsconf-year)))
         (emacsconf-extract-youtube-api-update-video video)))
     (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">items</span> (<span class="org-keyword">or</span> videos emacsconf-extract-youtube-api-videos)))))

(<span class="org-keyword">provide</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">emacsconf-extract</span>)

</pre>
</div>

<p></p>


</details>

<p>
I haven't quite figured out how to set <code>status</code> and <code>recordingDetails</code>
properly. The code sets them, but they don't stick. That's okay. I
think I can set those as a batch operation. <a href="https://www.reddit.com/r/youtube/comments/1289op8/how_do_i_get_the_bulk_edit_feature_in_studio_to/">It looks like I need to
change visibility one by one, though</a>, which might be a good
opportunity to check the end of the video for anything that needs to
be trimmed off.
</p>

<p>
I also want to figure out how to upload captions. I'm not entirely
sure how to do multipart form data yet with the <code>url</code> library or
<code>plz</code>. It might be nice to someday set up an HTTP server so that Emacs
can handle OAuth redirects itself. I'll save that for another blog
post and share my notes for now.
</p>

<p>
This code is in <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-extract.el">emacsconf-extract.el</a>.
</p>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2023%2F12%2Fupdating-youtube-videos-via-the-youtube-data-api-using-emacs-lisp-and-url-http-oauth%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">My new Google Hangouts On Air checklist, plus upcoming Nov 29 Q&amp;A on learning</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2013/11/my-new-google-hangouts-on-air-workflow-plus-upcoming-nov-29-qa-on-learning/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2013-11-17T01:43:35Z</updated>
    <published>2013-11-18T13:00:00Z</published>
    <category term="process" />
<category term="youtube" />
<category term="streaming" />
		<id>https://sachachua.com/blog/?p=26454</id>
		<content type="html"><![CDATA[<p><a href="http://hangouts.google.com/onair">Google Hangouts On Air</a> is a quick, free way to have a videocast with up to 10 participants and as many passive watchers as you want, thanks to streaming through YouTube. The stream is about 20 seconds delayed and the commenting interface is still kinda raw, but as a quick way to set up and broadcast video chats, it’s hard to beat that.</p>
<p>I picked up a lot of great ideas from Pat Flynn’s first Q&amp;A Hangout. He used <a href="http://chatwing.com">ChatWing</a> to set up a chat room that everyone could join, and the experience was much smoother than using CommentTracker or something like that.</p>
<p>Here’s my new Google Hangouts On Air workflow for the <a href="https://sachachua.com/blog/emacs-chat">Emacs Chats</a> series I’ve been doing. Since the Emacs crowd is fairly technical, I used IRC as my chat room, with a <a href="https://webchat.freenode.net">web interface</a> for others who didn’t have an IRC client handy. (Naturally, I used ERC to chat on IRC from within Emacs.) Having other people around worked out really well, because I could take a break and ask other people’s questions. =)</p>
<p><a href="https://sachachua.com/blog/wp-content/uploads/2013/11/2013-11-03-My-new-Google-Hangouts-workflow.png"><img loading="lazy" style="background-image: none; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border: 0px;" title="2013-11-03 My new Google Hangouts workflow" alt="2013-11-03 My new Google Hangouts workflow" src="https://sachachua.com/blog/wp-content/uploads/2013/11/2013-11-03-My-new-Google-Hangouts-workflow_thumb.png" width="640" height="486" border="0"></a></p>
<p>The other new thing I tried this time around was starting the broadcast really early (like, half an hour early) and setting it to share my screen with the coming-soon information, which meant that I could post the streaming URL in lots of different places.</p>
<p>I’d like to expand this to doing regular Hangouts On Air Q&amp;As or conversations. How about we use the <a href="http://sach.ac/live">sach.ac/live</a> URL to point to the next Hangout On Air I’ve scheduled? As of this writing, this will be a Q&amp;A on <a href="https://plus.google.com/events/c8i73ks3ruoi962ich22566s5tg">November 29 on learning and note-taking</a>. We’ll probably stream it over YouTube and have a chat room for discussion/Q&amp;A. Want to pick my brain? See you then!</p>
<p>(Want more one-on-one help? <a href="http://sach.ac/help">Book a Helpout session</a> – there’s a nominal charge to keep slots available instead of letting no-shows book them all.)</p>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2013%2F11%2Fmy-new-google-hangouts-on-air-workflow-plus-upcoming-nov-29-qa-on-learning%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry>
</feed>