<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Julien Wawrzyniak on Medium]]></title>
        <description><![CDATA[Stories by Julien Wawrzyniak on Medium]]></description>
        <link>https://medium.com/@wawrzyniakjulien?source=rss-501cb614cd0f------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*SbgU2UR0mITBhbPY7RZa0A.jpeg</url>
            <title>Stories by Julien Wawrzyniak on Medium</title>
            <link>https://medium.com/@wawrzyniakjulien?source=rss-501cb614cd0f------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 06 Nov 2026 16:31:15 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@wawrzyniakjulien/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Automating GitHub PRs Like a Pro with Graphite]]></title>
            <link>https://levelup.gitconnected.com/automating-github-prs-like-a-pro-with-graphite-f781acba45ce?source=rss-501cb614cd0f------2</link>
            <guid isPermaLink="false">https://medium.com/p/f781acba45ce</guid>
            <category><![CDATA[github]]></category>
            <category><![CDATA[ci]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[workflow]]></category>
            <category><![CDATA[git]]></category>
            <dc:creator><![CDATA[Julien Wawrzyniak]]></dc:creator>
            <pubDate>Mon, 10 Feb 2025 13:52:30 GMT</pubDate>
            <atom:updated>2025-02-10T13:52:30.209Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OkuWRq4nMSDpsrIyJp3vvg.png" /><figcaption>Graphite’s official Facebook banner</figcaption></figure><p>What I find exciting about software engineering is its potential to automate formerly tedious processes. For instance, realizing I could substitute a tiresome five-minute task with a quick shell command was a game-changer. This is precisely how <a href="https://graphite.dev/">Graphite</a> enhanced my productivity by streamlining GitHub pull request creation.</p><p>Let’s delve into how optimizing the process of creating GitHub pull requests with Graphite has made it easier for reviewers.</p><h3>What is Graphite?</h3><p>Imagine an assistant that ensures your code writing is smoother, faster, and less prone to errors. That’s what <a href="https://graphite.dev/">Graphite</a> is — a robust set of features designed specifically with the software engineer’s productivity in mind.</p><p>It’s a powerful alternative to GitHub’s native pull request management tool, packed with features designed to supercharge your productivity:</p><h4><strong>Graphite CLI</strong> — Streamline Git commands and stack pull requests</h4><p>Graphite CLI helps simplify Git workflows by enabling developers to efficiently <strong>stack</strong> multiple pull requests (PRs) on top of each other. This approach allows for <strong>smaller, incremental code changes</strong> that are easier to review and merge, reducing bottlenecks in the development process.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1o2cluH6Mup3IjHD.png" /><figcaption>Traditional vs stacked PRs</figcaption></figure><h4><strong>Pull Request Inbox</strong> — Manage PRs and review requests in one unified inbox</h4><p>A <strong>unified dashboard</strong> that helps developers stay on top of every <strong>PR and review request</strong> in one place. It provides a <strong>centralized view</strong> of pull requests, showing <strong>reviews, CI status, stack information, and more</strong> at a glance. With <strong>customizable filters</strong>, developers can sort PRs based on author, CI status, labels, and other criteria.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*jfCRm9XiKUYtuxbu" /><figcaption>A screenshot of Graphite Pull Request Inbox</figcaption></figure><h4><strong>Graphite Reviewer</strong> — AI-powered code review feedback</h4><p>An <strong>AI-powered code review tool</strong> that provides <strong>instant, actionable feedback</strong> on pull requests.</p><blockquote>I found it incredibly <strong>efficient at catching small typos and subtle errors</strong> that can be surprisingly <strong>hard for a human reviewer to spot. </strong>It doesn’t replace a human reviewer, but it’s a powerful complement to your CI flow.</blockquote><h4><strong>Merge Queue</strong> — Automate merges and prevent conflicts</h4><p>It ensures that pull requests are merged in order, without conflicts. It keeps the <strong>main branch green</strong>, prevents merge bottlenecks, and optimizes CI time by skipping redundant tests.</p><h4><strong>Insights and Reports</strong> — Real-time developer productivity metrics</h4><p><strong>T</strong>rack productivity and identify bottlenecks. Graphite helps teams make <strong>data-driven decisions</strong> by analyzing historical trends and engineering workflow efficiency.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*O0sDBAjTVdKFxM6f.png" /><figcaption>A screenshot of Graphite Insights dashboard</figcaption></figure><h3>Create a Pull Request in Just 30 Seconds</h3><ol><li>gt create --all --ai will create a new branch stacked on top of the current branch. The branch name and commit message are AI-generated based on your changes. No more context-switching or thinking up commit messages!</li><li>gt submit --ai will create a new pull request and generate a summary of all your changes, making it easier for reviewers to<strong> understand your work at a glance.</strong></li><li>You will be redirected to the pull request, allowing you <strong>to fine-tune the AI-generated description</strong> before publishing.</li><li>Publish!</li></ol><h3>Must-Know Graphite Commands</h3><blockquote>gt sync – Keep Your Stack Up-to-Date, Effortlessly 🔄</blockquote><p>✅ <strong>Auto-Fetch &amp; Restack</strong> — Pulls the latest changes from main and <strong>restacks</strong> your branches automatically.<br>✅ <strong>Clean Up Merged PRs</strong> – Removes stale branches once PRs are merged.<br>✅ <strong>Conflict Prevention</strong> – Stay ahead of merge conflicts before they become a problem.</p><blockquote>gt absorb – The Smartest Way to Keep Your Commits Clean ✨</blockquote><p>Automatically <strong>amends staged changes</strong> to the most relevant commit in your stack — <strong>without manual rebasing</strong></p><blockquote>gt log – Instantly Visualize Your Stacked Branches 🔍</blockquote><p>✅ <strong>See Your Stack at a Glance</strong> — No more guessing which branches are linked.<br>✅ <strong>Track Dependencies Easily</strong> — Know which PRs depend on others before merging.<br>✅ <strong>Debug Faster</strong> — Instantly spot misaligned branches or missing parents.</p><p>And many more in the <a href="https://graphite.dev/docs/command-reference">Graphite CLI documentation</a>!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f781acba45ce" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/automating-github-prs-like-a-pro-with-graphite-f781acba45ce">Automating GitHub PRs Like a Pro with Graphite</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to download YouTube captions using a Go script]]></title>
            <link>https://levelup.gitconnected.com/how-to-download-youtube-captions-using-a-go-script-3b0ae6df6046?source=rss-501cb614cd0f------2</link>
            <guid isPermaLink="false">https://medium.com/p/3b0ae6df6046</guid>
            <category><![CDATA[script]]></category>
            <category><![CDATA[go]]></category>
            <category><![CDATA[youtube]]></category>
            <dc:creator><![CDATA[Julien Wawrzyniak]]></dc:creator>
            <pubDate>Tue, 27 Aug 2024 16:36:37 GMT</pubDate>
            <atom:updated>2024-08-27T16:36:37.253Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MBpSTU2r6dWXSPm6" /><figcaption>Photo by <a href="https://unsplash.com/@monicadear?utm_source=medium&amp;utm_medium=referral">Monica Flores</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Recently, I worked on a side project where I needed to extract captions from YouTube videos. While there are plenty of tools available for this, I decided to create my own solution in Go. This was primarily for the sake of learning Go, and now, I’d like to share that experience with you.</p><h3>YouTube API</h3><p>My initial approach was to determine if extracting captions was feasible using the official YouTube API. I discovered an <a href="https://developers.google.com/youtube/v3/docs/captions/download">endpoint</a> that seemed to allow downloading video captions, so I quickly developed a Go script with the <a href="https://developers.google.com/youtube/v3/quickstart/go">official YouTube Go package</a> to test it out.</p><p>This was my first mistake.</p><p>I should have spent more time reading the endpoint documentation because, even if this is not clearly mentionned, only the video owner can use this API, or the video must have third-party contributions enabled for captions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4WA3Me8dc8l_HG_JbGa4wA.png" /></figure><p>But, not all my time was wasted. I also explored the playlist API, which will be useful to be able to download the captions for an entire playlist.</p><pre>func downloadPlaylistCaptions(service *youtube.Service, playlistID string, nextPageToken string) error {<br>   part := []string{&quot;snippet&quot;}<br>   playlistItemsList := service.PlaylistItems.List(part).PlaylistId(playlistID)<br>  <br>   if nextPageToken != &quot;&quot; {<br>      playlistItemsList = playlistItemsList.PageToken(nextPageToken)<br>   }<br>  <br>   items, err := playlistItemsList.Do()<br>   if err != nil {<br>      return fmt.Errorf(&quot;unable to list playlist items: %w&quot;, err)<br>   }<br><br>   for _, item := range items.Items {<br>      // ...<br>   }<br><br>   if items.NextPageToken != &quot;&quot; {<br>      return downloadPlaylistCaptions(service, playlistID, items.NextPageToken)<br>   }<br>  <br>   return nil<br>}</pre><h3>Unofficial YouTube API</h3><p>After researching other existing tools, I discovered a surprisingly simple method to download video captions.</p><p>All you need to do is open the YouTube video page, such as <a href="https://www.youtube.com/watch?v=mT0RNrTDHkI">https://www.youtube.com/watch?v=mT0RNrTDHkI</a>.</p><p>Then, open the browser console and enter:</p><pre>ytInitialPlayerResponse.captions.playerCaptionsTracklistRenderer.captionTracks[0].baseUrl</pre><p>This will give you a caption file URL.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QdevsJ_skLRxDP6iVFkfNA.png" /><figcaption>YoutTube video caption file</figcaption></figure><blockquote>Also note that the <strong><em>captionTracks</em></strong> array contains the captions for all the available languages.</blockquote><p>Then we just need to write a simple script that queries the video page, extracts the necessary variable, and retrieves the base url and language code for each available caption. From there, we can download the caption that interests us.</p><pre>package main<br><br>import (<br> &quot;encoding/json&quot;<br> &quot;fmt&quot;<br> &quot;io&quot;<br> &quot;log&quot;<br> &quot;net/http&quot;<br> &quot;os&quot;<br> &quot;path/filepath&quot;<br> &quot;strings&quot;<br>)<br><br>const baseURL = &quot;https://www.youtube.com/watch?v=&quot;<br><br>type ytInitialPlayerResponse struct {<br> Captions struct {<br>  PlayerCaptionsTracklistRenderer struct {<br>   CaptionTracks []struct {<br>    BaseUrl      string `json:&quot;baseUrl&quot;`<br>    LanguageCode string `json:&quot;languageCode&quot;`<br>   } `json:&quot;captionTracks&quot;`<br>  } `json:&quot;playerCaptionsTracklistRenderer&quot;`<br> } `json:&quot;captions&quot;`<br>}<br><br>type Caption struct {<br> BaseUrl      string<br> LanguageCode string<br>}<br><br>func (c *Caption) Download(targetPath string) error {<br> resp, err := http.Get(c.BaseUrl)<br> if err != nil {<br>  return fmt.Errorf(&quot;unable to download caption: %w&quot;, err)<br> }<br><br> defer resp.Body.Close()<br><br> file, err := os.Create(targetPath)<br> if err != nil {<br>  return fmt.Errorf(&quot;unable to create file: %w&quot;, err)<br> }<br><br> defer file.Close()<br><br> _, err = io.Copy(file, resp.Body)<br> if err != nil {<br>  return fmt.Errorf(&quot;unable to write file: %w&quot;, err)<br> }<br><br> return nil<br>}<br><br>func listVideoCaptions(videoID string) ([]Caption, error) {<br> resp, err := http.Get(baseURL + videoID)<br> if err != nil {<br>  return nil, fmt.Errorf(&quot;unable to download video page: %w&quot;, err)<br> }<br><br> defer resp.Body.Close()<br><br> content, err := io.ReadAll(resp.Body)<br> if err != nil {<br>  return nil, fmt.Errorf(&quot;unable to read response body: %w&quot;, err)<br> }<br><br> pageContent := string(content)<br><br> // Find ytInitialPlayerResponse variable<br> pageContentSplited := strings.Split(pageContent, &quot;ytInitialPlayerResponse = &quot;)<br> if len(pageContentSplited) &lt; 2 {<br>  return nil, fmt.Errorf(&quot;unable to find ytInitialPlayerResponse variable&quot;)<br> }<br><br> // Find the end of the variable<br> pageContentSplited = strings.Split(pageContentSplited[1], &quot;;&lt;/script&gt;&quot;)<br> if len(pageContentSplited) &lt; 2 {<br>  return nil, fmt.Errorf(&quot;unable to find the end of the ytInitialPlayerResponse variable&quot;)<br> }<br><br> ytInitialPlayerResponse := ytInitialPlayerResponse{}<br> err = json.Unmarshal([]byte(pageContentSplited[0]), &amp;ytInitialPlayerResponse)<br> if err != nil {<br>  return nil, fmt.Errorf(&quot;unable to unmarshal ytInitialPlayerResponse: %w&quot;, err)<br> }<br><br> captions := make([]Caption, 0, len(ytInitialPlayerResponse.Captions.PlayerCaptionsTracklistRenderer.CaptionTracks))<br> for _, caption := range ytInitialPlayerResponse.Captions.PlayerCaptionsTracklistRenderer.CaptionTracks {<br>  captions = append(captions, Caption{<br>   BaseUrl:      caption.BaseUrl,<br>   LanguageCode: caption.LanguageCode,<br>  })<br> }<br><br> return captions, nil<br>}<br><br>func main() {<br> if len(os.Args) &lt; 2 {<br>  log.Fatalf(&quot;usage: %s &lt;videoID&gt;&quot;, filepath.Base(os.Args[0]))<br> }<br><br> videoID := os.Args[1]<br><br> captions, err := listVideoCaptions(videoID)<br> if err != nil {<br>  log.Fatalf(&quot;unable to list video captions: %v&quot;, err)<br> }<br><br> for _, caption := range captions {<br>  if caption.LanguageCode == &quot;en&quot; {<br>   err := caption.Download(fmt.Sprintf(&quot;%s.xml&quot;, videoID))<br>   if err != nil {<br>    log.Printf(&quot;unable to download caption: %v&quot;, err)<br>   }<br>  }<br> }<br>}</pre><p>And try it with:</p><pre>go run main.go mT0RNrTDHkI &amp;&amp; cat mT0RNrTDHkI.xml</pre><p>And voilà! Here’s a <a href="https://github.com/wawrzy/yt-captions">link</a> to the GitHub repository. The implementation has evolved a bit, but the idea remains the same.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3b0ae6df6046" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/how-to-download-youtube-captions-using-a-go-script-3b0ae6df6046">How to download YouTube captions using a Go script</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My first marathon in emojis]]></title>
            <link>https://medium.com/@wawrzyniakjulien/my-first-marathon-in-emojis-91ddeccd9b96?source=rss-501cb614cd0f------2</link>
            <guid isPermaLink="false">https://medium.com/p/91ddeccd9b96</guid>
            <category><![CDATA[sports]]></category>
            <category><![CDATA[running]]></category>
            <category><![CDATA[paris]]></category>
            <category><![CDATA[marathon]]></category>
            <dc:creator><![CDATA[Julien Wawrzyniak]]></dc:creator>
            <pubDate>Wed, 21 Aug 2024 12:43:31 GMT</pubDate>
            <atom:updated>2024-08-22T17:57:56.238Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-ZavNCq9FBHAkD4SLmX_0g.png" /><figcaption>My marathon pace from start to finish</figcaption></figure><p>After 3 years of running, 2024 was the year I decided to run my first Paris marathon, but not everything went as planned.</p><h3>🫣 0–2km</h3><p>I’m feeling a bit stressed before the start, but I’m confident because I’ve followed all the recommendations to succeed in my first marathon:</p><ul><li>I stopped my 6 months of training 1 month before the race, because of a re-emerging autoimmune disease that prevents me from running without experiencing severe pain in my legs.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jOrk4rjW4KIqGgZQpBYfng.png" /><figcaption>My training logs over the last year</figcaption></figure><ul><li>To ensure I feel my best on race day and minimize discomfort, I took a heavy anti-inflammatory medication for three consecutive days leading up to the marathon.</li><li>12 hours before the race, I made adjustments to my nutrition and pacing strategies.</li></ul><p>Everything should be fine, right ? Right ?!?</p><h3>😀 2–27km</h3><p>What a nice race!</p><p>The marathon circuit is really beautiful and the cheering helps me feel good. The weather is very hot and humid for April in Paris, but at least it’s not raining.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*IOsxh9qfAJsR3OGL.jpg" /><figcaption>2024 Paris marathon circuit</figcaption></figure><p>I started slow and I decide to keep this pace because I’m feeling very well.</p><p>Good thing I decided to hide my heart rate on my watch because this is a bit scary:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BNy4_PuhI0oNTSbpJuQv9Q.png" /><figcaption>Heart rate during the race</figcaption></figure><p>To give you an idea, before I stopped my training I was used to be around 145 at this pace…</p><h3>🥵 27–32km</h3><p>Okay, now my disease is coming to say hello, but I carry on and don’t listen, but the descent into hell begins little by little, km after km.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/980/0*XFVwPH-i_4Ox2Wn6" /><figcaption>30 km Paris Marathon</figcaption></figure><h3>😭 32–41km</h3><p>Why I’m doing this?</p><p>When negative thoughts start creeping in during a race, it’s nearly impossible to shake them off.</p><p>Unfortunately, my body is also protesting, and the lack of training is becoming all too clear.</p><p>Still, I’m relieved because I know I’ll finish no matter what — I’ve already completed three-quarters of the race!</p><h3>🥹 41–42km</h3><p>I can finally see the end!</p><p>And…. I DID IT, I DIT ITTTTTTTTT!!!!!!!</p><p><a href="https://www.strava.com/activities/11127367442/overview">https://www.strava.com/activities/11127367442/overview</a></p><p>Maybe it’s time to get some rest, I can’t forget that I have a hospital appointment the next day at 7:30 a.m. I don’t know how I’m going to explain this to my doctor.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=91ddeccd9b96" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>