feat: Jellyfin media server

This commit is contained in:
Madeleine Sydney
2025-02-02 14:51:04 -07:00
parent 41029d72a0
commit 1b0d348533
6 changed files with 148 additions and 37 deletions

View File

@@ -219,6 +219,8 @@ CLOSED: [2025-02-02 Sun 14:06]
**** TODO [[https://github.com/promethial/paxedit][Paxedit]]
Seems old and broken?
**** TODO Evil stuff
**** TODO Text objects

View File

@@ -10,6 +10,7 @@
users.users = [
"lain"
"besties"
];
impermanence = {
@@ -46,17 +47,32 @@
../../public-keys/lain-at-deertopia.pub
];
repos = {
"/persist/vault/jellyfin/Documents" = {
managed = true;
symlinkToAnnexHome = "documents";
remotes = {
"guix-rebound" = "crumb@guix-rebound:Documents";
};
};
"/persist/vault/jellyfin/Music" = {
managed = true;
symlinkToAnnexHome = "music";
remotes = {
"guix-rebound" = "crumb@guix-rebound:Music";
};
};
"/persist/deertopia.net/dav/org" = {
managed = true;
symlinkToAnnexHome = "org";
remotes = {
"guix-rebound" = "crumb@guix-rebound:org";
# "fruitbook" = "crumble@fruitbook:org";
};
};
};
};
deertopia = {
jellyfin.enable = true;
nginx.enable = true;
webdav.enable = true;
bepasty.enable = true;

View File

@@ -0,0 +1,39 @@
{ config, lib, pkgs, ... }:
with lib;
let cfg = config.sydnix.deertopia.jellyfin;
in {
options = {
sydnix.deertopia.jellyfin = {
enable = mkEnableOption "Deertopia's Jellyfin media server";
};
};
config = mkIf cfg.enable {
sydnix.impermanence =
let jcfg = config.services.jellyfin;
in {
directories = [
jcfg.dataDir
jcfg.configDir
];
cache.directories = [
jcfg.cacheDir
];
};
services.jellyfin = {
enable = true;
openFirewall = true;
};
sydnix.deertopia.nginx.vhosts."watch".vhost = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:8096"; # Uses default port.
};
};
};
}

View File

