Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-09-22 23:53:30 +01:00
parent 248432b132
commit e41e8c2078
38 changed files with 453 additions and 1718 deletions

View File

@@ -132,7 +132,6 @@ in
"media"
"vaultwarden"
"nextcloud"
"jellyfin"
"gitea"
"outline"
"shlink"

View File

@@ -1,5 +1,10 @@
{ user, home }:
{ config, inputs, ... }:
{
config,
inputs,
pkgs,
...
}:
let
hmConfig = config.home-manager.users.${user};
@@ -17,7 +22,7 @@ let
in
{
imports = [
(import ./jellyfin { inherit user home; })
(import ./plex { inherit user home; })
(import ./jellyseerr {
inherit
user
@@ -58,6 +63,32 @@ in
sops.secrets."ntfy/tokens/jupiter/media".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
virtualisation.quadlet.networks.media = { };
virtualisation.quadlet = {
networks.media = { };
containers.authelia.containerConfig.volumes =
let
mediaConfig = (pkgs.formats.yaml { }).generate "media.yaml" {
access_control.rules = [
{
domain = "beta.media.karaolidis.com";
policy = "one_factor";
resources = [ "^/manage([/?].*)?$" ];
subject = [ "group:media" ];
}
{
domain = "beta.media.karaolidis.com";
policy = "deny";
resources = [ "^/manage([/?].*)?$" ];
}
{
domain = "beta.media.karaolidis.com";
policy = "bypass";
}
];
};
in
[ "${mediaConfig}:/etc/authelia/conf.d/media.yaml:ro" ];
};
};
}

View File

@@ -1,152 +0,0 @@
{ user, home }:
{
config,
inputs,
pkgs,
...
}:
let
hmConfig = config.home-manager.users.${user};
inherit (hmConfig.virtualisation.quadlet) volumes networks;
autheliaClientId = "59TRpNutxEeRRCAZbDsK7rsnrA5NC69HAdAO45CEfc740xl4hgIacDy2u03oiFc89Exb67udBQvmfwxgeAQtJPiNAJxA5OzGmdQf";
in
{
home-manager.users.${user} = {
sops = {
secrets = {
"jellyfin/admin".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
"jellyfin/authelia/password".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
"jellyfin/authelia/digest".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
"opensubtitles/username".sopsFile = "${inputs.secrets}/domains/personal/secrets.yaml";
"opensubtitles/password".sopsFile = "${inputs.secrets}/domains/personal/secrets.yaml";
};
templates = {
jellyfin-env.content = ''
JELLYFIN_ADMIN_PASSWORD=${hmConfig.sops.placeholder."jellyfin/admin"}
JELLYFIN_OIDC_SECRET=${hmConfig.sops.placeholder."jellyfin/authelia/password"}
OPENSUBTITLES_USERNAME=${hmConfig.sops.placeholder."opensubtitles/username"}
OPENSUBTITLES_PASSWORD=${hmConfig.sops.placeholder."opensubtitles/password"}
'';
authelia-jellyfin.content = builtins.readFile (
(pkgs.formats.yaml { }).generate "jellyfin.yaml" {
identity_providers.oidc = {
authorization_policies.jellyfin = {
default_policy = "deny";
rules = [
{
policy = "one_factor";
subject = "group:jellyfin";
}
];
};
clients = [
{
client_id = autheliaClientId;
client_name = "Jellyfin";
client_secret = hmConfig.sops.placeholder."jellyfin/authelia/digest";
redirect_uris = [ "https://beta.media.karaolidis.com/sso/OID/redirect/authelia" ];
authorization_policy = "jellyfin";
require_pkce = true;
pkce_challenge_method = "S256";
scopes = [
"openid"
"profile"
"groups"
];
token_endpoint_auth_method = "client_secret_post";
pre_configured_consent_duration = "1 month";
}
];
};
}
);
};
};
virtualisation.quadlet = {
networks.jellyfin = { };
volumes = {
jellyfin-config = { };
jellyfin-data = { };
jellyfin-metadata = { };
jellyfin-root = { };
jellyfin-log = { };
jellyfin-cache = { };
};
containers = {
jellyfin = {
containerConfig = {
image = "docker-archive:${pkgs.dockerImages.jellyfin}";
networks = [
networks.jellyfin.ref
networks.traefik.ref
];
volumes =
let
setup = pkgs.writeTextFile {
name = "setup.sh";
executable = true;
text = builtins.readFile ./setup.sh;
};
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"
"${volumes.jellyfin-metadata.ref}:/var/lib/jellyfin/metadata"
"${volumes.jellyfin-root.ref}:/var/lib/jellyfin/root"
"${volumes.jellyfin-log.ref}:/var/log/jellyfin"
"${volumes.jellyfin-cache.ref}:/tmp/jellyfin"
];
environments.JELLYFIN_OIDC_CLIENT_ID = autheliaClientId;
environmentFiles = [ hmConfig.sops.templates.jellyfin-env.path ];
labels = [
"traefik.enable=true"
"traefik.http.routers.jellyfin.rule=Host(`beta.media.karaolidis.com`)"
];
podmanArgs = [ "--cdi-spec-dir=/run/cdi" ];
devices = [ "nvidia.com/gpu=all" ];
};
unitConfig.After = [ "sops-nix.service" ];
};
authelia.containerConfig.volumes =
let
mediaConfig = (pkgs.formats.yaml { }).generate "media.yaml" {
access_control.rules = [
{
domain = "beta.media.karaolidis.com";
policy = "one_factor";
resources = [ "^/manage([/?].*)?$" ];
subject = [ "group:media" ];
}
{
domain = "beta.media.karaolidis.com";
policy = "deny";
resources = [ "^/manage([/?].*)?$" ];
}
{
domain = "beta.media.karaolidis.com";
policy = "bypass";
}
];
};
in
[
"${mediaConfig}:/etc/authelia/conf.d/media.yaml:ro"
"${hmConfig.sops.templates.authelia-jellyfin.path}:/etc/authelia/conf.d/jellyfin.yaml:ro"
];
};
};
};
}

View File

@@ -1,128 +0,0 @@
{
"LibraryOptions": {
"Enabled": true,
"EnableArchiveMediaFiles": false,
"EnablePhotos": true,
"EnableRealtimeMonitor": true,
"EnableLUFSScan": true,
"ExtractTrickplayImagesDuringLibraryScan": false,
"SaveTrickplayWithMedia": true,
"EnableTrickplayImageExtraction": true,
"ExtractChapterImagesDuringLibraryScan": false,
"EnableChapterImageExtraction": true,
"EnableInternetProviders": true,
"SaveLocalMetadata": true,
"EnableAutomaticSeriesGrouping": false,
"PreferredMetadataLanguage": "en",
"MetadataCountryCode": "JP",
"SeasonZeroDisplayName": "Specials",
"AutomaticRefreshIntervalDays": 30,
"EnableEmbeddedTitles": false,
"EnableEmbeddedExtrasTitles": false,
"EnableEmbeddedEpisodeInfos": false,
"AllowEmbeddedSubtitles": "AllowAll",
"SkipSubtitlesIfEmbeddedSubtitlesPresent": false,
"SkipSubtitlesIfAudioTrackMatches": false,
"SaveSubtitlesWithMedia": true,
"SaveLyricsWithMedia": false,
"RequirePerfectSubtitleMatch": true,
"AutomaticallyAddToCollection": true,
"PreferNonstandardArtistsTag": false,
"UseCustomTagDelimiters": false,
"MetadataSavers": ["Nfo"],
"TypeOptions": [
{
"Type": "Movie",
"MetadataFetchers": [
"TheMovieDb",
"The Open Movie Database",
"TheTVDB"
],
"MetadataFetcherOrder": [
"TheMovieDb",
"The Open Movie Database",
"TheTVDB"
],
"ImageFetchers": [
"TheMovieDb",
"The Open Movie Database",
"TheTVDB",
"Embedded Image Extractor",
"Screen Grabber"
],
"ImageFetcherOrder": [
"TheMovieDb",
"The Open Movie Database",
"TheTVDB",
"Embedded Image Extractor",
"Screen Grabber"
],
"ImageOptions": [
{
"Type": "Primary",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Art",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "BoxRear",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Banner",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Box",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Disc",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Logo",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Menu",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Thumb",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Backdrop",
"Limit": "1",
"MinWidth": "1280"
}
]
}
],
"LocalMetadataReaderOrder": ["Nfo"],
"SubtitleDownloadLanguages": [],
"CustomTagDelimiters": ["/", "|", ";", "\\"],
"DelimiterWhitelist": [],
"DisabledSubtitleFetchers": [],
"SubtitleFetcherOrder": [],
"DisabledLyricFetchers": [],
"LyricFetcherOrder": [],
"PathInfos": [
{
"Path": "/var/lib/media/libraries/anime/films"
}
]
}
}

View File

@@ -1,128 +0,0 @@
{
"LibraryOptions": {
"Enabled": true,
"EnableArchiveMediaFiles": false,
"EnablePhotos": true,
"EnableRealtimeMonitor": true,
"EnableLUFSScan": true,
"ExtractTrickplayImagesDuringLibraryScan": false,
"SaveTrickplayWithMedia": true,
"EnableTrickplayImageExtraction": true,
"ExtractChapterImagesDuringLibraryScan": false,
"EnableChapterImageExtraction": true,
"EnableInternetProviders": true,
"SaveLocalMetadata": true,
"EnableAutomaticSeriesGrouping": false,
"PreferredMetadataLanguage": "en",
"MetadataCountryCode": "US",
"SeasonZeroDisplayName": "Specials",
"AutomaticRefreshIntervalDays": 30,
"EnableEmbeddedTitles": false,
"EnableEmbeddedExtrasTitles": false,
"EnableEmbeddedEpisodeInfos": false,
"AllowEmbeddedSubtitles": "AllowAll",
"SkipSubtitlesIfEmbeddedSubtitlesPresent": false,
"SkipSubtitlesIfAudioTrackMatches": false,
"SaveSubtitlesWithMedia": true,
"SaveLyricsWithMedia": false,
"RequirePerfectSubtitleMatch": true,
"AutomaticallyAddToCollection": true,
"PreferNonstandardArtistsTag": false,
"UseCustomTagDelimiters": false,
"MetadataSavers": ["Nfo"],
"TypeOptions": [
{
"Type": "Movie",
"MetadataFetchers": [
"TheMovieDb",
"The Open Movie Database",
"TheTVDB"
],
"MetadataFetcherOrder": [
"TheMovieDb",
"The Open Movie Database",
"TheTVDB"
],
"ImageFetchers": [
"TheMovieDb",
"The Open Movie Database",
"TheTVDB",
"Embedded Image Extractor",
"Screen Grabber"
],
"ImageFetcherOrder": [
"TheMovieDb",
"The Open Movie Database",
"TheTVDB",
"Embedded Image Extractor",
"Screen Grabber"
],
"ImageOptions": [
{
"Type": "Primary",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Art",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "BoxRear",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Banner",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Box",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Disc",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Logo",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Menu",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Thumb",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Backdrop",
"Limit": "1",
"MinWidth": "1280"
}
]
}
],
"LocalMetadataReaderOrder": ["Nfo"],
"SubtitleDownloadLanguages": [],
"CustomTagDelimiters": ["/", "|", ";", "\\"],
"DelimiterWhitelist": [],
"DisabledSubtitleFetchers": [],
"SubtitleFetcherOrder": [],
"DisabledLyricFetchers": [],
"LyricFetcherOrder": [],
"PathInfos": [
{
"Path": "/var/lib/media/libraries/films"
}
]
}
}

View File

@@ -1,204 +0,0 @@
{
"LibraryOptions": {
"Enabled": true,
"EnableArchiveMediaFiles": false,
"EnablePhotos": true,
"EnableRealtimeMonitor": true,
"EnableLUFSScan": true,
"ExtractTrickplayImagesDuringLibraryScan": false,
"SaveTrickplayWithMedia": true,
"EnableTrickplayImageExtraction": true,
"ExtractChapterImagesDuringLibraryScan": false,
"EnableChapterImageExtraction": true,
"EnableInternetProviders": true,
"SaveLocalMetadata": true,
"EnableAutomaticSeriesGrouping": true,
"PreferredMetadataLanguage": "en",
"MetadataCountryCode": "JP",
"SeasonZeroDisplayName": "Specials",
"AutomaticRefreshIntervalDays": 30,
"EnableEmbeddedTitles": false,
"EnableEmbeddedExtrasTitles": false,
"EnableEmbeddedEpisodeInfos": false,
"AllowEmbeddedSubtitles": "AllowAll",
"SkipSubtitlesIfEmbeddedSubtitlesPresent": false,
"SkipSubtitlesIfAudioTrackMatches": false,
"SaveSubtitlesWithMedia": true,
"SaveLyricsWithMedia": false,
"RequirePerfectSubtitleMatch": true,
"AutomaticallyAddToCollection": false,
"PreferNonstandardArtistsTag": false,
"UseCustomTagDelimiters": false,
"MetadataSavers": ["Nfo"],
"TypeOptions": [
{
"Type": "Series",
"MetadataFetchers": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database",
"Missing Episode Fetcher"
],
"MetadataFetcherOrder": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database",
"Missing Episode Fetcher"
],
"ImageFetchers": ["TheTVDB", "TheMovieDb"],
"ImageFetcherOrder": ["TheTVDB", "TheMovieDb"],
"ImageOptions": [
{
"Type": "Primary",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Art",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "BoxRear",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Banner",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Box",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Disc",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Logo",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Menu",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Thumb",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Backdrop",
"Limit": "1",
"MinWidth": "1280"
}
]
},
{
"Type": "Season",
"MetadataFetchers": ["TheTVDB", "TheMovieDb"],
"MetadataFetcherOrder": ["TheTVDB", "TheMovieDb"],
"ImageFetchers": ["TheTVDB", "TheMovieDb"],
"ImageFetcherOrder": ["TheTVDB", "TheMovieDb"],
"ImageOptions": [
{
"Type": "Primary",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Art",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "BoxRear",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Banner",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Box",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Disc",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Logo",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Menu",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Thumb",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Backdrop",
"Limit": "0",
"MinWidth": "1280"
}
]
},
{
"Type": "Episode",
"MetadataFetchers": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database"
],
"MetadataFetcherOrder": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database"
],
"ImageFetchers": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database",
"Embedded Image Extractor",
"Screen Grabber"
],
"ImageFetcherOrder": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database",
"Embedded Image Extractor",
"Screen Grabber"
]
}
],
"LocalMetadataReaderOrder": ["Nfo"],
"SubtitleDownloadLanguages": [],
"CustomTagDelimiters": ["/", "|", ";", "\\"],
"DelimiterWhitelist": [],
"DisabledSubtitleFetchers": [],
"SubtitleFetcherOrder": [],
"DisabledLyricFetchers": [],
"LyricFetcherOrder": [],
"PathInfos": [
{
"Path": "/var/lib/media/libraries/anime/shows"
}
]
}
}

