Files
jellyfin-plugin-floatplane/install.sh
claude bf476134c2
Some checks failed
release / build-release (push) Has been cancelled
v0.2: All videos folder, shared watched state, release-order sorting, official icon, OAuth plumbing, docs
- per-creator "All videos" folder (release order across sub-channels); sub-channel copies use
  cpost: ids and UserDataMirror keeps played/resume state in sync
- SortNameUpdater forces release-order sort names so every client lists newest first by default
- official Floatplane icon as channel image (Primary only — a channel Thumb hid video thumbnails
  in Continue Watching) and as 16:9 plugin thumbnail; one-time image replacement on upgrade
- OAuth device-flow login (FloatplaneAuthService + settings UI); needs a Floatplane client id;
  Turnstile verified domain-bound so password login inside Jellyfin is impossible
- generic install.sh (linuxserver / official image / native), thumb.png in package + manifest
- README rewritten for a general audience, doc/usage.md with screenshots, 60 tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 16:36:32 +01:00

153 lines
7.3 KiB
Bash
Executable File

#!/bin/sh
# Install or upgrade the Floatplane plugin from the Gitea release.
# Works for a Jellyfin that runs in Docker (official image or linuxserver.io image) or
# natively (systemd). Nothing is changed before the summary is printed; --dry-run stops there.
#
# curl -fsSL https://gitea.brasse-pc.eu/brasse/jellyfin-plugin-floatplane/releases/download/latest/install.sh | sh
# curl -fsSL .../install.sh | sh -s -- --dry-run
#
# Environment (all optional):
# TAG release to install: latest (default) or vX.Y.Z
# JELLYFIN_CONTAINER docker container name (default: jellyfin; empty = native install)
# JELLYFIN_CONFIG_DIR host path of the container's /config (default: read from `docker inspect`),
# or for native installs the data dir (default /var/lib/jellyfin)
# PLUGINS_DIR override the plugins directory outright (skips the detection below)
# PUID / PGID owner for the plugin files (default: from the container's env, else the dir owner)
#
# Plugin directory detection:
# linuxserver/jellyfin -> <config>/data/plugins (JELLYFIN_DATA_DIR=/config/data)
# jellyfin/jellyfin -> <config>/plugins (JELLYFIN_DATA_DIR=/config)
# native -> /var/lib/jellyfin/plugins
#
# Needs: curl, unzip, md5sum (or md5), and jq if available (sed fallback otherwise).
# Run it as a user that may write to the plugin directory and restart Jellyfin (sudo, typically).
set -eu
REPO_URL="https://gitea.brasse-pc.eu/brasse/jellyfin-plugin-floatplane"
TAG="${TAG:-latest}"
JELLYFIN_CONTAINER="${JELLYFIN_CONTAINER-jellyfin}"
JELLYFIN_CONFIG_DIR="${JELLYFIN_CONFIG_DIR:-}"
PLUGINS_DIR="${PLUGINS_DIR:-}"
# --- locate the plugins directory -------------------------------------------------
if [ -z "$PLUGINS_DIR" ]; then
if [ -n "$JELLYFIN_CONTAINER" ] && command -v docker >/dev/null 2>&1 && docker inspect "$JELLYFIN_CONTAINER" >/dev/null 2>&1; then
if [ -z "$JELLYFIN_CONFIG_DIR" ]; then
JELLYFIN_CONFIG_DIR=$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/config"}}{{.Source}}{{end}}{{end}}' "$JELLYFIN_CONTAINER" 2>/dev/null || true)
fi
[ -n "$JELLYFIN_CONFIG_DIR" ] || { echo "error: cannot find the /config mount of container $JELLYFIN_CONTAINER; set JELLYFIN_CONFIG_DIR or PLUGINS_DIR" >&2; exit 1; }
DATA_DIR=$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$JELLYFIN_CONTAINER" 2>/dev/null | sed -n 's/^JELLYFIN_DATA_DIR=//p' | head -n1)
case "$DATA_DIR" in
/config/*) PLUGINS_DIR="$JELLYFIN_CONFIG_DIR/${DATA_DIR#/config/}/plugins" ;; # linuxserver: /config/data
*) if [ -d "$JELLYFIN_CONFIG_DIR/data/plugins" ]; then PLUGINS_DIR="$JELLYFIN_CONFIG_DIR/data/plugins"; else PLUGINS_DIR="$JELLYFIN_CONFIG_DIR/plugins"; fi ;;
esac
else
[ -n "$JELLYFIN_CONTAINER" ] && echo "note: container '$JELLYFIN_CONTAINER' not found via docker; assuming a native install (set JELLYFIN_CONTAINER= to silence)" >&2
JELLYFIN_CONTAINER=""
PLUGINS_DIR="${JELLYFIN_CONFIG_DIR:-/var/lib/jellyfin}/plugins"
fi
fi
DRY_RUN=0
for a in "$@"; do
case "$a" in
--dry-run|-n) DRY_RUN=1 ;;
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
*) echo "unknown argument: $a" >&2; exit 2 ;;
esac
done
need() { command -v "$1" >/dev/null 2>&1 || { echo "error: '$1' is required" >&2; exit 1; }; }
need curl; need unzip; need md5sum
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
MANIFEST_URL="$REPO_URL/releases/download/$TAG/manifest.json"
echo "fetching $MANIFEST_URL"
curl -fsSL "$MANIFEST_URL" -o "$TMP/manifest.json"
if command -v jq >/dev/null 2>&1; then
VERSION=$(jq -r '.[0].versions[0].version' "$TMP/manifest.json")
SOURCE_URL=$(jq -r '.[0].versions[0].sourceUrl' "$TMP/manifest.json")
CHECKSUM=$(jq -r '.[0].versions[0].checksum' "$TMP/manifest.json")
TARGET_ABI=$(jq -r '.[0].versions[0].targetAbi' "$TMP/manifest.json")
else
# fallback without jq: the manifest has exactly one version entry
VERSION=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$TMP/manifest.json" | head -1)
SOURCE_URL=$(sed -n 's/.*"sourceUrl": *"\([^"]*\)".*/\1/p' "$TMP/manifest.json" | head -1)
CHECKSUM=$(sed -n 's/.*"checksum": *"\([^"]*\)".*/\1/p' "$TMP/manifest.json" | head -1)
TARGET_ABI=$(sed -n 's/.*"targetAbi": *"\([^"]*\)".*/\1/p' "$TMP/manifest.json" | head -1)
fi
[ -n "$VERSION" ] && [ -n "$SOURCE_URL" ] || { echo "error: could not read version/sourceUrl from manifest" >&2; exit 1; }
DEST="$PLUGINS_DIR/Floatplane_$VERSION"
# owner: the container's PUID/PGID if we can read them, else 1000:1000
OWNER_UID="${PUID:-}"; OWNER_GID="${PGID:-}"
if { [ -z "$OWNER_UID" ] || [ -z "$OWNER_GID" ]; } && [ -n "$JELLYFIN_CONTAINER" ] && command -v docker >/dev/null 2>&1; then
ENVS=$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$JELLYFIN_CONTAINER" 2>/dev/null || true)
[ -z "$OWNER_UID" ] && OWNER_UID=$(printf '%s\n' "$ENVS" | sed -n 's/^PUID=//p' | head -1)
[ -z "$OWNER_GID" ] && OWNER_GID=$(printf '%s\n' "$ENVS" | sed -n 's/^PGID=//p' | head -1)
fi
OWNER_UID="${OWNER_UID:-1000}"; OWNER_GID="${OWNER_GID:-1000}"
OLD=$(ls -d "$PLUGINS_DIR"/Floatplane_* 2>/dev/null | grep -v "/Floatplane_$VERSION\$" || true)
echo
echo "Floatplane plugin $VERSION (release $TAG, targetAbi $TARGET_ABI)"
echo " package: $SOURCE_URL"
echo " md5: $CHECKSUM"
echo " install: $DEST/"
[ -n "$OLD" ] && echo " remove: $(echo "$OLD" | tr '\n' ' ')"
echo " owner: $OWNER_UID:$OWNER_GID"
if [ -n "$JELLYFIN_CONTAINER" ]; then echo " restart: docker restart $JELLYFIN_CONTAINER"; elif command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files jellyfin.service >/dev/null 2>&1; then echo " restart: systemctl restart jellyfin"; else echo " restart: (none - restart Jellyfin yourself)"; fi
echo
if [ "$DRY_RUN" = 1 ]; then
echo "dry run - nothing changed."
exit 0
fi
if [ ! -d "$PLUGINS_DIR" ]; then
echo "error: $PLUGINS_DIR does not exist - set PLUGINS_DIR (linuxserver image: <config>/data/plugins, official image: <config>/plugins, native: /var/lib/jellyfin/plugins)" >&2
exit 1
fi
echo "downloading $SOURCE_URL"
curl -fsSL "$SOURCE_URL" -o "$TMP/plugin.zip"
ACTUAL=$(md5sum "$TMP/plugin.zip" | cut -d' ' -f1)
if [ -n "$CHECKSUM" ] && [ "$CHECKSUM" != "null" ] && [ "$ACTUAL" != "$CHECKSUM" ]; then
echo "error: md5 mismatch (expected $CHECKSUM, got $ACTUAL)" >&2
exit 1
fi
echo "checksum ok"
mkdir -p "$TMP/unpack"
unzip -q -o "$TMP/plugin.zip" -d "$TMP/unpack"
[ -f "$TMP/unpack/Jellyfin.Plugin.Floatplane.dll" ] && [ -f "$TMP/unpack/meta.json" ] || { echo "error: zip does not contain the plugin" >&2; exit 1; }
rm -rf "$DEST"
mkdir -p "$DEST"
cp "$TMP/unpack"/* "$DEST/"
chown -R "$OWNER_UID:$OWNER_GID" "$DEST" 2>/dev/null || echo "warning: could not chown $DEST (run with sudo?)"
chmod -R u+rwX,go+rX "$DEST"
echo "installed into $DEST"
if [ -n "$OLD" ]; then
echo "$OLD" | while IFS= read -r d; do
[ -n "$d" ] && rm -rf "$d" && echo "removed old $d"
done
fi
if [ -n "$JELLYFIN_CONTAINER" ]; then
if command -v docker >/dev/null 2>&1; then
echo "restarting container $JELLYFIN_CONTAINER ..."
docker restart "$JELLYFIN_CONTAINER" >/dev/null && echo "restarted"
else
echo "docker not found - restart the Jellyfin container yourself so the plugin loads"
fi
fi
echo
echo "done. Next: Jellyfin Dashboard -> Plugins -> Floatplane -> paste your sails.sid cookie -> Save."