Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ NONFREE = yes
CREDIT_ANCHORTEXT = Mediaserver Appliance

include $(FAB_PATH)/common/mk/turnkey/fileserver.mk
include $(FAB_PATH)/common/mk/turnkey/php.mk
include $(FAB_PATH)/common/mk/turnkey.mk
8 changes: 6 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and on top of that:
- SSL support out of the box.

- Media server (`Jellyfin`_) configuration:

- Installed from Jellyfin's signed stable APT repository for Debian 13.

- Web UI listening on ports 8096 (http) and 8920 (https - uses Jellyfin's
own custom SSL/TLS certificate - see more below).
Expand Down Expand Up @@ -65,6 +67,9 @@ and on top of that:
- Pre-configured repositories (storage, user home directories).

- Default storage: */srv/storage*
- Check the signed Jellyfin update channel with
``turnkey-mediaserver-update --check``. Apply an available package update
with ``turnkey-mediaserver-update --apply``.
- Accessing file server via samba on the command line::

smbclient //1.0.0.61/storage -Uroot
Expand All @@ -87,7 +92,7 @@ Credentials *(passwords set at first boot)*
-------------------------------------------

- Jellyfin webUI: username **jellyfin**
- Webmin, Webshell, SSH, Samba: username **root**
- Webmin, SSH, Samba: username **root**
- Web based file manager (WebDAV CGI):

- username **root** (or Samba users)
Expand All @@ -98,4 +103,3 @@ Credentials *(passwords set at first boot)*
.. _Samba: https://www.samba.org/samba/what_is_samba.html
.. _issue #1188: https://github.com/turnkeylinux/tracker/issues/1188
.. _WebDAV CGI: https://github.com/DanRohde/webdavcgi

14 changes: 14 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
turnkey-mediaserver-19.0 (1) turnkey; urgency=low

* Update the appliance base to Debian 13 (Trixie).

* Install Jellyfin from its signed stable Debian 13 package repository.

* Configure the Jellyfin administrator through the supported API instead of
editing its private database schema.

* Add a signed-package update helper and v19 acceptance coverage for login,
media scanning, playback metadata, service supervision, and proxy access.

-- TurnKey Linux release engineering <release@turnkeylinux.org> Tue, 25 Aug 2026 00:00:00 +0000

turnkey-mediaserver-18.1 (1) turnkey; urgency=low

* Update Jellyfin to latest upstream - v10.9.9.
Expand Down
157 changes: 94 additions & 63 deletions conf.d/main
Original file line number Diff line number Diff line change
@@ -1,95 +1,126 @@
#!/bin/sh -ex
#!/bin/bash -e

ADMIN_USER="jellyfin"
ADMIN_PASS="$(mcookie)"
set -o pipefail
set -x

# download jellyfin repo key
APT_GPG_URL=https://repo.jellyfin.org/jellyfin_team.gpg.key
curl -sS $APT_GPG_URL | apt-key --keyring /usr/share/keyrings/jellyfin.gpg add -
ADMIN_USER=jellyfin
JELLYFIN_KEY_FINGERPRINT=4918AABC486CA052358D778D49023CD01DE21A7B
JELLYFIN_BASE=http://127.0.0.1:8096

install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key |
gpg --batch --dearmor --yes --output /etc/apt/keyrings/jellyfin.gpg
chmod 0644 /etc/apt/keyrings/jellyfin.gpg
installed_fingerprint=$(gpg --batch --show-keys --with-colons \
/etc/apt/keyrings/jellyfin.gpg | awk -F: '$1 == "fpr" {print $10; exit}')
[ "$installed_fingerprint" = "$JELLYFIN_KEY_FINGERPRINT" ]

# disable tkldev squid cache on install - works around weird local issue at
# build time
apt-get -o Acquire::https::proxy=false update
apt-get -o Acquire::https::proxy=false -y install jellyfin