View File

@@ -1,204 +0,0 @@
{
"LibraryOptions": {
"Enabled": true,
"EnableArchiveMediaFiles": false,
"EnablePhotos": true,
"EnableRealtimeMonitor": true,
"EnableLUFSScan": true,
"ExtractTrickplayImagesDuringLibraryScan": false,
"SaveTrickplayWithMedia": true,
"EnableTrickplayImageExtraction": true,
"ExtractChapterImagesDuringLibraryScan": false,
"EnableChapterImageExtraction": true,
"EnableInternetProviders": true,
"SaveLocalMetadata": true,
"EnableAutomaticSeriesGrouping": true,
"PreferredMetadataLanguage": "en",
"MetadataCountryCode": "US",
"SeasonZeroDisplayName": "Specials",
"AutomaticRefreshIntervalDays": 30,
"EnableEmbeddedTitles": false,
"EnableEmbeddedExtrasTitles": false,
"EnableEmbeddedEpisodeInfos": false,
"AllowEmbeddedSubtitles": "AllowAll",
"SkipSubtitlesIfEmbeddedSubtitlesPresent": false,
"SkipSubtitlesIfAudioTrackMatches": false,
"SaveSubtitlesWithMedia": true,
"SaveLyricsWithMedia": false,
"RequirePerfectSubtitleMatch": true,
"AutomaticallyAddToCollection": false,
"PreferNonstandardArtistsTag": false,
"UseCustomTagDelimiters": false,
"MetadataSavers": ["Nfo"],
"TypeOptions": [
{
"Type": "Series",
"MetadataFetchers": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database",
"Missing Episode Fetcher"
],
"MetadataFetcherOrder": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database",
"Missing Episode Fetcher"
],
"ImageFetchers": ["TheTVDB", "TheMovieDb"],
"ImageFetcherOrder": ["TheTVDB", "TheMovieDb"],
"ImageOptions": [
{
"Type": "Primary",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Art",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "BoxRear",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Banner",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Box",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Disc",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Logo",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Menu",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Thumb",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Backdrop",
"Limit": "1",
"MinWidth": "1280"
}
]
},
{
"Type": "Season",
"MetadataFetchers": ["TheTVDB", "TheMovieDb"],
"MetadataFetcherOrder": ["TheTVDB", "TheMovieDb"],
"ImageFetchers": ["TheTVDB", "TheMovieDb"],
"ImageFetcherOrder": ["TheTVDB", "TheMovieDb"],
"ImageOptions": [
{
"Type": "Primary",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Art",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "BoxRear",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Banner",
"Limit": 1,
"MinWidth": 0
},
{
"Type": "Box",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Disc",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Logo",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Menu",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Thumb",
"Limit": 0,
"MinWidth": 0
},
{
"Type": "Backdrop",
"Limit": "0",
"MinWidth": "1280"
}
]
},
{
"Type": "Episode",
"MetadataFetchers": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database"
],
"MetadataFetcherOrder": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database"
],
"ImageFetchers": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database",
"Embedded Image Extractor",
"Screen Grabber"
],
"ImageFetcherOrder": [
"TheTVDB",
"TheMovieDb",
"The Open Movie Database",
"Embedded Image Extractor",
"Screen Grabber"
]
}
],
"LocalMetadataReaderOrder": ["Nfo"],
"SubtitleDownloadLanguages": [],
"CustomTagDelimiters": ["/", "|", ";", "\\"],
"DelimiterWhitelist": [],
"DisabledSubtitleFetchers": [],
"SubtitleFetcherOrder": [],
"DisabledLyricFetchers": [],
"LyricFetcherOrder": [],
"PathInfos": [
{
"Path": "/var/lib/media/libraries/shows"
}
]
}
}

