All articles
13 min read

Youth Soccer Game Film in Jellyfin: A Pipeline Layout

How a nightly job fetches youth soccer game film, matches it to the team's match history, and files it in Jellyfin, with one human step for games not on GotSport.

Christopher MelsonHomelab Architect
Core Argument
A nightly pipeline can file youth soccer game film into Jellyfin automatically, league and tournament games alike, because the team's public match history on GotSport supplies the competition, and only games missing from it go to a person.
Target Audience
Technical parents and homelab operators who already pay for a game-recording account and run their own Jellyfin server.
Key Insight
Jellyfin behaves predictably when games are episodes named SxxEyy - date - opponent with small sequential season numbers and unique episode numbers, and when the writer records each filename it produces instead of parsing it later.

A nightly job on a homelab node can copy every recorded youth soccer game from the family's camera account into Jellyfin, match it to the team's public match history on GotSport, and file it with the right opponent, date and event, without anyone touching it. League and tournament games both file themselves. The one step that stays manual is labeling games that do not appear on GotSport at all, such as friendlies, and that is a design decision, not a shortcoming.

Key Takeaways

  • Every pipeline stage runs unattended for games on GotSport, league and tournament alike; only games missing from it, such as friendlies, need a person, by design.
  • Match each game to the team's public match history by date and nearest kickoff time to learn which event it belongs to.
  • Name games SxxEyy - YYYY-MM-DD - vs Opponent, keep season numbers small and episode numbers unique.
  • Record every filename the writer produces instead of parsing names back later.

What does the pipeline actually do, start to finish?

Five stages run in order: discover new games, fetch and join the two halves, enrich and match to the team's match history, classify as league or tournament, and file into Jellyfin. All five run unattended on a timer for games that appear on GotSport. A game that does not appear there pauses in a small queue until a person adds one label, then the same run completes it.

This is written as a layout, not a tutorial. I am a Homelab Architect by temperament as much as title, and I run this the way I run the rest of my self-hosted stack: small pieces, explicit contracts between them, and a human only where judgment is actually needed. The problem is ordinary. The camera platform, Trace, records games automatically and keeps the film in its own app. The schedule and competition context live in GotSport, which handles scheduling, live scoring and public pages. The family wants every game in its own Jellyfin library, with correct metadata, without a parent doing it by hand every weekend.

The five stages at a glance

The stages below show where automation runs and where a person sits. Blue stages never need attention. The amber segment is the only human checkpoint, and it applies only to games that do not appear on GotSport.

1
Discover new games (automated, nightly timer)
2
Fetch and join the two halves (automated)
3
Enrich and match to the team's match history (automated)
4
Classify: league or tournament (automated)
Classify: not on GotSport (human label)
5
File into Jellyfin (automated)
Source: author's pipeline design, 2026. Blue = automated, amber = human review for games not on GotSport.

Automation covers the whole path for any game on GotSport, and the single amber segment is where I deliberately kept a person in the loop, only for games with no authoritative source.

How does a nightly job find and assemble new games?

A systemd timer wakes the job each night, asks the family's camera account for recently processed games, and skips anything already filed. Each new game is fetched as two halves and joined into one file.

Discovery from the account the family already pays for

The trigger is a systemd timer, not cron and not a long-running daemon. Per the systemd.timer documentation, OnCalendar= defines wall-clock schedules, and Persistent=true stores the last trigger time on disk so a run missed during downtime fires when the machine returns. The same page documents RandomizedDelaySec=, which spreads start times so a job does not hit a remote service at an identical second every night.

Discovery itself is deliberately boring: list what the account says is newly processed, compare against what I have already filed, and work only on the difference. A recording is not available until the platform has finished processing it, so a game played in the afternoon may not show up until a later night.

Two halves, one file

Each game arrives as two halves. I fetch both and join them into a single file before anything else touches it. The Jellyfin TV Shows documentation describes multi-part episodes through part-style separators, and I would rather hand the server one unambiguous episode than rely on it stitching parts together. One game, one file, one library item.

If only one half exists, the run does not file a partial game. It marks the game as incomplete, notifies me, and tries again the next night. A half-game in the library is worse than a missing one, because nobody notices it is half until someone goes looking for the second period.

How is each game matched to a competition?

The game record supplies opponent, date and score. The pipeline then looks up the team's public match history on GotSport, which lists every match the team plays, league and tournament, with the event name. It picks the entry with the same date and the nearest kickoff time, and that entry supplies the event name the file is filed under.

Enrichment from the game record

