Site icon EnRoute

5 PRs, 5 Projects, 1 Day: My First Open-Source Contributions

I’ve been meaning to contribute to open source for a while. Not just in a vague “I should do that someday” way — but actually doing it. Finding a bug, fixing it, opening a PR, and shipping something real. Today I finally did it. Five times.

Here’s what I worked on, what I fixed, and what I learned along the way.


🧰 The Setup

My home lab runs on Unraid, and my media stack is the usual suspects: Plex, Radarr, Sonarr, Tautulli, and a few syncing tools. These are projects I use every single day — which made them the perfect place to start. I already knew where the rough edges were.

I connected the GitHub CLI (gh) to my account, cloned each repo, and got to work.


1. PlexTraktSync — Watchlist Pagination Bug

The Project

PlexTraktSync is a Python tool that keeps your Plex watch history and watchlist in sync with Trakt.tv. If you’ve watched something in Plex, it marks it on Trakt. If something’s on your Trakt watchlist, it appears in Plex. It’s one of those tools that just lives quietly in the background and makes your media life better.

The Bug

Issue #2452: If you had more than 100 items on your Trakt watchlist, only the first 100 were ever fetched. Worse—on a sync, anything beyond item 100 would get removed from Plex, because as far as PlexTraktSync could see, those items weren’t in your watchlist at all.

The Fix

The underlying pytrakt library makes a single API call to users/{username}/watchlist/movies with no pagination. Trakt’s API returns at most 100 items per page by default.

The fix: a new get_watchlist() function in pytrakt_extensions.py that loops through pages of 1000 items until it gets a partial page, signaling the end of the list. One while loop. No more phantom removals.

def get_watchlist(media_type: str) -> list:
page = 1
limit = 1000
results = []
while True:
data = trakt.core.api().request("get", f"users/me/watchlist/{media_type}", {"page": page, "limit": limit})
if not data:
break
# ... build results ...
if len(data) < limit:
break
page += 1
return results

🔗 PR #2469


2. Tautulli—Gotify Notifications Ate My Line Breaks

The Project

Tautulli is a monitoring and analytics tool for Plex. It tracks what you (and your friends) are watching, and sends notifications for just about anything—new media added, streams started, playback paused, you name it. I use it to push alerts to Gotify, a self-hosted notification server.

The Bug

Issue #2702: When you typed a notification message in Tautulli with multiple lines—pressing Enter between them — the Gotify notification arrived as a single squished line. Line one\nLine two became Line one Line two.

The cause: Tautulli sends notifications to Gotify using text/markdown content type (required to display poster images). In Markdown, a single newline \n isn’t a line break — it collapses to a space. You need two trailing spaces before the newline: \n.

The Fix

One line of Python in notifiers.py:

# Before
'message': body,
# After
'message': body.replace('\n', ' \n'),

Poster images still work. Line breaks now render correctly. Zero breaking changes.

🔗 PR #2708


3. Sonarr — The {Season Year} Renaming Token

The Project

Sonarr is an automated TV show downloader and organizer. It watches RSS feeds, grabs new episodes, and files them away in whatever folder structure you define using naming tokens like {Series Title}, {Season}, and {Series Year}.

The Feature

Issue #7667: A user wanted season folders that reflected when that season aired, not just the year the whole series premiered. A show that started in 2010 with a new season in 2024 would have all its season folders labelled (2010) — because {Series Year} is the only year token available.

The ask: a {Season Year} token that resolves to the year of the first episode in that season.

The Fix

Sonarr is a C# backend with a React/TypeScript frontend, so this was a different kind of lift. Changes across four files:

Now you can use Season {season:00} ({Season Year}) and get folders like Season 01 (2013), Season 02 (2014). If no air date is available, the token gracefully returns Unknown.

🔗 PR #8622


4. WordPress Gutenberg — Cleaning Up CI

The Project

Gutenberg is the block editor that powers WordPress. It’s one of the largest and most active open-source projects in the world, with thousands of contributors and a famously competitive “good first issue” board — most tickets get claimed within hours.

The Fix

Issue #78203: The build-plugin-zip.yml GitHub Actions workflow was defining a custom job_status output to pass the build result to downstream jobs — essentially re-implementing something GitHub Actions already provides natively via needs.<job_id>.result.

This is the kind of thing that seems harmless but can introduce subtle bugs or security issues as the codebase evolves. The fix: remove the custom output, use the native context instead.

# Before
needs.build.outputs.job_status == 'success'
# After
needs.build.result == 'success'

Three lines changed. Identical behavior. No footguns left behind.

🔗 PR #78319


5. Radarr — Freebox Download Client Crash

The Project

Radarr is the movie equivalent of Sonarr — it monitors RSS feeds for movies you want, downloads them, and organizes your library. It supports a wide range of download clients, including the Freebox — a French ISP’s router with a built-in download manager.

The Bug

Issue #11388: If your Freebox download queue was completely empty, Radarr would throw an unhandled ArgumentNullException and show a health error. The cause: when the queue is empty, the Freebox API returns { "success": true } with no result field. Radarr’s GetTasks() method passed that null straight to callers, which then called .Where() on it and exploded.

The Fix

One null-coalescing operator in FreeboxDownloadProxy.cs:

// Before
return ProcessRequest<List<FreeboxDownloadTask>>(request, settings).Result;
// After
return ProcessRequest<List<FreeboxDownloadTask>>(request, settings).Result ?? new List<FreeboxDownloadTask>();

Empty queue? Return an empty list. No crash. No health warning. No drama.

🔗 PR #11464


What I Learned


All the PRs


Update — May 15, 2026

A few of yesterday’s PRs already have responses worth sharing.

Gutenberg #78319 was approved by maintainer @desrosj — but then closed as a duplicate of #78208, which was opened a few hours earlier with an identical change. The silver lining: the Gutenberg team awards contributor props even on duplicate PRs, so my name will appear in the merge commit when #78208 lands. First approved PR, even if it didn’t make it in.

Sonarr #8622 was closed by maintainer @markus101. Turns out a PR for the same {Season Year} token (#8052) had already existed but was auto-closed by a bot after 90 days of inactivity — not because the feature was rejected, but because the prerequisite changes in core haven’t landed yet. When they do, the team will reopen the original. Lesson learned: always search for prior art, including closed PRs.

Tautulli #2708 was flagged by a bot — PRs need to target the nightly branch, not master. A quick fix: I’ve updated the base branch and resubmitted.

The other two — PlexTraktSync #2469 and Radarr #11464 — are still open with no feedback yet.

Two are closed, one needs a fix, and two are waiting. Open-source moves fast.

Exit mobile version