# Rename the file server for WebDAVCGI
CONF=/var/www/webdavcgi/webdav.conf
sed -i "s|FILESERVER|MEDIASERVER|" $CONF

# Rename the samba service
CONF=/etc/samba/smb.conf
sed -i "s|FILESERVER|MEDIASERVER|" $CONF
sed -i "s|FileServer|MediaServer|" $CONF
jellyfin_version=$(dpkg-query -W -f='${Version}' jellyfin)
install -d -m 0755 /usr/local/share/turnkey-mediaserver
cat > /usr/local/share/turnkey-mediaserver/source <<EOF
package=jellyfin
installed_version=$jellyfin_version
repository=https://repo.jellyfin.org/debian
suite=trixie
component=main
signing_fingerprint=$JELLYFIN_KEY_FINGERPRINT
channel=official Jellyfin stable APT packages for Debian 13
EOF

sed -i 's|FILESERVER|MEDIASERVER|' /var/www/webdavcgi/webdav.conf
sed -i 's|FILESERVER|MEDIASERVER|; s|FileServer|MediaServer|' /etc/samba/smb.conf

# Change default group to users
usermod -g users jellyfin

# Add jellyfin to video user group (required to support HW encoding)
usermod -aG video jellyfin

# Setup default media directories
for fn in Music Movies TVShows Photos; do
mkdir -p /srv/storage/$fn
chown jellyfin:users /srv/storage/$fn
chmod g+w /srv/storage/$fn
for name in Music Movies TVShows Photos; do
install -d -o jellyfin -g users -m 0775 "/srv/storage/$name"
done

# TurnKey credit on Login page
mv /usr/local/src/jellyfin-branding.xml /etc/jellyfin/branding.xml
install -o root -g jellyfin -m 0644 \
/usr/local/src/jellyfin-branding.xml /etc/jellyfin/branding.xml

# Apache config for WebDavCGI
a2dissite 000-default.conf
a2ensite webdavcgi.conf
a2ensite tkl-webcp.conf

# dirty hack to allow jellyfin to run in a chroot
mkdir -p /sys/class/net/eth0
mkdir -p /sys/class/net/lo
echo 1500 > /sys/class/net/eth0/mtu
echo 65536 > /sys/class/net/lo/mtu
# The build sandbox has no mounted sysfs, but Jellyfin inspects these paths.
install -d -m 0755 /sys/class/net/eth0 /sys/class/net/lo
printf '1500\n' > /sys/class/net/eth0/mtu
printf '65536\n' > /sys/class/net/lo/mtu

. /etc/default/jellyfin

export JELLYFIN_DATA_DIR JELLYFIN_CONFIG_DIR JELLYFIN_LOG_DIR JELLYFIN_CACHE_DIR

JELLYFIN_LOG_FILE=/var/log/jellyfin/jellyfin_log
JELLYFIN_LOG_FILE=/var/log/jellyfin/turnkey-build.log

su -s /bin/bash jellyfin -c "/usr/bin/jellyfin \
${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} \
${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} \
${JELLYFIN_NOWEBAPP_OPT}" > $JELLYFIN_LOG_FILE&

until grep 'Startup complete' $JELLYFIN_LOG_FILE
do
sleep 2
done
pid=$(pgrep jellyfin)

sleep 2

