Initial import of Hugo site to Forgejo

This commit is contained in:
Iman Alipour 2026-07-28 12:47:20 +00:00
commit cb1ba0317f
1259 changed files with 236349 additions and 0 deletions

View file

@ -0,0 +1,674 @@
<!DOCTYPE html>
<html lang="en" dir="auto">
<head><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow">
<title>Movie Night Over the Internet: Jellyfin &#43; Tailscale on a Raspberry Pi 4 | AlipourIm journeys</title>
<meta name="keywords" content="raspberrypi, jellyfin, tailscale, docker, homelab, movienight">
<meta name="description" content="A cozy, slightly chaotic, very fun setup story: turning a Raspberry Pi 4 into a private movie-night server with Jellyfin, Docker, and Tailscale—starting on microSD and graduating to SSD later.">
<meta name="author" content="Iman Alipour">
<link rel="canonical" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/boredom/">
<link crossorigin="anonymous" href="/assets/css/stylesheet.31b150d909186c48d6dbbf653acc2489945fa9ac2c8f8cd3fe8448e89e40fccf.css" integrity="sha256-MbFQ2QkYbEjW279lOswkiZRfqawsj4zT/oRI6J5A/M8=" rel="preload stylesheet" as="style">
<link rel="icon" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon-32x32.png">
<link rel="apple-touch-icon" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/apple-touch-icon.png">
<link rel="mask-icon" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/safari-pinned-tab.svg">
<meta name="theme-color" content="#2e2e33">
<meta name="msapplication-TileColor" content="#2e2e33">
<link rel="alternate" hreflang="en" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/boredom/">
<noscript>
<style>
#theme-toggle,
.top-link {
display: none;
}
</style>
<style>
@media (prefers-color-scheme: dark) {
:root {
--theme: rgb(29, 30, 32);
--entry: rgb(46, 46, 51);
--primary: rgb(218, 218, 219);
--secondary: rgb(155, 156, 157);
--tertiary: rgb(65, 66, 68);
--content: rgb(196, 196, 197);
--code-block-bg: rgb(46, 46, 51);
--code-bg: rgb(55, 56, 62);
--border: rgb(51, 51, 51);
}
.list {
background: var(--theme);
}
.list:not(.dark)::-webkit-scrollbar-track {
background: 0 0;
}
.list:not(.dark)::-webkit-scrollbar-thumb {
border-color: var(--theme);
}
}
</style>
</noscript><script>
(function () {
window.goatcounter = { endpoint: "/count" };
const s = document.createElement("script");
s.async = true;
s.src = "/js/count.js";
document.head.appendChild(s);
})();
</script>
<script>
(function () {
const GC = "";
const nf = new Intl.NumberFormat();
const SING = "visit";
const PLUR = "visits";
function ready(fn) {
if (document.readyState === "complete" || document.readyState === "interactive") setTimeout(fn, 0);
else document.addEventListener("DOMContentLoaded", fn);
}
function gcPath() {
try {
return window.goatcounter && window.goatcounter.get_data
? window.goatcounter.get_data().p
: location.pathname;
} catch (_) {
return location.pathname;
}
}
async function fetchCount(path, period) {
try {
const q = new URLSearchParams({ t: String(Date.now()) });
if (period) q.set("start", period);
const url = `${GC}/counter/${encodeURIComponent(path)}.json?${q}`;
const r = await fetch(url, { credentials: "omit" });
if (r.status === 404) return 0;
if (!r.ok) throw 0;
const j = await r.json();
const n = Number(String(j.count).replace(/,/g, ""));
return Number.isFinite(n) ? n : 0;
} catch {
return null;
}
}
const labelVisits = (n) =>
n === null ? "—" : `${nf.format(n)} ${n === 1 ? SING : PLUR}`;
ready(async function () {
const post = document.querySelector(".post-single");
if (post) {
const meta = post.querySelector(
".post-header .post-meta, .post-header .entry-meta, .post-meta"
);
const visits = await fetchCount(gcPath());
if (meta) {
let span = meta.querySelector(".post-views");
if (!span) {
span = document.createElement("span");
span.className = "post-views";
meta.appendChild(span);
}
span.textContent = labelVisits(visits ?? 0);
}
}
const cards = document.querySelectorAll("article.post-entry");
await Promise.all(
Array.from(cards).map(async (card) => {
const meta = card.querySelector(".entry-footer, .post-meta");
const link = card.querySelector("a.entry-link, h2 a, .entry-title a");
if (!meta || !link) return;
try {
const u = new URL(link.getAttribute("href"), location.origin);
if (u.origin !== location.origin) return;
let span = meta.querySelector(".post-views");
if (!span) {
span = document.createElement("span");
span.className = "post-views";
meta.appendChild(span);
}
span.textContent = labelVisits((await fetchCount(u.pathname)) ?? 0);
} catch {}
})
);
const uvEl = document.getElementById("gc_site_visitors_week");
if (uvEl) {
const n = await fetchCount("TOTAL", "week");
uvEl.textContent = n === null ? "—" : nf.format(n);
}
});
})();
</script>
</head>
<body class="" id="top">
<script>
if (localStorage.getItem("pref-theme") === "dark") {
document.body.classList.add('dark');
} else if (localStorage.getItem("pref-theme") === "light") {
document.body.classList.remove('dark')
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
<header class="header">
<nav class="nav">
<div class="logo">
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/" accesskey="h" title="AlipourIm journeys (Alt + H)">AlipourIm journeys</a>
<div class="logo-switches">
<button id="theme-toggle" accesskey="t" title="(Alt + T)" aria-label="Toggle theme">
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
</div>
</div>
<ul id="menu">
<li>
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/" title="Posts">
<span>Posts</span>
</a>
</li>
<li>
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/phd_journey/" title="PhD_journey">
<span>PhD_journey</span>
</a>
</li>
<li>
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/about/" title="About">
<span>About</span>
</a>
</li>
</ul>
</nav>
</header>
<main class="main">
<article class="post-single">
<header class="post-header">
<div class="breadcrumbs"><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/">Home</a>&nbsp;»&nbsp;<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/">Posts</a></div>
<h1 class="post-title entry-hint-parent">
Movie Night Over the Internet: Jellyfin &#43; Tailscale on a Raspberry Pi 4
</h1>
<div class="post-description">
A cozy, slightly chaotic, very fun setup story: turning a Raspberry Pi 4 into a private movie-night server with Jellyfin, Docker, and Tailscale—starting on microSD and graduating to SSD later.
</div>
<div class="post-meta"><span title='2025-12-21 09:30:00 +0100 +0100'>December 21, 2025</span>&nbsp;·&nbsp;9 min&nbsp;·&nbsp;Iman Alipour
<span class="post-meta-item">
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/boredom/#isso-thread" class="isso-comments-link">Comments</a>
</span>
</div>
</header> <div class="toc">
<details open>
<summary accesskey="c" title="(Alt + C)">
<span class="details">Table of Contents</span>
</summary>
<div class="inner"><ul>
<li>
<a href="#the-idea" aria-label="The idea">The idea</a></li>
<li>
<a href="#what-you-need" aria-label="What you need">What you need</a></li>
<li>
<a href="#step-one-choose-a-home-for-your-stuff" aria-label="Step one: choose a home for your stuff">Step one: choose a home for your stuff</a></li>
<li>
<a href="#step-two-install-docker-and-start-thinking-in-containers" aria-label="Step two: install Docker and start thinking in containers">Step two: install Docker and start thinking in containers</a></li>
<li>
<a href="#step-three-jellyfin-running-happily-in-docker" aria-label="Step three: Jellyfin, running happily in Docker">Step three: Jellyfin, running happily in Docker</a></li>
<li>
<a href="#step-four-tailscale-the-magic-door-for-friends" aria-label="Step four: Tailscale, the magic door for friends">Step four: Tailscale, the magic door for friends</a></li>
<li>
<a href="#step-five-create-a-jellyfin-user-for-your-friend" aria-label="Step five: create a Jellyfin user for your friend">Step five: create a Jellyfin user for your friend</a></li>
<li>
<a href="#step-six-the-secret-to-a-smooth-pi-movie-night-is-direct-play" aria-label="Step six: the secret to a smooth Pi movie night is Direct Play">Step six: the secret to a smooth Pi movie night is Direct Play</a></li>
<li>
<a href="#step-seven-syncplay-aka-we-pressed-play-together" aria-label="Step seven: SyncPlay, aka “we pressed play together”">Step seven: SyncPlay, aka “we pressed play together”</a></li>
<li>
<a href="#starting-on-microsd-graduating-to-ssd-later" aria-label="Starting on microSD, graduating to SSD later">Starting on microSD, graduating to SSD later</a></li>
<li>
<a href="#where-this-goes-next" aria-label="Where this goes next">Where this goes next</a>
</li>
</ul>
</div>
</details>
</div>
<div class="post-content"><p>Ok. Let me tell you the why first. So&hellip;. I was turning from one side to the other on my bed, going through YouTube. The thing that I used to silence my pain, to let the time pass, and to forget. But it didn&rsquo;t hit the same anymore. I was bored. Eternal boredom. Then the loneliness came back. It&rsquo;s funny how you can be in the middle of dozens of people, i.e., not alone, but still feel lonely. Anyway. I though to myself &ldquo;let&rsquo;t message one of my two best friends&rdquo;, We eventually went into a google meet, talked and talked, and decided to watch a movie together at the same time. I told him I still have my Raspberry Pi lying around, let me put it to good use. Few hours passed, and I was ready. Here is what happened in those few hours.</p>
<p>I wanted the kind of movie night where everyone presses play at the same moment, laughs at the same joke, and groans at the same plot twist—even when were scattered across different apartments and time zones. I also wanted it to be a little nerdy, a little elegant, and the kind of project that makes you feel like you own your gadgets again. Enter: a Raspberry Pi 4 (8GB), Jellyfin, and Tailscale.</p>
<p>This post is the “how it went” and the “how to do it” version. Its written for the future-me who will forget everything the second it works, and for anyone who wants a private streaming setup without punching holes in their router for the entire internet to poke at.</p>
<h2 id="the-idea">The idea<a hidden class="anchor" aria-hidden="true" href="#the-idea">#</a></h2>
<p>Jellyfin is the media server that turns your own movie files into a nice Netflix-like library. Tailscale is the secret tunnel that lets your friends reach your server as if they were sitting on your WiFi—without you exposing ports to the public internet. Docker is the little shipping container that keeps everything tidy and portable, which matters because I started on a microSD card and planned to move to an SSD later.</p>
<p>The trick to making a Raspberry Pi feel “fast enough” is to avoid video transcoding whenever possible. When Jellyfin can “direct play” a file, the Pi mostly just serves bytes. When it has to transcode, it suddenly feels like you asked a hamster to tow a car. So the theme of this build is compatibility: store media in formats that phones, browsers, and TVs can play directly.</p>
<h2 id="what-you-need">What you need<a hidden class="anchor" aria-hidden="true" href="#what-you-need">#</a></h2>
<p>My setup uses Ubuntu (64-bit is the long-term-friendly choice), a Raspberry Pi 4, a microSD card to get started, and eventually a USB 3 SSD for real “server vibes.” Youll also want a folder of movies youre allowed to stream to your friends. Streaming DRM content from Netflix or rental platforms through Jellyfin isnt supported, and Im intentionally keeping this firmly in the “your own files” lane.</p>
<h2 id="step-one-choose-a-home-for-your-stuff">Step one: choose a home for your stuff<a hidden class="anchor" aria-hidden="true" href="#step-one-choose-a-home-for-your-stuff">#</a></h2>
<p>Before installing anything, I picked paths that would survive my future SSD upgrade. The idea is simple: keep all persistent app data under one directory, keep all media under another directory, and let Docker mount those paths into containers.</p>
<p>On the Pi, I created a little directory neighborhood under <code>/srv</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo mkdir -p /srv/jellyfin/<span style="color:#f92672">{</span>config,cache<span style="color:#f92672">}</span> /srv/media /srv/compose/jellyfin
</span></span><span style="display:flex;"><span>sudo chown -R $USER:$USER /srv
</span></span></code></pre></div><p>If youre not ready to move movies into <code>/srv/media</code> yet, thats okay. The important part is that you pick a place you can later mount your SSD onto without rewriting your whole setup.</p>
<h2 id="step-two-install-docker-and-start-thinking-in-containers">Step two: install Docker and start thinking in containers<a hidden class="anchor" aria-hidden="true" href="#step-two-install-docker-and-start-thinking-in-containers">#</a></h2>
<p>I installed Docker and verified that <code>docker compose</code> worked. After that, everything became a file called <code>docker-compose.yml</code> and the comforting feeling that I can rebuild my server from a single folder if life gets weird.</p>
<p>A typical install flow on Raspberry Pi OS looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt update
</span></span><span style="display:flex;"><span>sudo apt install -y ca-certificates curl
</span></span><span style="display:flex;"><span>curl -fsSL https://get.docker.com | sh
</span></span><span style="display:flex;"><span>sudo usermod -aG docker $USER
</span></span><span style="display:flex;"><span>newgrp docker
</span></span><span style="display:flex;"><span>docker --version
</span></span><span style="display:flex;"><span>docker compose version
</span></span></code></pre></div><p>Now for the part that feels like summoning a friendly daemon.</p>
<h2 id="step-three-jellyfin-running-happily-in-docker">Step three: Jellyfin, running happily in Docker<a hidden class="anchor" aria-hidden="true" href="#step-three-jellyfin-running-happily-in-docker">#</a></h2>
<p>In <code>/srv/compose/jellyfin/docker-compose.yml</code>, I defined a Jellyfin service and mounted my config, cache, and media directories into it. The key mental shift is that Jellyfin can only see what you mount into the container. If you try to browse to <code>/home/hub/Documents/movies</code> from inside the Jellyfin UI and it looks like the folder doesnt exist, its not being dramatic—it genuinely cant see it. Containers are like that.</p>
<p>Heres a simple <code>docker-compose.yml</code> that works well on a Pi:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">jellyfin</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">jellyfin/jellyfin:latest</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">jellyfin</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">user</span>: <span style="color:#e6db74">&#34;1000:1000&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#e6db74">&#34;8096:8096/tcp&#34;</span>
</span></span><span style="display:flex;"><span> - <span style="color:#e6db74">&#34;7359:7359/udp&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">/srv/jellyfin/config:/config</span>
</span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">/srv/jellyfin/cache:/cache</span>
</span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">/srv/media:/media</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span></code></pre></div><p>If your user isnt UID 1000, check it with <code>id -u</code> and <code>id -g</code>, then update the <code>user:</code> line accordingly.</p>
<p>I started Jellyfin like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cd /srv/compose/jellyfin
</span></span><span style="display:flex;"><span>docker compose up -d
</span></span></code></pre></div><p>After that, Jellyfin lived at <code>http://&lt;pi-lan-ip&gt;:8096</code> on my home network, which is the most satisfying “its alive” moment of the whole build.</p>
<h2 id="step-four-tailscale-the-magic-door-for-friends">Step four: Tailscale, the magic door for friends<a hidden class="anchor" aria-hidden="true" href="#step-four-tailscale-the-magic-door-for-friends">#</a></h2>
<p>Now comes the part that makes everything feel effortless. Instead of forwarding ports, configuring certificates, and hoping I didnt accidentally publish my server to the open ocean, I installed Tailscale on the Pi host.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -fsSL https://tailscale.com/install.sh | sh
</span></span><span style="display:flex;"><span>sudo tailscale up
</span></span><span style="display:flex;"><span>tailscale ip -4
</span></span></code></pre></div><p>That last command gives you a <code>100.x.y.z</code> address. Its your Pis Tailscale IP, and its the address your friends will use to reach Jellyfin. From anywhere, the server becomes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>http://100.x.y.z:8096
</span></span></code></pre></div><p>Or if you enabled magicDNS:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>http://rpi:8096
</span></span></code></pre></div><p>The moment you can open that URL from your phone on mobile data and see your own server is the moment you start plotting your next homelab project.</p>
<p>If you want friends to access only this server and not everything else in your tailnet, Tailscale has a device sharing flow thats perfect for “heres the Pi, please dont look in the rest of my digital house.”</p>
<h2 id="step-five-create-a-jellyfin-user-for-your-friend">Step five: create a Jellyfin user for your friend<a hidden class="anchor" aria-hidden="true" href="#step-five-create-a-jellyfin-user-for-your-friend">#</a></h2>
<p>Jellyfin accounts are local to your server. Your friend wont “sign up” on some global Jellyfin site; instead, you create a user for them inside your Jellyfin admin dashboard. The nice part is that you can give them access only to the libraries you want, and you can keep admin permissions to yourself.</p>
<p>If typing passwords on a TV app makes you feel old instantly, Jellyfins Quick Connect feature can make logins much smoother. Your friend gets a short code on their device, you approve it from your logged-in browser, and nobody has to poke letters into a remote control one by one.</p>
<h2 id="step-six-the-secret-to-a-smooth-pi-movie-night-is-direct-play">Step six: the secret to a smooth Pi movie night is Direct Play<a hidden class="anchor" aria-hidden="true" href="#step-six-the-secret-to-a-smooth-pi-movie-night-is-direct-play">#</a></h2>
<p>A Raspberry Pi 4 is perfectly happy serving media files. It becomes less happy when it has to convert them on the fly. The goal is to store your library in a way that most clients can play without conversion.</p>
<p>For “plays on basically everything,” I like MP4 with H.264 video and AAC audio. When I inspected one of my files, I found H.264 video already, but the audio was AC3. AC3 is fine on some devices, but its not universal in browsers, and “my friend has no audio” is a movie-night mood killer.</p>
<p>The fix was to keep the video as-is and convert only the audio to AAC, which is much lighter than re-encoding the whole video. This command copies the H.264 video stream, converts audio to AAC stereo, and produces an MP4 thats friendlier to phones and browsers:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ffmpeg -i <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.BluRay.RARBG.FilmKio.mkv&#34;</span> -map 0:v:0 -map 0:a:0 -c:v copy -c:a aac -b:a 384k -ac <span style="color:#ae81ff">2</span> -movflags +faststart <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.mp4&#34;</span>
</span></span></code></pre></div><p>Subtitles deserve their own tiny cautionary tale. Some subtitle formats trigger transcoding because the client cant render them natively and asks the server to burn them into the video. Text-based SRT subtitles are usually the easiest path. If your MKV contains an SRT track, you can extract it and keep it next to your MP4:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ffmpeg -i <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.BluRay.RARBG.FilmKio.mkv&#34;</span> -map 0:s:0 <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.srt&#34;</span>
</span></span></code></pre></div><p>Once your friend is watching, you can confirm how Jellyfin is handling playback by checking the active session in the admin dashboard. Seeing “Direct Play” feels like winning.</p>
<h2 id="step-seven-syncplay-aka-we-pressed-play-together">Step seven: SyncPlay, aka “we pressed play together”<a hidden class="anchor" aria-hidden="true" href="#step-seven-syncplay-aka-we-pressed-play-together">#</a></h2>
<p>Jellyfin has SyncPlay for “group watch” nights, and its wonderfully simple when everyone is using compatible clients. In practice, the web client is an excellent baseline because its consistent and easy for friends. When we do movie night, we start a SyncPlay group, join it from each device, and keep a voice call open for commentary and laughter. Its not complicated; its just… cozy.</p>
<h2 id="starting-on-microsd-graduating-to-ssd-later">Starting on microSD, graduating to SSD later<a hidden class="anchor" aria-hidden="true" href="#starting-on-microsd-graduating-to-ssd-later">#</a></h2>
<p>Yes, you can run all of this on microSD to begin with. It works. It also feels like using a napkin as a hard drive if you add heavy-write services later. My plan is to move the system to an SSD when convenient, because SSDs are faster and they handle constant writes far better than microSD cards.</p>
<p>The reason the <code>/srv</code> layout is so helpful is that migration becomes a simple copy-and-mount story. When the SSD arrives, you can copy <code>/srv</code> to the SSD, mount the SSD at <code>/srv</code>, reboot, and your containers will come back exactly as before. Docker will happily reuse the same volume paths; Jellyfin will happily reuse the same library; and youll feel like a wizard who planned ahead.</p>
<h2 id="where-this-goes-next">Where this goes next<a hidden class="anchor" aria-hidden="true" href="#where-this-goes-next">#</a></h2>
<p>Once Jellyfin and Tailscale are stable, its easy to imagine the Pi doing more. Pi-hole is light and useful. A smart home hub is fun. Nextcloud is possible, especially with SSD storage and some attention to databases and caching. The practical advice Im giving future-me is to add services one at a time and keep <code>/srv</code> as the center of gravity, because nothing ruins movie night like “I tweaked DNS and now nothing works.”</p>
<p>For now, though, this little Pi does the job: it hosts a library, it welcomes friends through a private tunnel, and it turns “lets watch something” into a real shared moment—even when were not in the same room.</p>
<p>Happy streaming.</p>
<p>The movie night went great, I even posted a BeReal on it. The choice of movie was not random either. I&rsquo;ll write a review for it later on. It was just amazing.</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
<p><strong>Downloads:</strong>
<a href="/sources/posts/boredom.md">Markdown</a> ·
<a href="/sources/posts/boredom.md.asc">Signature (.asc)</a>
</p><details class="signature">
<summary>View OpenPGP signature</summary>
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">-----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbugACgkQtYgoUOBM
jSrqzw/8Crod3Gm5xGMMrOtsoVm9NFwTAD7xdc8H5LcFC8P5OZmyEYBxF/SEHk6m
TDhSyj6bNdShWIrzkKL0XHm6ntjhkBX4&#43;dFzPbKjpHZ84on/xfNwIOh8br&#43;WJEBF
V3zCialBjjxxE37PVLkqsNVVtMgT2Wv5GnR7SWLKkovJWpIn/FP0gSsQFUIvRvdC
Xhy3nvODqXZqfg77kKllmbkPI5ePkxl95CK9fd4yzhI13G41vveVO4dt2JhWVR6H
dfRv6XG53WGP0nVWyEdHJjJhiT1JTd7//AD3DsRCTmBNyaxweRlPsvqqICquGx1U
LGQ62y0oZL5WDqjiD7T2ZJAJ6sqr6NngFGjh7RaKOWDT1&#43;8fTdXfQg/t91lTHVfh
efVqOiH&#43;B6SlzqPM4LgzRjf&#43;36XdSu9R1w75sGykQpGRBfq2IymoM6RPQLEwgm3J
haMjYRqx5jZSLj9FpjOZFYEuqYCa0ut0lUgY9BDHf0u8kKrW6OQZFVdhN31g&#43;Lvf
5qjzC&#43;ZzR4BLMpA4E33NKmYT4Rt1i5mSPiGY85yqhJdjFJRtPfXXf05&#43;m7VGjRPp
sWQmqO1o7cChFqVnF5IalDFCNIsGavBf8F0/7tu3y4bLIqoBMBfgIgg9KMORVHIn
kqUsV4LpNgVWdTzp0QURkHUOL5uP72Jqa3ppVYEXlJQbhuLpCDY=
=/K6E
-----END PGP SIGNATURE-----
</pre>
</details><p><em>Verify locally:</em></p>
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/sources/posts/boredom.md
torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/sources/posts/boredom.md.asc
gpg --verify boredom.md.asc boredom.md
</pre>
</div>
</div>
<footer class="post-footer">
<ul class="post-tags">
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/raspberrypi/">Raspberrypi</a></li>
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/jellyfin/">Jellyfin</a></li>
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/tailscale/">Tailscale</a></li>
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/docker/">Docker</a></li>
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/homelab/">Homelab</a></li>
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/movienight/">Movienight</a></li>
</ul>
<nav class="paginav">
<a class="prev" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/seeing_the_light/">
<span class="title">« Prev</span>
<br>
<span>Seeing the &#34;Light&#34;</span>
</a>
<a class="next" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/how_i_am_doing/">
<span class="title">Next »</span>
<br>
<span>How am I doing?</span>
</a>
</nav>
</footer><div class="comments-switch" style="display:flex;gap:.5rem;margin:.5rem 0 1rem;">
<button id="btn-isso" type="button" aria-pressed="true">Anonymous (Isso)</button>
<button id="btn-giscus" type="button" aria-pressed="false">GitHub (Giscus)</button>
<a href="https://github.com/AlipourIm/blog-comments/discussions/categories/comments" target="_blank" rel="noopener">Open on GitHub ↗</a>
</div>
<div id="panel-giscus" style="display:none">
<section id="giscus-thread"></section>
</div>
<div id="panel-isso">
<section id="isso-thread"
data-isso-id="/posts/boredom/"
data-title="Movie Night Over the Internet: Jellyfin &#43; Tailscale on a Raspberry Pi 4">
<noscript>Javascript needs to be activated to view comments.</noscript>
</section>
<script
src="/isso/js/embed.min.js"
data-isso="/isso/"
data-isso-css="false"
data-isso-lang="en"
data-isso-max-comments-nested="5"
data-isso-sorting="newest"
async>
</script>
<small class="isso-powered" style="display:block;margin:.5rem 0;color:var(--secondary,#888)">
Comments powered by <a href="https://isso-comments.de" target="_blank" rel="noopener">Isso</a>
</small>
</div>
<script>
(function () {
var panelIsso = document.getElementById('panel-isso');
var panelGisc = document.getElementById('panel-giscus');
var issoBtn = document.getElementById('btn-isso');
var giscBtn = document.getElementById('btn-giscus');
var gLoaded = false;
function isDark() {
var pref = localStorage.getItem('pref-theme');
if (pref === 'dark') return true;
if (pref === 'light') return false;
var html = document.documentElement, body = document.body;
if (
(html.classList && html.classList.contains('dark')) ||
(body.classList && body.classList.contains('dark')) ||
html.getAttribute('data-theme') === 'dark' ||
body.getAttribute('data-theme') === 'dark' ||
(html.classList && html.classList.contains('theme-dark')) ||
(body.classList && body.classList.contains('theme-dark'))
) return true;
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
function gTheme(){ return isDark() ? 'dark_dimmed' : 'noborder_light'; }
function loadGiscus() {
if (gLoaded) return;
var s = document.createElement('script');
s.src = 'https://giscus.app/client.js';
s.async = true;
s.crossOrigin = 'anonymous';
s.setAttribute('data-repo','AlipourIm/blog-comments');
s.setAttribute('data-repo-id','R_kgDOQGARyA');
s.setAttribute('data-category','Comments');
s.setAttribute('data-category-id','DIC_kwDOQGARyM4Cw3x-');
s.setAttribute('data-mapping','pathname');
s.setAttribute('data-strict','0');
s.setAttribute('data-reactions-enabled','1');
s.setAttribute('data-emit-metadata','0');
s.setAttribute('data-input-position','bottom');
s.setAttribute('data-lang','en');
s.setAttribute('data-theme', gTheme());
document.getElementById('giscus-thread').appendChild(s);
function reTheme(){
var iframe = document.querySelector('iframe.giscus-frame');
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(
{ giscus: { setConfig: { theme: gTheme() } } },
'https://giscus.app'
);
}
}
var tbtn = document.getElementById('theme-toggle');
if (tbtn) tbtn.addEventListener('click', function(){ setTimeout(reTheme, 0); });
var mo = new MutationObserver(function(){ reTheme(); });
mo.observe(document.documentElement, { attributes:true, attributeFilter:['class','data-theme'] });
mo.observe(document.body, { attributes:true, attributeFilter:['class','data-theme'] });
gLoaded = true;
}
function show(which) {
var showIsso = which === 'isso';
panelIsso.style.display = showIsso ? 'block' : 'none';
panelGisc.style.display = showIsso ? 'none' : 'block';
if (issoBtn) issoBtn.setAttribute('aria-pressed', showIsso ? 'true' : 'false');
if (giscBtn) giscBtn.setAttribute('aria-pressed', showIsso ? 'false' : 'true');
if (!showIsso) loadGiscus();
}
if (issoBtn) issoBtn.addEventListener('click', function(e){ e.preventDefault(); show('isso'); });
if (giscBtn) giscBtn.addEventListener('click', function(e){ e.preventDefault(); show('giscus'); });
show('isso');
})();
</script>
</article>
</main>
<footer class="footer">
<span>&copy; 2026 <a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/">AlipourIm journeys</a></span> ·
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
</span>
</footer>
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" />
</svg>
</a>
<div class="site-meta-row" style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;">
<a href="/index.xml" rel="alternate" type="application/rss+xml" title="RSS"
class="rss-link" style="display:inline-flex;align-items:center;gap:.35rem;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M4 11a9 9 0 0 1 9 9" />
<path d="M4 4a16 16 0 0 1 16 16" />
<circle cx="5" cy="19" r="1" />
</svg>
<span>RSS</span>
</a>
<span aria-hidden="true">·</span>
<span>Visitors this week: <span id="gc_site_visitors_week"></span></span>
</div>
<script>
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
}
}
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
var id = this.getAttribute("href").substr(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
behavior: "smooth"
});
} else {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
}
if (id === "top") {
history.replaceState(null, null, " ");
} else {
history.pushState(null, null, `#${id}`);
}
});
});
</script>
<script>
var mybutton = document.getElementById("top-link");
window.onscroll = function () {
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
mybutton.style.visibility = "visible";
mybutton.style.opacity = "1";
} else {
mybutton.style.visibility = "hidden";
mybutton.style.opacity = "0";
}
};
</script>
<script>
document.getElementById("theme-toggle").addEventListener("click", () => {
if (document.body.className.includes("dark")) {
document.body.classList.remove('dark');
localStorage.setItem("pref-theme", 'light');
} else {
document.body.classList.add('dark');
localStorage.setItem("pref-theme", 'dark');
}
})
</script>
<script>
document.querySelectorAll('pre > code').forEach((codeblock) => {
const container = codeblock.parentNode.parentNode;
const copybutton = document.createElement('button');
copybutton.classList.add('copy-code');
copybutton.innerHTML = 'copy';
function copyingDone() {
copybutton.innerHTML = 'copied!';
setTimeout(() => {
copybutton.innerHTML = 'copy';
}, 2000);
}
copybutton.addEventListener('click', (cb) => {
if ('clipboard' in navigator) {
navigator.clipboard.writeText(codeblock.textContent);
copyingDone();
return;
}
const range = document.createRange();
range.selectNodeContents(codeblock);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
try {
document.execCommand('copy');
copyingDone();
} catch (e) { };
selection.removeRange(range);
});
if (container.classList.contains("highlight")) {
container.appendChild(copybutton);
} else if (container.parentNode.firstChild == container) {
} else if (codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "TABLE") {
codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(copybutton);
} else {
codeblock.parentNode.appendChild(copybutton);
}
});
</script>
</body>
</html>