diff --git a/hosts/jupiter/users/storm/configs/console/podman/media/jellyfin/default.nix b/hosts/jupiter/users/storm/configs/console/podman/media/jellyfin/default.nix index 68dab09..c5f1b3c 100644 --- a/hosts/jupiter/users/storm/configs/console/podman/media/jellyfin/default.nix +++ b/hosts/jupiter/users/storm/configs/console/podman/media/jellyfin/default.nix @@ -47,7 +47,7 @@ in client_id = autheliaClientId; client_name = "Jellyfin"; client_secret = hmConfig.sops.placeholder."jellyfin/authelia/digest"; - redirect_uris = [ "https://media.karaolidis.com/sso/OID/redirect/authelia" ]; + redirect_uris = [ "https://beta.media.karaolidis.com/sso/OID/redirect/authelia" ]; authorization_policy = "jellyfin"; require_pkce = true; pkce_challenge_method = "S256"; @@ -97,6 +97,8 @@ in [ "/mnt/storage/private/storm/containers/storage/volumes/media/_data:/var/lib/media" "${setup}:/etc/jellyfin/setup.sh:ro" + # FIXME: https://github.com/9p4/jellyfin-plugin-sso/issues/189#issuecomment-3262794524 + "${./sso-button.js}:/etc/jellyfin/sso-button.js:ro" "${./libraries}:/etc/jellyfin/libraries:ro" "${volumes.jellyfin-config.ref}:/etc/jellyfin" "${volumes.jellyfin-data.ref}:/var/lib/jellyfin/data" @@ -109,7 +111,7 @@ in environmentFiles = [ hmConfig.sops.templates.jellyfin-env.path ]; labels = [ "traefik.enable=true" - "traefik.http.routers.jellyfin.rule=Host(`media.karaolidis.com`)" + "traefik.http.routers.jellyfin.rule=Host(`beta.media.karaolidis.com`)" ]; podmanArgs = [ "--cdi-spec-dir=/run/cdi" ]; devices = [ "nvidia.com/gpu=all" ]; @@ -123,18 +125,18 @@ in mediaConfig = (pkgs.formats.yaml { }).generate "media.yaml" { access_control.rules = [ { - domain = "media.karaolidis.com"; + domain = "beta.media.karaolidis.com"; policy = "one_factor"; resources = [ "^/manage([/?].*)?$" ]; subject = [ "group:media" ]; } { - domain = "media.karaolidis.com"; + domain = "beta.media.karaolidis.com"; policy = "deny"; resources = [ "^/manage([/?].*)?$" ]; } { - domain = "media.karaolidis.com"; + domain = "beta.media.karaolidis.com"; policy = "bypass"; } ]; diff --git a/hosts/jupiter/users/storm/configs/console/podman/media/jellyfin/setup.sh b/hosts/jupiter/users/storm/configs/console/podman/media/jellyfin/setup.sh index e0f6832..ccf73b2 100644 --- a/hosts/jupiter/users/storm/configs/console/podman/media/jellyfin/setup.sh +++ b/hosts/jupiter/users/storm/configs/console/podman/media/jellyfin/setup.sh @@ -173,26 +173,34 @@ a.raised.emby-button, EOF ) -login_disclaimer=$(cat < - - -EOF -) - curl -sf "$JELLYFIN_HOST/System/Configuration/branding" \ -H "Authorization: MediaBrowser Token=$token" | jq --arg custom_css "$custom_css" \ - --arg login_disclaimer "$login_disclaimer" \ - '.CustomCss = $custom_css | .LoginDisclaimer = $login_disclaimer' | + '.CustomCss = $custom_css' | curl -sf "$JELLYFIN_HOST/System/Configuration/branding" \ -X POST \ -H 'Content-Type: application/json' \ -H "Authorization: MediaBrowser Token=$token" \ --data-binary @- +jq -Rn --rawfile script /etc/jellyfin/sso-button.js ' + { + CustomJavaScripts: [ + { + Name: "SSO Button", + Script: $script, + Enabled: true, + RequiresAuthentication: false + } + ] + } +' \ + | curl -sf "$JELLYFIN_HOST/Plugins/f5a34f7b-2e8a-4e6a-a722-3a216a81b374/Configuration" \ + -X POST \ + -H 'Content-Type: application/json' \ + -H 'Authorization: MediaBrowser Token="'"$token"'"' \ + --data-binary @- + existing_libraries="$(curl -sf "$JELLYFIN_HOST/Library/VirtualFolders" \ -H 'Authorization: MediaBrowser Token="'"$token"'"')" diff --git a/hosts/jupiter/users/storm/configs/console/podman/media/jellyfin/sso-button.js b/hosts/jupiter/users/storm/configs/console/podman/media/jellyfin/sso-button.js new file mode 100644 index 0000000..99c9b61 --- /dev/null +++ b/hosts/jupiter/users/storm/configs/console/podman/media/jellyfin/sso-button.js @@ -0,0 +1,180 @@ +const SSO_AUTH_URL = "https://beta.media.karaolidis.com/sso/OID/start/authelia"; + +// SSO provider customization. Available options are: +// generic, authentik, authelia, keycloak, zitadel +const PROVIDER = "authelia"; + +// Self-executing function that waits for the document body to be available +(function waitForBody() { + // If document.body doesn't exist yet, retry in 100ms + if (!document.body) { + return setTimeout(waitForBody, 100); + } + + /** + * Determines if the current page is a login page by checking multiple indicators + * @returns {boolean} True if this appears to be a login page + */ + function isLoginPage() { + const hash = location.hash.toLowerCase(); + const pathname = location.pathname.toLowerCase(); + // Check for URL patterns that typically indicate login pages + const hasLoginUrl = + hash === "" || + hash === "#/" || + hash === "#/home" || + hash === "#/login" || + hash.startsWith("#/login") || + pathname.includes("/login"); + + // Check for DOM elements that indicate a login form is present + const hasLoginElements = + document.querySelector('input[type="password"]') !== null || + document.querySelector(".loginPage") !== null || + document.querySelector("#txtUserName") !== null; + + return hasLoginUrl || hasLoginElements; + } + + /** + * Checks if the current page should be excluded from SSO button insertion + * These are typically pages where users are already authenticated + * @returns {boolean} True if this page should be excluded + */ + function shouldExcludePage() { + const hash = location.hash.toLowerCase(); + // List of page patterns where we don't want to show the SSO button + const excludePatterns = [ + "#/dashboard", + "#/home.html", + "#/movies", + "#/tv", + "#/music", + "#/livetv", + "#/search", + "#/settings", + "#/wizardstart", + "#/wizardfinish", + "#/mypreferencesmenu", + "#/userprofile", + ]; + + return excludePatterns.some((pattern) => hash.startsWith(pattern)); + } + + /** + * Initializes the OAuth device ID in localStorage if it doesn't exist + * This is required for Jellyfin native apps to maintain device identification + */ + function oAuthInitDeviceId() { + // Only set device ID if it's not already set and we're in a native shell environment + if ( + !localStorage.getItem("_deviceId2") && + window.NativeShell?.AppHost?.deviceId + ) { + localStorage.setItem("_deviceId2", window.NativeShell.AppHost.deviceId()); + } + } + + /** + * Creates and inserts the SSO login button into the login page + * Only runs if we're on a valid login page and the button doesn't already exist + */ + function insertSSOButton() { + // Safety check: ensure we're on the right page before proceeding + if (!isLoginPage() || shouldExcludePage()) return; + + // Try to find a suitable container for the SSO button + const loginContainer = + document.querySelector(".readOnlyContent") || + document.querySelector("form")?.parentNode || + document.querySelector(".loginPage") || + document.querySelector("#loginPage"); + + // Exit if no container found or button already exists + if (!loginContainer || document.querySelector("#custom-sso-button")) return; + + switch (PROVIDER.toLowerCase()) { + case "authentik": + SSO_BUTTON_HTML = + 'Login with Authentik'; + break; + + case "authelia": + SSO_BUTTON_HTML = + 'Login with Authelia'; + break; + + case "keycloak": + SSO_BUTTON_HTML = + 'Login with Keycloak'; + break; + + case "zitadel": + SSO_BUTTON_HTML = + 'Login with Zitadel'; + break; + + default: + SSO_BUTTON_HTML = + 'shieldLogin with SSO'; + } + + // Skip insertion for Jellyfin Media Player (JMP) as it may have different auth handling + const isJMP = navigator.userAgent.includes("JellyfinMediaPlayer"); + if (isJMP) return; + + // Create the SSO button element + const button = document.createElement("button"); + button.id = "custom-sso-button"; + button.className = "raised block emby-button button-submit"; + // Style the button to match Jellyfin's design while being visually distinct + button.style = + "display: flex; align-items: center; justify-content: center; gap: 10px; padding: 12px 20px; font-size: 16px; background-color: #3949ab; color: white; margin-top: 16px;"; + // Add icon and text content + button.innerHTML = SSO_BUTTON_HTML; + // Handle button click - prevent form submission and redirect to SSO + button.onclick = function (e) { + e.preventDefault(); + oAuthInitDeviceId(); // Ensure device ID is set before SSO redirect + window.location.href = SSO_AUTH_URL; + }; + + // Add the button to the login container + loginContainer.appendChild(button); + } + + // Initial setup: Check if we should insert the SSO button when script first loads + if (isLoginPage() && !shouldExcludePage()) { + // Delay insertion slightly to ensure all page elements are fully loaded + setTimeout(insertSSOButton, 500); + } + + // Set up a MutationObserver to watch for dynamic page changes + // This handles cases where Jellyfin loads content dynamically via JavaScript + const observer = new MutationObserver(() => { + if (isLoginPage() && !shouldExcludePage()) { + // Check if login elements are ready and button hasn't been inserted yet + const ready = + document.querySelector(".readOnlyContent") || + document.querySelector("form") || + document.querySelector(".loginPage"); + if (ready && !document.querySelector("#custom-sso-button")) { + insertSSOButton(); + } + } + }); + + // Start observing changes to the entire document body and its children + observer.observe(document.body, { childList: true, subtree: true }); + + // Listen for hash changes (when navigating between pages in Jellyfin's SPA) + window.addEventListener("hashchange", () => { + // Small delay to allow page transition to complete + setTimeout(() => { + if (isLoginPage() && !shouldExcludePage()) { + insertSSOButton(); + } + }, 300); + }); +})(); diff --git a/hosts/jupiter/users/storm/configs/console/podman/media/jellyseerr/default.nix b/hosts/jupiter/users/storm/configs/console/podman/media/jellyseerr/default.nix index ae6fd6c..96dcded 100644 --- a/hosts/jupiter/users/storm/configs/console/podman/media/jellyseerr/default.nix +++ b/hosts/jupiter/users/storm/configs/console/podman/media/jellyseerr/default.nix @@ -54,7 +54,7 @@ in name = "jupiter"; ip = "jellyfin"; port = 8096; - externalHostname = "https://media.karaolidis.com"; + externalHostname = "https://beta.media.karaolidis.com"; jellyfinForgotPasswordUrl = "https://id.karaolidis.com/reset-password/step1"; }; diff --git a/hosts/jupiter/users/storm/configs/console/podman/media/prowlarr/default.nix b/hosts/jupiter/users/storm/configs/console/podman/media/prowlarr/default.nix index 92661c9..50dc38a 100644 --- a/hosts/jupiter/users/storm/configs/console/podman/media/prowlarr/default.nix +++ b/hosts/jupiter/users/storm/configs/console/podman/media/prowlarr/default.nix @@ -78,7 +78,7 @@ in environmentFiles = [ hmConfig.sops.templates.prowlarr-env.path ]; labels = [ "traefik.enable=true" - "traefik.http.routers.prowlarr.rule=Host(`media.karaolidis.com`) && PathPrefix(`/manage/indexers`)" + "traefik.http.routers.prowlarr.rule=Host(`beta.media.karaolidis.com`) && PathPrefix(`/manage/indexers`)" "traefik.http.routers.prowlarr.middlewares=authelia@docker" ]; }; diff --git a/hosts/jupiter/users/storm/configs/console/podman/media/radarr/apps/common.nix b/hosts/jupiter/users/storm/configs/console/podman/media/radarr/apps/common.nix index e4f86ba..14532b7 100644 --- a/hosts/jupiter/users/storm/configs/console/podman/media/radarr/apps/common.nix +++ b/hosts/jupiter/users/storm/configs/console/podman/media/radarr/apps/common.nix @@ -83,7 +83,7 @@ rec { activeDirectory = "/var/lib/media/libraries${mediaFolderBase}"; minimumAvailability = "released"; isDefault = !isAnime; - externalUrl = "https://media.karaolidis.com${urlBase}"; + externalUrl = "https://beta.media.karaolidis.com${urlBase}"; syncEnabled = true; }; } diff --git a/hosts/jupiter/users/storm/configs/console/podman/media/radarr/default.nix b/hosts/jupiter/users/storm/configs/console/podman/media/radarr/default.nix index 44ffebd..e9cdc35 100644 --- a/hosts/jupiter/users/storm/configs/console/podman/media/radarr/default.nix +++ b/hosts/jupiter/users/storm/configs/console/podman/media/radarr/default.nix @@ -82,7 +82,7 @@ in environmentFiles = [ hmConfig.sops.templates."${radarr.hostName}-env".path ]; labels = [ "traefik.enable=true" - "traefik.http.routers.${radarr.hostName}.rule=Host(`media.karaolidis.com`) && PathPrefix(`${radarr.urlBase}`)" + "traefik.http.routers.${radarr.hostName}.rule=Host(`beta.media.karaolidis.com`) && PathPrefix(`${radarr.urlBase}`)" "traefik.http.routers.${radarr.hostName}.middlewares=authelia@docker" ]; }; diff --git a/hosts/jupiter/users/storm/configs/console/podman/media/sonarr/apps/common.nix b/hosts/jupiter/users/storm/configs/console/podman/media/sonarr/apps/common.nix index 4160ac8..cb80008 100644 --- a/hosts/jupiter/users/storm/configs/console/podman/media/sonarr/apps/common.nix +++ b/hosts/jupiter/users/storm/configs/console/podman/media/sonarr/apps/common.nix @@ -85,7 +85,7 @@ rec { activeAnimeDirectory = "/var/lib/media/libraries${mediaFolderBase}"; isDefault = !isAnime; enableSeasonFolders = true; - externalUrl = "https://media.karaolidis.com${urlBase}"; + externalUrl = "https://beta.media.karaolidis.com${urlBase}"; syncEnabled = true; }; } diff --git a/hosts/jupiter/users/storm/configs/console/podman/media/sonarr/default.nix b/hosts/jupiter/users/storm/configs/console/podman/media/sonarr/default.nix index f1686dc..857e067 100644 --- a/hosts/jupiter/users/storm/configs/console/podman/media/sonarr/default.nix +++ b/hosts/jupiter/users/storm/configs/console/podman/media/sonarr/default.nix @@ -82,7 +82,7 @@ in environmentFiles = [ hmConfig.sops.templates."${sonarr.hostName}-env".path ]; labels = [ "traefik.enable=true" - "traefik.http.routers.${sonarr.hostName}.rule=Host(`media.karaolidis.com`) && PathPrefix(`${sonarr.urlBase}`)" + "traefik.http.routers.${sonarr.hostName}.rule=Host(`beta.media.karaolidis.com`) && PathPrefix(`${sonarr.urlBase}`)" "traefik.http.routers.${sonarr.hostName}.middlewares=authelia@docker" ]; }; diff --git a/hosts/jupiter/users/storm/configs/console/podman/media/transmission/default.nix b/hosts/jupiter/users/storm/configs/console/podman/media/transmission/default.nix index 3f24f39..1c01bae 100644 --- a/hosts/jupiter/users/storm/configs/console/podman/media/transmission/default.nix +++ b/hosts/jupiter/users/storm/configs/console/podman/media/transmission/default.nix @@ -55,7 +55,7 @@ in }; labels = [ "traefik.enable=true" - "traefik.http.routers.transmission.rule=Host(`media.karaolidis.com`) && PathPrefix(`/manage/torrents`)" + "traefik.http.routers.transmission.rule=Host(`beta.media.karaolidis.com`) && PathPrefix(`/manage/torrents`)" "traefik.http.routers.transmission.middlewares=authelia@docker" ]; }; diff --git a/hosts/jupiter/users/storm/configs/console/podman/traefik/default.nix b/hosts/jupiter/users/storm/configs/console/podman/traefik/default.nix index 87f9914..26f17c7 100644 --- a/hosts/jupiter/users/storm/configs/console/podman/traefik/default.nix +++ b/hosts/jupiter/users/storm/configs/console/podman/traefik/default.nix @@ -70,7 +70,7 @@ in "--entrypoints.https.http.tls=true" "--entrypoints.https.http.tls.certResolver=letsencrypt" "--entrypoints.https.http.tls.domains[0].main=karaolidis.com" - "--entrypoints.https.http.tls.domains[0].sans=*.karaolidis.com,*.tunnel.karaolidis.com,*.gaming.karaolidis.com" + "--entrypoints.https.http.tls.domains[0].sans=*.karaolidis.com,*.tunnel.karaolidis.com,*.gaming.karaolidis.com,beta.media.karaolidis.com" "--entrypoints.https.http.tls.domains[1].main=krlds.com" "--entrypoints.https.http.tls.domains[1].sans=*.krlds.com" "--entryPoints.https.http3" @@ -110,7 +110,7 @@ in "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true" "traefik.http.middlewares.security-headers.headers.stsPreload=true" "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true" - "traefik.http.middlewares.security-headers.headers.frameDeny=true" + "traefik.http.middlewares.security-headers.headers.customFrameOptionsValue=SAMEORIGIN" ]; environmentFiles = [ hmConfig.sops.templates.traefik-env.path ]; }; diff --git a/overlays/default.nix b/overlays/default.nix index bb4a981..2c89852 100644 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -57,6 +57,7 @@ final: prev: jellyfinPlugins = prev.jellyfinPlugins or { } // { bookshelf = final.jellyfin-plugin-bookshelf-bin; intro-skipper = final.jellyfin-plugin-intro-skipper-bin; + javascript-injector = final.jellyfin-plugin-javascript-injector-bin; opensubtitles = final.jellyfin-plugin-opensubtitles-bin; playbackreporting = final.jellyfin-plugin-playbackreporting-bin; reports = final.jellyfin-plugin-reports-bin; diff --git a/packages/default.nix b/packages/default.nix index 6207872..ab07245 100644 --- a/packages/default.nix +++ b/packages/default.nix @@ -54,6 +54,9 @@ jellyfin-plugin-bookshelf-bin = import ./jellyfin/plugins/bookshelf { inherit pkgs; }; jellyfin-plugin-intro-skipper-bin = import ./jellyfin/plugins/intro-skipper { inherit pkgs; }; + jellyfin-plugin-javascript-injector-bin = import ./jellyfin/plugins/javascript-injector { + inherit pkgs; + }; jellyfin-plugin-opensubtitles-bin = import ./jellyfin/plugins/opensubtitles { inherit pkgs; }; jellyfin-plugin-playbackreporting-bin = import ./jellyfin/plugins/playbackreporting { inherit pkgs; diff --git a/packages/docker/jellyfin/default.nix b/packages/docker/jellyfin/default.nix index 64ee2e0..27dac58 100644 --- a/packages/docker/jellyfin/default.nix +++ b/packages/docker/jellyfin/default.nix @@ -38,6 +38,7 @@ pkgs.dockerTools.buildImage { ++ (with jellyfinPlugins; [ bookshelf intro-skipper + javascript-injector opensubtitles playbackreporting reports diff --git a/packages/jellyfin/plugins/javascript-injector/default.nix b/packages/jellyfin/plugins/javascript-injector/default.nix new file mode 100644 index 0000000..9def6a1 --- /dev/null +++ b/packages/jellyfin/plugins/javascript-injector/default.nix @@ -0,0 +1,17 @@ +{ pkgs, ... }: +# AUTO-UPDATE: nix-update --flake jellyfin-plugin-javascript-injector-bin +pkgs.stdenv.mkDerivation (finalAttrs: { + pname = "javascript-injector"; + version = "2.0.0.0"; + + src = pkgs.fetchzip { + url = "https://github.com/n00bcodr/Jellyfin-JavaScript-Injector/releases/download/${finalAttrs.version}/javascript-injector-${finalAttrs.version}.zip"; + sha256 = "sha256-BzT4Hk4ulHsnkV9eKyy2oK6su98Am0x6rydfjAY/AWY="; + stripRoot = false; + }; + + installPhase = '' + mkdir -p $out/var/lib/jellyfin/plugins + cp -r $src $out/var/lib/jellyfin/plugins/javascript-injector + ''; +})