@@ -31,6 +31,11 @@ in {
default = true;
};
symlinkToAnnexHome = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
};
path = lib.mkOption {
description = ''
Path to the repo.
@@ -47,6 +52,9 @@ in {
user = lib.mkOption {
type = lib.types.str;
default = "annex";
description = ''
The user that the Git-annex assistant will be run as.
'';
};
group = lib.mkOption {
@@ -81,11 +89,21 @@ in {
# Necessary to enable cloning over SSH.
isNormalUser = true;
group = "annex";
# TODO: Don't hardcode extra groups!
extraGroups = [ "jellyfin" ];
home = "/var/sydnix/annex";
homeMode = "770";
createHome = true;
openssh.authorizedKeys.keyFiles = cfg.keyFiles;
};
# HACK: Some Jellyfin libraries are served by Git-annex. Give Jellyfin
# permission to symlink some of those libraries into the annex user's home
# dir.
users.users.${config.services.jellyfin.user}.extraGroups =
lib.mkIf config.sydnix.deertopia.jellyfin.enable
[ "annex" ];
system.activationScripts.initialiseUserAnnex =
let gitconfig-file = pkgs.writeText "gitconfig" ''
[user]
@@ -95,23 +113,16 @@ in {
defaultBranch = main
[core]
symlinks = true
[safe]
${lib.strings.concatMapStrings
(repo: "\tdirectory = ${repo.path}\n")
(builtins.attrValues cfg.repos)}
'';
in ''
set -e
annexHome="${config.users.users.annex.home}"
ln -sf "${gitconfig-file}" "$annexHome/.gitconfig"
# Symlink repos into annex's home for easy access. This is particularly nice for cloning:
# git clone annex@deertopia.net:org
# instead of
# git clone annex@deertopia.net:/persist/deertopia.net/org
# Less assumptions about the host's file system!
${lib.strings.toShellVar "repos" (builtins.attrNames cfg.repos)}
for repoPath in ''${!repos[@]}; do
target="$annexHome/$(basename "$repoPath")"
ln -sf "$repoPath" "$target"
done
'';
systemd.services =
@@ -124,8 +135,18 @@ in {
[ -e .git ] || git init
[ -e .git/annex ] || git annex init
# Symlink repo into user `annex` for easy access.
ln -sf "$(pwd)" "$HOME/"
# git config set user.name "${cfg.user.name}"
# git config set user.email "${cfg.user.email}"
# Symlink repos into annex's home for easy access. This is
# particularly nice for cloning:
# git clone annex@deertopia.net:org
# instead of
# git clone annex@deertopia.net:/persist/deertopia.net/dav/org
# Less assumptions about the host's file system!
annexHome="${config.users.users.annex.home}"
${lib.optionalString (repo.symlinkToAnnexHome != null)
''ln -sf "$PWD" "$annexHome/${repo.symlinkToAnnexHome}"''}
${lib.strings.toShellVar "remotes" repo.remotes}
for remoteName in ''${!remotes[@]}; do
@@ -140,26 +161,26 @@ in {
'';
mkAssistantService = repo: {
description = "git-annex assistant (${repo.path})";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig.User = repo.user;
serviceConfig.WorkingDirectory = repo.path;
path = [ pkgs.git pkgs.git-annex pkgs.openssh ];
# Set the default file mode for new files created by this process to
# 775. Oddly, `UMask = x` will set the mode to `0777 & x`, so 002
# gets us the desired figure of 775. We want 775 because Nginx,
# running under user `nginx` needs to be able to read/write these
# files to server them over WebDAV.
serviceConfig.UMask = "002";
serviceConfig.ExecStartPre =
lib.optionalString
repo.managed
"${pkgs.bash}/bin/bash ${init-if-necessary repo}";
serviceConfig.ExecStart = ''
${pkgs.git-annex}/bin/git-annex assistant --foreground
'';
};
description = "git-annex assistant (${repo.path})";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig.User = repo.user;
serviceConfig.WorkingDirectory = repo.path;
path = [ pkgs.git pkgs.git-annex pkgs.openssh ];
# Set the default file mode for new files created by this process to
# 775. Oddly, `UMask = x` will set the mode to `0777 & x`, so 002
# gets us the desired figure of 775. We want 775 because Nginx,
# running under user `nginx` needs to be able to read/write these
# files to server them over WebDAV.
serviceConfig.UMask = "002";
serviceConfig.ExecStartPre =
lib.optionalString
repo.managed
"${pkgs.bash}/bin/bash ${init-if-necessary repo}";
serviceConfig.ExecStart = ''
${pkgs.git-annex}/bin/git-annex assistant --foreground
'';
};
in builtins.listToAttrs
(builtins.map
(repo: {

View File

@@ -38,6 +38,28 @@ in {
Name of the group whose members have access to the persist directory.
'';
};
cache = {
directories = mkOption {
description = ''
While functionally identical to `directories` (at the moment),
`cache.directories` carries additional semantics: these directories
/can/ be erased, but typically /shouldn't/ be.
'';
default = [];
type = types.listOf types.anything;
};
files = mkOption {
description = ''
While functionally identical to `files` (at the moment),
`cache.files` carries additional semantics: these files /can/ be
erased, but typically /shouldn't/ be.
'';
default = [];
type = types.listOf types.anything;
};
};
};
};
@@ -72,8 +94,8 @@ in {
programs.fuse.userAllowOther = true;
environment.persistence."${cfg.persistDirectory}/root" = {
directories = cfg.directories;
files = cfg.files;
directories = cfg.directories ++ cfg.cache.directories;
files = cfg.files ++ cfg.cache.files;
};
};
}

View File

@@ -3,7 +3,18 @@
isNormalUser = true;
# TODO: Don't hard-code `persist`. Use
# config.sydnix.impermanence.persistGroupName.
extraGroups = [ "wheel" "persist" "nginx" "annex" ];
extraGroups = [
# Admin account.
"wheel"
# Default permissions to modify /persist.
"persist"
# Can modify the files served by Nginx.
"nginx"
# Can modify Deertopia's git-annex repos.
"annex"
# Can modify Deertopia's Jellyfin libraries.
"jellyfin"
];
initialHashedPassword =
"$y$j9T$aEFDDwdTZbAc6VQRXrkBJ0$K8wxTGTWDihyX1wxJ.ZMH//wmQFfrGGUkLkxIU0Lyq8";