View File

@@ -1,222 +0,0 @@
# shellcheck shell=sh
JELLYFIN_HOST="${JELLYFIN_HOST:-http://localhost:8096}"
JELLYFIN_ADMIN_USERNAME="${JELLYFIN_ADMIN_USERNAME:-admin}"
until response="$(curl -sf "$JELLYFIN_HOST/System/Info/Public")"; do
echo "Waiting for Jellyfin to be ready..."
sleep 1
done
setup="$(echo "$response" | jq -r '.StartupWizardCompleted')"
if [ "$setup" = "false" ]; then
curl -sf "$JELLYFIN_HOST/Startup/Configuration" \
-X POST \
-H 'Content-Type: application/json' \
--data-raw '{"UICulture":"en-US","MetadataCountryCode":"US","PreferredMetadataLanguage":"en"}'
curl -sf "$JELLYFIN_HOST/Startup/User"
curl -sf "$JELLYFIN_HOST/Startup/User" \
-X POST \
-H 'Content-Type: application/json' \
--data-raw '{"Name":"'"$JELLYFIN_ADMIN_USERNAME"'","Password":"'"$JELLYFIN_ADMIN_PASSWORD"'"}'
curl -sf "$JELLYFIN_HOST/Startup/RemoteAccess" \
-X POST \
-H 'Content-Type: application/json' \
--data-raw '{"EnableRemoteAccess":true,"EnableAutomaticPortMapping":false}'
curl -sf "$JELLYFIN_HOST/Startup/Complete" \
-X POST
fi
token="$(curl -sf "$JELLYFIN_HOST/Users/AuthenticateByName" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: MediaBrowser Client="jellyfin-init", Device="sh", DeviceId="sh", Version="1.0"' \
--data-raw '{"Username":"'"$JELLYFIN_ADMIN_USERNAME"'","Pw":"'"$JELLYFIN_ADMIN_PASSWORD"'"}' \
| jq -r '.AccessToken')"
curl -sf "$JELLYFIN_HOST/System/Configuration" \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
| jq '.EnableMetrics = true
| .ServerName = "jupiter"
| .RemoteClientBitrateLimit = 1024000000
| .TrickplayOptions.EnableHwAcceleration = true
| .TrickplayOptions.EnableHwEncoding = true' \
| curl -sf "$JELLYFIN_HOST/System/Configuration" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
--data-binary @-
curl -sf "$JELLYFIN_HOST/System/Configuration/encoding" \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
| jq '.EnableThrottling = true
| .HardwareAccelerationType = "nvenc"
| .EnableTonemapping = true
| .EnableDecodingColorDepth12HevcRext = true
| .AllowHevcEncoding = true
| .HardwareDecodingCodecs = ["h264", "hevc", "mpeg2video", "mpeg4", "vc1", "vp8", "vp9", "av1"]' \
| curl -sf "$JELLYFIN_HOST/System/Configuration/encoding" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
--data-binary @-
curl -sf "$JELLYFIN_HOST/Plugins/c83d86bb-a1e0-4c35-a113-e2101cf4ee6b/Configuration" \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
| jq '.AnalyzeSeasonZero = true
| .AnalyzeMovies = true' \
| curl -sf "$JELLYFIN_HOST/Plugins/c83d86bb-a1e0-4c35-a113-e2101cf4ee6b/Configuration" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
--data-binary @-
curl -sf "$JELLYFIN_HOST/Plugins/4b9ed42f-5185-48b5-9803-6ff2989014c4/Configuration" \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
| jq --arg username "$OPENSUBTITLES_USERNAME" \
--arg password "$OPENSUBTITLES_PASSWORD" \
'.Username = $username
| .Password = $password' \
| curl -sf "$JELLYFIN_HOST/Plugins/4b9ed42f-5185-48b5-9803-6ff2989014c4/Configuration" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
--data-binary @-
curl -sf "$JELLYFIN_HOST/Plugins/b8715ed1-6c47-4528-9ad3-f72deb539cd4/Configuration" \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
| jq '.IncludeAdult = true' \
| curl -sf "$JELLYFIN_HOST/Plugins/b8715ed1-6c47-4528-9ad3-f72deb539cd4/Configuration" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
--data-binary @-
curl -sf "$JELLYFIN_HOST/Plugins/505ce9d1-d916-42fa-86ca-673ef241d7df/Configuration" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
--data-binary @- <<EOF
{
"SamlConfigs": {},
"OidConfigs": {
"authelia": {
"OidProviderName": "authelia",
"OidEndpoint": "https://id.karaolidis.com",
"OidClientId": "$JELLYFIN_OIDC_CLIENT_ID",
"OidSecret": "$JELLYFIN_OIDC_SECRET",
"RoleClaim": "groups",
"DefaultUsernameClaim": "preferred_username",
"Enabled": true,
"EnableAuthorization": true,
"EnableAllFolders": true,
"EnableFolderRoles": false,
"EnableLiveTvRoles": false,
"EnableLiveTv": false,
"EnableLiveTvManagement": false,
"DisableHttps": false,
"DoNotValidateEndpoints": false,
"DoNotValidateIssuerName": false,
"Roles": [
"jellyfin"
],
"AdminRoles": [
"admin"
],
"LiveTvRoles": [],
"LiveTvManagementRoles": [],
"OidScopes": [
"groups"
],
"EnabledFolders": [],
"FolderRoleMapping": [],
"SchemeOverride": "https"
}
}
}
EOF
# https://github.com/9p4/jellyfin-plugin-sso/issues/16#issuecomment-2953811762
custom_css=$(cat <<EOF
a.raised.emby-button,
.loginDisclaimerContainer,
.loginDisclaimer,
.manualLoginForm {
all: unset;
}
.btnQuick,
.btnSelectServer,
.btnForgotPassword,
a.raised.emby-button,
.emby-button.block,
.loginDisclaimerContainer,
.loginDisclaimer {
margin-left: auto;
margin-right: auto;
margin-bottom: 1em;
color: inherit !important;
}
.btnForgotPassword {
display: none !important;
}
.manualLoginForm > :not(:first-child) {
display: none !important;
}
EOF
)
curl -sf "$JELLYFIN_HOST/System/Configuration/branding" \
-H "Authorization: MediaBrowser Token=$token" |
jq --arg custom_css "$custom_css" \
'.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"'"')"
find /etc/jellyfin/libraries -name "*.json" | sort -V | while IFS= read -r filepath; do
collectionType=$(jq -rn --arg s "$(basename "$(dirname "$filepath")")" '$s|@uri')
name=$(jq -rn --arg s "$(basename "$filepath" .json)" '$s|@uri')
if echo "$existing_libraries" | jq -e --arg name "$name" 'any(.[]; .Name | @uri == $name)'; then
echo "Skipping existing virtual folder: $name"
continue
fi
echo "Creating virtual folder: $name"
curl -sf "$JELLYFIN_HOST/Library/VirtualFolders?collectionType=$collectionType&name=$name" \
-X POST \
-H "Content-Type: application/json" \
-H 'Authorization: MediaBrowser Token="'"$token"'"' \
--data-binary @"$filepath"
done