curl 'http://localhost:8096/Startup/Configuration' --data-raw 'UICulture=en-US&MetadataCountryCode=US&PreferredMetadataLanguage=en'
curl 'http://localhost:8096/Startup/User'
curl 'http://localhost:8096/Startup/User' --data-raw "Name=$ADMIN_USER&Password=$ADMIN_PASS"
curl 'http://localhost:8096/Library/VirtualFolders?collectionType=music&refreshLibrary=false&name=Music' --data-raw '{"LibraryOptions":{"EnableArchiveMediaFiles":false,"EnablePhotos":true,"EnableRealtimeMonitor":true,"ExtractChapterImagesDuringLibraryScan":false,"EnableChapterImageExtraction":false,"DownloadImagesInAdvance":false,"EnableInternetProviders":true,"ImportMissingEpisodes":false,"SaveLocalMetadata":false,"EnableAutomaticSeriesGrouping":false,"PreferredMetadataLanguage":"","MetadataCountryCode":"","SeasonZeroDisplayName":"Specials","AutomaticRefreshIntervalDays":0,"EnableEmbeddedTitles":false,"EnableEmbeddedEpisodeInfos":false,"SkipSubtitlesIfEmbeddedSubtitlesPresent":false,"SkipSubtitlesIfAudioTrackMatches":false,"SaveSubtitlesWithMedia":true,"RequirePerfectSubtitleMatch":true,"MetadataSavers":[],"TypeOptions":[{"Type":"MusicArtist","MetadataFetchers":["MusicBrainz"],"MetadataFetcherOrder":["MusicBrainz","TheAudioDB"]},{"Type":"MusicAlbum","MetadataFetchers":["MusicBrainz"],"MetadataFetcherOrder":["MusicBrainz","TheAudioDB"]},{"Type":"MusicVideo","MetadataFetchers":["TheMovieDb"],"MetadataFetcherOrder":["TheMovieDb"],"ImageFetchers":["TheMovieDb","Screen Grabber"],"ImageFetcherOrder":["TheMovieDb","Screen Grabber"]},{"Type":"Audio","ImageFetchers":["Image Extractor"],"ImageFetcherOrder":["Image Extractor"]}],"LocalMetadataReaderOrder":["Nfo"],"SubtitleDownloadLanguages":[],"DisabledSubtitleFetchers":[],"SubtitleFetcherOrder":[],"PathInfos":[{"Path":"/srv/storage/Music"}]}}'
curl 'http://localhost:8096/Library/VirtualFolders?collectionType=movies&refreshLibrary=false&name=Movies' --data-raw '{"LibraryOptions":{"EnableArchiveMediaFiles":false,"EnablePhotos":true,"EnableRealtimeMonitor":true,"ExtractChapterImagesDuringLibraryScan":false,"EnableChapterImageExtraction":false,"DownloadImagesInAdvance":false,"EnableInternetProviders":true,"ImportMissingEpisodes":false,"SaveLocalMetadata":false,"EnableAutomaticSeriesGrouping":false,"PreferredMetadataLanguage":"","MetadataCountryCode":"","SeasonZeroDisplayName":"Specials","AutomaticRefreshIntervalDays":0,"EnableEmbeddedTitles":false,"EnableEmbeddedEpisodeInfos":false,"SkipSubtitlesIfEmbeddedSubtitlesPresent":false,"SkipSubtitlesIfAudioTrackMatches":false,"SaveSubtitlesWithMedia":true,"RequirePerfectSubtitleMatch":true,"MetadataSavers":[],"TypeOptions":[{"Type":"Movie","MetadataFetchers":["TheMovieDb","The Open Movie Database"],"MetadataFetcherOrder":["TheMovieDb","The Open Movie Database"],"ImageFetchers":["TheMovieDb","The Open Movie Database","Screen Grabber"],"ImageFetcherOrder":["TheMovieDb","The Open Movie Database","Screen Grabber"]}],"LocalMetadataReaderOrder":["Nfo"],"SubtitleDownloadLanguages":[],"DisabledSubtitleFetchers":[],"SubtitleFetcherOrder":[],"PathInfos":[{"Path":"/srv/storage/Movies"}]}}'
curl 'http://localhost:8096/Library/VirtualFolders?collectionType=tvshows&refreshLibrary=false&name=TV%20Shows' --data-raw '{"LibraryOptions":{"EnableArchiveMediaFiles":false,"EnablePhotos":true,"EnableRealtimeMonitor":true,"ExtractChapterImagesDuringLibraryScan":false,"EnableChapterImageExtraction":false,"DownloadImagesInAdvance":false,"EnableInternetProviders":true,"ImportMissingEpisodes":false,"SaveLocalMetadata":false,"EnableAutomaticSeriesGrouping":false,"PreferredMetadataLanguage":"","MetadataCountryCode":"","SeasonZeroDisplayName":"Specials","AutomaticRefreshIntervalDays":0,"EnableEmbeddedTitles":false,"EnableEmbeddedEpisodeInfos":false,"SkipSubtitlesIfEmbeddedSubtitlesPresent":false,"SkipSubtitlesIfAudioTrackMatches":false,"SaveSubtitlesWithMedia":true,"RequirePerfectSubtitleMatch":true,"MetadataSavers":[],"TypeOptions":[{"Type":"Series","MetadataFetchers":["TheTVDB","The Open Movie Database"],"MetadataFetcherOrder":["TheTVDB","TheMovieDb","The Open Movie Database"],"ImageFetchers":["TheTVDB"],"ImageFetcherOrder":["TheTVDB","TheMovieDb"]},{"Type":"Season","MetadataFetchers":[],"MetadataFetcherOrder":["TheMovieDb"],"ImageFetchers":["TheTVDB","TheMovieDb"],"ImageFetcherOrder":["TheTVDB","TheMovieDb"]},{"Type":"Episode","MetadataFetchers":["TheTVDB"],"MetadataFetcherOrder":["TheTVDB","TheMovieDb","The Open Movie Database"],"ImageFetchers":["TheTVDB","Screen Grabber"],"ImageFetcherOrder":["TheTVDB","TheMovieDb","The Open Movie Database","Screen Grabber"]}],"LocalMetadataReaderOrder":["Nfo"],"SubtitleDownloadLanguages":[],"DisabledSubtitleFetchers":[],"SubtitleFetcherOrder":[],"PathInfos":[{"Path":"/srv/storage/TVShows"}]}}'
curl 'http://localhost:8096/Library/VirtualFolders?collectionType=homevideos&refreshLibrary=false&name=Photos' --data-raw '{"LibraryOptions":{"EnableArchiveMediaFiles":false,"EnablePhotos":true,"EnableRealtimeMonitor":true,"ExtractChapterImagesDuringLibraryScan":false,"EnableChapterImageExtraction":false,"DownloadImagesInAdvance":false,"EnableInternetProviders":true,"ImportMissingEpisodes":false,"SaveLocalMetadata":false,"EnableAutomaticSeriesGrouping":false,"PreferredMetadataLanguage":"","MetadataCountryCode":"","SeasonZeroDisplayName":"Specials","AutomaticRefreshIntervalDays":0,"EnableEmbeddedTitles":false,"EnableEmbeddedEpisodeInfos":false,"SkipSubtitlesIfEmbeddedSubtitlesPresent":false,"SkipSubtitlesIfAudioTrackMatches":false,"SaveSubtitlesWithMedia":true,"RequirePerfectSubtitleMatch":true,"MetadataSavers":[],"TypeOptions":[{"Type":"Video","ImageFetchers":["Screen Grabber"],"ImageFetcherOrder":["Screen Grabber"]}],"LocalMetadataReaderOrder":["Nfo"],"SubtitleDownloadLanguages":[],"DisabledSubtitleFetchers":[],"SubtitleFetcherOrder":[],"PathInfos":[{"Path":"/srv/storage/Photos"}]}}'
curl 'http://localhost:8096/Startup/Configuration' --data-raw 'UICulture=en-US&MetadataCountryCode=US&PreferredMetadataLanguage=en'
curl 'http://localhost:8096/Startup/RemoteAccess' --data-raw 'EnableRemoteAccess=true&EnableAutomaticPortMapping=false'
curl 'http://localhost:8096/Startup/Complete' --data-raw ''
until grep 'Scan Media Library Completed' $JELLYFIN_LOG_FILE
do
${JELLYFIN_NOWEBAPP_OPT}" > "$JELLYFIN_LOG_FILE" 2>&1 &

