add jellyfin and rclone fuse mount capability finally
This commit is contained in:
parent
8ac2ec8520
commit
a5e11b9918
|
@ -0,0 +1,6 @@
|
||||||
|
[Media]
|
||||||
|
type = webdav
|
||||||
|
url = https://storage-webdav.owo.monster/Media/
|
||||||
|
vendor = other
|
||||||
|
user = media
|
||||||
|
pass = MEDIA_PASSWORD
|
100
hosts/hetzner-arm/containers/jellyfin/default.nix
Normal file
100
hosts/hetzner-arm/containers/jellyfin/default.nix
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
hostPath,
|
||||||
|
tree,
|
||||||
|
inputs,
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
containerName = "jellyfin";
|
||||||
|
|
||||||
|
containerAddresses = import "${hostPath}/data/containerAddresses.nix";
|
||||||
|
|
||||||
|
hostIP = containerAddresses.host;
|
||||||
|
containerIP = containerAddresses.containers.${containerName};
|
||||||
|
in {
|
||||||
|
containers.jellyfin = {
|
||||||
|
autoStart = true;
|
||||||
|
privateNetwork = true;
|
||||||
|
hostAddress = hostIP;
|
||||||
|
localAddress = containerIP;
|
||||||
|
|
||||||
|
bindMounts = {
|
||||||
|
"/dev/fuse" = {
|
||||||
|
hostPath = "/dev/fuse";
|
||||||
|
isReadOnly = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Allow rclone mount in container
|
||||||
|
allowedDevices = [
|
||||||
|
{
|
||||||
|
modifier = "rwm";
|
||||||
|
node = "/dev/fuse";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
modifier = "rwm";
|
||||||
|
node = "/dev/mapper/control";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
specialArgs = {
|
||||||
|
inherit inputs;
|
||||||
|
inherit tree;
|
||||||
|
inherit self;
|
||||||
|
inherit hostPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {...}: {
|
||||||
|
nixpkgs.pkgs = pkgs;
|
||||||
|
|
||||||
|
imports = with tree;
|
||||||
|
[
|
||||||
|
presets.nixos.containerBase
|
||||||
|
./secrets.nix
|
||||||
|
]
|
||||||
|
++ (with hosts.hetzner-arm.containers.jellyfin.profiles; [
|
||||||
|
mediaMount
|
||||||
|
jellyfin
|
||||||
|
restic
|
||||||
|
]);
|
||||||
|
|
||||||
|
home-manager.users.root.home.stateVersion = "23.05";
|
||||||
|
system.stateVersion = "23.05";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."jellyfin.owo.monster" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
extraConfig = ''
|
||||||
|
client_max_body_size 512M;
|
||||||
|
|
||||||
|
# Security / XSS Mitigation Headers
|
||||||
|
# NOTE: X-Frame-Options may cause issues with the webOS app
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN";
|
||||||
|
add_header X-XSS-Protection "0"; # Do NOT enable. This is obsolete/dangerous
|
||||||
|
add_header X-Content-Type-Options "nosniff";
|
||||||
|
|
||||||
|
# COOP/COEP. Disable if you use external plugins/images/assets
|
||||||
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||||
|
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||||
|
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||||
|
|
||||||
|
# Permissions policy. May cause issues on some clients
|
||||||
|
add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()" always;
|
||||||
|
|
||||||
|
# Tell browsers to use per-origin process isolation
|
||||||
|
add_header Origin-Agent-Cluster "?1" always;
|
||||||
|
'';
|
||||||
|
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://${containerIP}:8096";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = ''
|
||||||
|
proxy_buffering off;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{...}: {
|
||||||
|
services.jellyfin = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
users.users.jellyfin.uid = 1000;
|
||||||
|
users.groups.jellyfin.gid = 1000;
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
secrets = config.services.secrets.secrets;
|
||||||
|
rcloneMedia = pkgs.writeShellScriptBin "rclone-media" ''
|
||||||
|
${pkgs.rclone}/bin/rclone --config ${secrets.rclone_config.path} "$@"
|
||||||
|
'';
|
||||||
|
mountMedia = pkgs.writeShellScriptBin "mount-media" ''
|
||||||
|
${rcloneMedia}/bin/rclone-media mount Media: /Media \
|
||||||
|
--allow-other \
|
||||||
|
--uid=${toString config.users.users.jellyfin.uid} \
|
||||||
|
--gid=${toString config.users.groups.jellyfin.gid} \
|
||||||
|
--fast-list \
|
||||||
|
--umask=666 \
|
||||||
|
--log-level=INFO "$@"
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
rclone
|
||||||
|
rcloneMedia
|
||||||
|
fuse
|
||||||
|
fuse3
|
||||||
|
mountMedia
|
||||||
|
];
|
||||||
|
|
||||||
|
programs.fuse.userAllowOther = true;
|
||||||
|
|
||||||
|
systemd.services.jellyfin = {
|
||||||
|
wants = ["media-mount.service"];
|
||||||
|
after = ["media-mount.service"];
|
||||||
|
serviceConfig.ReadWritePaths = "/Media";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.media-mount = {
|
||||||
|
wantedBy = ["jellyfin.service"];
|
||||||
|
partOf = ["jellyfin.service"];
|
||||||
|
path = with pkgs; [
|
||||||
|
fuse
|
||||||
|
fuse3
|
||||||
|
];
|
||||||
|
serviceConfig.ExecStart = "${mountMedia}/bin/mount-media --syslog";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /Media - jellyfin jellyfin"
|
||||||
|
|
||||||
|
"d /root/.config - root root"
|
||||||
|
"d /root/.config/rclone - root root"
|
||||||
|
"L /root/.config/rclone/rclone.conf - - - - ${secrets.rclone_config.path}"
|
||||||
|
];
|
||||||
|
}
|
39
hosts/hetzner-arm/containers/jellyfin/profiles/restic.nix
Normal file
39
hosts/hetzner-arm/containers/jellyfin/profiles/restic.nix
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
secrets = config.services.secrets.secrets;
|
||||||
|
in {
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
restic
|
||||||
|
(pkgs.writeShellScriptBin "restic-jellyfin" ''
|
||||||
|
env \
|
||||||
|
RESTIC_PASSWORD_FILE=${secrets.restic_password.path} \
|
||||||
|
$(cat ${secrets.restic_env.path}) \
|
||||||
|
${pkgs.restic}/bin/restic $@
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
|
||||||
|
services.restic.backups.jellyfin = {
|
||||||
|
user = "root";
|
||||||
|
paths = [
|
||||||
|
"/var/lib/jellyfin"
|
||||||
|
];
|
||||||
|
|
||||||
|
# repository is overrided in environmentFile to contain auth
|
||||||
|
# make sure to keep up to date when changing repository
|
||||||
|
repository = "rest:https://storage-restic.owo.monster/Jellyfin";
|
||||||
|
passwordFile = "${secrets.restic_password.path}";
|
||||||
|
environmentFile = "${secrets.restic_env.path}";
|
||||||
|
|
||||||
|
pruneOpts = [
|
||||||
|
"--keep-last 5"
|
||||||
|
];
|
||||||
|
|
||||||
|
timerConfig = {
|
||||||
|
OnBootSec = "10m";
|
||||||
|
OnCalendar = "8h";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
54
hosts/hetzner-arm/containers/jellyfin/secrets.nix
Normal file
54
hosts/hetzner-arm/containers/jellyfin/secrets.nix
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{pkgs, ...}: {
|
||||||
|
services.secrets = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
packages = with pkgs; [
|
||||||
|
rclone
|
||||||
|
];
|
||||||
|
|
||||||
|
vaultLogin = {
|
||||||
|
enable = true;
|
||||||
|
loginUsername = "hetzner-arm-container-jellyfin";
|
||||||
|
};
|
||||||
|
|
||||||
|
autoSecrets = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
requiredVaultPaths = [
|
||||||
|
"api-keys/data/storage/webdav/Media"
|
||||||
|
"api-keys/data/storage/restic/Jellyfin"
|
||||||
|
|
||||||
|
"private-public-keys/data/restic/Jellyfin"
|
||||||
|
];
|
||||||
|
|
||||||
|
secrets = {
|
||||||
|
vault_password = {
|
||||||
|
manual = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
rclone_config = {
|
||||||
|
user = "jellyfin";
|
||||||
|
group = "jellyfin";
|
||||||
|
fetchScript = ''
|
||||||
|
cp ${./data/rclone_config.template} "$secretFile"
|
||||||
|
MEDIA_PASSWORD="$(simple_get "/api-keys/storage/webdav/Media" .media)"
|
||||||
|
MEDIA_PASSWORD="$(rclone obscure "$MEDIA_PASSWORD")"
|
||||||
|
sed -i "s/MEDIA_PASSWORD/$MEDIA_PASSWORD/" "$secretFile"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
restic_password = {
|
||||||
|
fetchScript = ''
|
||||||
|
simple_get "/private-public-keys/restic/Jellyfin" .password > "$secretFile"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
restic_env = {
|
||||||
|
fetchScript = ''
|
||||||
|
RESTIC_PASSWORD=$(simple_get "/api-keys/storage/restic/Jellyfin" .restic)
|
||||||
|
echo "RESTIC_REPOSITORY=rest:https://restic:$RESTIC_PASSWORD@storage-restic.owo.monster/Jellyfin" > "$secretFile"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
inherit (lib.modules) mkIf mkForce;
|
||||||
|
|
||||||
|
mailConfig = config.services.mailserver;
|
||||||
|
in {
|
||||||
|
config = mkIf (mailConfig.enable && mailConfig.roundcube.enable) {
|
||||||
|
services.roundcube = {
|
||||||
|
enable = true;
|
||||||
|
package = mailConfig.roundcube.package;
|
||||||
|
plugins =
|
||||||
|
mailConfig.roundcube.plugins
|
||||||
|
++ [
|
||||||
|
"managesieve"
|
||||||
|
];
|
||||||
|
hostName = "${mailConfig.roundcube.domain}";
|
||||||
|
extraConfig = ''
|
||||||
|
$config['smtp_server'] = "tls://${mailConfig.fqdn}";
|
||||||
|
$config['smtp_user'] = "%u";
|
||||||
|
$config['smtp_pass'] = "%p";
|
||||||
|
$config['managesieve_host'] = 'tls://${mailConfig.fqdn}';
|
||||||
|
${mailConfig.roundcube.extraConfig}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."${mailConfig.roundcube.domain}" = {
|
||||||
|
forceSSL = mkForce mailConfig.roundcube.forceSSL;
|
||||||
|
enableACME = mkForce mailConfig.roundcube.enableACME;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
269
hosts/hetzner-arm/containers/music/modules/mpd-fork.nix
Normal file
269
hosts/hetzner-arm/containers/music/modules/mpd-fork.nix
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
with lib; let
|
||||||
|
name = "mpd";
|
||||||
|
|
||||||
|
uid = config.ids.uids.mpd;
|
||||||
|
gid = config.ids.gids.mpd;
|
||||||
|
cfg = config.services.mpd-fork;
|
||||||
|
|
||||||
|
credentialsPlaceholder = creds: let
|
||||||
|
placeholders =
|
||||||
|
imap0
|
||||||
|
(i: c: ''password "{{password-${toString i}}}@${concatStringsSep "," c.permissions}"'')
|
||||||
|
creds;
|
||||||
|
in
|
||||||
|
concatStringsSep "\n" placeholders;
|
||||||
|
|
||||||
|
mpdConf = pkgs.writeText "mpd.conf" ''
|
||||||
|
# This file was automatically generated by NixOS. Edit mpd's configuration
|
||||||
|
# via NixOS' configuration.nix, as this file will be rewritten upon mpd's
|
||||||
|
# restart.
|
||||||
|
|
||||||
|
music_directory "${cfg.musicDirectory}"
|
||||||
|
playlist_directory "${cfg.playlistDirectory}"
|
||||||
|
${lib.optionalString (cfg.dbFile != null) ''
|
||||||
|
db_file "${cfg.dbFile}"
|
||||||
|
''}
|
||||||
|
state_file "${cfg.dataDir}/state"
|
||||||
|
sticker_file "${cfg.dataDir}/sticker.sql"
|
||||||
|
|
||||||
|
${optionalString (cfg.network.listenAddress != "any") ''bind_to_address "${cfg.network.listenAddress}"''}
|
||||||
|
${optionalString (cfg.network.port != 6600) ''port "${toString cfg.network.port}"''}
|
||||||
|
${optionalString (cfg.fluidsynth) ''
|
||||||
|
decoder {
|
||||||
|
plugin "fluidsynth"
|
||||||
|
soundfont "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2"
|
||||||
|
}
|
||||||
|
''}
|
||||||
|
|
||||||
|
${optionalString (cfg.credentials != []) (credentialsPlaceholder cfg.credentials)}
|
||||||
|
|
||||||
|
${cfg.extraConfig}
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
###### interface
|
||||||
|
|
||||||
|
options = {
|
||||||
|
services.mpd-fork = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Whether to enable MPD, the music player daemon.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
package = mkPackageOption pkgs "mpd" {};
|
||||||
|
|
||||||
|
startWhenNeeded = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
If set, {command}`mpd` is socket-activated; that
|
||||||
|
is, instead of having it permanently running as a daemon,
|
||||||
|
systemd will start it on the first incoming connection.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
musicDirectory = mkOption {
|
||||||
|
type = with types; either path (strMatching "(http|https|nfs|smb)://.+");
|
||||||
|
default = "${cfg.dataDir}/music";
|
||||||
|
defaultText = literalExpression ''"''${dataDir}/music"'';
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
The directory or NFS/SMB network share where MPD reads music from. If left
|
||||||
|
as the default value this directory will automatically be created before
|
||||||
|
the MPD server starts, otherwise the sysadmin is responsible for ensuring
|
||||||
|
the directory exists with appropriate ownership and permissions.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
playlistDirectory = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "${cfg.dataDir}/playlists";
|
||||||
|
defaultText = literalExpression ''"''${dataDir}/playlists"'';
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
The directory where MPD stores playlists. If left as the default value
|
||||||
|
this directory will automatically be created before the MPD server starts,
|
||||||
|
otherwise the sysadmin is responsible for ensuring the directory exists
|
||||||
|
with appropriate ownership and permissions.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraConfig = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Extra directives added to to the end of MPD's configuration file,
|
||||||
|
mpd.conf. Basic configuration like file location and uid/gid
|
||||||
|
is added automatically to the beginning of the file. For available
|
||||||
|
options see {manpage}`mpd.conf(5)`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/${name}";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
The directory where MPD stores its state, tag cache, playlists etc. If
|
||||||
|
left as the default value this directory will automatically be created
|
||||||
|
before the MPD server starts, otherwise the sysadmin is responsible for
|
||||||
|
ensuring the directory exists with appropriate ownership and permissions.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = name;
|
||||||
|
description = lib.mdDoc "User account under which MPD runs.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = name;
|
||||||
|
description = lib.mdDoc "Group account under which MPD runs.";
|
||||||
|
};
|
||||||
|
|
||||||
|
network = {
|
||||||
|
listenAddress = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
example = "any";
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
The address for the daemon to listen on.
|
||||||
|
Use `any` to listen on all addresses.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 6600;
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
This setting is the TCP port that is desired for the daemon to get assigned
|
||||||
|
to.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
dbFile = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = "${cfg.dataDir}/tag_cache";
|
||||||
|
defaultText = literalExpression ''"''${dataDir}/tag_cache"'';
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
The path to MPD's database. If set to `null` the
|
||||||
|
parameter is omitted from the configuration.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
credentials = mkOption {
|
||||||
|
type = types.listOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
passwordFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Path to file containing the password.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
permissions = let
|
||||||
|
perms = ["read" "add" "control" "admin"];
|
||||||
|
in
|
||||||
|
mkOption {
|
||||||
|
type = types.listOf (types.enum perms);
|
||||||
|
default = ["read"];
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
List of permissions that are granted with this password.
|
||||||
|
Permissions can be "${concatStringsSep "\", \"" perms}".
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Credentials and permissions for accessing the mpd server.
|
||||||
|
'';
|
||||||
|
default = [];
|
||||||
|
example = [
|
||||||
|
{
|
||||||
|
passwordFile = "/var/lib/secrets/mpd_readonly_password";
|
||||||
|
permissions = ["read"];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
passwordFile = "/var/lib/secrets/mpd_admin_password";
|
||||||
|
permissions = ["read" "add" "control" "admin"];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
fluidsynth = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
If set, add fluidsynth soundfont and configure the plugin.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
###### implementation
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
# install mpd units
|
||||||
|
systemd.packages = [cfg.package];
|
||||||
|
|
||||||
|
systemd.sockets.mpd = mkIf cfg.startWhenNeeded {
|
||||||
|
wantedBy = ["sockets.target"];
|
||||||
|
listenStreams = [
|
||||||
|
"" # Note: this is needed to override the upstream unit
|
||||||
|
(
|
||||||
|
if pkgs.lib.hasPrefix "/" cfg.network.listenAddress
|
||||||
|
then cfg.network.listenAddress
|
||||||
|
else "${optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"}${toString cfg.network.port}"
|
||||||
|
)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.mpd = {
|
||||||
|
wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
|
||||||
|
|
||||||
|
preStart =
|
||||||
|
''
|
||||||
|
set -euo pipefail
|
||||||
|
install -m 600 ${mpdConf} /run/mpd/mpd.conf
|
||||||
|
''
|
||||||
|
+ optionalString (cfg.credentials != [])
|
||||||
|
(concatStringsSep "\n"
|
||||||
|
(imap0
|
||||||
|
(i: c: ''${pkgs.replace-secret}/bin/replace-secret '{{password-${toString i}}}' '${c.passwordFile}' /run/mpd/mpd.conf'')
|
||||||
|
cfg.credentials));
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
User = "${cfg.user}";
|
||||||
|
# Note: the first "" overrides the ExecStart from the upstream unit
|
||||||
|
ExecStart = ["" "${cfg.package}/bin/mpd --systemd /run/mpd/mpd.conf"];
|
||||||
|
RuntimeDirectory = "mpd";
|
||||||
|
StateDirectory =
|
||||||
|
[]
|
||||||
|
++ optionals (cfg.dataDir == "/var/lib/${name}") [name]
|
||||||
|
++ optionals (cfg.playlistDirectory == "/var/lib/${name}/playlists") [name "${name}/playlists"]
|
||||||
|
++ optionals (cfg.musicDirectory == "/var/lib/${name}/music") [name "${name}/music"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users = optionalAttrs (cfg.user == name) {
|
||||||
|
"${name}" = {
|
||||||
|
inherit uid;
|
||||||
|
group = cfg.group;
|
||||||
|
extraGroups = ["audio"];
|
||||||
|
description = "Music Player Daemon user";
|
||||||
|
home = "${cfg.dataDir}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups = optionalAttrs (cfg.group == name) {
|
||||||
|
"${name}".gid = gid;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
55
hosts/hetzner-arm/containers/roundcube/default.nix
Normal file
55
hosts/hetzner-arm/containers/roundcube/default.nix
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
tree,
|
||||||
|
inputs,
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
hostPath,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
containerAddresses = import "${hostPath}/data/containerAddresses.nix";
|
||||||
|
hostIP = containerAddresses.host;
|
||||||
|
containerIP = containerAddresses.containers.roundcube;
|
||||||
|
in {
|
||||||
|
containers.roundcube = {
|
||||||
|
autoStart = true;
|
||||||
|
privateNetwork = true;
|
||||||
|
hostAddress = hostIP;
|
||||||
|
localAddress = containerIP;
|
||||||
|
|
||||||
|
specialArgs = {
|
||||||
|
inherit inputs;
|
||||||
|
inherit tree;
|
||||||
|
inherit self;
|
||||||
|
inherit hostPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {...}: {
|
||||||
|
nixpkgs.pkgs = pkgs;
|
||||||
|
|
||||||
|
imports = with tree; [
|
||||||
|
presets.nixos.containerBase
|
||||||
|
|
||||||
|
profiles.nginx
|
||||||
|
profiles.sshd
|
||||||
|
profiles.firewallAllow.ssh
|
||||||
|
|
||||||
|
./profiles/roundcube.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
home-manager.users.root.home.stateVersion = "23.05";
|
||||||
|
system.stateVersion = "23.05";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
virtualHosts."mail.owo.monster" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://unix:/var/lib/nixos-containers/roundcube/var/sockets/roundcube.sock";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
hostPath,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
inherit (lib.modules) mkForce;
|
||||||
|
|
||||||
|
localContainersAddresses = import "${hostPath}/data/containerAddresses.nix";
|
||||||
|
in {
|
||||||
|
services.roundcube = {
|
||||||
|
enable = true;
|
||||||
|
hostName = "mail.owo.monster";
|
||||||
|
package = pkgs.roundcube.withPlugins (_plugins:
|
||||||
|
with pkgs.roundcubePlugins; [
|
||||||
|
persistent_login
|
||||||
|
]);
|
||||||
|
plugins = [
|
||||||
|
"persistent_login"
|
||||||
|
"managesieve"
|
||||||
|
];
|
||||||
|
|
||||||
|
database = {
|
||||||
|
host = localContainersAddresses.containers.postgresql;
|
||||||
|
passwordFile = builtins.toFile "pw" "";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
$config['smtp_server'] = "tls://mail.owo.monster";
|
||||||
|
$config['smtp_user'] = "%u";
|
||||||
|
$config['smtp_pass'] = "%p";
|
||||||
|
$config['managesieve_host'] = 'tls://mail.owo.monster';
|
||||||
|
$config['session_lifetime'] = (60 * 24 * 7 * 2); # 2 Weeks
|
||||||
|
$config['product_name'] = 'Chaos Mail';
|
||||||
|
$config['username_domain'] = "owo.monster";
|
||||||
|
$config['username_domain_forced'] = true;
|
||||||
|
$config['log_driver'] = 'syslog';
|
||||||
|
$config['smtp_debug'] = true;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/sockets - nginx nginx"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.nginx.serviceConfig.ReadWritePaths = [
|
||||||
|
"/var/sockets"
|
||||||
|
];
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."mail.owo.monster" = {
|
||||||
|
forceSSL = mkForce false;
|
||||||
|
enableACME = mkForce false;
|
||||||
|
extraConfig = "listen unix:/var/sockets/roundcube.sock;";
|
||||||
|
};
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ in {
|
||||||
restic_forgejo = restic + 6;
|
restic_forgejo = restic + 6;
|
||||||
restic_caldav = restic + 7;
|
restic_caldav = restic + 7;
|
||||||
restic_owncast = restic + 8;
|
restic_owncast = restic + 8;
|
||||||
|
restic_jellyfin = restic + 9;
|
||||||
|
|
||||||
http_music = http + 0;
|
http_music = http + 0;
|
||||||
http_public = http + 1;
|
http_public = http + 1;
|
||||||
|
|
|
@ -24,6 +24,25 @@ in {
|
||||||
hostAddress = hostIP;
|
hostAddress = hostIP;
|
||||||
localAddress = containerIP;
|
localAddress = containerIP;
|
||||||
|
|
||||||
|
bindMounts = {
|
||||||
|
"/dev/fuse" = {
|
||||||
|
hostPath = "/dev/fuse";
|
||||||
|
isReadOnly = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Allow rclone mount in container
|
||||||
|
allowedDevices = [
|
||||||
|
{
|
||||||
|
modifier = "rwm";
|
||||||
|
node = "/dev/fuse";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
modifier = "rwm";
|
||||||
|
node = "/dev/mapper/control";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
specialArgs = {
|
specialArgs = {
|
||||||
inherit inputs;
|
inherit inputs;
|
||||||
inherit tree;
|
inherit tree;
|
||||||
|
@ -46,7 +65,11 @@ in {
|
||||||
users
|
users
|
||||||
]);
|
]);
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [rclone];
|
environment.systemPackages = with pkgs; [
|
||||||
|
rclone
|
||||||
|
fuse
|
||||||
|
fuse3
|
||||||
|
];
|
||||||
|
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
@ -99,6 +122,7 @@ in {
|
||||||
"/Forgejo/".proxyPass = "http://${containerIP}:${toString ports.restic_forgejo}";
|
"/Forgejo/".proxyPass = "http://${containerIP}:${toString ports.restic_forgejo}";
|
||||||
"/CalDAV/".proxyPass = "http://${containerIP}:${toString ports.restic_caldav}";
|
"/CalDAV/".proxyPass = "http://${containerIP}:${toString ports.restic_caldav}";
|
||||||
"/Owncast/".proxyPass = "http://${containerIP}:${toString ports.restic_owncast}";
|
"/Owncast/".proxyPass = "http://${containerIP}:${toString ports.restic_owncast}";
|
||||||
|
"/Jellyfin/".proxyPass = "http://${containerIP}:${toString ports.restic_jellyfin}";
|
||||||
};
|
};
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
client_max_body_size ${clientMaxBodySize};
|
client_max_body_size ${clientMaxBodySize};
|
||||||
|
|
|
@ -197,6 +197,16 @@ in {
|
||||||
"--baseurl=/Owncast/"
|
"--baseurl=/Owncast/"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
id = "restic-jellyfin";
|
||||||
|
remote = "StorageBox:Backups/Restic/Jellyfin";
|
||||||
|
type = "restic";
|
||||||
|
extraArgs = [
|
||||||
|
"--addr=0.0.0.0:${toString ports.restic_jellyfin}"
|
||||||
|
"--htpasswd=${secrets.restic_jellyfin_htpasswd.path}"
|
||||||
|
"--baseurl=/Jellyfin/"
|
||||||
|
];
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"api-keys/data/storage/restic/Forgejo"
|
"api-keys/data/storage/restic/Forgejo"
|
||||||
"api-keys/data/storage/restic/CalDAV"
|
"api-keys/data/storage/restic/CalDAV"
|
||||||
"api-keys/data/storage/restic/Owncast"
|
"api-keys/data/storage/restic/Owncast"
|
||||||
|
"api-keys/data/storage/restic/Jellyfin"
|
||||||
|
|
||||||
"api-keys/data/storage/webdav/Main"
|
"api-keys/data/storage/webdav/Main"
|
||||||
"api-keys/data/storage/webdav/Media"
|
"api-keys/data/storage/webdav/Media"
|
||||||
|
@ -169,6 +170,14 @@
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
restic_jellyfin_htpasswd = {
|
||||||
|
user = "storage";
|
||||||
|
group = "storage";
|
||||||
|
fetchScript = ''
|
||||||
|
simple_get_htpasswd "/api-keys/storage/restic/Jellyfin" "$secretFile"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
webdav_main_htpasswd = {
|
webdav_main_htpasswd = {
|
||||||
user = "storage";
|
user = "storage";
|
||||||
group = "storage";
|
group = "storage";
|
||||||
|
|
9
hosts/hetzner-arm/containers/stream/data/ports.nix
Normal file
9
hosts/hetzner-arm/containers/stream/data/ports.nix
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
mpd = 6600;
|
||||||
|
mpd-opus-low = 4242;
|
||||||
|
mpd-opus-medium = 4243;
|
||||||
|
mpd-opus-high = 4244;
|
||||||
|
mpd-flac = 4245;
|
||||||
|
slskd = 5000;
|
||||||
|
slskd-web = 5001;
|
||||||
|
}
|
9
hosts/hetzner-arm/containers/stream/music/data/ports.nix
Normal file
9
hosts/hetzner-arm/containers/stream/music/data/ports.nix
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
mpd = 6600;
|
||||||
|
mpd-opus-low = 4242;
|
||||||
|
mpd-opus-medium = 4243;
|
||||||
|
mpd-opus-high = 4244;
|
||||||
|
mpd-flac = 4245;
|
||||||
|
slskd = 5000;
|
||||||
|
slskd-web = 5001;
|
||||||
|
}
|
123
hosts/hetzner-arm/containers/stream/music/default.nix
Normal file
123
hosts/hetzner-arm/containers/stream/music/default.nix
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
hostPath,
|
||||||
|
tree,
|
||||||
|
lib,
|
||||||
|
inputs,
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
inherit (lib.modules) mkMerge;
|
||||||
|
inherit (lib.lists) forEach;
|
||||||
|
|
||||||
|
containerName = "music";
|
||||||
|
|
||||||
|
containerAddresses = import "${hostPath}/data/containerAddresses.nix";
|
||||||
|
|
||||||
|
hostIP = containerAddresses.host;
|
||||||
|
containerIP = containerAddresses.containers.${containerName};
|
||||||
|
|
||||||
|
ports = import ./data/ports.nix;
|
||||||
|
|
||||||
|
# these secrets should probs be in host but im lazy
|
||||||
|
containerSecrets = config.containers.${containerName}.config.services.secrets.secrets;
|
||||||
|
pathInContainer = path: "/var/lib/nixos-containers/${containerName}" + path;
|
||||||
|
in {
|
||||||
|
containers.music = {
|
||||||
|
autoStart = true;
|
||||||
|
privateNetwork = true;
|
||||||
|
hostAddress = hostIP;
|
||||||
|
localAddress = containerIP;
|
||||||
|
|
||||||
|
specialArgs = {
|
||||||
|
inherit inputs;
|
||||||
|
inherit tree;
|
||||||
|
inherit self;
|
||||||
|
inherit hostPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {...}: {
|
||||||
|
nixpkgs.pkgs = pkgs;
|
||||||
|
|
||||||
|
imports = with tree;
|
||||||
|
[
|
||||||
|
presets.nixos.containerBase
|
||||||
|
|
||||||
|
profiles.nginx
|
||||||
|
profiles.firewallAllow.httpCommon
|
||||||
|
|
||||||
|
./secrets.nix
|
||||||
|
]
|
||||||
|
++ (with hosts.hetzner-arm.containers.music.profiles; [
|
||||||
|
mpd
|
||||||
|
musicSync
|
||||||
|
soulseek
|
||||||
|
]);
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = with ports; [
|
||||||
|
mpd
|
||||||
|
mpd-opus-low
|
||||||
|
mpd-opus-medium
|
||||||
|
mpd-opus-high
|
||||||
|
mpd-flac
|
||||||
|
slskd
|
||||||
|
slskd-web
|
||||||
|
];
|
||||||
|
|
||||||
|
home-manager.users.root.home.stateVersion = "23.05";
|
||||||
|
system.stateVersion = "23.05";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."soulseek.owo.monster" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://${containerIP}:${toString ports.slskd-web}";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."mpd.owo.monster" = let
|
||||||
|
extraConfig = ''
|
||||||
|
auth_basic "Music Password";
|
||||||
|
auth_basic_user_file ${pathInContainer containerSecrets.music_stream_passwd.path};
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations = mkMerge [
|
||||||
|
{
|
||||||
|
"/flac" = {
|
||||||
|
proxyPass = "http://${containerIP}:${toString ports.mpd-flac}";
|
||||||
|
inherit extraConfig;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(mkMerge (forEach ["low" "medium" "high"] (quality: {
|
||||||
|
"/opus-${quality}" = {
|
||||||
|
proxyPass = "http://${containerIP}:${toString ports."mpd-opus-${quality}"}";
|
||||||
|
inherit extraConfig;
|
||||||
|
};
|
||||||
|
})))
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
networking = {
|
||||||
|
nat.forwardPorts = [
|
||||||
|
{
|
||||||
|
sourcePort = ports.mpd;
|
||||||
|
destination = "${containerIP}\:${toString ports.mpd}";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
sourcePort = ports.slskd;
|
||||||
|
destination = "${containerIP}\:${toString ports.slskd}";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
firewall.allowedTCPPorts = with ports; [
|
||||||
|
mpd
|
||||||
|
slskd
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
69
hosts/hetzner-arm/containers/stream/music/profiles/mpd.nix
Normal file
69
hosts/hetzner-arm/containers/stream/music/profiles/mpd.nix
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
inherit (lib.strings) concatStringsSep;
|
||||||
|
inherit (lib.lists) forEach;
|
||||||
|
|
||||||
|
ports = import ../data/ports.nix;
|
||||||
|
secrets = config.services.secrets.secrets;
|
||||||
|
in {
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
mpc_cli
|
||||||
|
];
|
||||||
|
|
||||||
|
services.mpd = {
|
||||||
|
enable = true;
|
||||||
|
network.listenAddress = "0.0.0.0";
|
||||||
|
musicDirectory = "/Music";
|
||||||
|
credentials = [
|
||||||
|
{
|
||||||
|
passwordFile = "${secrets.mpd_control_password.path}";
|
||||||
|
permissions = ["read" "add" "control" "admin"];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
extraConfig =
|
||||||
|
''
|
||||||
|
host_permissions "127.0.0.1 read,add,control,admin"
|
||||||
|
samplerate_converter "0"
|
||||||
|
metadata_to_use "title,artist"
|
||||||
|
auto_update "yes"
|
||||||
|
audio_buffer_size "4096"
|
||||||
|
replaygain "track"
|
||||||
|
audio_output_format "44100:16:2"
|
||||||
|
''
|
||||||
|
+ concatStringsSep "\n" (forEach ["low" "medium" "high"] (quality: let
|
||||||
|
bitrates = {
|
||||||
|
"low" = "64";
|
||||||
|
"medium" = "96";
|
||||||
|
"high" = "128";
|
||||||
|
};
|
||||||
|
bitrate = bitrates.${quality};
|
||||||
|
in ''
|
||||||
|
audio_output {
|
||||||
|
type "httpd"
|
||||||
|
name "HTTP Opus ${bitrate}k"
|
||||||
|
encoder "opus"
|
||||||
|
port "${toString ports."mpd-opus-${quality}"}"
|
||||||
|
bitrate "${bitrate}000"
|
||||||
|
format "44100:16:2"
|
||||||
|
always_on "yes"
|
||||||
|
tags "yes"
|
||||||
|
signal "music"
|
||||||
|
}
|
||||||
|
''))
|
||||||
|
+ ''
|
||||||
|
audio_output {
|
||||||
|
type "httpd"
|
||||||
|
name "HTTP FLAC"
|
||||||
|
encoder "flac"
|
||||||
|
port "${toString ports.mpd-flac}"
|
||||||
|
format "44100:16:2"
|
||||||
|
always_on "yes"
|
||||||
|
tags "yes"
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
{pkgs, ...}: let
|
||||||
|
inherit (pkgs) writeShellScriptBin;
|
||||||
|
inherit (builtins) toFile;
|
||||||
|
|
||||||
|
rcloneConfig = toFile "rclone.conf" ''
|
||||||
|
[Music]
|
||||||
|
type = webdav
|
||||||
|
url = https://storage-webdav.owo.monster/MusicRO/
|
||||||
|
vendor = other
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
rclone
|
||||||
|
(writeShellScriptBin "rclone-music" ''
|
||||||
|
rclone --config ${rcloneConfig} "$@"
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /Music - mpd mpd"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.music-sync = {
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
after = ["network.target"];
|
||||||
|
partOf = ["mpd.service"];
|
||||||
|
|
||||||
|
path = with pkgs; [bash rclone];
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
set -e
|
||||||
|
rclone --config ${rcloneConfig} sync Music: /Music
|
||||||
|
chown -R mpd:mpd /Music
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.timers.music-sync = {
|
||||||
|
wantedBy = ["timers.target"];
|
||||||
|
partOf = ["music-sync.service"];
|
||||||
|
timerConfig.OnCalendar = "hourly";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.mpd = {
|
||||||
|
after = ["music-copy.service"];
|
||||||
|
serviceConfig = {
|
||||||
|
ReadOnlyPaths = "/Music";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
ports = import ../data/ports.nix;
|
||||||
|
secrets = config.services.secrets.secrets;
|
||||||
|
|
||||||
|
inherit (lib.modules) mkForce;
|
||||||
|
in {
|
||||||
|
services.slskd = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
environmentFile = secrets.slskd_env.path;
|
||||||
|
settings = {
|
||||||
|
remote_configuration = false;
|
||||||
|
remote_file_management = true;
|
||||||
|
soulseek = {
|
||||||
|
username = "chaoticryptidz";
|
||||||
|
description = "chaos's soulseek";
|
||||||
|
listen_port = ports.slskd;
|
||||||
|
};
|
||||||
|
web = {
|
||||||
|
port = ports.slskd-web;
|
||||||
|
authentication = {
|
||||||
|
username = "chaos";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
shares.directories = [
|
||||||
|
"/Music"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
nginx = {
|
||||||
|
enable = true; # I don't think this is even cheked
|
||||||
|
domainName = "soulseek.owo.monster";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."soulseek.owo.monster" = {
|
||||||
|
forceSSL = mkForce false;
|
||||||
|
enableACME = mkForce false;
|
||||||
|
};
|
||||||
|
}
|
57
hosts/hetzner-arm/containers/stream/music/secrets.nix
Normal file
57
hosts/hetzner-arm/containers/stream/music/secrets.nix
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
{pkgs, ...}: {
|
||||||
|
services.secrets = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
vaultLogin = {
|
||||||
|
enable = true;
|
||||||
|
loginUsername = "hetzner-arm-container-music";
|
||||||
|
};
|
||||||
|
|
||||||
|
autoSecrets = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
requiredVaultPaths = [
|
||||||
|
"api-keys/data/mpd"
|
||||||
|
"api-keys/data/music-stream"
|
||||||
|
"passwords/data/soulseek"
|
||||||
|
"passwords/data/slskd"
|
||||||
|
];
|
||||||
|
|
||||||
|
packages = with pkgs; [
|
||||||
|
apacheHttpd
|
||||||
|
];
|
||||||
|
|
||||||
|
secrets = {
|
||||||
|
vault_password = {
|
||||||
|
manual = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
mpd_control_password = {
|
||||||
|
user = "mpd";
|
||||||
|
group = "mpd";
|
||||||
|
fetchScript = ''
|
||||||
|
simple_get "/api-keys/mpd" .password > "$secretFile"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
music_stream_passwd = {
|
||||||
|
user = "nginx";
|
||||||
|
group = "nginx";
|
||||||
|
fetchScript = ''
|
||||||
|
username=$(simple_get "/api-keys/music-stream" .username)
|
||||||
|
password=$(simple_get "/api-keys/music-stream" .password)
|
||||||
|
htpasswd -bc "$secretFile" "$username" "$password" 2>/dev/null
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
slskd_env = {
|
||||||
|
fetchScript = ''
|
||||||
|
soulseek_password=$(simple_get "/passwords/soulseek" .password)
|
||||||
|
slskd_password=$(simple_get "/passwords/slskd" .password)
|
||||||
|
echo > "$secretFile"
|
||||||
|
echo "SLSKD_SLSK_PASSWORD=$soulseek_password" >> "$secretFile"
|
||||||
|
echo "SLSKD_PASSWORD=$slskd_password" >> "$secretFile"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
69
hosts/hetzner-arm/containers/stream/profiles/mpd.nix
Normal file
69
hosts/hetzner-arm/containers/stream/profiles/mpd.nix
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
inherit (lib.strings) concatStringsSep;
|
||||||
|
inherit (lib.lists) forEach;
|
||||||
|
|
||||||
|
ports = import ../data/ports.nix;
|
||||||
|
secrets = config.services.secrets.secrets;
|
||||||
|
in {
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
mpc_cli
|
||||||
|
];
|
||||||
|
|
||||||
|
services.mpd = {
|
||||||
|
enable = true;
|
||||||
|
network.listenAddress = "0.0.0.0";
|
||||||
|
musicDirectory = "/Music";
|
||||||
|
credentials = [
|
||||||
|
{
|
||||||
|
passwordFile = "${secrets.mpd_control_password.path}";
|
||||||
|
permissions = ["read" "add" "control" "admin"];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
extraConfig =
|
||||||
|
''
|
||||||
|
host_permissions "127.0.0.1 read,add,control,admin"
|
||||||
|
samplerate_converter "0"
|
||||||
|
metadata_to_use "title,artist"
|
||||||
|
auto_update "yes"
|
||||||
|
audio_buffer_size "4096"
|
||||||
|
replaygain "track"
|
||||||
|
audio_output_format "44100:16:2"
|
||||||
|
''
|
||||||
|
+ concatStringsSep "\n" (forEach ["low" "medium" "high"] (quality: let
|
||||||
|
bitrates = {
|
||||||
|
"low" = "64";
|
||||||
|
"medium" = "96";
|
||||||
|
"high" = "128";
|
||||||
|
};
|
||||||
|
bitrate = bitrates.${quality};
|
||||||
|
in ''
|
||||||
|
audio_output {
|
||||||
|
type "httpd"
|
||||||
|
name "HTTP Opus ${bitrate}k"
|
||||||
|
encoder "opus"
|
||||||
|
port "${toString ports."mpd-opus-${quality}"}"
|
||||||
|
bitrate "${bitrate}000"
|
||||||
|
format "44100:16:2"
|
||||||
|
always_on "yes"
|
||||||
|
tags "yes"
|
||||||
|
signal "music"
|
||||||
|
}
|
||||||
|
''))
|
||||||
|
+ ''
|
||||||
|
audio_output {
|
||||||
|
type "httpd"
|
||||||
|
name "HTTP FLAC"
|
||||||
|
encoder "flac"
|
||||||
|
port "${toString ports.mpd-flac}"
|
||||||
|
format "44100:16:2"
|
||||||
|
always_on "yes"
|
||||||
|
tags "yes"
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
43
hosts/hetzner-arm/containers/stream/profiles/soulseek.nix
Normal file
43
hosts/hetzner-arm/containers/stream/profiles/soulseek.nix
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
ports = import ../data/ports.nix;
|
||||||
|
secrets = config.services.secrets.secrets;
|
||||||
|
|
||||||
|
inherit (lib.modules) mkForce;
|
||||||
|
in {
|
||||||
|
services.slskd = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
environmentFile = secrets.slskd_env.path;
|
||||||
|
settings = {
|
||||||
|
remote_configuration = false;
|
||||||
|
remote_file_management = true;
|
||||||
|
soulseek = {
|
||||||
|
username = "chaoticryptidz";
|
||||||
|
description = "chaos's soulseek";
|
||||||
|
listen_port = ports.slskd;
|
||||||
|
};
|
||||||
|
web = {
|
||||||
|
port = ports.slskd-web;
|
||||||
|
authentication = {
|
||||||
|
username = "chaos";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
shares.directories = [
|
||||||
|
"/Music"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
nginx = {
|
||||||
|
enable = true; # I don't think this is even cheked
|
||||||
|
domainName = "soulseek.owo.monster";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.virtualHosts."soulseek.owo.monster" = {
|
||||||
|
forceSSL = mkForce false;
|
||||||
|
enableACME = mkForce false;
|
||||||
|
};
|
||||||
|
}
|
57
hosts/hetzner-arm/containers/stream/secrets.nix
Normal file
57
hosts/hetzner-arm/containers/stream/secrets.nix
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
{pkgs, ...}: {
|
||||||
|
services.secrets = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
vaultLogin = {
|
||||||
|
enable = true;
|
||||||
|
loginUsername = "hetzner-arm-container-music";
|
||||||
|
};
|
||||||
|
|
||||||
|
autoSecrets = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
requiredVaultPaths = [
|
||||||
|
"api-keys/data/mpd"
|
||||||
|
"api-keys/data/music-stream"
|
||||||
|
"passwords/data/soulseek"
|
||||||
|
"passwords/data/slskd"
|
||||||
|
];
|
||||||
|
|
||||||
|
packages = with pkgs; [
|
||||||
|
apacheHttpd
|
||||||
|
];
|
||||||
|
|
||||||
|
secrets = {
|
||||||
|
vault_password = {
|
||||||
|
manual = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
mpd_control_password = {
|
||||||
|
user = "mpd";
|
||||||
|
group = "mpd";
|
||||||
|
fetchScript = ''
|
||||||
|
simple_get "/api-keys/mpd" .password > "$secretFile"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
music_stream_passwd = {
|
||||||
|
user = "nginx";
|
||||||
|
group = "nginx";
|
||||||
|
fetchScript = ''
|
||||||
|
username=$(simple_get "/api-keys/music-stream" .username)
|
||||||
|
password=$(simple_get "/api-keys/music-stream" .password)
|
||||||
|
htpasswd -bc "$secretFile" "$username" "$password" 2>/dev/null
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
slskd_env = {
|
||||||
|
fetchScript = ''
|
||||||
|
soulseek_password=$(simple_get "/passwords/soulseek" .password)
|
||||||
|
slskd_password=$(simple_get "/passwords/slskd" .password)
|
||||||
|
echo > "$secretFile"
|
||||||
|
echo "SLSKD_SLSK_PASSWORD=$soulseek_password" >> "$secretFile"
|
||||||
|
echo "SLSKD_PASSWORD=$slskd_password" >> "$secretFile"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -10,5 +10,6 @@
|
||||||
piped-fi = "10.0.1.8";
|
piped-fi = "10.0.1.8";
|
||||||
caldav = "10.0.1.9";
|
caldav = "10.0.1.9";
|
||||||
owncast = "10.0.1.10";
|
owncast = "10.0.1.10";
|
||||||
|
jellyfin = "10.0.1.11";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ in {
|
||||||
"forgejo"
|
"forgejo"
|
||||||
"caldav"
|
"caldav"
|
||||||
"owncast"
|
"owncast"
|
||||||
|
"jellyfin"
|
||||||
] (name: ./containers + "/${name}"))
|
] (name: ./containers + "/${name}"))
|
||||||
++ (with hosts.hetzner-arm.profiles; [
|
++ (with hosts.hetzner-arm.profiles; [
|
||||||
staticSites
|
staticSites
|
||||||
|
|
8
profiles/minimalServer.nix
Normal file
8
profiles/minimalServer.nix
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{lib, ...}: let
|
||||||
|
inherit (lib.modules) mkDefault;
|
||||||
|
in {
|
||||||
|
environment.noXlibs = mkDefault true;
|
||||||
|
documentation.man.enable = mkDefault false;
|
||||||
|
documentation.doc.enable = mkDefault false;
|
||||||
|
fonts.fontconfig.enable = mkDefault false;
|
||||||
|
}
|
Loading…
Reference in a new issue