Add beta media endpoint

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-09-19 19:57:47 +01:00
parent 183b5e334f
commit fd4f01c6a8
16 changed files with 239 additions and 25 deletions

View File

@@ -121,6 +121,8 @@ in
"||*^"
# Personal
"@@||karaolidis.com^$important"
"@@||media.karaolidis.com^$important"
"@@||beta.media.karaolidis.com^$important"
# Connectivity Check
"@@||clients3.google.com^"
"@@||clients.l.google.com^"

View File

@@ -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";
}
];

View File

@@ -173,26 +173,34 @@ a.raised.emby-button,
EOF
)
login_disclaimer=$(cat <<EOF
<form action="https://media.karaolidis.com/sso/OID/start/authelia">
<button class="raised block emby-button button-submit">
Sign in with Authelia
</button>
</form>
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"'"')"

View File

@@ -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 =
'<img src="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authentik.svg" width="21em"><span>Login with Authentik</span>';
break;
case "authelia":
SSO_BUTTON_HTML =
'<img src="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authelia-light.svg" width="21em"><span>Login with Authelia</span>';
break;
case "keycloak":
SSO_BUTTON_HTML =
'<img src="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/keycloak.svg" width="21em"><span>Login with Keycloak</span>';
break;
case "zitadel":
SSO_BUTTON_HTML =
'<img src="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/zitadel.svg" width="21em"><span>Login with Zitadel</span>';
break;
default:
SSO_BUTTON_HTML =
'<span class="material-icons">shield</span><span>Login with SSO</span>';
}
// 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);
});
})();

View File

@@ -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";
};

View File

@@ -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"
];
};

View File

@@ -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;
};
}

View File

@@ -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"
];
};

View File

@@ -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;
};
}

View File

@@ -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"
];
};

View File

@@ -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"
];
};

View File

@@ -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 ];
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -38,6 +38,7 @@ pkgs.dockerTools.buildImage {
++ (with jellyfinPlugins; [
bookshelf
intro-skipper
javascript-injector
opensubtitles
playbackreporting
reports

View File

@@ -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
'';
})