ready=false
for attempt in $(seq 1 120); do
if curl -fsS "$JELLYFIN_BASE/Startup/Configuration" >/dev/null 2>&1; then
ready=true
break
fi
sleep 2
done

kill $pid
while kill -0 $pid; do
[ "$ready" = true ]
pid=$(pgrep -o -u jellyfin jellyfin)

set +x
ADMIN_PASS=$(mcookie)
install -m 0600 /dev/null /etc/jellyfin/turnkey-bootstrap-password
printf '%s\n' "$ADMIN_PASS" > /etc/jellyfin/turnkey-bootstrap-password

curl -fsS -X POST "$JELLYFIN_BASE/Startup/Configuration" \
-H 'Content-Type: application/json' \
--data '{"UICulture":"en-US","MetadataCountryCode":"US","PreferredMetadataLanguage":"en"}' \
>/dev/null
curl -fsS "$JELLYFIN_BASE/Startup/User" >/dev/null
curl -fsS -X POST "$JELLYFIN_BASE/Startup/User" \
-H 'Content-Type: application/json' \
--data "$(jq -n --arg name "$ADMIN_USER" --arg password "$ADMIN_PASS" \
'{Name:$name,Password:$password}')" >/dev/null

add_library() {
name=$1
collection_type=$2
path=$3
curl -fsS -X POST \
"$JELLYFIN_BASE/Library/VirtualFolders?collectionType=$collection_type&refreshLibrary=false&name=$name&paths=$path" \
-H 'Content-Type: application/json' \
--data '{"LibraryOptions":{"EnableRealtimeMonitor":true}}' >/dev/null
}
add_library Music music /srv/storage/Music
add_library Movies movies /srv/storage/Movies
add_library 'TV%20Shows' tvshows /srv/storage/TVShows
add_library Photos homevideos /srv/storage/Photos

