lol

Merge pull request #255033 from AleXoundOS/castopod

castopod: init at 1.6.4

authored by

Alexander Bantyev and committed by
GitHub
299e0b95 09ff92e2

+550
+6
maintainers/maintainer-list.nix
··· 759 759 github = "Alexnortung"; 760 760 githubId = 1552267; 761 761 }; 762 + alexoundos = { 763 + email = "alexoundos@gmail.com"; 764 + github = "AleXoundOS"; 765 + githubId = 464913; 766 + name = "Alexander Tomokhov"; 767 + }; 762 768 alexshpilkin = { 763 769 email = "ashpilkin@gmail.com"; 764 770 github = "alexshpilkin";
+2
nixos/doc/manual/release-notes/rl-2311.section.md
··· 41 41 42 42 - [GoToSocial](https://gotosocial.org/), an ActivityPub social network server, written in Golang. Available as [services.gotosocial](#opt-services.gotosocial.enable). 43 43 44 + - [Castopod](https://castopod.org/), an open-source hosting platform made for podcasters who want to engage and interact with their audience. Available as [services.castopod](#opt-services.castopod.enable). 45 + 44 46 - [Typesense](https://github.com/typesense/typesense), a fast, typo-tolerant search engine for building delightful search experiences. Available as [services.typesense](#opt-services.typesense.enable). 45 47 46 48 * [NS-USBLoader](https://github.com/developersu/ns-usbloader/), an all-in-one tool for managing Nintendo Switch homebrew. Available as [programs.ns-usbloader](#opt-programs.ns-usbloader.enable).
+1
nixos/modules/module-list.nix
··· 324 324 ./services/amqp/rabbitmq.nix 325 325 ./services/audio/alsa.nix 326 326 ./services/audio/botamusique.nix 327 + ./services/audio/castopod.nix 327 328 ./services/audio/gmediarender.nix 328 329 ./services/audio/gonic.nix 329 330 ./services/audio/goxlr-utility.nix
+22
nixos/modules/services/audio/castopod.md
··· 1 + # Castopod {#module-services-castopod} 2 + 3 + Castopod is an open-source hosting platform made for podcasters who want to engage and interact with their audience. 4 + 5 + ## Quickstart {#module-services-castopod-quickstart} 6 + 7 + Use the following configuration to start a public instance of Castopod on `castopod.example.com` domain: 8 + 9 + ```nix 10 + networking.firewall.allowedTCPPorts = [ 80 443 ]; 11 + services.castopod = { 12 + enable = true; 13 + database.createLocally = true; 14 + nginx.virtualHost = { 15 + serverName = "castopod.example.com"; 16 + enableACME = true; 17 + forceSSL = true; 18 + }; 19 + }; 20 + ``` 21 + 22 + Go to `https://castopod.example.com/cp-install` to create superadmin account after applying the above configuration.
+287
nixos/modules/services/audio/castopod.nix
··· 1 + { config, lib, pkgs, ... }: 2 + let 3 + cfg = config.services.castopod; 4 + fpm = config.services.phpfpm.pools.castopod; 5 + 6 + user = "castopod"; 7 + stateDirectory = "/var/lib/castopod"; 8 + 9 + # https://docs.castopod.org/getting-started/install.html#requirements 10 + phpPackage = pkgs.php.withExtensions ({ enabled, all }: with all; [ 11 + intl 12 + curl 13 + mbstring 14 + gd 15 + exif 16 + mysqlnd 17 + ] ++ enabled); 18 + in 19 + { 20 + meta.doc = ./castopod.md; 21 + meta.maintainers = with lib.maintainers; [ alexoundos misuzu ]; 22 + 23 + options.services = { 24 + castopod = { 25 + enable = lib.mkEnableOption (lib.mdDoc "Castopod"); 26 + package = lib.mkOption { 27 + type = lib.types.package; 28 + default = pkgs.castopod; 29 + defaultText = lib.literalMD "pkgs.castopod"; 30 + description = lib.mdDoc "Which Castopod package to use."; 31 + }; 32 + database = { 33 + createLocally = lib.mkOption { 34 + type = lib.types.bool; 35 + default = true; 36 + description = lib.mdDoc '' 37 + Create the database and database user locally. 38 + ''; 39 + }; 40 + hostname = lib.mkOption { 41 + type = lib.types.str; 42 + default = "localhost"; 43 + description = lib.mdDoc "Database hostname."; 44 + }; 45 + name = lib.mkOption { 46 + type = lib.types.str; 47 + default = "castopod"; 48 + description = lib.mdDoc "Database name."; 49 + }; 50 + user = lib.mkOption { 51 + type = lib.types.str; 52 + default = user; 53 + description = lib.mdDoc "Database user."; 54 + }; 55 + passwordFile = lib.mkOption { 56 + type = lib.types.nullOr lib.types.path; 57 + default = null; 58 + example = "/run/keys/castopod-dbpassword"; 59 + description = lib.mdDoc '' 60 + A file containing the password corresponding to 61 + [](#opt-services.castopod.database.user). 62 + ''; 63 + }; 64 + }; 65 + settings = lib.mkOption { 66 + type = with lib.types; attrsOf (oneOf [ str int bool ]); 67 + default = { }; 68 + example = { 69 + "email.protocol" = "smtp"; 70 + "email.SMTPHost" = "localhost"; 71 + "email.SMTPUser" = "myuser"; 72 + "email.fromEmail" = "castopod@example.com"; 73 + }; 74 + description = lib.mdDoc '' 75 + Environment variables used for Castopod. 76 + See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example) 77 + for available environment variables. 78 + ''; 79 + }; 80 + environmentFile = lib.mkOption { 81 + type = lib.types.nullOr lib.types.path; 82 + default = null; 83 + example = "/run/keys/castopod-env"; 84 + description = lib.mdDoc '' 85 + Environment file to inject e.g. secrets into the configuration. 86 + See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example) 87 + for available environment variables. 88 + ''; 89 + }; 90 + configureNginx = lib.mkOption { 91 + type = lib.types.bool; 92 + default = true; 93 + description = lib.mdDoc "Configure nginx as a reverse proxy for CastoPod."; 94 + }; 95 + localDomain = lib.mkOption { 96 + type = lib.types.str; 97 + example = "castopod.example.org"; 98 + description = lib.mdDoc "The domain serving your CastoPod instance."; 99 + }; 100 + poolSettings = lib.mkOption { 101 + type = with lib.types; attrsOf (oneOf [ str int bool ]); 102 + default = { 103 + "pm" = "dynamic"; 104 + "pm.max_children" = "32"; 105 + "pm.start_servers" = "2"; 106 + "pm.min_spare_servers" = "2"; 107 + "pm.max_spare_servers" = "4"; 108 + "pm.max_requests" = "500"; 109 + }; 110 + description = lib.mdDoc '' 111 + Options for Castopod's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives. 112 + ''; 113 + }; 114 + }; 115 + }; 116 + 117 + config = lib.mkIf cfg.enable { 118 + services.castopod.settings = 119 + let 120 + sslEnabled = with config.services.nginx.virtualHosts.${cfg.localDomain}; addSSL || forceSSL || onlySSL || enableACME || useACMEHost != null; 121 + baseURL = "http${lib.optionalString sslEnabled "s"}://${cfg.localDomain}"; 122 + in 123 + lib.mapAttrs (name: lib.mkDefault) { 124 + "app.forceGlobalSecureRequests" = sslEnabled; 125 + "app.baseURL" = baseURL; 126 + 127 + "media.baseURL" = "/"; 128 + "media.root" = "media"; 129 + "media.storage" = stateDirectory; 130 + 131 + "admin.gateway" = "admin"; 132 + "auth.gateway" = "auth"; 133 + 134 + "database.default.hostname" = cfg.database.hostname; 135 + "database.default.database" = cfg.database.name; 136 + "database.default.username" = cfg.database.user; 137 + "database.default.DBPrefix" = "cp_"; 138 + 139 + "cache.handler" = "file"; 140 + }; 141 + 142 + services.phpfpm.pools.castopod = { 143 + inherit user; 144 + group = config.services.nginx.group; 145 + phpPackage = phpPackage; 146 + phpOptions = '' 147 + # https://code.castopod.org/adaures/castopod/-/blob/main/docker/production/app/uploads.ini 148 + file_uploads = On 149 + memory_limit = 512M 150 + upload_max_filesize = 500M 151 + post_max_size = 512M 152 + max_execution_time = 300 153 + max_input_time = 300 154 + ''; 155 + settings = { 156 + "listen.owner" = config.services.nginx.user; 157 + "listen.group" = config.services.nginx.group; 158 + } // cfg.poolSettings; 159 + }; 160 + 161 + systemd.services.castopod-setup = { 162 + after = lib.optional config.services.mysql.enable "mysql.service"; 163 + requires = lib.optional config.services.mysql.enable "mysql.service"; 164 + wantedBy = [ "multi-user.target" ]; 165 + path = [ pkgs.openssl phpPackage ]; 166 + script = 167 + let 168 + envFile = "${stateDirectory}/.env"; 169 + media = "${cfg.settings."media.storage"}/${cfg.settings."media.root"}"; 170 + in 171 + '' 172 + mkdir -p ${stateDirectory}/writable/{cache,logs,session,temp,uploads} 173 + 174 + if [ ! -d ${lib.escapeShellArg media} ]; then 175 + cp --no-preserve=mode,ownership -r ${cfg.package}/share/castopod/public/media ${lib.escapeShellArg media} 176 + fi 177 + 178 + if [ ! -f ${stateDirectory}/salt ]; then 179 + openssl rand -base64 33 > ${stateDirectory}/salt 180 + fi 181 + 182 + cat <<'EOF' > ${envFile} 183 + ${lib.generators.toKeyValue { } cfg.settings} 184 + EOF 185 + 186 + echo "analytics.salt=$(cat ${stateDirectory}/salt)" >> ${envFile} 187 + 188 + ${if (cfg.database.passwordFile != null) then '' 189 + echo "database.default.password=$(cat ${lib.escapeShellArg cfg.database.passwordFile})" >> ${envFile} 190 + '' else '' 191 + echo "database.default.password=" >> ${envFile} 192 + ''} 193 + 194 + ${lib.optionalString (cfg.environmentFile != null) '' 195 + cat ${lib.escapeShellArg cfg.environmentFile}) >> ${envFile} 196 + ''} 197 + 198 + php spark castopod:database-update 199 + ''; 200 + serviceConfig = { 201 + StateDirectory = "castopod"; 202 + WorkingDirectory = "${cfg.package}/share/castopod"; 203 + Type = "oneshot"; 204 + RemainAfterExit = true; 205 + User = user; 206 + Group = config.services.nginx.group; 207 + }; 208 + }; 209 + 210 + systemd.services.castopod-scheduled = { 211 + after = [ "castopod-setup.service" ]; 212 + wantedBy = [ "multi-user.target" ]; 213 + path = [ phpPackage ]; 214 + script = '' 215 + php public/index.php scheduled-activities 216 + php public/index.php scheduled-websub-publish 217 + php public/index.php scheduled-video-clips 218 + ''; 219 + serviceConfig = { 220 + StateDirectory = "castopod"; 221 + WorkingDirectory = "${cfg.package}/share/castopod"; 222 + Type = "oneshot"; 223 + User = user; 224 + Group = config.services.nginx.group; 225 + }; 226 + }; 227 + 228 + systemd.timers.castopod-scheduled = { 229 + wantedBy = [ "timers.target" ]; 230 + timerConfig = { 231 + OnCalendar = "*-*-* *:*:00"; 232 + Unit = "castopod-scheduled.service"; 233 + }; 234 + }; 235 + 236 + services.mysql = lib.mkIf cfg.database.createLocally { 237 + enable = true; 238 + package = lib.mkDefault pkgs.mariadb; 239 + ensureDatabases = [ cfg.database.name ]; 240 + ensureUsers = [{ 241 + name = cfg.database.user; 242 + ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; 243 + }]; 244 + }; 245 + 246 + services.nginx = lib.mkIf cfg.configureNginx { 247 + enable = true; 248 + virtualHosts."${cfg.localDomain}" = { 249 + root = lib.mkForce "${cfg.package}/share/castopod/public"; 250 + 251 + extraConfig = '' 252 + try_files $uri $uri/ /index.php?$args; 253 + index index.php index.html; 254 + ''; 255 + 256 + locations."^~ /${cfg.settings."media.root"}/" = { 257 + root = cfg.settings."media.storage"; 258 + extraConfig = '' 259 + add_header Access-Control-Allow-Origin "*"; 260 + expires max; 261 + access_log off; 262 + ''; 263 + }; 264 + 265 + locations."~ \.php$" = { 266 + fastcgiParams = { 267 + SERVER_NAME = "$host"; 268 + }; 269 + extraConfig = '' 270 + fastcgi_intercept_errors on; 271 + fastcgi_index index.php; 272 + fastcgi_pass unix:${fpm.socket}; 273 + try_files $uri =404; 274 + fastcgi_read_timeout 3600; 275 + fastcgi_send_timeout 3600; 276 + ''; 277 + }; 278 + }; 279 + }; 280 + 281 + users.users.${user} = lib.mapAttrs (name: lib.mkDefault) { 282 + description = "Castopod user"; 283 + isSystemUser = true; 284 + group = config.services.nginx.group; 285 + }; 286 + }; 287 + }
+1
nixos/tests/all-tests.nix
··· 158 158 cagebreak = handleTest ./cagebreak.nix {}; 159 159 calibre-web = handleTest ./calibre-web.nix {}; 160 160 calibre-server = handleTest ./calibre-server.nix {}; 161 + castopod = handleTest ./castopod.nix {}; 161 162 cassandra_3_0 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_0; }; 162 163 cassandra_3_11 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_11; }; 163 164 cassandra_4 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_4; };
+87
nixos/tests/castopod.nix
··· 1 + import ./make-test-python.nix ({ pkgs, lib, ... }: 2 + { 3 + name = "castopod"; 4 + meta = with lib.maintainers; { 5 + maintainers = [ alexoundos misuzu ]; 6 + }; 7 + nodes.castopod = { nodes, ... }: { 8 + networking.firewall.allowedTCPPorts = [ 80 ]; 9 + networking.extraHosts = '' 10 + 127.0.0.1 castopod.example.com 11 + ''; 12 + services.castopod = { 13 + enable = true; 14 + database.createLocally = true; 15 + localDomain = "castopod.example.com"; 16 + }; 17 + environment.systemPackages = 18 + let 19 + username = "admin"; 20 + email = "admin@castood.example.com"; 21 + password = "v82HmEp5"; 22 + testRunner = pkgs.writers.writePython3Bin "test-runner" 23 + { 24 + libraries = [ pkgs.python3Packages.selenium ]; 25 + flakeIgnore = [ 26 + "E501" 27 + ]; 28 + } '' 29 + from selenium.webdriver.common.by import By 30 + from selenium.webdriver import Firefox 31 + from selenium.webdriver.firefox.options import Options 32 + from selenium.webdriver.support.ui import WebDriverWait 33 + from selenium.webdriver.support import expected_conditions as EC 34 + 35 + options = Options() 36 + options.add_argument('--headless') 37 + driver = Firefox(options=options) 38 + try: 39 + driver.implicitly_wait(20) 40 + driver.get('http://castopod.example.com/cp-install') 41 + 42 + wait = WebDriverWait(driver, 10) 43 + 44 + wait.until(EC.title_contains("installer")) 45 + 46 + driver.find_element(By.CSS_SELECTOR, '#username').send_keys( 47 + '${username}' 48 + ) 49 + driver.find_element(By.CSS_SELECTOR, '#email').send_keys( 50 + '${email}' 51 + ) 52 + driver.find_element(By.CSS_SELECTOR, '#password').send_keys( 53 + '${password}' 54 + ) 55 + driver.find_element(By.XPATH, "//button[contains(., 'Finish install')]").click() 56 + 57 + wait.until(EC.title_contains("Auth")) 58 + 59 + driver.find_element(By.CSS_SELECTOR, '#email').send_keys( 60 + '${email}' 61 + ) 62 + driver.find_element(By.CSS_SELECTOR, '#password').send_keys( 63 + '${password}' 64 + ) 65 + driver.find_element(By.XPATH, "//button[contains(., 'Login')]").click() 66 + 67 + wait.until(EC.title_contains("Admin dashboard")) 68 + finally: 69 + driver.close() 70 + driver.quit() 71 + ''; 72 + in 73 + [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ]; 74 + }; 75 + testScript = '' 76 + start_all() 77 + castopod.wait_for_unit("castopod-setup.service") 78 + castopod.wait_for_file("/run/phpfpm/castopod.sock") 79 + castopod.wait_for_unit("nginx.service") 80 + castopod.wait_for_open_port(80) 81 + castopod.wait_until_succeeds("curl -sS -f http://castopod.example.com") 82 + castopod.succeed("curl -s http://localhost/cp-install | grep 'Create your Super Admin account' > /dev/null") 83 + 84 + with subtest("Create superadmin and log in"): 85 + castopod.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner") 86 + ''; 87 + })
+53
pkgs/applications/audio/castopod/default.nix
··· 1 + { stdenv 2 + , fetchurl 3 + , ffmpeg-headless 4 + , lib 5 + , nixosTests 6 + , stateDirectory ? "/var/lib/castopod" 7 + }: 8 + stdenv.mkDerivation { 9 + pname = "castopod"; 10 + version = "1.6.4"; 11 + 12 + src = fetchurl { 13 + url = "https://code.castopod.org/adaures/castopod/uploads/ce56d4f149242f12bedd20f9a2b0916d/castopod-1.6.4.tar.gz"; 14 + sha256 = "080jj91yxbn3xsbs0sywzwa2f5in9bp9qi2zwqcfqpaxlq9ga62v"; 15 + }; 16 + 17 + dontBuild = true; 18 + dontFixup = true; 19 + 20 + postPatch = '' 21 + # not configurable at runtime unfortunately: 22 + substituteInPlace app/Config/Paths.php \ 23 + --replace "__DIR__ . '/../../writable'" "'${stateDirectory}/writable'" 24 + 25 + # configuration file must be writable, place it to ${stateDirectory} 26 + substituteInPlace modules/Install/Controllers/InstallController.php \ 27 + --replace "ROOTPATH" "'${stateDirectory}/'" 28 + substituteInPlace public/index.php spark \ 29 + --replace "DotEnv(ROOTPATH)" "DotEnv('${stateDirectory}')" 30 + 31 + # ffmpeg is required for Video Clips feature 32 + substituteInPlace modules/MediaClipper/VideoClipper.php \ 33 + --replace "ffmpeg" "${ffmpeg-headless}/bin/ffmpeg" 34 + substituteInPlace modules/Admin/Controllers/VideoClipsController.php \ 35 + --replace "which ffmpeg" "echo ${ffmpeg-headless}/bin/ffmpeg" 36 + ''; 37 + 38 + installPhase = '' 39 + mkdir -p $out/share/castopod 40 + cp -r . $out/share/castopod 41 + ''; 42 + 43 + passthru.tests.castopod = nixosTests.castopod; 44 + passthru.updateScript = ./update.sh; 45 + 46 + meta = with lib; { 47 + description = "An open-source hosting platform made for podcasters who want to engage and interact with their audience"; 48 + homepage = "https://castopod.org"; 49 + license = licenses.agpl3Only; 50 + maintainers = with maintainers; [ alexoundos misuzu ]; 51 + platforms = platforms.all; 52 + }; 53 + }
+89
pkgs/applications/audio/castopod/update.sh
··· 1 + #! /usr/bin/env nix-shell 2 + #! nix-shell -i bash -p curl jq 3 + set -euo pipefail 4 + 5 + nixpkgs="$(git rev-parse --show-toplevel)" 6 + castopod_nix="$nixpkgs/pkgs/applications/audio/castopod/default.nix" 7 + 8 + # https://www.meetup.com/api/guide/#p02-querying-section 9 + query=' 10 + query allReleases($fullPath: ID!, $first: Int, $last: Int, $before: String, $after: String, $sort: ReleaseSort) { 11 + project(fullPath: $fullPath) { 12 + id 13 + releases( 14 + first: $first 15 + last: $last 16 + before: $before 17 + after: $after 18 + sort: $sort 19 + ) { 20 + nodes { 21 + ...Release 22 + __typename 23 + } 24 + __typename 25 + } 26 + __typename 27 + } 28 + } 29 + 30 + fragment Release on Release { 31 + id 32 + name 33 + tagName 34 + releasedAt 35 + createdAt 36 + upcomingRelease 37 + historicalRelease 38 + assets { 39 + links { 40 + nodes { 41 + id 42 + name 43 + url 44 + directAssetUrl 45 + linkType 46 + __typename 47 + } 48 + __typename 49 + } 50 + __typename 51 + } 52 + __typename 53 + } 54 + ' 55 + variables='{ 56 + "fullPath": "adaures/castopod", 57 + "first": 1, 58 + "sort": "RELEASED_AT_DESC" 59 + }' 60 + 61 + post=$(cat <<EOF 62 + {"query": "$(echo $query)", "variables": $(echo $variables)} 63 + EOF 64 + ) 65 + 66 + json="$(curl -s -X POST https://code.castopod.org/api/graphql \ 67 + -H 'Content-Type: application/json' \ 68 + -d "$post")" 69 + 70 + echo "$json" 71 + TAG=$(echo $json | jq -r '.data.project.releases.nodes[].tagName') 72 + ASSET_URL=$(echo $json | jq -r '.data.project.releases.nodes[].assets.links.nodes[].url' | grep .tar.gz$) 73 + 74 + CURRENT_VERSION=$(nix eval -f "$nixpkgs" --raw castopod.version) 75 + VERSION=${TAG:1} 76 + 77 + if [[ "$CURRENT_VERSION" == "$VERSION" ]]; then 78 + echo "castopod is up-to-date: ${CURRENT_VERSION}" 79 + exit 0 80 + fi 81 + 82 + SHA256=$(nix-prefetch-url "$ASSET_URL") 83 + 84 + URL=$(echo $ASSET_URL | sed -e 's/[\/&]/\\&/g') 85 + 86 + sed -e "s/version =.*;/version = \"$VERSION\";/g" \ 87 + -e "s/url =.*;/url = \"$URL\";/g" \ 88 + -e "s/sha256 =.*;/sha256 = \"$SHA256\";/g" \ 89 + -i "$castopod_nix"
+2
pkgs/top-level/all-packages.nix
··· 3583 3583 3584 3584 callaudiod = callPackage ../applications/audio/callaudiod { }; 3585 3585 3586 + castopod = callPackage ../applications/audio/castopod { }; 3587 + 3586 3588 calls = callPackage ../applications/networking/calls { }; 3587 3589 3588 3590 castnow = callPackage ../tools/networking/castnow { };