Commit ea51ff3a5e7e
Changed files (7)
modules
pkgs
jellyfin-auto-collections
secrets
systems
rhea
modules/jellyfin-auto-collections.nix
@@ -0,0 +1,234 @@
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+
+with lib;
+
+let
+ cfg = config.services.jellyfin-auto-collections;
+
+ # Generate complete YAML config
+ # We'll use a template and substitute secrets at runtime
+ configYaml = lib.generators.toYAML { } (
+ {
+ jellyfin = {
+ server_url = cfg.jellyfinUrl;
+ api_key = "JELLYFIN_API_KEY_PLACEHOLDER";
+ user_id = cfg.userId;
+ };
+ }
+ // lib.optionalAttrs (cfg.jellyseerr.enable && cfg.jellyseerr.passwordFile != null) {
+ jellyseerr = {
+ server_url = cfg.jellyseerr.serverUrl;
+ email = cfg.jellyseerr.email;
+ password = "JELLYSEERR_PASSWORD_PLACEHOLDER";
+ user_type = cfg.jellyseerr.userType;
+ };
+ }
+ // cfg.settings
+ );
+
+ # Script to generate config file and run the application
+ startScript = pkgs.writeShellScript "jellyfin-auto-collections-start" ''
+ # Load API key from file if specified
+ ${lib.optionalString (cfg.apiKeyFile != null) ''
+ JELLYFIN_API_KEY=$(cat ${cfg.apiKeyFile})
+ ''}
+
+ # Load Jellyseerr password from file if specified
+ ${lib.optionalString (cfg.jellyseerr.enable && cfg.jellyseerr.passwordFile != null) ''
+ JELLYSEERR_PASSWORD=$(cat ${cfg.jellyseerr.passwordFile})
+ ''}
+
+ # Generate config.yml by substituting the placeholders
+ cat > ${cfg.dataDir}/config.yml << 'EOF'
+ ${configYaml}
+ EOF
+
+ # Replace the placeholders with actual secrets
+ sed -i "s/JELLYFIN_API_KEY_PLACEHOLDER/$JELLYFIN_API_KEY/g" ${cfg.dataDir}/config.yml
+ ${lib.optionalString (cfg.jellyseerr.enable && cfg.jellyseerr.passwordFile != null) ''
+ sed -i "s/JELLYSEERR_PASSWORD_PLACEHOLDER/$JELLYSEERR_PASSWORD/g" ${cfg.dataDir}/config.yml
+ ''}
+
+ chmod 600 ${cfg.dataDir}/config.yml
+
+ # Run the main script with config file
+ exec ${cfg.package}/bin/jellyfin-auto-collections --config ${cfg.dataDir}/config.yml
+ '';
+in
+{
+ options.services.jellyfin-auto-collections = {
+ enable = mkEnableOption "Jellyfin Auto Collections service";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.jellyfin-auto-collections;
+ defaultText = literalExpression "pkgs.jellyfin-auto-collections";
+ description = "The jellyfin-auto-collections package to use.";
+ };
+
+ interval = mkOption {
+ type = types.str;
+ default = "daily";
+ description = ''
+ How often to run the collection update.
+ Uses systemd.time format (e.g., "hourly", "daily", "weekly", "*:0/30" for every 30 minutes).
+ '';
+ };
+
+ jellyfinUrl = mkOption {
+ type = types.str;
+ example = "http://localhost:8096";
+ description = "URL of the Jellyfin server";
+ };
+
+ apiKeyFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Path to a file containing the Jellyfin API key.
+ The file should contain only the API key.
+ '';
+ };
+
+ userId = mkOption {
+ type = types.str;
+ description = "Jellyfin user ID";
+ };
+
+ jellyseerr = {
+ enable = mkEnableOption "Jellyseerr integration";
+
+ serverUrl = mkOption {
+ type = types.str;
+ default = "http://localhost:5055";
+ description = "URL of the Jellyseerr server";
+ };
+
+ email = mkOption {
+ type = types.str;
+ description = "Jellyseerr user email";
+ };
+
+ passwordFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Path to a file containing the Jellyseerr password.
+ The file should contain only the password.
+ '';
+ };
+
+ userType = mkOption {
+ type = types.str;
+ default = "local";
+ description = "Jellyseerr user type (local or plex)";
+ };
+ };
+
+ settings = mkOption {
+ type = types.attrs;
+ default = { };
+ description = ''
+ Configuration for Jellyfin Auto Collections.
+ This will be converted to a config file.
+ '';
+ example = literalExpression ''
+ {
+ jellyfin = {
+ url = "http://localhost:8096";
+ user_id = "your-user-id";
+ };
+ lists = [
+ {
+ type = "imdb";
+ url = "https://www.imdb.com/list/ls123456789/";
+ name = "IMDb Top 250";
+ }
+ ];
+ }
+ '';
+ };
+
+ dataDir = mkOption {
+ type = types.path;
+ default = "/var/lib/jellyfin-auto-collections";
+ description = "Directory to store jellyfin-auto-collections data and cache";
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = "jellyfin-auto-collections";
+ description = "User account under which jellyfin-auto-collections runs.";
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = "jellyfin-auto-collections";
+ description = "Group under which jellyfin-auto-collections runs.";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ # Create the user and group
+ users.users.${cfg.user} = {
+ isSystemUser = true;
+ group = cfg.group;
+ home = cfg.dataDir;
+ createHome = true;
+ };
+
+ users.groups.${cfg.group} = { };
+
+ # Systemd service
+ systemd.services.jellyfin-auto-collections = {
+ description = "Jellyfin Auto Collections";
+ after = [ "network.target" ];
+
+ serviceConfig = {
+ Type = "oneshot";
+ User = cfg.user;
+ Group = cfg.group;
+ WorkingDirectory = cfg.dataDir;
+
+ ExecStart = "${startScript}";
+
+ # Security hardening
+ PrivateTmp = true;
+ ProtectSystem = "strict";
+ ProtectHome = true;
+ ReadWritePaths = [ cfg.dataDir ];
+ NoNewPrivileges = true;
+ ProtectKernelTunables = true;
+ ProtectKernelModules = true;
+ ProtectControlGroups = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ PrivateMounts = true;
+ LockPersonality = true;
+ };
+
+ environment = {
+ JELLYFIN_SERVER_URL = cfg.jellyfinUrl;
+ JELLYFIN_USER_ID = cfg.userId;
+ HOME = cfg.dataDir;
+ };
+ };
+
+ # Systemd timer
+ systemd.timers.jellyfin-auto-collections = {
+ description = "Timer for Jellyfin Auto Collections";
+ wantedBy = [ "timers.target" ];
+
+ timerConfig = {
+ OnCalendar = cfg.interval;
+ Persistent = true;
+ RandomizedDelaySec = "5m";
+ };
+ };
+ };
+}
pkgs/jellyfin-auto-collections/default.nix
@@ -0,0 +1,75 @@
+{
+ lib,
+ python3,
+ fetchFromGitHub,
+ makeWrapper,
+}:
+
+python3.pkgs.buildPythonApplication rec {
+ pname = "jellyfin-auto-collections";
+ version = "unstable-2024-01-01";
+
+ format = "other"; # No setup.py or pyproject.toml
+
+ src = fetchFromGitHub {
+ owner = "ghomasHudson";
+ repo = "Jellyfin-Auto-Collections";
+ rev = "master";
+ hash = "sha256-4dfpOgZ6mCbyzFKeP53ZQpfjK1dOp45rsclQzoQjzeQ=";
+ };
+
+ nativeBuildInputs = [ makeWrapper ];
+
+ propagatedBuildInputs = with python3.pkgs; [
+ apscheduler
+ beautifulsoup4
+ certifi
+ charset-normalizer
+ loguru
+ pluginlib
+ pyaml-env
+ pytz
+ pyyaml
+ requests
+ requests-cache
+ setuptools
+ six
+ soupsieve
+ tzlocal
+ urllib3
+ url-normalize
+ numpy
+ packaging
+ pillow
+ pyparsing
+ python-dateutil
+ attrs
+ cattrs
+ platformdirs
+ ];
+
+ dontBuild = true;
+ dontCheck = true;
+
+ installPhase = ''
+ runHook preInstall
+
+ mkdir -p $out/bin $out/lib/jellyfin-auto-collections
+ cp -r * $out/lib/jellyfin-auto-collections/
+
+ # Create wrapper script with proper PYTHONPATH
+ makeWrapper ${python3}/bin/python $out/bin/jellyfin-auto-collections \
+ --add-flags "$out/lib/jellyfin-auto-collections/main.py" \
+ --prefix PYTHONPATH : "$PYTHONPATH:$out/lib/jellyfin-auto-collections"
+
+ runHook postInstall
+ '';
+
+ meta = with lib; {
+ description = "Automatically make jellyfin collections from IMDB, Letterboxd lists and more";
+ homepage = "https://github.com/ghomasHudson/Jellyfin-Auto-Collections";
+ license = licenses.mit;
+ maintainers = [ ];
+ mainProgram = "jellyfin-auto-collections";
+ };
+}
pkgs/default.nix
@@ -28,6 +28,7 @@ in
toggle-color-scheme = pkgs.callPackage ./toggle-color-scheme { };
homepage = pkgs.callPackage ./homepage { inherit globals; };
audible-converter = pkgs.callPackage ./audible-converter { };
+ jellyfin-auto-collections = pkgs.callPackage ./jellyfin-auto-collections { };
chmouzies-ai = pkgs.callPackage ./chmouzies/ai.nix { };
chmouzies-git = pkgs.callPackage ./chmouzies/git.nix { };
secrets/rhea/jellyfin-auto-collections-api-key.age
@@ -0,0 +1,9 @@
+age-encryption.org/v1
+-> piv-p256 ItIHHA AywJmaCJGUKHR0T9x+IXegpGHI1axa3UIcjHFobQdL6Q
+D/bTcVMZaAc5OQadPW/sZxsOXV+P/SApQ9cZCn6FAOw
+-> piv-p256 ViCCtQ AhPO81SYNmbozFavfDXmeCwu6bVOD4JfIFaInBjgrNs9
+U8rTIr2LIsZTqeObLOyL5KvxGOHfaTWRq0dXoxgAUqc
+-> ssh-ed25519 EboMJg Y/G2U9hW2ivhG6ke0n96iXfqhoLC1SyNU14yG7lzBAE
+78QB8erMohDu25KzKR6cjZRXz9gLjcttBLpD2ofyW1M
+--- JkUTgCdJ/ikyj4OxZnU+AOwd9yzSnTviwoQpWI7oO00
+���Xе���U�U��W}O^�a�O���:ȸ�ɧTށ�����r� -O�%�"k�Ƒ������
\ No newline at end of file
secrets/rhea/jellyfin-auto-collections-jellyseerr-password.age
@@ -0,0 +1,9 @@
+age-encryption.org/v1
+-> piv-p256 ItIHHA AuNrSh0isDY8oA8bXmOte+CYoCyt09I+Rb9m6P8hqUSx
+2yTFbjZ8q0OVVvBrierbC1NaLXBAwlxtffU7iCURw1M
+-> piv-p256 ViCCtQ AousyeYnVbivF+2623GoI74yIEreUx6a0lV82g8XRvPk
+RHGkuxxayRIoYJPv8+TcqZ0HOTLdt2n8XyExOcIREic
+-> ssh-ed25519 EboMJg iLpG/uCXgrxThlzYh19SiwmkdeGMASD0fIQ/GaySJzk
+C+AiFk6YaL/HuUAti8aKeWzjUTihx3bwNamlaxIgxME
+--- sOryvjZJ0Tl9AyZQQMeda2E//f2LKuzgQb0HvrM1wjE
+��� �ט X������s�|o�C_ ����^�`h�i0İ
\ No newline at end of file
systems/rhea/extra.nix
@@ -52,6 +52,7 @@ in
../common/services/samba.nix
../common/services/homepage.nix
../../modules/audible-sync.nix
+ ../../modules/jellyfin-auto-collections.nix
];
# Age secrets: gandi.env + generated exportarr secrets
@@ -62,6 +63,16 @@ in
owner = "traefik";
group = "traefik";
};
+ "jellyfin-auto-collections-api-key" = {
+ file = ../../secrets/rhea/jellyfin-auto-collections-api-key.age;
+ mode = "400";
+ owner = "jellyfin-auto-collections";
+ };
+ "jellyfin-auto-collections-jellyseerr-password" = {
+ file = ../../secrets/rhea/jellyfin-auto-collections-jellyseerr-password.age;
+ mode = "400";
+ owner = "jellyfin-auto-collections";
+ };
}
// lib.mapAttrs' (
name: _cfg:
@@ -429,6 +440,102 @@ in
topic = "homelab";
};
};
+ jellyfin-auto-collections = {
+ enable = true;
+ jellyfinUrl = "http://localhost:8096";
+ userId = "400fef4e0ab2448cb8a2bc8ca2facc4f";
+ apiKeyFile = config.age.secrets."jellyfin-auto-collections-api-key".path;
+ interval = "daily"; # Run daily at midnight
+
+ jellyseerr = {
+ enable = false; # Enable when password secret is created
+ serverUrl = "http://localhost:5055";
+ email = "vincent@sbr.pm";
+ # Uncomment when jellyseerr password secret is created
+ # passwordFile = config.age.secrets."jellyfin-auto-collections-jellyseerr-password".path;
+ userType = "local";
+ };
+
+ settings = {
+ plugins = {
+ imdb_chart = {
+ enabled = true;
+ list_ids = [
+ "top"
+ "moviemeter"
+ ];
+ clear_collection = true;
+ };
+ imdb_list = {
+ enabled = true;
+ list_ids = [
+ "ls055592025" # IMDb Top 250
+ ];
+ };
+ jellyfin_api = {
+ enabled = true;
+ list_ids = [
+ # Marvel Cinematic Universe
+ {
+ studios = [
+ "Marvel Studios"
+ "Marvel Entertainment"
+ ];
+ list_name = "Marvel Cinematic Universe";
+ includeItemTypes = [ "Movie" ];
+ }
+ # Pixar Animation
+ {
+ studios = [ "Pixar" ];
+ list_name = "Pixar Collection";
+ includeItemTypes = [ "Movie" ];
+ }
+ # Studio Ghibli
+ {
+ studios = [ "Studio Ghibli" ];
+ list_name = "Studio Ghibli Collection";
+ includeItemTypes = [ "Movie" ];
+ }
+ # Sing Movies (Illumination)
+ {
+ searchTerm = "Sing";
+ studios = [ "Illumination Entertainment" ];
+ list_name = "Sing Movies";
+ includeItemTypes = [ "Movie" ];
+ }
+ # Christopher Nolan Films
+ {
+ person = [ "Christopher Nolan" ];
+ list_name = "Christopher Nolan Collection";
+ includeItemTypes = [ "Movie" ];
+ }
+ # Highly Rated Sci-Fi
+ {
+ genres = [ "Science Fiction" ];
+ minCriticRating = [ "8" ];
+ list_name = "Top Sci-Fi Movies";
+ includeItemTypes = [ "Movie" ];
+ }
+ # Recent Movies (2024-2025)
+ {
+ years = [
+ 2024
+ 2025
+ ];
+ list_name = "Recent Releases";
+ includeItemTypes = [ "Movie" ];
+ }
+ # Award Winners
+ {
+ tags = [ "Oscar Winner" ];
+ list_name = "Oscar Winners";
+ includeItemTypes = [ "Movie" ];
+ }
+ ];
+ };
+ };
+ };
+ };
transmission = serviceDefaults // {
enable = true;
package = pkgs.transmission_4;
secrets.nix
@@ -99,5 +99,7 @@ in
"secrets/rhea/exportarr-prowlarr-apikey.age".publicKeys = users ++ [ rhea ];
"secrets/rhea/exportarr-readarr-apikey.age".publicKeys = users ++ [ rhea ];
"secrets/rhea/exportarr-bazarr-apikey.age".publicKeys = users ++ [ rhea ];
+ "secrets/rhea/jellyfin-auto-collections-api-key.age".publicKeys = users ++ [ rhea ];
+ "secrets/rhea/jellyfin-auto-collections-jellyseerr-password.age".publicKeys = users ++ [ rhea ];
"secrets/demeter/mosquitto-homeassistant-password.age".publicKeys = users ++ [ demeter ];
}