curl -fsS -X POST "$JELLYFIN_BASE/Startup/RemoteAccess" \
-H 'Content-Type: application/json' \
--data '{"EnableRemoteAccess":true,"EnableAutomaticPortMapping":false}' \
>/dev/null
curl -fsS -X POST "$JELLYFIN_BASE/Startup/Complete" \
-H 'Content-Type: application/json' --data '{}' >/dev/null
unset ADMIN_PASS
set -x

kill -TERM "$pid"
for attempt in $(seq 1 60); do
kill -0 "$pid" 2>/dev/null || break
sleep 1
done
chown -R jellyfin:adm /var/lib/jellyfin
rm -r /sys/*
! kill -0 "$pid" 2>/dev/null

chown -R jellyfin:adm /var/lib/jellyfin /etc/jellyfin
rmdir /sys/class/net/eth0 /sys/class/net/lo /sys/class/net /sys/class /sys \
2>/dev/null || true

# Apache config for reverse proxy
echo 'Listen 12322' >> /etc/apache2/ports.conf
a2ensite jellyfin-proxy.conf
a2enmod proxy_http
Expand Down
31 changes: 31 additions & 0 deletions docs/v19-source-decision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# MediaServer v19 source decision

MediaServer v19 installs Jellyfin from the upstream project's signed stable
APT repository for Debian 13. This keeps the appliance on Jellyfin's supported
Trixie package path and lets normal Debian package tooling handle dependency
resolution and upgrades.

The source contract is:

- repository: `https://repo.jellyfin.org/debian`
- suite: `trixie`
- component: `main`
- signing-key fingerprint: `4918 AABC 486C A052 358D 778D 4902 3CD0 1DE2 1A7B`
- minimum validated package: `jellyfin 10.11.11+deb13`
- corresponding upstream tag: `v10.11.11`
- corresponding upstream commit: `1fbd8739292cce610231be93daf43368733edf63`

The build verifies the dearmored key fingerprint before refreshing package
metadata. It records the installed version, repository, suite, component,
fingerprint, and channel in `/usr/local/share/turnkey-mediaserver/source`.
The updater checks that record, the deb822 source, and the installed key before
querying or applying an update.

The acceptance boundary is the official stable APT channel rather than a
single frozen package. Later signed stable releases remain eligible while the
recorded `10.11.11+deb13` package establishes the v19 migration floor.

Upstream release notes and support are available from the Jellyfin repository.
Security reports belong in Jellyfin's private advisory form at
`https://github.com/jellyfin/jellyfin/security/advisories/new`, not in a public
issue.
16 changes: 16 additions & 0 deletions docs/v19-test-crosswalk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# MediaServer v19 acceptance crosswalk

| Criterion | Acceptance proof |
| --- | --- |
| Official Trixie package source | Check the recorded repository, suite, signing fingerprint, installed dpkg version, and Jellyfin public API version. |
| Administrator login | Authenticate the `jellyfin` administrator through `Users/AuthenticateByName` and retain the returned access token for protected API requests. |
| Media library | Verify all four default library paths, add a one-second synthetic audio file, request a scan, and find the indexed item by its exact path. |
| Playback metadata | Request playback information for the indexed item and require a playable media-source identifier and an audio stream. |
| Web and reverse proxy | Read the public system information through direct HTTP and the appliance TLS reverse proxy, then compare their Jellyfin versions. |
| Service supervision | Require the `jellyfin` systemd unit to be enabled and active. |
| Signed update channel | Run updater check and dry-run apply, require an installed candidate, and require the signed APT transaction to resolve. |
| Machine-readable result | Emit exactly the seven v19 result keys after every preceding assertion succeeds. |

The test generates its audio fixture locally with Jellyfin's packaged ffmpeg
binary, so library and playback metadata proof does not depend on a network
media source.
40 changes: 40 additions & 0 deletions docs/v19-test-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# MediaServer v19 candidate evidence

## Candidate boundary

The candidate uses Jellyfin's signed Debian 13 stable repository and current
HTTP APIs for initial administrator creation, first-boot password replacement,
library configuration, authenticated scan inspection, and playback metadata.
The Apache configuration uses the current `/socket` websocket endpoint and
Apache 2.4 authorization syntax.

The focused test is `tests/v19.sh`. It covers the normal appliance path:
administrator login, the four preconfigured media directories, a tiny media
scan, playback metadata, the systemd service, direct API access, TLS proxy
access, and the signed updater check and dry run.

## Verification state

Exact run `20260826t155731z-1143-18806` passed at source
`d075b5651852cac080a7bd28fe8c4d3e2e189cc4`. It built and imported the Trixie
root, completed boot and firstboot, authenticated with the configured Jellyfin
administrator, verified the four libraries, indexed generated audio, obtained
playback metadata, exercised direct and proxied APIs, checked service state,
and resolved the signed updater check and dry run. The retained report SHA-256
is `5e8c6806648517a83985f365a413636b819a9722894a1601b1b19d5b620562f9`.

Product-fix loops used: 2 of 3. The fixes keep the firstboot password out of
process arguments and wait for the Jellyfin startup API before configuring the
server. Readiness-log suppression and removal of an unused PHP configuration
pass were acceptance-path corrections and consumed zero product loops.

## Deferred coverage

- MEDIUM: Hardware-accelerated transcoding depends on the host GPU, device
passthrough, and matching Debian drivers. The acceptance path checks media
discovery and playback metadata without asserting host-specific acceleration.
- MEDIUM: Direct Jellyfin HTTPS on port 8920 requires the administrator to add
a custom PKCS #12 certificate. The appliance-managed TLS proxy on port 12322
is the v19 acceptance path.
- LOW: The API test proves administrator authentication and the web server path
without browser-driven user-interface automation.
Loading