diff --git a/ct/pinepods.sh b/ct/pinepods.sh new file mode 100644 index 0000000000..8d028b7653 --- /dev/null +++ b/ct/pinepods.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/DevScripts/main" +_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func" +source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func") +# Copyright (c) 2021-2026 community-scripts ORG +# Author: untect (untect) +# License: MIT | https://github.com/community-scripts/DevScripts/raw/main/LICENSE +# Source: https://github.com/madeofpendletonwool/PinePods + +APP="PinePods" +var_tags="${var_tags:-podcast;media}" +var_cpu="${var_cpu:-4}" +var_ram="${var_ram:-4096}" +var_disk="${var_disk:-10}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +#var_arm64="${var_arm64:-no}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + + if [[ ! -d /opt/pinepods ]]; then + msg_error "No PinePods Installation Found!" + exit + fi + + if check_for_gh_release "pinepods" "madeofpendletonwool/PinePods"; then + msg_info "Stopping PinePods" + systemctl stop pinepods + msg_ok "Stopped PinePods" + + RUST_PROFILE="minimal" setup_rust + CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="downloads backups local-media" fetch_and_deploy_gh_release "pinepods" "madeofpendletonwool/PinePods" "tarball" + PYTHON_VERSION=$(sed -n 's/^FROM python:\([0-9.]*\)-.*/\1/p' /opt/pinepods/dockerfile) + TRUNK_VERSION=$(sed -n 's#.*trunk/releases/download/\(v[0-9.]*\)/.*#\1#p' /opt/pinepods/dockerfile) + if [[ -z "$PYTHON_VERSION" || -z "$TRUNK_VERSION" ]]; then + msg_error "Could not read the Python or trunk version from the PinePods dockerfile" + exit 1 + fi + setup_uv + fetch_and_deploy_gh_release "trunk" "trunk-rs/trunk" "prebuild" "$TRUNK_VERSION" "/usr/local/bin" "trunk-$(arch_resolve "x86_64" "aarch64")-unknown-linux-gnu.tar.gz" + + msg_info "Rebuilding PinePods" + $STD rustup target add wasm32-unknown-unknown + cd /opt/pinepods/web + RUSTFLAGS="--cfg=web_sys_unstable_apis --cfg getrandom_backend=\"wasm_js\"" $STD trunk build --features server_build --release + rm -rf /var/www/html + cp -r /opt/pinepods/web/dist /var/www/html + cp -r /opt/pinepods/web/src/translations /var/www/html/static/translations + cd /opt/pinepods/rust-api + $STD cargo build --release + cp /opt/pinepods/rust-api/target/release/pinepods-api /usr/local/bin/pinepods-api + rm -rf /opt/pinepods/web/target /opt/pinepods/web/dist /opt/pinepods/rust-api/target + cp ~/.pinepods /pinepods/current_version + msg_ok "Rebuilt PinePods" + + msg_info "Updating Python Runtime" + $STD uv venv --python "$PYTHON_VERSION" /opt/pinepods/.venv + $STD uv pip install --python /opt/pinepods/.venv/bin/python -r /opt/pinepods/requirements.txt + msg_ok "Updated Python Runtime" + + msg_info "Starting PinePods" + systemctl start pinepods + msg_ok "Started PinePods" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed Successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW}Access it using the following URL:${CL}" +echo -e "${GATEWAY}${BGN}http://${IP}:8040${CL}" diff --git a/install/pinepods-install.sh b/install/pinepods-install.sh new file mode 100644 index 0000000000..877dd76dae --- /dev/null +++ b/install/pinepods-install.sh @@ -0,0 +1,257 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: untect +# License: MIT | https://github.com/community-scripts/DevScripts/raw/main/LICENSE +# Source: https://github.com/madeofpendletonwool/PinePods + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y \ + build-essential \ + pkg-config \ + libssl-dev \ + libmariadb-dev \ + nginx \ + valkey-server +msg_ok "Installed Dependencies" + +PG_VERSION="18" setup_postgresql +PG_DB_NAME="pinepods_database" PG_DB_USER="pinepods" setup_postgresql_db +RUST_PROFILE="minimal" setup_rust +setup_ffmpeg + +fetch_and_deploy_gh_release "yt-dlp" "yt-dlp/yt-dlp" "singlefile" "latest" "/usr/local/bin" "$(arch_resolve "yt-dlp_linux" "yt-dlp_linux_aarch64")" +fetch_and_deploy_gh_release "pinepods" "madeofpendletonwool/PinePods" "tarball" + +PYTHON_VERSION=$(sed -n 's/^FROM python:\([0-9.]*\)-.*/\1/p' /opt/pinepods/dockerfile) +TRUNK_VERSION=$(sed -n 's#.*trunk/releases/download/\(v[0-9.]*\)/.*#\1#p' /opt/pinepods/dockerfile) +if [[ -z "$PYTHON_VERSION" || -z "$TRUNK_VERSION" ]]; then + msg_error "Could not read the Python or trunk version from the PinePods dockerfile" + exit 1 +fi +setup_uv +fetch_and_deploy_gh_release "trunk" "trunk-rs/trunk" "prebuild" "$TRUNK_VERSION" "/usr/local/bin" "trunk-$(arch_resolve "x86_64" "aarch64")-unknown-linux-gnu.tar.gz" + +msg_info "Building PinePods" +$STD rustup target add wasm32-unknown-unknown +cd /opt/pinepods/web +RUSTFLAGS="--cfg=web_sys_unstable_apis --cfg getrandom_backend=\"wasm_js\"" $STD trunk build --features server_build --release +rm -rf /var/www/html +cp -r /opt/pinepods/web/dist /var/www/html +cp -r /opt/pinepods/web/src/translations /var/www/html/static/translations +cd /opt/pinepods/rust-api +$STD cargo build --release +cp /opt/pinepods/rust-api/target/release/pinepods-api /usr/local/bin/pinepods-api +rm -rf /opt/pinepods/web/target /opt/pinepods/web/dist /opt/pinepods/rust-api/target +msg_ok "Built PinePods" + +msg_info "Setting up Python Runtime" +$STD uv venv --python "$PYTHON_VERSION" /opt/pinepods/.venv +$STD uv pip install --python /opt/pinepods/.venv/bin/python -r /opt/pinepods/requirements.txt +msg_ok "Set up Python Runtime" + +msg_info "Configuring PinePods" +mkdir -p /opt/pinepods/{downloads,backups,local-media} /opt/pinepods_data /pinepods +cp ~/.pinepods /pinepods/current_version +cat </opt/pinepods_data/.env +DB_TYPE=postgresql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_USER=${PG_DB_USER} +DB_PASSWORD=${PG_DB_PASS} +DB_NAME=${PG_DB_NAME} +VALKEY_HOST=127.0.0.1 +VALKEY_PORT=6379 +SEARCH_API_URL=https://search.pinepods.online/api/search +PEOPLE_API_URL=https://people.pinepods.online +HOSTNAME=http://${LOCAL_IP}:8040 +SERVER_URL=http://${LOCAL_IP}:8040 +DEFAULT_LANGUAGE=en +DEBUG_MODE=false +EOF +chmod 600 /opt/pinepods_data/.env +msg_ok "Configured PinePods" + +msg_info "Configuring Nginx" +cat <<'EOF' >/etc/nginx/sites-available/pinepods +map $http_x_forwarded_proto $fwd_proto { + default $http_x_forwarded_proto; + '' $scheme; +} +map $http_x_forwarded_host $fwd_host { + default $http_x_forwarded_host; + '' $http_host; +} + +server { + listen 8040; + server_name _; + client_max_body_size 0; + + root /var/www/html; + index index.html; + + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always; + add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; + + location /rss/ { + rewrite ^/rss/(\d+)(?:/(\d+))?$ /api/feed/$1$2 last; + proxy_pass http://127.0.0.1:8032; + proxy_set_header Host $http_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-Host $fwd_host; + proxy_set_header X-Forwarded-Proto $fwd_proto; + proxy_set_header Api-Key $arg_api_key; + add_header Content-Type "application/rss+xml; charset=utf-8"; + expires 1h; + add_header Cache-Control "public, no-transform"; + } + + location / { + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + try_files $uri $uri/ /index.html; + } + + location /api { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always; + add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; + + proxy_pass http://127.0.0.1:8032; + proxy_set_header Host $http_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-Host $fwd_host; + proxy_set_header X-Forwarded-Proto $fwd_proto; + + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + } + + location ~ ^/(api/2|auth|subscriptions|devices|updates|episodes|settings|lists|favorites|sync-devices|search|suggestions|toplist|tag|tags|data)/ { + proxy_pass http://127.0.0.1:8032; + proxy_set_header Host $http_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-Host $fwd_host; + proxy_set_header X-Forwarded-Proto $fwd_proto; + + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always; + + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + + proxy_read_timeout 300; + proxy_send_timeout 300; + } + + location /api/gpodder { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always; + add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; + + proxy_pass http://127.0.0.1:8032; + proxy_set_header Host $http_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-Host $fwd_host; + proxy_set_header X-Forwarded-Proto $fwd_proto; + proxy_read_timeout 1800; + proxy_send_timeout 1800; + + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Api-Key,Authorization' always; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + } + + location /ws/api/ { + proxy_pass http://127.0.0.1:8032; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $http_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-Host $fwd_host; + proxy_set_header X-Forwarded-Proto $fwd_proto; + proxy_read_timeout 86400; + proxy_send_timeout 86400; + } + + location ~* \.wasm$ { + types { + application/wasm wasm; + } + } +} +EOF +nginx_enable_site "pinepods" +msg_ok "Configured Nginx" + +msg_info "Creating Service" +cat </etc/systemd/system/pinepods.service +[Unit] +Description=PinePods Service +After=network.target postgresql.service valkey-server.service +Requires=postgresql.service valkey-server.service + +[Service] +Type=simple +User=root +TimeoutStartSec=infinity +WorkingDirectory=/opt/pinepods +EnvironmentFile=/opt/pinepods_data/.env +ExecStartPre=/opt/pinepods/.venv/bin/python /opt/pinepods/startup/setup_database_new.py +ExecStart=/usr/local/bin/pinepods-api +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now pinepods +msg_ok "Created Service" + +motd_ssh +customize +cleanup_lxc diff --git a/json/pinepods.json b/json/pinepods.json new file mode 100644 index 0000000000..044182cc1f --- /dev/null +++ b/json/pinepods.json @@ -0,0 +1,45 @@ +{ + "name": "PinePods", + "slug": "pinepods", + "categories": [ + 13 + ], + "date_created": "2026-11-02", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 8040, + "documentation": "https://www.pinepods.online/docs/Introduction", + "website": "https://www.pinepods.online/", + "repository": "https://github.com/madeofpendletonwool/PinePods", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/pinepods.webp", + "description": "Pinepods is a complete podcast management system that allows you to play, download, and keep track of podcasts you enjoy. All self hosted and enjoyed on your own server!", + "install_methods": [ + { + "type": "default", + "script": "ct/pinepods.sh", + "config_path": "/opt/pinepods_data/.env", + "resources": { + "cpu": 4, + "ram": 4096, + "hdd": 10, + "os": "Debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "There is no default account. The first visit to the web UI asks you to create the admin user.", + "type": "info" + }, + { + "text": "HOSTNAME and SERVER_URL in /opt/pinepods_data/.env must match the address you open PinePods at. Change them for a new IP or a reverse proxy, then run systemctl restart pinepods.", + "type": "info" + } + ] +}