The camera platform already knows who the game was against, when it was played and how it ended. I treat that record as the source of truth for opponent, date and score and do not try to improve on it. Everything downstream, from the episode title to the notification text, is built from those fields.

Matching to the match history by date and nearest kickoff

Youth schedules are messy. Kickoff times shift, and a team can play twice in a day during a tournament. Matching on date alone would misfire on double-header days, and matching on exact time would miss every game that started ten minutes late. So the rule is two-step: filter the history to the game's date, then choose the entry whose kickoff is nearest the recorded start.

When the match is clear, the game inherits its competition and moves on. When it is not, because no entry exists for that date or two entries sit almost equally close, the pipeline does not guess. It routes the game to the human step and says why. A wrong automatic label is more expensive than a short wait, since a mislabeled game sits quietly in the library and looks correct.

Which games still need a human?

Games that appear in the team's match history, league and tournament alike, resolve cleanly, so they file themselves. Only games that do not appear on GotSport at all, such as friendlies and scrimmages, have no authoritative source for the event. I park those in a small queue file where a person adds a label and releases them. Earlier versions of the pipeline needed a manual label for every tournament game; switching the lookup to the team's own match history is what removed that step.

League and tournament games file themselves

For any game in the match history the match is enough. The event is known, the season is known, and the filing stage takes over with no prompt to me.

The pending queue: one small file, one label, then release

Games missing from GotSport go into a pending queue, a small plain file with one entry per waiting game. Each entry already carries everything the pipeline knows: opponent, date, score, the joined video. The only blank is the event label. I fill it in, mark the entry released, and the next run files the game like any other.

I want to be direct about why this exists. For a friendly or scrimmage there is no authoritative source to ask, so any automatic label would be a guess. The honest design is to acknowledge the limit and put a person at that one point. The cost is a minute or two now and then. The benefit is that the archive never contains a confident wrong answer.

The queue has one more job: it ages. If an entry sits unreleased for too long, I get a notification, so a forgotten game does not become a permanent hole.

How should games be named and filed so Jellyfin behaves?

Use a TV Shows style library: one folder per team, one season per soccer season, and each game an episode named SxxEyy - YYYY-MM-DD - vs Opponent. Keep season numbers small and sequential, and make every episode number unique. Games then sort chronologically and show opponent and date in the interface.

One folder per team, one season per soccer season

The Jellyfin TV Shows documentation asks for season folders named with a number, recommends zero-padding, and describes episodes as SxxEyy. Each team is a show, each soccer season a season, each game an episode. A parent browsing the library sees a team, opens a season, and scrolls through games in order.

Why never the year as the season number

It is tempting to use the calendar year as the season number. I would not. The Jellyfin documentation does not describe year-based seasons, and in my own library very large season numbers, in the 200-plus range, were treated as malformed. That is my observation, not documented behavior, so treat it as a caution and not a rule. Small sequential numbers have never given me trouble, and a soccer season that straddles two calendar years makes the year a poor key anyway.

The second trap is versions. Jellyfin supports multiple versions of the same item, and per the Jellyfin Movies documentation, files that share one base name are grouped as versions of a single item. In my own Jellyfin library I saw the same thing happen to episodes that shared an episode number, and to same-date items. A double-header, or two games on one tournament Saturday, is exactly the case that triggers it. The defense is boring: give every game its own episode number within the season, and never let two files share one.

Episode pattern and why a TV Shows library

The pattern is SxxEyy - YYYY-MM-DD - vs Opponent. Season and episode come first because that is what the scanner reads. The date and opponent follow as title text, so the interface shows them and the episode number still gives a stable sort. Episode numbers come from order of play within the season, not from any external identifier, which keeps them unique by construction.

I chose a TV Shows library over a flat home-videos layout because the season and episode structure is the feature. It gives grouping by season, chronological order and a clean per-team view with no custom views to maintain.

What makes the nightly run safe to leave alone?

Four habits: skip anything already filed, record every filename the writer produced rather than parsing names later, keep credentials outside the repo with overlapping tokens for rotation, and make each failure mode visible through a notification. A pipeline you trust is one that tells you when it cannot do its job.

Idempotency: skip already-filed games

Every run starts by comparing what the account offers against what has been filed and proceeds only with the difference. Idempotency is what lets the timer's catch-up behavior be safe rather than dangerous.

Derive, never reconstruct: the writer records every filename

The writer records every filename it produces, along with the game it belongs to, in a manifest. When a later step needs to know where a game went, it reads the manifest. It never parses a filename back into opponent, date or episode number.

