feat(jellyfin): add media-server app using new path-type setting
First app to use the core-26.10-alpha `path` setting type. User picks their existing media folder (MEDIA_PATH=/mnt/media) in the install form; core installer validates the path server-side; compose mounts it read-only at /media. Docker-managed volumes hold /config and /cache; admin account is created from the first browser visit to :8096. No HW transcoding yet — that's a later schema extension. Also bumps the vendored manifest schema in scripts/vendor/ to match core 26.10-alpha — catches up both the missing `open_url` field (gap since 26.6-alpha) and the new `path` setting type. No changes needed to validate-catalog.py itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a2e47c2c0c
commit
fcf2f22a47
6 changed files with 95 additions and 1 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -6,6 +6,25 @@ Versioning: CalVer (`YY.N`) — same scheme as the core Furtka repo.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Jellyfin** (v1.0.0, image `jellyfin/jellyfin:latest`). Media
|
||||
server for movies, shows, music on the LAN. First app to use the new
|
||||
`path`-type setting (core ≥ 26.10-alpha): the user picks their
|
||||
existing media folder in the install form (`MEDIA_PATH=/mnt/media`),
|
||||
the installer validates the path server-side, and docker-compose
|
||||
mounts it read-only at `/media`. Docker-managed volumes handle
|
||||
`/config` and `/cache`. Admin account bootstraps from the first
|
||||
browser visit to port 8096. Ports: 8096. No HW transcoding yet —
|
||||
that's a later schema extension.
|
||||
|
||||
### Changed
|
||||
|
||||
- Vendored manifest schema (`scripts/vendor/furtka_manifest.py`)
|
||||
caught up to core 26.10-alpha: adds the `open_url` field (missing
|
||||
since 26.6-alpha cut) and the new `path` setting type. `validate-
|
||||
catalog.py` now accepts path-type settings without changes.
|
||||
|
||||
## [26.9-alpha] - 2026-04-21
|
||||
|
||||
### Added
|
||||
|
|
|
|||
1
apps/jellyfin/.env.example
Normal file
1
apps/jellyfin/.env.example
Normal file
|
|
@ -0,0 +1 @@
|
|||
MEDIA_PATH=
|
||||
38
apps/jellyfin/docker-compose.yaml
Normal file
38
apps/jellyfin/docker-compose.yaml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Furtka Jellyfin — media server.
|
||||
#
|
||||
# Two Docker-managed volumes (config, cache) for app state + one
|
||||
# user-supplied host path (MEDIA_PATH) mounted read-only for the media
|
||||
# library. Admin account bootstraps from the first browser visit to
|
||||
# :8096 — that's why this app has no manifest.settings for admin creds.
|
||||
#
|
||||
# MEDIA_PATH is a `path`-type setting (furtka/manifest.py ≥ 26.10-alpha
|
||||
# schema). The install form asks for it, the installer validates that
|
||||
# the directory exists and isn't a system path, and docker-compose
|
||||
# substitutes the value below at `docker compose up` time.
|
||||
#
|
||||
# TODO(image-pin): `:latest` is shaky for production — pin to a digest
|
||||
# (`jellyfin/jellyfin@sha256:...`) or a stable tag once we've verified
|
||||
# one against the upstream registry. MVP drift risk accepted.
|
||||
#
|
||||
# No HW transcoding yet — /dev/dri passthrough is a separate, later
|
||||
# schema extension. 1080p software transcode + Direct Play over LAN
|
||||
# are fine for the Medion-Haswell target.
|
||||
|
||||
services:
|
||||
jellyfin:
|
||||
image: jellyfin/jellyfin:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8096:8096"
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
volumes:
|
||||
- furtka_jellyfin_config:/config
|
||||
- furtka_jellyfin_cache:/cache
|
||||
- ${MEDIA_PATH}:/media:ro
|
||||
|
||||
volumes:
|
||||
furtka_jellyfin_config:
|
||||
external: true
|
||||
furtka_jellyfin_cache:
|
||||
external: true
|
||||
5
apps/jellyfin/icon.svg
Normal file
5
apps/jellyfin/icon.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="32" cy="32" r="26" opacity="0.28"/>
|
||||
<path d="M12 18 H52 V46 H12 Z M12 26 H52 M18 22 L18 22 M24 22 L24 22 M30 22 L30 22"/>
|
||||
<path d="M28 32 L28 40 L38 36 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
20
apps/jellyfin/manifest.json
Normal file
20
apps/jellyfin/manifest.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "jellyfin",
|
||||
"display_name": "Jellyfin",
|
||||
"version": "1.0.0",
|
||||
"description": "Media server for movies, shows, and music on the LAN.",
|
||||
"description_long": "Ein eigener Streaming-Server für deine Filme, Serien und Musik. Apps für Smart-TVs, Handy und Web-Browser verfügbar. Admin-Konto wird beim ersten Besuch im Browser angelegt. Medien werden nur gelesen, nicht verändert.",
|
||||
"volumes": ["config", "cache"],
|
||||
"ports": [8096],
|
||||
"icon": "icon.svg",
|
||||
"open_url": "http://{host}:8096/",
|
||||
"settings": [
|
||||
{
|
||||
"name": "MEDIA_PATH",
|
||||
"label": "Medienordner",
|
||||
"description": "Absoluter Pfad zu deinem Filme-/Serien-Ordner auf dieser Maschine, z.B. /mnt/media. Wird nur gelesen.",
|
||||
"type": "path",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
13
scripts/vendor/furtka_manifest.py
vendored
13
scripts/vendor/furtka_manifest.py
vendored
|
|
@ -13,7 +13,7 @@ REQUIRED_FIELDS = (
|
|||
"icon",
|
||||
)
|
||||
|
||||
VALID_SETTING_TYPES = frozenset({"text", "password", "number"})
|
||||
VALID_SETTING_TYPES = frozenset({"text", "password", "number", "path"})
|
||||
SETTING_NAME_RE = re.compile(r"^[A-Z_][A-Z0-9_]*$")
|
||||
|
||||
|
||||
|
|
@ -42,6 +42,12 @@ class Manifest:
|
|||
icon: str
|
||||
description_long: str = ""
|
||||
settings: tuple[Setting, ...] = field(default_factory=tuple)
|
||||
# Optional "Open" link for the landing page + installed-app row.
|
||||
# `{host}` is substituted with the current browser hostname at render
|
||||
# time so the URL follows whatever the user typed to reach Furtka —
|
||||
# furtka.local, a raw IP, a future reverse-proxy hostname. Apps with
|
||||
# no frontend (CLI-only, background workers) leave this empty.
|
||||
open_url: str = ""
|
||||
|
||||
def volume_name(self, short: str) -> str:
|
||||
# Namespace volume names so two apps can each declare e.g. "data"
|
||||
|
|
@ -127,6 +133,10 @@ def load_manifest(path: Path, expected_name: str | None = None) -> Manifest:
|
|||
|
||||
settings = _parse_settings(raw.get("settings"), path)
|
||||
|
||||
open_url_raw = raw.get("open_url", "")
|
||||
if not isinstance(open_url_raw, str):
|
||||
raise ManifestError(f"{path}: open_url must be a string if set")
|
||||
|
||||
return Manifest(
|
||||
name=name,
|
||||
display_name=str(raw["display_name"]),
|
||||
|
|
@ -137,4 +147,5 @@ def load_manifest(path: Path, expected_name: str | None = None) -> Manifest:
|
|||
icon=str(raw["icon"]),
|
||||
description_long=str(raw.get("description_long", "")),
|
||||
settings=settings,
|
||||
open_url=open_url_raw,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue