diff --git a/configuration/hosts/hainich/configuration.nix b/configuration/hosts/hainich/configuration.nix
index 64d5c63..fcdcc96 100644
--- a/configuration/hosts/hainich/configuration.nix
+++ b/configuration/hosts/hainich/configuration.nix
@@ -15,6 +15,7 @@
# ./k8s.nix
./services/docker.nix
./services/gitlab-runner.nix
+ ./services/funkwhale.nix
];
boot.loader.grub.enable = true;
boot.loader.grub.version = 2;
diff --git a/configuration/hosts/hainich/services/funkwhale.nix b/configuration/hosts/hainich/services/funkwhale.nix
new file mode 100644
index 0000000..de1a18a
--- /dev/null
+++ b/configuration/hosts/hainich/services/funkwhale.nix
@@ -0,0 +1,55 @@
+{ config, lib, pkgs, ... }:
+
+{
+ containers.funkwhale = {
+ inherit pkgs;
+ privateNetwork = true;
+ hostAddress = "192.168.100.1";
+ localAddress = "192.168.100.4";
+ autoStart = true;
+ config = { config, lib, pkgs, ... }: {
+ imports = [
+ ../../../../modules
+ ];
+ services.coredns = {
+ enable = true;
+ config = ''
+ .:53 {
+ forward . 1.1.1.1
+ }
+ '';
+ };
+ networking.firewall.enable = false;
+ services.funkwhale = {
+ enable = true;
+ apiIp = "192.168.100.4";
+ hostname = "funkwhale.hacc.media";
+ protocol = "https";
+ defaultFromEmail = "funkwhale@hacc.media";
+ api.djangoSecretKey = "TwsgANNKid+HZ0HwhR/FgTcxFIW6sZ8s4n7HxV6zPdU=";
+ };
+ services.nginx.virtualHosts."funkwhale.hacc.media" = {
+ enableACME = lib.mkForce false;
+ forceSSL = lib.mkForce false;
+ };
+ };
+ };
+ services.nginx.virtualHosts."funkwhale.hacc.media" = {
+ forceSSL = true;
+ enableACME = true;
+ locations."/" = {
+ proxyPass = "http://192.168.100.4";
+ extraConfig = ''
+ proxy_pass_request_headers on;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $http_host;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $http_connection;
+ proxy_buffering off;
+ '';
+ };
+ };
+}
diff --git a/modules/default.nix b/modules/default.nix
index 44820de..8a70e68 100644
--- a/modules/default.nix
+++ b/modules/default.nix
@@ -8,5 +8,6 @@ let
in {
imports = [
"${immaeNix}/modules/webapps/peertube.nix"
+ ./funkwhale
];
}
diff --git a/modules/funkwhale/default.nix b/modules/funkwhale/default.nix
new file mode 100644
index 0000000..d88fe11
--- /dev/null
+++ b/modules/funkwhale/default.nix
@@ -0,0 +1,563 @@
+{config, lib, pkgs, ...}:
+
+with lib;
+
+let
+ django-cacheops = with final; with pkgs.python3.pkgs; ( buildPythonPackage rec {
+ pname = "django-cacheops";
+ version = "5.1";
+
+ src = fetchPypi {
+ inherit pname version;
+ sha256 = "sha256-1YUc178whzhKH87PqN3bj1UDDu39b98SciW3W8oPmd0=";
+ };
+ propagatedBuildInputs = [ django redis six funcy ];
+ doCheck = false;
+ });
+ pythonEnv = (pkgs.python3.override {
+ packageOverrides = self: super: rec {
+ django = self.django_2_2;
+ };
+ }).withPackages (ps: [
+ django-cacheops
+ ps.aioredis
+ ps.aiohttp
+ ps.arrow
+ ps.autobahn
+ ps.av
+ ps.bleach
+ ps.boto3
+ ps.celery
+ ps.channels
+ ps.channels-redis
+ ps.click
+ ps.django
+ ps.django-allauth
+ ps.django-auth-ldap
+ ps.django-oauth-toolkit
+ ps.django-cleanup
+ ps.django-cors-headers
+ ps.django-dynamic-preferences
+ ps.django_environ
+ ps.django-filter
+ ps.django_redis
+ ps.django-rest-auth
+ ps.djangorestframework
+ ps.djangorestframework-jwt
+ ps.django-storages
+ ps.django_taggit
+ ps.django-versatileimagefield
+ ps.feedparser
+ ps.gunicorn
+ ps.kombu
+ ps.ldap
+ ps.markdown
+ ps.mutagen
+ ps.musicbrainzngs
+ ps.pillow
+ ps.pendulum
+ ps.persisting-theory
+ ps.psycopg2
+ ps.pyacoustid
+ ps.pydub
+ ps.PyLD
+ ps.pymemoize
+ ps.pyopenssl
+ ps.python_magic
+ ps.pytz
+ ps.redis
+ ps.requests
+ ps.requests-http-signature
+ ps.service-identity
+ ps.unidecode
+ ps.unicode-slugify
+ ps.uvicorn
+ ps.watchdog
+ ]);
+ cfg = config.services.funkwhale;
+ databasePassword = if (cfg.database.passwordFile != null)
+ then builtins.readFile cfg.database.passwordFile
+ else cfg.database.password;
+ databaseUrl = if (cfg.database.createLocally && cfg.database.socket != null)
+ then "postgresql:///${cfg.database.name}?host=${cfg.database.socket}"
+ else "postgresql://${cfg.database.user}:${databasePassword}@${cfg.database.host}:${toString cfg.database.port}/${cfg.database.name}";
+
+ funkwhaleEnvironment = [
+ "FUNKWHALE_URL=${cfg.hostname}"
+ "FUNKWHALE_HOSTNAME=${cfg.hostname}"
+ "FUNKWHALE_PROTOCOL=${cfg.protocol}"
+ "EMAIL_CONFIG=${cfg.emailConfig}"
+ "DEFAULT_FROM_EMAIL=${cfg.defaultFromEmail}"
+ "REVERSE_PROXY_TYPE=nginx"
+ "DATABASE_URL=${databaseUrl}"
+ "CACHE_URL=redis://localhost:${toString config.services.redis.port}/0"
+ "MEDIA_ROOT=${cfg.api.mediaRoot}"
+ "STATIC_ROOT=${cfg.api.staticRoot}"
+ "DJANGO_SECRET_KEY=${cfg.api.djangoSecretKey}"
+ "RAVEN_ENABLED=${boolToString cfg.enableRaven}"
+ "RAVEN_DSN=${cfg.ravenDsn}"
+ "MUSIC_DIRECTORY_PATH=${cfg.musicDirectoryPath}"
+ "MUSIC_DIRECTORY_SERVE_PATH=${cfg.musicDirectoryPath}"
+ "FUNKWHALE_FRONTEND_PATH=${cfg.dataDir}/front/dist"
+ ];
+ funkwhaleEnvFileData = builtins.concatStringsSep "\n" funkwhaleEnvironment;
+ funkwhaleEnvScriptData = builtins.concatStringsSep " " funkwhaleEnvironment;
+
+ funkwhaleEnvFile = pkgs.writeText "funkwhale.env" funkwhaleEnvFileData;
+ funkwhaleEnv = {
+ ENV_FILE = "${funkwhaleEnvFile}";
+ };
+in
+ {
+
+ options = {
+ services.funkwhale = {
+ enable = mkEnableOption "funkwhale";
+
+ user = mkOption {
+ type = types.str;
+ default = "funkwhale";
+ description = "User under which Funkwhale is ran.";
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = "funkwhale";
+ description = "Group under which Funkwhale is ran.";
+ };
+
+ database = {
+ host = mkOption {
+ type = types.str;
+ default = "localhost";
+ description = "Database host address.";
+ };
+
+ port = mkOption {
+ type = types.int;
+ default = 5432;
+ defaultText = "5432";
+ description = "Database host port.";
+ };
+
+ name = mkOption {
+ type = types.str;
+ default = "funkwhale";
+ description = "Database name.";
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = "funkwhale";
+ description = "Database user.";
+ };
+
+ password = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ The password corresponding to .
+ Warning: this is stored in cleartext in the Nix store!
+ Use instead.
+ '';
+ };
+
+ passwordFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ example = "/run/keys/funkwhale-dbpassword";
+ description = ''
+ A file containing the password corresponding to
+ .
+ '';
+ };
+
+ socket = mkOption {
+ type = types.nullOr types.path;
+ default = "/run/postgresql";
+ defaultText = "/run/postgresql";
+ example = "/run/postgresql";
+ description = "Path to the unix socket file to use for authentication for local connections.";
+ };
+
+ createLocally = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Create the database and database user locally.";
+ };
+ };
+
+ dataDir = mkOption {
+ type = types.str;
+ default = "/srv/funkwhale";
+ description = ''
+ Where to keep the funkwhale data.
+ '';
+ };
+
+ apiIp = mkOption {
+ type = types.str;
+ default = "127.0.0.1";
+ description = ''
+ Funkwhale API IP.
+ '';
+ };
+
+ webWorkers = mkOption {
+ type = types.int;
+ default = 1;
+ description = ''
+ Funkwhale number of web workers.
+ '';
+ };
+
+ apiPort = mkOption {
+ type = types.port;
+ default = 5000;
+ description = ''
+ Funkwhale API Port.
+ '';
+ };
+
+ hostname = mkOption {
+ type = types.str;
+ description = ''
+ The definitive, public domain you will use for your instance.
+ '';
+ example = "funkwhale.yourdomain.net";
+ };
+
+ protocol = mkOption {
+ type = types.enum [ "http" "https" ];
+ default = "https";
+ description = ''
+ Web server protocol.
+ '';
+ };
+
+ emailConfig = mkOption {
+ type = types.str;
+ default = "consolemail://";
+ description = ''
+ Configure email sending. By default, it outputs emails to console instead of sending them. See https://docs.funkwhale.audio/configuration.html#email-config for details.
+ '';
+ example = "smtp+ssl://user@:password@youremail.host:465";
+ };
+
+ defaultFromEmail = mkOption {
+ type = types.str;
+ description = ''
+ The email address to use to send system emails.
+ '';
+ example = "funkwhale@yourdomain.net";
+ };
+
+ api = {
+ mediaRoot = mkOption {
+ type = types.str;
+ default = "/srv/funkwhale/media";
+ description = ''
+ Where media files (such as album covers or audio tracks) should be stored on your system ? Ensure this directory actually exists.
+ '';
+ };
+
+ staticRoot = mkOption {
+ type = types.str;
+ default = "/srv/funkwhale/static";
+ description = ''
+ Where static files (such as API css or icons) should be compiled on your system ? Ensure this directory actually exists.
+ '';
+ };
+
+ djangoSecretKey = mkOption {
+ type = types.str;
+ description = ''
+ Django secret key. Generate one using `openssl rand -base64 45` for example.
+ '';
+ example = "6VhAWVKlqu/dJSdz6TVgEJn/cbbAidwsFvg9ddOwuPRssEs0OtzAhJxLcLVC";
+ };
+ };
+
+ musicDirectoryPath = mkOption {
+ type = types.str;
+ default = "/srv/funkwhale/music";
+ description = ''
+ In-place import settings.
+ '';
+ };
+
+ enableRaven = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Sentry/Raven error reporting (server side).
+ Enable Raven if you want to help improve funkwhale by
+ automatically sending error reports to the funkwhale developers Sentry instance.
+ This will help them detect and correct bugs.
+ '';
+ };
+
+ ravenDsn = mkOption {
+ type = types.str;
+ default = "https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5";
+ description = ''
+ Sentry/Raven DSN.
+ The default is the Funkwhale developers instance DSN.
+ '';
+ };
+
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [
+ { assertion = cfg.database.passwordFile != null || cfg.database.password != "" || cfg.database.socket != null;
+ message = "one of services.funkwhale.database.socket, services.funkwhale.database.passwordFile, or services.funkwhale.database.password must be set";
+ }
+ { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
+ message = "services.funkwhale.database.user must be set to ${cfg.user} if services.funkwhale.database.createLocally is set true";
+ }
+ { assertion = cfg.database.createLocally -> cfg.database.socket != null;
+ message = "services.funkwhale.database.socket must be set if services.funkwhale.database.createLocally is set to true";
+ }
+ { assertion = cfg.database.createLocally -> cfg.database.host == "localhost";
+ message = "services.funkwhale.database.host must be set to localhost if services.funkwhale.database.createLocally is set to true";
+ }
+ ];
+
+ users.users.funkwhale = mkIf (cfg.user == "funkwhale")
+ { name = "funkwhale";
+ group = cfg.group;
+ };
+
+ users.groups.funkwhale = mkIf (cfg.group == "funkwhale") { name = "funkwhale"; };
+
+ services.postgresql = mkIf cfg.database.createLocally {
+ enable = true;
+ ensureDatabases = [ cfg.database.name ];
+ ensureUsers = [
+ { name = cfg.database.user;
+ ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+ }
+ ];
+ };
+
+ services.redis.enable = true;
+
+ services.nginx = {
+ enable = true;
+ appendHttpConfig = ''
+ upstream funkwhale-api {
+ server ${cfg.apiIp}:${toString cfg.apiPort};
+ }
+ '';
+ virtualHosts =
+ let proxyConfig = ''
+ # global proxy conf
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host:$server_port;
+ proxy_set_header X-Forwarded-Port $server_port;
+ proxy_redirect off;
+
+ # websocket support
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ '';
+ withSSL = cfg.protocol == "https";
+ in {
+ "${cfg.hostname}" = {
+ enableACME = withSSL;
+ forceSSL = withSSL;
+ root = "${pkgs.funkwhale}/front";
+ # gzip config is nixos nginx recommendedGzipSettings with gzip_types from funkwhale doc (https://docs.funkwhale.audio/changelog.html#id5)
+ extraConfig = ''
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; object-src 'none'; media-src 'self' data:";
+ add_header Referrer-Policy "strict-origin-when-cross-origin";
+
+ gzip on;
+ gzip_disable "msie6";
+ gzip_proxied any;
+ gzip_comp_level 5;
+ gzip_types
+ application/javascript
+ application/vnd.geo+json
+ application/vnd.ms-fontobject
+ application/x-font-ttf
+ application/x-web-app-manifest+json
+ font/opentype
+ image/bmp
+ image/svg+xml
+ image/x-icon
+ text/cache-manifest
+ text/css
+ text/plain
+ text/vcard
+ text/vnd.rim.location.xloc
+ text/vtt
+ text/x-component
+ text/x-cross-domain-policy;
+ gzip_vary on;
+ '';
+ locations = {
+ "/" = {
+ extraConfig = proxyConfig;
+ proxyPass = "http://funkwhale-api/";
+ };
+ "/front/" = {
+ alias = "${pkgs.funkwhale}/front/";
+ extraConfig = ''
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; object-src 'none'; media-src 'self' data:";
+ add_header Referrer-Policy "strict-origin-when-cross-origin";
+ expires 30d;
+ add_header Pragma public;
+ add_header Cache-Control "public, must-revalidate, proxy-revalidate";
+ '';
+ };
+ "= /front/embed.html" = {
+ alias = "${pkgs.funkwhale}/front/embed.html";
+ extraConfig = ''
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; object-src 'none'; media-src 'self' data:";
+ add_header Referrer-Policy "strict-origin-when-cross-origin";
+ add_header X-Frame-Options "ALLOW";
+ expires 30d;
+ add_header Pragma public;
+ add_header Cache-Control "public, must-revalidate, proxy-revalidate";
+ '';
+ };
+ "/federation/" = {
+ extraConfig = proxyConfig;
+ proxyPass = "http://funkwhale-api/federation/";
+ };
+ "/rest/" = {
+ extraConfig = proxyConfig;
+ proxyPass = "http://funkwhale-api/api/subsonic/rest/";
+ };
+ "/.well-known/" = {
+ extraConfig = proxyConfig;
+ proxyPass = "http://funkwhale-api/.well-known/";
+ };
+ "/media/".alias = "${cfg.api.mediaRoot}/";
+ "/_protected/media/" = {
+ extraConfig = ''
+ internal;
+ '';
+ alias = "${cfg.api.mediaRoot}/";
+ };
+ "/_protected/music/" = {
+ extraConfig = ''
+ internal;
+ '';
+ alias = "${cfg.musicDirectoryPath}/";
+ };
+ "/staticfiles/".alias = "${cfg.api.staticRoot}/";
+ };
+ };
+ };
+ };
+
+ systemd.tmpfiles.rules = [
+ "d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.group} - -"
+ "d ${cfg.api.mediaRoot} 0755 ${cfg.user} ${cfg.group} - -"
+ "d ${cfg.api.staticRoot} 0755 ${cfg.user} ${cfg.group} - -"
+ "d ${cfg.musicDirectoryPath} 0755 ${cfg.user} ${cfg.group} - -"
+ ];
+
+ systemd.targets.funkwhale = {
+ description = "Funkwhale";
+ wants = ["funkwhale-server.service" "funkwhale-worker.service" "funkwhale-beat.service"];
+ };
+ systemd.services =
+ let serviceConfig = {
+ User = "${cfg.user}";
+ WorkingDirectory = "${pkgs.funkwhale}";
+ EnvironmentFile = "${funkwhaleEnvFile}";
+ };
+ in {
+ funkwhale-psql-init = mkIf cfg.database.createLocally {
+ description = "Funkwhale database preparation";
+ after = [ "redis.service" "postgresql.service" ];
+ wantedBy = [ "funkwhale-init.service" ];
+ before = [ "funkwhale-init.service" ];
+ serviceConfig = {
+ User = "postgres";
+ ExecStart = '' ${config.services.postgresql.package}/bin/psql -d ${cfg.database.name} -c 'CREATE EXTENSION IF NOT EXISTS "unaccent";CREATE EXTENSION IF NOT EXISTS "citext";' '';
+ };
+ };
+ funkwhale-init = {
+ description = "Funkwhale initialization";
+ wantedBy = [ "funkwhale-server.service" "funkwhale-worker.service" "funkwhale-beat.service" ];
+ before = [ "funkwhale-server.service" "funkwhale-worker.service" "funkwhale-beat.service" ];
+ environment = funkwhaleEnv;
+ serviceConfig = {
+ User = "${cfg.user}";
+ Group = "${cfg.group}";
+ };
+ script = ''
+ ${pythonEnv}/bin/python ${pkgs.funkwhale}/manage.py migrate
+ ${pythonEnv}/bin/python ${pkgs.funkwhale}/manage.py collectstatic --no-input
+ if ! test -e ${cfg.dataDir}/createSuperUser.sh; then
+ echo "#!/bin/sh
+
+ ${funkwhaleEnvScriptData} ${pythonEnv}/bin/python ${pkgs.funkwhale}/manage.py createsuperuser" > ${cfg.dataDir}/createSuperUser.sh
+ chmod u+x ${cfg.dataDir}/createSuperUser.sh
+ chown -R ${cfg.user}.${cfg.group} ${cfg.dataDir}
+ fi
+ if ! test -e ${cfg.dataDir}/config; then
+ mkdir -p ${cfg.dataDir}/config
+ ln -s ${funkwhaleEnvFile} ${cfg.dataDir}/config/.env
+ ln -s ${funkwhaleEnvFile} ${cfg.dataDir}/.env
+ fi
+ '';
+ };
+
+ funkwhale-server = {
+ description = "Funkwhale application server";
+ partOf = [ "funkwhale.target" ];
+
+ serviceConfig = serviceConfig // {
+ ExecStart = "${pythonEnv}/bin/gunicorn config.asgi:application -w ${toString cfg.webWorkers} -k uvicorn.workers.UvicornWorker -b ${cfg.apiIp}:${toString cfg.apiPort}";
+ };
+ environment = funkwhaleEnv;
+
+ wantedBy = [ "multi-user.target" ];
+ };
+
+ funkwhale-worker = {
+ description = "Funkwhale celery worker";
+ partOf = [ "funkwhale.target" ];
+
+ serviceConfig = serviceConfig // {
+ RuntimeDirectory = "funkwhaleworker";
+ ExecStart = "${pythonEnv}/bin/celery -A funkwhale_api.taskapp worker -l INFO";
+ };
+ environment = funkwhaleEnv;
+
+ wantedBy = [ "multi-user.target" ];
+ };
+
+ funkwhale-beat = {
+ description = "Funkwhale celery beat process";
+ partOf = [ "funkwhale.target" ];
+
+ serviceConfig = serviceConfig // {
+ RuntimeDirectory = "funkwhalebeat";
+ ExecStart = '' ${pythonEnv}/bin/celery -A funkwhale_api.taskapp beat -l INFO --schedule="/run/funkwhalebeat/celerybeat-schedule.db" --pidfile="/run/funkwhalebeat/celerybeat.pid" '';
+ };
+ environment = funkwhaleEnv;
+
+ wantedBy = [ "multi-user.target" ];
+ };
+
+ };
+
+ };
+
+ meta = {
+ maintainers = with lib.maintainers; [ mmai ];
+ };
+ }
+
diff --git a/pkgs/default.nix b/pkgs/default.nix
index 78f307f..ba9014b 100644
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -19,6 +19,7 @@ let
wasi = import wasiSrc { inherit wasiSrc; pkgs = pkgs // newpkgs; };
peertube = callPackage ./peertube { mylibs = import "${immaeNix}/lib" { inherit pkgs; }; };
alps = callPackage ./alps {};
+ funkwhale = callPackage ./funkwhale {};
};
in newpkgs
diff --git a/pkgs/funkwhale/default.nix b/pkgs/funkwhale/default.nix
new file mode 100644
index 0000000..e0a8ec0
--- /dev/null
+++ b/pkgs/funkwhale/default.nix
@@ -0,0 +1,47 @@
+{ stdenv, fetchurl, unzip }:
+
+# Look for the correct urls for build_front and build_api artifacts on the tags page of the project : https://dev.funkwhale.audio/funkwhale/funkwhale/pipelines?scope=tags
+# Attention : do not use the url "https://dev.funkwhale.audio/funkwhale/funkwhale/-/jobs/artifacts/${release}/download?job=" : it is not guaranteed to be stable
+
+let
+ release = "1.0.1";
+ srcs = {
+ api = fetchurl {
+ url = https://dev.funkwhale.audio/funkwhale/funkwhale/-/jobs/56793/artifacts/download;
+ name = "api.zip";
+ sha256 = "0p21r8kbn7sr33chp7404fi9pm4yz6qhfz4z7gxf3vamg9fbsbsc";
+ };
+ frontend = fetchurl {
+ url = https://dev.funkwhale.audio/funkwhale/funkwhale/-/jobs/56790/artifacts/download;
+ name = "frontend.zip";
+ sha256 = "0hz4d59sva6zi5q53wj3f6yaw5didcl9z148s6rsy2m6gyr8566d";
+ };
+ };
+in stdenv.mkDerivation {
+ name = "funkwhale";
+ version = "${release}";
+ src = srcs.api;
+ nativeBuildInputs = [ unzip ];
+ postPatch = ''
+ substituteInPlace requirements/base.txt \
+ --replace "django-cleanup==3.2.0" django-cleanup
+ '';
+
+ installPhase = ''
+ mkdir $out
+ cp -R ./* $out
+ unzip ${srcs.frontend} -d $out
+ mv $out/front/ $out/front_tmp
+ mv $out/front_tmp/dist $out/front
+ rmdir $out/front_tmp
+ '';
+
+ meta = with stdenv.lib; {
+ description = "A modern, convivial and free music server";
+ homepage = https://funkwhale.audio/;
+ license = licenses.agpl3;
+ platforms = platforms.linux;
+ maintainers = with maintainers; [ mmai ];
+ };
+ }
+