Reconstructing from convention works right up to the first opponent whose name contains a dash, an apostrophe or a number, and then it fails in a way that is hard to trace.

Credentials outside the repo, overlapping tokens for rotation

Credentials live outside the repo, and I will not say more than that. The design point worth sharing is overlap: when a token is about to expire, I bring the replacement online while the old one is still valid, so rotation never creates a gap where a nightly run fails. If you have read my notes on homelab firewall posture, the principle is the same. Assume credentials change, and design so a change is routine.

Failure modes and notifications

I track four failures, each with a notification that says what happened and what to do:

  • A missing half: one of the two recordings has not appeared yet. The game is held and retried.
  • No match-history entry: no entry for that date, or an ambiguous one. The game goes to the pending queue.
  • An expired or rejected token: the run stops early and tells me, instead of reporting an empty night as success.
  • An aging pending queue: a waiting entry has sat unlabeled too long.

The third one matters most. A run that silently finds nothing because it could not authenticate looks identical to a quiet week. Distinguishing "nothing new" from "could not look" is most of what monitoring means here.

What are the trade-offs of this design?

The design trades a little convenience for trustworthiness. A person handles labels for games not on GotSport, games can appear a day after they are played, and the layout is tied to a TV Shows library. In return, nothing is mislabeled, nothing is filed twice, and the archive stays correct with almost no maintenance.

The human step is the obvious cost, and I accept it. The less obvious one is that naming is a commitment. Changing the season convention after a few years of games means touching every file and every manifest entry, which is why I fixed the pattern before filing the first game.

The infrastructure side is light. It needs a node that is usually on, enough storage for video and a Jellyfin server. If you are building the serving side from scratch, my Jellyfin Live TV setup with Dispatcharr covers the server end, and my write-up on macvlan in Docker Swarm explains how I give containerized services their own network identity. Neither is required for this pipeline. The ingest job only needs somewhere to write files that Jellyfin can read.

What I would tell another builder is to start with the two boring stages, fetch and file, and get naming right. Add schedule matching next and the pending queue after that. The archive is useful long before the smart parts exist, and the smart parts are far easier to trust once the boring ones have run for a month without surprises.

Frequently Asked Questions

How do I get youth soccer game video into Jellyfin automatically?

Run a scheduled job on a homelab machine that fetches new recordings from the account your family already pays for, joins any split files, and writes them into a Jellyfin TV Shows library with a consistent naming pattern. A systemd timer with catch-up enabled makes the job survive downtime. Add a notification path so failures are visible rather than silent.

Can Jellyfin show the opponent and date for each game?

Yes, if you treat each game as an episode and put both in the episode title, such as SxxEyy - YYYY-MM-DD - vs Opponent. Jellyfin reads the season and episode numbers from the SxxEyy part and shows the rest as the title. Sorting by episode number then gives you chronological order.

Why not use the year as the season number for sports in Jellyfin?

Jellyfin's documentation asks for season folders with a number but does not describe year-based seasons. In my own library, very large season numbers (200 and above) were treated as malformed, so I use small sequential numbers, one per soccer season. That observation is mine, not documented Jellyfin behavior.

Do tournament games need manual labeling?

No. The pipeline reads the team's public match history on GotSport, which lists league and tournament games with the event name, and matches each game by date and nearest kickoff time, so both file themselves. Only games that do not appear on GotSport at all, such as friendlies and scrimmages, go to a small queue where a person types a label and releases them. It costs seconds per game and avoids guessing.

How do you keep a nightly video job from filing the same game twice?

The writer records every filename it produces, along with the game it came from, in a manifest. Each run checks the manifest before fetching anything and skips games already filed. Because the manifest is written at filing time, the job never has to reconstruct or parse names back.

Do I need a Home Videos library for game film?

No. A TV Shows library gives you season and episode structure, which is what makes games sort chronologically and group by season. A Home Videos library is a flatter structure. The TV Shows layout was the better fit for me, though either can work depending on how you browse.


Works Cited

  • [1] Jellyfin. "TV Shows." Jellyfin Docs, 2026. jellyfin.org
  • [2] Jellyfin. "Movies" (Multiple Versions naming). Jellyfin Docs, 2026. jellyfin.org
  • [3] systemd. "systemd.timer(5)." Linux man-pages, man7.org, 2026. man7.org
  • [4] Trace. "Trace: automatic game recording." Trace, 2026. traceup.com
  • [5] GotSport. "Scheduling & Game-Day Tools." GotSport, 2026. home.gotsport.com
All articles

Original content published on chris.melson.us