View File

@@ -1,180 +0,0 @@
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

@@ -15,22 +15,13 @@ let
hmConfig = config.home-manager.users.${user};
inherit (hmConfig.virtualisation.quadlet) containers volumes networks;
arrs = radarrs ++ sonarrs;
autheliaClientId = "s8QyVqBdiEStH5WXeEYNSrEh8ls2xHif0qyTGbC7V8nHNcqHi5NhqHUapCHuVFT4kEtngqgLry2SKOKepQl3AiqCWlhTjlIxr7LI";
in
{
home-manager.users.${user} = {
sops = {
secrets = {
"jellyseerr/smtp".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
"jellyseerr/authelia/password".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
"jellyseerr/authelia/digest".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
};
secrets."jellyseerr/smtp".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
templates = {
jellyseerr-env.content = ''
JELLYFIN_ADMIN_PASSWORD=${hmConfig.sops.placeholder."jellyfin/admin"}
'';
jellyseerr.content = builtins.readFile (
(pkgs.formats.json { }).generate "setings.json" {
main = {
@@ -42,38 +33,51 @@ in
# 32 | 4194304 | 67108864
defaultPermissions = 71303200;
localLogin = false;
mediaServerLogin = false;
oidcLogin = true;
newPlexLogin = false;
mediaServerType = 2;
mediaServerLogin = true;
oidcLogin = false;
newPlexLogin = true;
mediaServerType = 1;
partialRequestsEnabled = true;
enableSpecialEpisodes = true;
};
jellyfin = {
plex = {
name = "jupiter";
ip = "jellyfin";
port = 8096;
ip = "beta.media.karaolidis.com";
port = 443;
useSsl = true;
libraries = [
{
id = "1";
name = "Films";
enabled = true;
type = "movie";
}
{
id = "2";
name = "Shows";
enabled = true;
type = "show";
}
{
id = "3";
name = "Films (Anime)";
enabled = true;
type = "movie";
}
{
id = "4";
name = "Shows (Anime)";
enabled = true;
type = "show";
}
];
externalHostname = "https://beta.media.karaolidis.com";
jellyfinForgotPasswordUrl = "https://id.karaolidis.com/reset-password/step1";
webAppUrl = "https://beta.media.karaolidis.com";
machineId = hmConfig.sops.placeholder."plex/processedMachineIdentifier";
};
oidc.providers = [
{
slug = "authelia";
name = "Authelia";
issuerUrl = "https://id.karaolidis.com";
clientId = autheliaClientId;
clientSecret = hmConfig.sops.placeholder."jellyseerr/authelia/password";
scopes = lib.strings.concatStringsSep " " [
"openid"
"profile"
"email"
"groups"
];
newUserLogin = true;
}
];
jellyfin = { };
radarr = [ ];
@@ -111,34 +115,6 @@ in
network.trustProxy = true;
}
);
authelia-jellyseerr.content = builtins.readFile (
(pkgs.formats.yaml { }).generate "jellyseerr.yaml" {
identity_providers.oidc = {
authorization_policies.jellyseerr = {
default_policy = "deny";
rules = [
{
policy = "one_factor";
subject = "group:jellyfin";
}
];
};
clients = [
{
client_id = autheliaClientId;
client_name = "jellyseerr";
client_secret = hmConfig.sops.placeholder."jellyseerr/authelia/digest";
redirect_uris = [ "https://request.karaolidis.com/login?provider=authelia&callback=true" ];
authorization_policy = "jellyseerr";
token_endpoint_auth_method = "client_secret_post";
pre_configured_consent_duration = "1 month";
}
];
};
}
);
}
// builtins.listToAttrs (
builtins.map (arr: {
@@ -153,57 +129,49 @@ in
virtualisation.quadlet = {
volumes.jellyseerr = { };
containers = {
jellyseerr = {
containerConfig = {
image = "docker-archive:${pkgs.dockerImages.jellyseerr}";
networks = [
networks.jellyfin.ref
networks.media.ref
networks.traefik.ref
];
volumes =
let
preStart = pkgs.writeTextFile {
name = "pre-start.sh";
executable = true;
text = builtins.readFile ./pre-start.sh;
};
in
[
"${preStart}:/etc/jellyseerr/pre-start.sh:ro"
"${hmConfig.sops.templates.jellyseerr.path}:/etc/jellyseerr/settings.default.json:ro"
"${volumes.jellyseerr.ref}:/var/lib/jellyseerr"
]
++ builtins.map (
radarr:
"${
hmConfig.sops.templates."jellyseerr-${radarr.hostName}".path
}:/etc/jellyseerr/apps/radarr/${radarr.hostName}.json:ro"
) radarrs
++ builtins.map (
sonarr:
"${
hmConfig.sops.templates."jellyseerr-${sonarr.hostName}".path
}:/etc/jellyseerr/apps/sonarr/${sonarr.hostName}.json:ro"
) sonarrs;
environmentFiles = [ hmConfig.sops.templates.jellyseerr-env.path ];
labels = [
"traefik.enable=true"
"traefik.http.routers.jellyseerr.rule=Host(`request.karaolidis.com`)"
];
};
unitConfig.After =
containers.jellyseerr = {
containerConfig = {
image = "docker-archive:${pkgs.dockerImages.jellyseerr}";
networks = [
networks.media.ref
networks.traefik.ref
];
volumes =
let
arrServices = builtins.map (arr: "${containers.${arr.hostName}._serviceName}.service") arrs;
preStart = pkgs.writeTextFile {
name = "pre-start.sh";
executable = true;
text = builtins.readFile ./pre-start.sh;
};
in
[ "sops-nix.service" ] ++ arrServices;
[
"${preStart}:/etc/jellyseerr/pre-start.sh:ro"
"${hmConfig.sops.templates.jellyseerr.path}:/etc/jellyseerr/settings.default.json:ro"
"${volumes.jellyseerr.ref}:/var/lib/jellyseerr"
]
++ builtins.map (
radarr:
"${
hmConfig.sops.templates."jellyseerr-${radarr.hostName}".path
}:/etc/jellyseerr/apps/radarr/${radarr.hostName}.json:ro"
) radarrs
++ builtins.map (
sonarr:
"${
hmConfig.sops.templates."jellyseerr-${sonarr.hostName}".path
}:/etc/jellyseerr/apps/sonarr/${sonarr.hostName}.json:ro"
) sonarrs;
labels = [
"traefik.enable=true"
"traefik.http.routers.jellyseerr.rule=Host(`request.karaolidis.com`)"
];
};
authelia.containerConfig.volumes = [
"${hmConfig.sops.templates.authelia-jellyseerr.path}:/etc/authelia/conf.d/jellyseerr.yaml:ro"
];
unitConfig.After =
let
arrServices = builtins.map (arr: "${containers.${arr.hostName}._serviceName}.service") arrs;
in
[ "sops-nix.service" ] ++ arrServices;
};
};
};

View File

@@ -1,59 +1,5 @@
# shellcheck shell=sh
JELLYFIN_HOST="${JELLYFIN_HOST:-http://jellyfin:8096}"
JELLYFIN_ADMIN_USERNAME="${JELLYFIN_ADMIN_USERNAME:-admin}"
until public="$(curl -sf "$JELLYFIN_HOST/System/Info/Public")"; do
echo "Waiting for Jellyfin to be ready..."
sleep 1
done
until [ "$(echo "$public" | jq -r '.StartupWizardCompleted')" = "true" ]; do
echo "Waiting for Jellyfin setup wizard to finish..."
sleep 1
public="$(curl -sf "${JELLYFIN_HOST}/System/Info/Public")"
done
token="$(curl -sf "$JELLYFIN_HOST/Users/AuthenticateByName" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: MediaBrowser Client="jellyseerr-init", Device="sh", DeviceId="sh", Version="1.0"' \
--data-raw '{"Username":"'"$JELLYFIN_ADMIN_USERNAME"'","Pw":"'"$JELLYFIN_ADMIN_PASSWORD"'"}' \
| jq -r '.AccessToken')"
keys="$(curl -sf "$JELLYFIN_HOST/Auth/Keys" \
-H 'Authorization: MediaBrowser Token="'"$token"'"')"
jellyseerr_key="$(echo "$keys" | jq -r '.Items[] | select(.AppName=="Jellyseerr") | .AccessToken')"
if [ -z "$jellyseerr_key" ] || [ "$jellyseerr_key" = "null" ]; then
curl -sf "$JELLYFIN_HOST/Auth/Keys?App=Jellyseerr" \
-X POST \
-H 'Authorization: MediaBrowser Token="'"$token"'"'
keys="$(curl -sf "$JELLYFIN_HOST/Auth/Keys" \
-H 'Authorization: MediaBrowser Token="'"$token"'"')"
jellyseerr_key="$(echo "$keys" | jq -r '.Items[] | select(.AppName=="Jellyseerr") | .AccessToken')"
fi
serverId="$(echo "$public" | jq -r '.Id')"
libraries="$(curl -sf "$JELLYFIN_HOST/Library/VirtualFolders" \
-H 'Authorization: MediaBrowser Token="'"$token"'"')"
libraries="$(
echo "$libraries" | jq '[
.[]
| {
id: .ItemId,
name: .Name,
enabled: .LibraryOptions.Enabled,
type: ( .CollectionType | if . == "movies" then "movie" elif . == "tvshows" then "show" else . end )
}
]'
)"
try_forever() {
until "$@" 2>&1; do
echo "Try failed: $* - retrying in 1s"
@@ -129,15 +75,9 @@ done
tmpfile=$(mktemp)
jq -s \
--argjson libs "$libraries" \
--argjson radarr "$radarr_json" \
--argjson sonarr "$sonarr_json" \
--arg serverId "$serverId" \
--arg apiKey "$jellyseerr_key" \
'.[0] * .[1]
| .jellyfin.serverId = $serverId
| .jellyfin.apiKey = $apiKey
| .jellyfin.libraries = $libs
| .radarr = $radarr
| .sonarr = $sonarr' \
/var/lib/jellyseerr/settings.json \

View File

@@ -0,0 +1,109 @@
{ user, home }:
{
config,
inputs,
pkgs,
...
}:
let
hmConfig = config.home-manager.users.${user};
inherit (hmConfig.virtualisation.quadlet) volumes networks;
in
{
networking.firewall.allowedTCPPorts = [ 32400 ];
home-manager.users.${user} = {
sops = {
secrets = {
"plex/machineIdentifier".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
"plex/processedMachineIdentifier".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
"plex/anonymousMachineIdentifier".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
"plex/certificateUuid".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
"plex/plexOnlineToken".sopsFile = "${inputs.secrets}/hosts/jupiter/secrets.yaml";
};
templates.plex.content = ''
<?xml version="1.0" encoding="utf-8"?>
<Preferences
MachineIdentifier="${hmConfig.sops.placeholder."plex/machineIdentifier"}"
ProcessedMachineIdentifier="${hmConfig.sops.placeholder."plex/processedMachineIdentifier"}"
AnonymousMachineIdentifier="${hmConfig.sops.placeholder."plex/anonymousMachineIdentifier"}"
CertificateUUID="${hmConfig.sops.placeholder."plex/certificateUuid"}"
PlexOnlineToken="${hmConfig.sops.placeholder."plex/plexOnlineToken"}"
FriendlyName="jupiter"
PlexOnlineUsername="karaolidis"
PlexOnlineMail="nick@karaolidis.com"
PlexOnlineHome="1"
PublishServerOnPlexOnlineKey="1"
AcceptedEULA="1"
DlnaEnabled="0"
customConnections="https://beta.media.karaolidis.com:443"
secureConnections="1"
IPNetworkType="v4only"
PushNotificationsEnabled="1"
logDebug="0"
sendCrashReports="0"
WanTotalMaxUploadRate="1000000"
GdmEnabled="0"
RelayEnabled="0"
FSEventLibraryPartialScanEnabled="1"
FSEventLibraryUpdatesEnabled="1"
GenerateAdMarkerBehavior="asap"
GenerateBIFBehavior="asap"
GenerateChapterThumbBehavior="asap"
GenerateVADBehavior="asap"
LoudnessAnalysisBehavior="asap"
MusicAnalysisBehavior="asap"
ScheduledLibraryUpdatesEnabled="1"
watchMusicSections="1"
HardwareDevicePath="10de:24dd:17aa:3a54@0000:01:00.0"
OptimizerTranscodeCountLimit="0"
ButlerTaskRefreshLibraries="1"
CinemaTrailersFromBluRay="1"
CinemaTrailersFromTheater="1"
CinemaTrailersType="0"
/>
'';
};
virtualisation.quadlet = {
networks.plex = { };
volumes.plex = { };
containers.plex = {
containerConfig = {
image = "docker-archive:${pkgs.dockerImages.plex}";
networks = [
networks.plex.ref
networks.traefik.ref
];
volumes =
let
postStart = pkgs.writeTextFile {
name = "post-start.sh";
executable = true;
text = builtins.readFile ./post-start.sh;
};
in
[
"${hmConfig.sops.templates.plex.path}:/etc/plex/Preferences.xml:ro"
"${postStart}:/etc/plex/post-start.sh:ro"
"/mnt/storage/private/storm/containers/storage/volumes/media/_data:/var/lib/media"
"${volumes.plex.ref}:/var/lib/plex"
];
labels = [
"traefik.enable=true"
"traefik.http.routers.plex.rule=Host(`beta.media.karaolidis.com`)"
];
podmanArgs = [ "--cdi-spec-dir=/run/cdi" ];
devices = [ "nvidia.com/gpu=all" ];
addCapabilities = [ "SYS_ADMIN" ];
publishPorts = [ "32400:32400/tcp" ];
};
unitConfig.After = [ "sops-nix.service" ];
};
};
};
}

View File

@@ -0,0 +1,112 @@
# shellcheck shell=sh
HOST="http://localhost:32400"
TOKEN="$(getPref "PlexOnlineToken")"
try_forever() {
until "$@" 2>&1; do
echo "Try failed: $* - retrying in 1s"
sleep 1
done
}
wait_for_api() {
try_forever curl -sf -H "X-Plex-Token: $TOKEN" "$HOST/identity"
echo "API is up!"
}
call() {
method="$1"
path="$2"
params="${3:-}"
curl -sf \
-X "$method" \
-H "Accept: application/json" \
-H "X-Plex-Token: $TOKEN" \
"$HOST/$path?$params"
}
get_resources() {
endpoint="$1"
call GET "$endpoint" | jq -r '.MediaContainer.Directory // []'
}
get_resource_id() {
endpoint="$1"
ident_field="$2"
name="$3"
get_resources "$endpoint" | jq -r --arg field "$ident_field" --arg name "$name" '.[] | select(.[$field] == $name) | .key // empty'
}
upsert_resource() {
endpoint="$1"
ident_field="$2"
name="$3"
params="$4"
id="$(get_resource_id "$endpoint" "$ident_field" "$name")"
if [ -n "$id" ] && [ "$id" != "null" ]; then
echo "Updating library '$name' (id=$id)"
call PUT "$endpoint/$id" "$params"
else
echo "Creating library '$name'"
call POST "$endpoint" "$params"
fi
}
prune_resources() {
endpoint="$1"
ident_field="$2"
shift 2
keep_list="$(printf '%s\n' "$@" | jq -R . | jq -s .)"
resources="$(get_resources "$endpoint")"
printf '%s' "$resources" | jq -c '.[]' | while IFS= read -r library; do
name="$(printf '%s' "$library" | jq -r --arg field "$ident_field" '.[$field]')"
id="$(printf '%s' "$library" | jq -r '.key')"
found="$(printf '%s' "$keep_list" | jq -r --arg name "$name" 'index($name)')"
if [ "$found" = "null" ]; then
echo "Deleting extra library '$name' (id=$id)"
call DELETE "$endpoint/$id" || echo "failed to delete $name, continuing"
fi
done
}
build_films_payload() {
cat <<-EOF
name=Films&type=movie&agent=tv.plex.agents.movie&scanner=Plex%20Movie&language=en-US&location=%2Fvar%2Flib%2Fmedia%2Flibraries%2Ffilms&prefs%5BuseRedbandTrailers%5D=1&prefs%5BincludeAdultContent%5D=1&prefs%5BautoCollectionThreshold%5D=2&prefs%5BcollectionMode%5D=1&prefs%5BenableAdMarkerGeneration%5D=2
EOF
}
build_shows_payload() {
cat <<-EOF
name=Shows&type=show&agent=tv.plex.agents.series&scanner=Plex%20TV%20Series&language=en-US&location=%2Fvar%2Flib%2Fmedia%2Flibraries%2Fshows&prefs%5BuseSeasonTitles%5D=1&prefs%5BuseRedbandTrailers%5D=1&prefs%5BincludeAdultContent%5D=1&prefs%5BcollectionMode%5D=1&prefs%5BenableAdMarkerGeneration%5D=2
EOF
}
build_anime_films_payload() {
cat <<-EOF
name=Films%20%28Anime%29&type=movie&agent=tv.plex.agents.movie&scanner=Plex%20Movie&language=en-US&location=%2Fvar%2Flib%2Fmedia%2Flibraries%2Fanime%2Ffilms&prefs%5Bcountry%5D=JP&prefs%5BuseRedbandTrailers%5D=1&prefs%5BincludeAdultContent%5D=1&prefs%5BautoCollectionThreshold%5D=2&prefs%5BcollectionMode%5D=1&prefs%5BenableAdMarkerGeneration%5D=2
EOF
}
build_anime_shows_payload() {
cat <<-EOF
name=Shows%20%28Anime%29&type=show&agent=tv.plex.agents.series&scanner=Plex%20TV%20Series&language=en-US&location=%2Fvar%2Flib%2Fmedia%2Flibraries%2Fanime%2Fshows&prefs%5Bcountry%5D=JP&prefs%5BuseSeasonTitles%5D=1&prefs%5BuseRedbandTrailers%5D=1&prefs%5BincludeAdultContent%5D=1&prefs%5BcollectionMode%5D=1&prefs%5BenableAdMarkerGeneration%5D=2
EOF
}
wait_for_api
try_forever upsert_resource "library/sections" "title" "Films" "$(build_films_payload)"
try_forever upsert_resource "library/sections" "title" "Shows" "$(build_shows_payload)"
try_forever upsert_resource "library/sections" "title" "Films (Anime)" "$(build_anime_films_payload)"
try_forever upsert_resource "library/sections" "title" "Shows (Anime)" "$(build_anime_shows_payload)"
prune_resources "library/sections" "title" "Films" "Shows" "Films (Anime)" "Shows (Anime)"