Add beta media endpoint
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -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^"
|
||||
|
@@ -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";
|
||||
}
|
||||
];
|
||||
|
@@ -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"'"')"
|
||||
|
||||
|
@@ -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);
|
||||
});
|
||||
})();
|
@@ -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";
|
||||
};
|
||||
|
||||
|
@@ -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"
|
||||
];
|
||||
};
|
||||
|
@@ -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;
|
||||
};
|
||||
}
|
||||
|
@@ -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"
|
||||
];
|
||||
};
|
||||
|
@@ -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;
|
||||
};
|
||||
}
|
||||
|
@@ -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"
|
||||
];
|
||||
};
|
||||
|
@@ -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"
|
||||
];
|
||||
};
|
||||
|
@@ -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 ];
|
||||
};
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -38,6 +38,7 @@ pkgs.dockerTools.buildImage {
|
||||
++ (with jellyfinPlugins; [
|
||||
bookshelf
|
||||
intro-skipper
|
||||
javascript-injector
|
||||
opensubtitles
|
||||
playbackreporting
|
||||
reports
|
||||
|
17
packages/jellyfin/plugins/javascript-injector/default.nix
Normal file
17
packages/jellyfin/plugins/javascript-injector/default.nix
Normal 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
|
||||
'';
|
||||
})
|
Reference in New Issue
Block a user