Compare commits

..

67 Commits

Author SHA1 Message Date
CanbiZ (MickLesk)
51a8b5365f github: add PocketBase bot workflow
Add a GitHub Actions workflow that listens for `/pocketbase` issue comments and runs a self-hosted job executing an inline Node.js bot. The script authenticates to PocketBase using configured secrets, checks commenter association (OWNER/MEMBER), and supports updating record fields, setting multiline/code-block fields, managing notes, and editing install method resources. It provides GitHub reactions and status comments and uses GITHUB_TOKEN for issue/comment interactions.
2026-03-19 07:20:29 +01:00
community-scripts-pr-app[bot]
2a7b6c8d86 Update CHANGELOG.md (#13073)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-19 05:56:22 +00:00
CanbiZ (MickLesk)
ba01175bc6 core: reorder hwaccel setup and adjust GPU group usermod (#13072)
* fix(tdarr): use curl_with_retry and verify binaries before enabling service

Tdarr_Updater downloads the actual server/node binaries from tdarr.io at
runtime. If tdarr.io is blocked by local DNS (e.g. OPNsense OISD blocklists),
the updater exits silently with code 0, leaving no binaries on disk. The
subsequent systemctl enable then fails with 'Operation not permitted' (exit 1)
because the ExecStart paths don't exist.

Changes:
- Replace bare curl with curl_with_retry for versions.json and Tdarr_Updater.zip
  downloads to gain retry logic, DNS pre-check and exponential backoff
- Add msg_info before Tdarr_Updater run so users see this step in the log
- Check that Tdarr_Server and Tdarr_Node binaries exist after the updater
  runs; fail immediately with a clear message pointing to tdarr.io connectivity
  instead of letting systemctl fail with a confusing 'Operation not permitted'

Fixes: #13030

* Improve Tdarr installer error handling

Refine post-update validation and failure behavior in tdarr-install.sh: remove a redundant status message, simplify the updater check to only require the Tdarr_Server binary, and replace the previous fatal path with msg_error plus an explicit exit 250. This makes failures (for example when tdarr.io is blocked by local DNS) clearer and avoids false negatives from the Tdarr_Node existence check.

* Use curl_with_retry and handle updater failure

Replace direct curl calls with curl_with_retry for fetching versions.json and downloading Tdarr_Updater.zip to improve network reliability. Add a post-update check that verifies /opt/tdarr/Tdarr_Server/Tdarr_Server exists; if missing, log an error suggesting possible DNS blocking and exit with code 250. Minor cleanup of updater artifacts remains unchanged.

* Reorder hwaccel setup and adjust GPU group usermod

Move setup_hwaccel invocations in emby, jellyfin, ollama, and plex installers to occur after package installation/configuration so GPU drivers/repos are present before enabling hardware acceleration. Update _setup_gpu_permissions to call usermod directly (remove $STD wrapper) when adding service users to render/video groups. Includes minor whitespace/ordering cleanups in the installer scripts.
2026-03-19 06:55:56 +01:00
community-scripts-pr-app[bot]
607eff0939 Update .app files (#13064)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-18 21:46:04 +01:00
community-scripts-pr-app[bot]
df51f7114e Update CHANGELOG.md (#13063)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 20:41:28 +00:00
CanbiZ (MickLesk)
79805f5f3d Alpine-Ntfy (#13048) 2026-03-18 21:41:02 +01:00
community-scripts-pr-app[bot]
59c601e0e2 Update CHANGELOG.md (#13061)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 19:59:24 +00:00
CanbiZ (MickLesk)
7c467bee7b Tdarr: use curl_with_retry and correct exit code (#13060) 2026-03-18 20:58:58 +01:00
CanbiZ (MickLesk)
d2e5991416 qf start service (podman) 2026-03-18 18:52:07 +01:00
community-scripts-pr-app[bot]
73e6d4b855 Update CHANGELOG.md (#13059)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 17:37:14 +00:00
community-scripts-pr-app[bot]
a53a851cc9 Update CHANGELOG.md (#13058)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 17:35:58 +00:00
Tobias
2001a43229 reitti: fix: v4 (#13039) 2026-03-18 18:35:43 +01:00
community-scripts-pr-app[bot]
d35249b8f4 Update CHANGELOG.md (#13057)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 17:35:33 +00:00
CanbiZ (MickLesk)
9f73b6756e refactor(podman): replace deprecated commands with Quadlets (#13052) 2026-03-18 18:35:01 +01:00
CanbiZ (MickLesk)
192e2950e7 chore: fix pocketbase workflows 2026-03-18 16:57:20 +01:00
community-scripts-pr-app[bot]
20c4657f39 Update CHANGELOG.md (#13054)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 15:45:45 +00:00
CanbiZ (MickLesk)
e20fed1a2d tools.func Implement pg_cron setup for setup_postgresql (#13053)
* tools.func Implement PostgreSQL setup and upgrade function

Added setup_postgresql function to install or upgrade PostgreSQL, including optional modules and backup restoration.

* correct diff

* Update tools.func

* Update tools.func

* Update tools.func

* Update tools.func
2026-03-18 16:45:11 +01:00
community-scripts-pr-app[bot]
8b4f0f60e1 Update CHANGELOG.md (#13047)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 10:57:01 +00:00
CanbiZ (MickLesk)
bd91c4d07f tools: Centralize GPU group setup via setup_hwaccel (#13044)
improve hardware-acceleration setup to centralize service user group management. Install scripts (emby, plex, ollama, channels) now pass a service user to setup_hwaccel (or no user for channels) and have had inline /etc/group sed/usermod tweaks removed. misc/tools.func updated: setup_hwaccel accepts an optional service_user and forwards it to _setup_gpu_permissions, which now adds the service user to render and video groups if provided. This consolidates GPU permission changes in one place and removes duplicated per-service group edits.
2026-03-18 11:56:32 +01:00
community-scripts-pr-app[bot]
6bb264491c Update CHANGELOG.md (#13046)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 10:56:01 +00:00
CanbiZ (MickLesk)
852e557b1b Refactor: Jellyfin repo, ffmpeg package and symlinks (#13045)
Co-authored-by: Tobias <96661824+CrazyWolf13@users.noreply.github.com>
2026-03-18 11:55:38 +01:00
CanbiZ (MickLesk)
c5caca97fd Update termix.sh 2026-03-18 11:55:32 +01:00
CanbiZ (MickLesk)
dff876fb5c termix: migrate to .env 2026-03-18 11:50:41 +01:00
CanbiZ (MickLesk)
02e7e5af8d fix guacd service 2026-03-18 11:39:41 +01:00
CanbiZ (MickLesk)
0f4bfc0b5a add missing func? 2026-03-18 11:15:02 +01:00
community-scripts-pr-app[bot]
152be10741 Update CHANGELOG.md (#13043)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 09:54:04 +00:00
CanbiZ (MickLesk)
341489ea3f Termix: add guacd build and systemd integration (#12999) 2026-03-18 10:53:34 +01:00
community-scripts-pr-app[bot]
ee7829496f Update .app files (#13037)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-18 08:44:39 +01:00
community-scripts-pr-app[bot]
b67cbc1585 Update CHANGELOG.md (#13036)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 07:44:05 +00:00
push-app-to-main[bot]
bf17edcc89 split-pro (#12975)
* Add split-pro (ct)

* Update ct/split-pro.sh

Co-authored-by: Slaviša Arežina <58952836+tremor021@users.noreply.github.com>

* Enhance setup completion feedback in split-pro.sh

Add success message and access URL after setup completion.

* Refactor dependency installation command

* Apply suggestion from @tremor021

---------

Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
Co-authored-by: Slaviša Arežina <58952836+tremor021@users.noreply.github.com>
Co-authored-by: Tobias <96661824+CrazyWolf13@users.noreply.github.com>
2026-03-18 08:43:40 +01:00
community-scripts-pr-app[bot]
8aa4e5b8cb Update CHANGELOG.md (#13035)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 07:33:23 +00:00
community-scripts-pr-app[bot]
7c770e2ee7 Update CHANGELOG.md (#13034)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 07:33:03 +00:00
CanbiZ (MickLesk)
6d15a22d94 Paperless-NGX: increase default RAM to 3GB (#13018) 2026-03-18 08:32:58 +01:00
CanbiZ (MickLesk)
3db4ac1050 Plex: restart service after update to apply new version (#13017) 2026-03-18 08:32:34 +01:00
community-scripts-pr-app[bot]
16dfad5c77 Update CHANGELOG.md (#13033)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 07:32:08 +00:00
CanbiZ (MickLesk)
5197d759d7 pve-scripts-local: Increase default disk size from 4GB to 10GB (#13009) 2026-03-18 08:31:41 +01:00
CanbiZ (MickLesk)
c073286d53 sparkyfitness switch to pnpm 2026-03-18 08:18:04 +01:00
community-scripts-pr-app[bot]
cbaba2f133 Update CHANGELOG.md (#13032)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 07:01:01 +00:00
CanbiZ (MickLesk)
48afb6c017 tools.func: Implement check_for_gh_tag function (#12998)
* tools.func Implement check_for_gh_tag function

Adds a function to check for new GitHub tags for repositories without releases. (needed for termix / guacd-server)

* Update documentation for check_for_gh_tag function
2026-03-18 08:00:33 +01:00
community-scripts-pr-app[bot]
37f2e0242d Update CHANGELOG.md (#13031)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-18 06:44:35 +00:00
CanbiZ (MickLesk)
7c62147a00 tools.func: Implement fetch_and_deploy_gh_tag function (#13000)
* tools.func: Implement fetch_and_deploy_gh_tag function

Adds function to fetch and deploy GitHub tag-based source tarballs.

* Refactor fetch_and_deploy_gh_tag function and comments

Updated the function to fetch and deploy GitHub tags, enhancing its description and usage instructions.

* cleanuo
2026-03-18 07:44:13 +01:00
community-scripts-pr-app[bot]
7d11f9acd6 Update CHANGELOG.md (#13029)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 23:00:07 +00:00
community-scripts-pr-app[bot]
55a877a3e2 Update CHANGELOG.md (#13028)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 22:59:55 +00:00
CanbiZ (MickLesk)
27c9d7fd07 fix(gluetun): add OpenVPN process user and cleanup stale config (#13016)
- Add OPENVPN_PROCESS_USER=root, PUID=0, PGID=0 to default .env
  to prevent gluetun from injecting 'user' directive into target.ovpn
  which causes 'Unrecognized option' errors on Debian
- Add ExecStartPre to remove stale /etc/openvpn/target.ovpn before start
- Fixes #12988
2026-03-17 23:59:40 +01:00
CanbiZ (MickLesk)
c907e10334 Frigate: check OpenVino model files exist before configuring detector and use curl_with_retry instead of default wget (#13019)
* fix(frigate): check OpenVino model files exist before configuring detector

When the OpenVino model build fails (e.g. TensorFlow import error),
the model files are not created but the config still references them
if the CPU supports avx/sse4_2, causing Frigate to crash on start
with FileNotFoundError.

Now also checks that ssdlite_mobilenet_v2.xml and coco_91cl_bkgr.txt
actually exist before configuring the OpenVino detector, falling back
to CPU model otherwise.

Fixes #12808

* fix(frigate): use curl_with_retry for all downloads

Replace all wget and bare curl calls with curl_with_retry from
tools.func for robust downloads with retry logic, exponential
backoff, and DNS pre-checks. This prevents install failures from
transient network issues during model and dependency downloads.
2026-03-17 23:59:28 +01:00
community-scripts-pr-app[bot]
804c462dd3 Update CHANGELOG.md (#13020)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 20:20:06 +00:00
Slaviša Arežina
9df9a2831e Update (#13008) 2026-03-17 21:19:36 +01:00
community-scripts-pr-app[bot]
4aa83fd98e Update CHANGELOG.md (#12996)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 07:32:12 +00:00
CanbiZ (MickLesk)
6747f0c340 fix broken rocm setup 2026-03-17 08:31:42 +01:00
MickLesk
8ef2c445c8 immich: increase uv timeout to 300 2026-03-16 20:41:21 +01:00
MickLesk
e4a7ed6965 immcih: crop sha from pnpm 2026-03-16 20:36:21 +01:00
MickLesk
c69dae5326 immich: use curl with retry function 2026-03-16 20:23:46 +01:00
MickLesk
fee4617802 qf: add gcc13 fallback and use gcc14 2026-03-16 20:03:45 +01:00
community-scripts-pr-app[bot]
339301947b Update .app files (#12982)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-16 17:56:32 +01:00
community-scripts-pr-app[bot]
5df3c2cd34 Update CHANGELOG.md (#12981)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-16 16:45:48 +00:00
push-app-to-main[bot]
6832e23ff1 Add gluetun (ct) (#12976)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-03-16 17:45:22 +01:00
community-scripts-pr-app[bot]
d08ba7a0c4 Update .app files (#12980)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2026-03-16 17:39:18 +01:00
community-scripts-pr-app[bot]
780c0e055f Update CHANGELOG.md (#12979)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-16 16:34:08 +00:00
push-app-to-main[bot]
c55d0784e2 Anytype-Server (#12974)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-03-16 17:33:32 +01:00
community-scripts-pr-app[bot]
2080603464 Update CHANGELOG.md (#12978)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-16 16:32:45 +00:00
CanbiZ (MickLesk)
13aea57207 fix(immich): use gcc-13 for compilation & add uv python pre-install with retry (#12935)
- Install gcc-13/g++-13 and export CC/CXX before compiling custom
  photo-processing libraries to work around GCC-14 ICE segfaults
  on Debian 13 Trixie (closes #12895)
- Pre-install Python via 'uv python install' with 3-attempt retry
  logic before running 'uv sync' to prevent connection reset failures
  during machine-learning setup (closes #12926)
- Applied to both fresh install and update paths
2026-03-16 17:32:16 +01:00
community-scripts-pr-app[bot]
815cbb4ffc Update CHANGELOG.md (#12965)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-16 10:37:02 +00:00
CanbiZ (MickLesk)
5ee3ad2702 fix(hwaccel): remove ROCm install from AMD APU setup (#12958)
AMD APUs (Radeon 780M/760M/740M and similar integrated graphics) do not
benefit from the full ROCm compute stack in LXC containers. ROCm is a
multi-GB GPGPU framework primarily designed for discrete AMD GPUs and
ML/AI workloads, not for video transcoding with integrated graphics.

For APUs the Mesa VA-API drivers (mesa-va-drivers, mesa-opencl-icd) and
firmware (firmware-amd-graphics) provide all the hardware acceleration
needed for media tasks. Installing ROCm on top adds ~4GB of packages
that frequently fail or time out for this class of hardware.

Discrete AMD GPUs (GPU_TYPE=AMD) are unaffected and still receive ROCm.
2026-03-16 11:36:38 +01:00
CanbiZ (MickLesk)
d06af6aa63 fix(tautulli): add setuptools<81 constraint to update script (#12959)
The update function was missing the setuptools version pin that exists
in the install script. Without it, setuptools 82+ gets installed which
breaks Tautulli's startup (exit code 1/FAILURE).

Fixes #12950
2026-03-16 11:36:16 +01:00
CanbiZ (MickLesk)
be2986075c Seerr: add missing build deps (#12960)
* fix(seerr): add python3-setuptools to install and update deps

node-gyp's bundled node-gyp (v8.4.1) uses distutils which was removed
from Python 3.12+. Adding python3-setuptools provides the distutils
shim needed to compile native sqlite3 bindings.

Also adds build-essential + python3-setuptools before pnpm install in
the update function to match the install script's dependency setup.

Fixes #12939

* fix(seerr): use apt instead of apt-get

* fix(seerr): use ensure_dependencies in update script
2026-03-16 11:36:12 +01:00
community-scripts-pr-app[bot]
c397a64847 Update CHANGELOG.md (#12962)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-16 08:16:10 +00:00
Tobias
16edbdd274 fix: yubal update (#12961) 2026-03-16 09:15:45 +01:00
46 changed files with 2019 additions and 257 deletions

View File

@@ -75,7 +75,8 @@ jobs:
const http = require('http'); const http = require('http');
const url = require('url'); const url = require('url');
function request(fullUrl, opts) { function request(fullUrl, opts, redirectCount) {
redirectCount = redirectCount || 0;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
const u = url.parse(fullUrl); const u = url.parse(fullUrl);
const isHttps = u.protocol === 'https:'; const isHttps = u.protocol === 'https:';
@@ -90,6 +91,13 @@ jobs:
if (body) options.headers['Content-Length'] = Buffer.byteLength(body); if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
const lib = isHttps ? https : http; const lib = isHttps ? https : http;
const req = lib.request(options, function(res) { const req = lib.request(options, function(res) {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
const redirectUrl = url.resolve(fullUrl, res.headers.location);
res.resume();
resolve(request(redirectUrl, opts, redirectCount + 1));
return;
}
let data = ''; let data = '';
res.on('data', function(chunk) { data += chunk; }); res.on('data', function(chunk) { data += chunk; });
res.on('end', function() { res.on('end', function() {

675
.github/workflows/pocketbase-bot.yml generated vendored Normal file
View File

@@ -0,0 +1,675 @@
name: PocketBase Bot
on:
issue_comment:
types: [created]
permissions:
issues: write
pull-requests: write
contents: read
jobs:
pocketbase-bot:
runs-on: self-hosted
# Only act on /pocketbase commands
if: startsWith(github.event.comment.body, '/pocketbase')
steps:
- name: Execute PocketBase bot command
env:
POCKETBASE_URL: ${{ secrets.POCKETBASE_URL }}
POCKETBASE_COLLECTION: ${{ secrets.POCKETBASE_COLLECTION }}
POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_ID: ${{ github.event.comment.id }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
ACTOR: ${{ github.event.comment.user.login }}
ACTOR_ASSOCIATION: ${{ github.event.comment.author_association }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
node << 'ENDSCRIPT'
(async function () {
const https = require('https');
const http = require('http');
const url = require('url');
// ── HTTP helper with redirect following ────────────────────────────
function request(fullUrl, opts, redirectCount) {
redirectCount = redirectCount || 0;
return new Promise(function (resolve, reject) {
const u = url.parse(fullUrl);
const isHttps = u.protocol === 'https:';
const body = opts.body;
const options = {
hostname: u.hostname,
port: u.port || (isHttps ? 443 : 80),
path: u.path,
method: opts.method || 'GET',
headers: opts.headers || {}
};
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
const lib = isHttps ? https : http;
const req = lib.request(options, function (res) {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
const redirectUrl = url.resolve(fullUrl, res.headers.location);
res.resume();
resolve(request(redirectUrl, opts, redirectCount + 1));
return;
}
let data = '';
res.on('data', function (chunk) { data += chunk; });
res.on('end', function () {
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
});
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
// ── GitHub API helpers ─────────────────────────────────────────────
const owner = process.env.REPO_OWNER;
const repo = process.env.REPO_NAME;
const issueNumber = parseInt(process.env.ISSUE_NUMBER, 10);
const commentId = parseInt(process.env.COMMENT_ID, 10);
const actor = process.env.ACTOR;
function ghRequest(path, method, body) {
const headers = {
'Authorization': 'Bearer ' + process.env.GITHUB_TOKEN,
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'PocketBase-Bot'
};
const bodyStr = body ? JSON.stringify(body) : undefined;
if (bodyStr) headers['Content-Type'] = 'application/json';
return request('https://api.github.com' + path, { method: method || 'GET', headers, body: bodyStr });
}
async function addReaction(content) {
try {
await ghRequest(
'/repos/' + owner + '/' + repo + '/issues/comments/' + commentId + '/reactions',
'POST', { content }
);
} catch (e) {
console.warn('Could not add reaction:', e.message);
}
}
async function postComment(text) {
const res = await ghRequest(
'/repos/' + owner + '/' + repo + '/issues/' + issueNumber + '/comments',
'POST', { body: text }
);
if (!res.ok) console.warn('Could not post comment:', res.body);
}
// ── Permission check ───────────────────────────────────────────────
// author_association: OWNER = repo/org owner, MEMBER = org member (includes Contributors team)
const association = process.env.ACTOR_ASSOCIATION;
if (association !== 'OWNER' && association !== 'MEMBER') {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: @' + actor + ' is not authorized to use this command.\n' +
'Only org members (Contributors team) can use `/pocketbase`.'
);
process.exit(0);
}
// ── Acknowledge ────────────────────────────────────────────────────
await addReaction('eyes');
// ── Parse command ──────────────────────────────────────────────────
// Formats (first line of comment):
// /pocketbase <slug> field=value [field=value ...] ← field updates (simple values)
// /pocketbase <slug> set <field> ← value from code block below
// /pocketbase <slug> note list|add|edit|remove ... ← note management
// /pocketbase <slug> method list ← list install methods
// /pocketbase <slug> method <type> cpu=N ram=N hdd=N ← edit install method resources
const commentBody = process.env.COMMENT_BODY || '';
const lines = commentBody.trim().split('\n');
const firstLine = lines[0].trim();
const withoutCmd = firstLine.replace(/^\/pocketbase\s+/, '').trim();
// Extract code block content from comment body (```...``` or ```lang\n...```)
function extractCodeBlock(body) {
const m = body.match(/```[^\n]*\n([\s\S]*?)```/);
return m ? m[1].trim() : null;
}
const codeBlockValue = extractCodeBlock(commentBody);
const HELP_TEXT =
'**Field update (simple):** `/pocketbase <slug> field=value [field=value ...]`\n\n' +
'**Field update (HTML/multiline) — value from code block:**\n' +
'````\n' +
'/pocketbase <slug> set description\n' +
'```html\n' +
'<p>Your <b>HTML</b> or multi-line content here</p>\n' +
'```\n' +
'````\n\n' +
'**Note management:**\n' +
'```\n' +
'/pocketbase <slug> note list\n' +
'/pocketbase <slug> note add <type> "<text>"\n' +
'/pocketbase <slug> note edit <type> "<old text>" "<new text>"\n' +
'/pocketbase <slug> note remove <type> "<text>"\n' +
'```\n\n' +
'**Install method resources:**\n' +
'```\n' +
'/pocketbase <slug> method list\n' +
'/pocketbase <slug> method <type> hdd=10\n' +
'/pocketbase <slug> method <type> cpu=4 ram=2048 hdd=20\n' +
'```\n\n' +
'**Editable fields:** `name` `description` `logo` `documentation` `website` `project_url` `github` ' +
'`config_path` `port` `default_user` `default_passwd` ' +
'`updateable` `privileged` `has_arm` `is_dev` ' +
'`is_disabled` `disable_message` `is_deleted` `deleted_message`';
if (!withoutCmd) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: No slug or command specified.\n\n' + HELP_TEXT);
process.exit(0);
}
const spaceIdx = withoutCmd.indexOf(' ');
const slug = (spaceIdx === -1 ? withoutCmd : withoutCmd.substring(0, spaceIdx)).trim();
const rest = spaceIdx === -1 ? '' : withoutCmd.substring(spaceIdx + 1).trim();
if (!rest) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: No command specified for slug `' + slug + '`.\n\n' + HELP_TEXT);
process.exit(0);
}
// ── Allowed fields and their types ─────────────────────────────────
// ── PocketBase: authenticate (shared by all paths) ─────────────────
const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
const coll = process.env.POCKETBASE_COLLECTION;
const authRes = await request(apiBase + '/collections/users/auth-with-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identity: process.env.POCKETBASE_ADMIN_EMAIL,
password: process.env.POCKETBASE_ADMIN_PASSWORD
})
});
if (!authRes.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: PocketBase authentication failed. CC @' + owner + '/maintainers');
process.exit(1);
}
const token = JSON.parse(authRes.body).token;
// ── PocketBase: find record by slug (shared by all paths) ──────────
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
const filter = "(slug='" + slug.replace(/'/g, "''") + "')";
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
headers: { 'Authorization': token }
});
const list = JSON.parse(listRes.body);
const record = list.items && list.items[0];
if (!record) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: No record found for slug `' + slug + '`.\n\n' +
'Make sure the script was already pushed to PocketBase (JSON must exist and have been synced).'
);
process.exit(0);
}
// ── Route: dispatch to subcommand handler ──────────────────────────
const noteMatch = rest.match(/^note\s+(list|add|edit|remove)\b/i);
const methodMatch = rest.match(/^method\b/i);
const setMatch = rest.match(/^set\s+(\S+)/i);
if (noteMatch) {
// ── NOTE SUBCOMMAND (reads/writes notes_json on script record) ────
const noteAction = noteMatch[1].toLowerCase();
const noteArgsStr = rest.substring(noteMatch[0].length).trim();
// Parse notes_json from the already-fetched script record
// PocketBase may return JSON fields as already-parsed objects
let notesArr = [];
try {
const rawNotes = record.notes_json;
notesArr = Array.isArray(rawNotes) ? rawNotes : JSON.parse(rawNotes || '[]');
} catch (e) { notesArr = []; }
// Token parser: unquoted-word OR "quoted string" (supports \" escapes)
function parseNoteTokens(str) {
const tokens = [];
let pos = 0;
while (pos < str.length) {
while (pos < str.length && /\s/.test(str[pos])) pos++;
if (pos >= str.length) break;
if (str[pos] === '"') {
pos++;
let start = pos;
while (pos < str.length && str[pos] !== '"') {
if (str[pos] === '\\') pos++;
pos++;
}
tokens.push(str.substring(start, pos).replace(/\\"/g, '"'));
if (pos < str.length) pos++;
} else {
let start = pos;
while (pos < str.length && !/\s/.test(str[pos])) pos++;
tokens.push(str.substring(start, pos));
}
}
return tokens;
}
function formatNotesList(arr) {
if (arr.length === 0) return '*None*';
return arr.map(function (n, i) {
return (i + 1) + '. **`' + (n.type || '?') + '`**: ' + (n.text || '');
}).join('\n');
}
async function patchNotesJson(arr) {
const res = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ notes_json: JSON.stringify(arr) })
});
if (!res.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: Failed to update `notes_json`:\n```\n' + res.body + '\n```');
process.exit(1);
}
}
if (noteAction === 'list') {
await addReaction('+1');
await postComment(
' **PocketBase Bot**: Notes for **`' + slug + '`** (' + notesArr.length + ' total)\n\n' +
formatNotesList(notesArr)
);
} else if (noteAction === 'add') {
const tokens = parseNoteTokens(noteArgsStr);
if (tokens.length < 2) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: `note add` requires `<type>` and `"<text>"`.\n\n' +
'**Usage:** `/pocketbase ' + slug + ' note add <type> "<text>"`'
);
process.exit(0);
}
const noteType = tokens[0].toLowerCase();
const noteText = tokens.slice(1).join(' ');
notesArr.push({ type: noteType, text: noteText });
await patchNotesJson(notesArr);
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Added note to **`' + slug + '`**\n\n' +
'- **Type:** `' + noteType + '`\n' +
'- **Text:** ' + noteText + '\n\n' +
'*Executed by @' + actor + '*'
);
} else if (noteAction === 'edit') {
const tokens = parseNoteTokens(noteArgsStr);
if (tokens.length < 3) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: `note edit` requires `<type>`, `"<old text>"`, and `"<new text>"`.\n\n' +
'**Usage:** `/pocketbase ' + slug + ' note edit <type> "<old text>" "<new text>"`\n\n' +
'Use `/pocketbase ' + slug + ' note list` to see current notes.'
);
process.exit(0);
}
const noteType = tokens[0].toLowerCase();
const oldText = tokens[1];
const newText = tokens[2];
const idx = notesArr.findIndex(function (n) {
return n.type.toLowerCase() === noteType && n.text === oldText;
});
if (idx === -1) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: No `' + noteType + '` note found with that exact text.\n\n' +
'**Current notes for `' + slug + '`:**\n' + formatNotesList(notesArr)
);
process.exit(0);
}
notesArr[idx].text = newText;
await patchNotesJson(notesArr);
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Edited note in **`' + slug + '`**\n\n' +
'- **Type:** `' + noteType + '`\n' +
'- **Old:** ' + oldText + '\n' +
'- **New:** ' + newText + '\n\n' +
'*Executed by @' + actor + '*'
);
} else if (noteAction === 'remove') {
const tokens = parseNoteTokens(noteArgsStr);
if (tokens.length < 2) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: `note remove` requires `<type>` and `"<text>"`.\n\n' +
'**Usage:** `/pocketbase ' + slug + ' note remove <type> "<text>"`\n\n' +
'Use `/pocketbase ' + slug + ' note list` to see current notes.'
);
process.exit(0);
}
const noteType = tokens[0].toLowerCase();
const noteText = tokens[1];
const before = notesArr.length;
notesArr = notesArr.filter(function (n) {
return !(n.type.toLowerCase() === noteType && n.text === noteText);
});
if (notesArr.length === before) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: No `' + noteType + '` note found with that exact text.\n\n' +
'**Current notes for `' + slug + '`:**\n' + formatNotesList(notesArr)
);
process.exit(0);
}
await patchNotesJson(notesArr);
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Removed note from **`' + slug + '`**\n\n' +
'- **Type:** `' + noteType + '`\n' +
'- **Text:** ' + noteText + '\n\n' +
'*Executed by @' + actor + '*'
);
}
} else if (methodMatch) {
// ── METHOD SUBCOMMAND (reads/writes install_methods_json on script record) ──
const methodArgs = rest.replace(/^method\s*/i, '').trim();
const methodListMode = !methodArgs || methodArgs.toLowerCase() === 'list';
// Parse install_methods_json from the already-fetched script record
// PocketBase may return JSON fields as already-parsed objects
let methodsArr = [];
try {
const rawMethods = record.install_methods_json;
methodsArr = Array.isArray(rawMethods) ? rawMethods : JSON.parse(rawMethods || '[]');
} catch (e) { methodsArr = []; }
function formatMethodsList(arr) {
if (arr.length === 0) return '*None*';
return arr.map(function (im, i) {
const r = im.resources || {};
return (i + 1) + '. **`' + (im.type || '?') + '`** — CPU: `' + (r.cpu != null ? r.cpu : '?') +
'` · RAM: `' + (r.ram != null ? r.ram : '?') + ' MB` · HDD: `' + (r.hdd != null ? r.hdd : '?') + ' GB`';
}).join('\n');
}
async function patchInstallMethodsJson(arr) {
const res = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ install_methods_json: JSON.stringify(arr) })
});
if (!res.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: Failed to update `install_methods_json`:\n```\n' + res.body + '\n```');
process.exit(1);
}
}
if (methodListMode) {
await addReaction('+1');
await postComment(
' **PocketBase Bot**: Install methods for **`' + slug + '`** (' + methodsArr.length + ' total)\n\n' +
formatMethodsList(methodsArr)
);
} else {
// Parse: <type> cpu=N ram=N hdd=N
const methodParts = methodArgs.match(/^(\S+)\s+(.+)$/);
if (!methodParts) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: Invalid `method` syntax.\n\n' +
'**Usage:**\n```\n/pocketbase ' + slug + ' method list\n/pocketbase ' + slug + ' method <type> hdd=10\n/pocketbase ' + slug + ' method <type> cpu=4 ram=2048 hdd=20\n```'
);
process.exit(0);
}
const targetType = methodParts[1].toLowerCase();
const resourcesStr = methodParts[2];
// Parse resource fields (only cpu/ram/hdd allowed)
const RESOURCE_FIELDS = { cpu: true, ram: true, hdd: true };
const resourceChanges = {};
const rePairs = /([a-z]+)=(\d+)/gi;
let m;
while ((m = rePairs.exec(resourcesStr)) !== null) {
const key = m[1].toLowerCase();
if (RESOURCE_FIELDS[key]) resourceChanges[key] = parseInt(m[2], 10);
}
if (Object.keys(resourceChanges).length === 0) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: No valid resource fields found. Use `cpu=N`, `ram=N`, `hdd=N`.');
process.exit(0);
}
// Find matching method by type name (case-insensitive)
const idx = methodsArr.findIndex(function (im) {
return (im.type || '').toLowerCase() === targetType;
});
if (idx === -1) {
await addReaction('-1');
const availableTypes = methodsArr.map(function (im) { return im.type || '?'; });
await postComment(
'❌ **PocketBase Bot**: No install method with type `' + targetType + '` found for `' + slug + '`.\n\n' +
'**Available types:** `' + (availableTypes.length ? availableTypes.join('`, `') : '(none)') + '`\n\n' +
'Use `/pocketbase ' + slug + ' method list` to see all methods.'
);
process.exit(0);
}
if (!methodsArr[idx].resources) methodsArr[idx].resources = {};
if (resourceChanges.cpu != null) methodsArr[idx].resources.cpu = resourceChanges.cpu;
if (resourceChanges.ram != null) methodsArr[idx].resources.ram = resourceChanges.ram;
if (resourceChanges.hdd != null) methodsArr[idx].resources.hdd = resourceChanges.hdd;
await patchInstallMethodsJson(methodsArr);
const changesLines = Object.entries(resourceChanges)
.map(function ([k, v]) { return '- `' + k + '` → `' + v + (k === 'ram' ? ' MB' : k === 'hdd' ? ' GB' : '') + '`'; })
.join('\n');
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Updated install method **`' + methodsArr[idx].type + '`** for **`' + slug + '`**\n\n' +
'**Changes applied:**\n' + changesLines + '\n\n' +
'*Executed by @' + actor + '*'
);
}
} else if (setMatch) {
// ── SET SUBCOMMAND (multi-line / HTML / special chars via code block) ──
const fieldName = setMatch[1].toLowerCase();
const SET_ALLOWED = {
name: 'string', description: 'string', logo: 'string',
documentation: 'string', website: 'string', project_url: 'string', github: 'string',
config_path: 'string', disable_message: 'string', deleted_message: 'string'
};
if (!SET_ALLOWED[fieldName]) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: `set` only supports text fields.\n\n' +
'**Allowed:** `' + Object.keys(SET_ALLOWED).join('`, `') + '`\n\n' +
'For boolean/number fields use `field=value` syntax instead.'
);
process.exit(0);
}
if (!codeBlockValue) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: `set` requires a code block with the value.\n\n' +
'**Usage:**\n````\n/pocketbase ' + slug + ' set ' + fieldName + '\n```\nYour content here (HTML, multiline, special chars all fine)\n```\n````'
);
process.exit(0);
}
const setPayload = {};
setPayload[fieldName] = codeBlockValue;
const setPatchRes = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify(setPayload)
});
if (!setPatchRes.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: PATCH failed for `' + slug + '`:\n```\n' + setPatchRes.body + '\n```');
process.exit(1);
}
const preview = codeBlockValue.length > 300 ? codeBlockValue.substring(0, 300) + '…' : codeBlockValue;
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Set `' + fieldName + '` for **`' + slug + '`**\n\n' +
'**Value set:**\n```\n' + preview + '\n```\n\n' +
'*Executed by @' + actor + '*'
);
} else {
// ── FIELD=VALUE PATH ─────────────────────────────────────────────
const fieldsStr = rest;
// Skipped: slug, script_created/updated, created (auto), categories/
// install_methods/notes/type (relations), github_data/install_methods_json/
// notes_json (auto-generated), execute_in (select relation), last_update_commit (auto)
const ALLOWED_FIELDS = {
name: 'string',
description: 'string',
logo: 'string',
documentation: 'string',
website: 'string',
project_url: 'string',
github: 'string',
config_path: 'string',
port: 'number',
default_user: 'nullable_string',
default_passwd: 'nullable_string',
updateable: 'boolean',
privileged: 'boolean',
has_arm: 'boolean',
is_dev: 'boolean',
is_disabled: 'boolean',
disable_message: 'string',
is_deleted: 'boolean',
deleted_message: 'string',
};
// Field=value parser (handles quoted values and empty=null)
function parseFields(str) {
const fields = {};
let pos = 0;
while (pos < str.length) {
while (pos < str.length && /\s/.test(str[pos])) pos++;
if (pos >= str.length) break;
let keyStart = pos;
while (pos < str.length && str[pos] !== '=' && !/\s/.test(str[pos])) pos++;
const key = str.substring(keyStart, pos).trim();
if (!key || pos >= str.length || str[pos] !== '=') { pos++; continue; }
pos++;
let value;
if (str[pos] === '"') {
pos++;
let valStart = pos;
while (pos < str.length && str[pos] !== '"') {
if (str[pos] === '\\') pos++;
pos++;
}
value = str.substring(valStart, pos).replace(/\\"/g, '"');
if (pos < str.length) pos++;
} else {
let valStart = pos;
while (pos < str.length && !/\s/.test(str[pos])) pos++;
value = str.substring(valStart, pos);
}
fields[key] = value;
}
return fields;
}
const parsedFields = parseFields(fieldsStr);
const unknownFields = Object.keys(parsedFields).filter(function (f) { return !ALLOWED_FIELDS[f]; });
if (unknownFields.length > 0) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: Unknown field(s): `' + unknownFields.join('`, `') + '`\n\n' +
'**Allowed fields:** `' + Object.keys(ALLOWED_FIELDS).join('`, `') + '`'
);
process.exit(0);
}
if (Object.keys(parsedFields).length === 0) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: Could not parse any valid `field=value` pairs.\n\n' + HELP_TEXT);
process.exit(0);
}
// Cast values to correct types
const payload = {};
for (const [key, rawVal] of Object.entries(parsedFields)) {
const type = ALLOWED_FIELDS[key];
if (type === 'boolean') {
if (rawVal === 'true') payload[key] = true;
else if (rawVal === 'false') payload[key] = false;
else {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: `' + key + '` must be `true` or `false`, got: `' + rawVal + '`');
process.exit(0);
}
} else if (type === 'number') {
const n = parseInt(rawVal, 10);
if (isNaN(n)) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: `' + key + '` must be a number, got: `' + rawVal + '`');
process.exit(0);
}
payload[key] = n;
} else if (type === 'nullable_string') {
payload[key] = rawVal === '' ? null : rawVal;
} else {
payload[key] = rawVal;
}
}
const patchRes = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!patchRes.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: PATCH failed for `' + slug + '`:\n```\n' + patchRes.body + '\n```');
process.exit(1);
}
await addReaction('+1');
const changesLines = Object.entries(payload)
.map(function ([k, v]) { return '- `' + k + '` → `' + JSON.stringify(v) + '`'; })
.join('\n');
await postComment(
'✅ **PocketBase Bot**: Updated **`' + slug + '`** successfully!\n\n' +
'**Changes applied:**\n' + changesLines + '\n\n' +
'*Executed by @' + actor + '*'
);
}
console.log('Done.');
})().catch(function (e) {
console.error('Fatal error:', e.message || e);
process.exit(1);
});
ENDSCRIPT
shell: bash

View File

@@ -48,7 +48,8 @@ jobs:
const https = require('https'); const https = require('https');
const http = require('http'); const http = require('http');
const url = require('url'); const url = require('url');
function request(fullUrl, opts) { function request(fullUrl, opts, redirectCount) {
redirectCount = redirectCount || 0;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
const u = url.parse(fullUrl); const u = url.parse(fullUrl);
const isHttps = u.protocol === 'https:'; const isHttps = u.protocol === 'https:';
@@ -63,6 +64,13 @@ jobs:
if (body) options.headers['Content-Length'] = Buffer.byteLength(body); if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
const lib = isHttps ? https : http; const lib = isHttps ? https : http;
const req = lib.request(options, function(res) { const req = lib.request(options, function(res) {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
const redirectUrl = url.resolve(fullUrl, res.headers.location);
res.resume();
resolve(request(redirectUrl, opts, redirectCount + 1));
return;
}
let data = ''; let data = '';
res.on('data', function(chunk) { data += chunk; }); res.on('data', function(chunk) { data += chunk; });
res.on('end', function() { res.on('end', function() {
@@ -125,15 +133,15 @@ jobs:
var osVersionToId = {}; var osVersionToId = {};
try { try {
const res = await request(apiBase + '/collections/z_ref_note_types/records?perPage=500', { headers: { 'Authorization': token } }); const res = await request(apiBase + '/collections/z_ref_note_types/records?perPage=500', { headers: { 'Authorization': token } });
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) noteTypeToId[item.type] = item.id; }); if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) { noteTypeToId[item.type] = item.id; noteTypeToId[item.type.toLowerCase()] = item.id; } });
} catch (e) { console.warn('z_ref_note_types:', e.message); } } catch (e) { console.warn('z_ref_note_types:', e.message); }
try { try {
const res = await request(apiBase + '/collections/z_ref_install_method_types/records?perPage=500', { headers: { 'Authorization': token } }); const res = await request(apiBase + '/collections/z_ref_install_method_types/records?perPage=500', { headers: { 'Authorization': token } });
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) installMethodTypeToId[item.type] = item.id; }); if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) { installMethodTypeToId[item.type] = item.id; installMethodTypeToId[item.type.toLowerCase()] = item.id; } });
} catch (e) { console.warn('z_ref_install_method_types:', e.message); } } catch (e) { console.warn('z_ref_install_method_types:', e.message); }
try { try {
const res = await request(apiBase + '/collections/z_ref_os/records?perPage=500', { headers: { 'Authorization': token } }); const res = await request(apiBase + '/collections/z_ref_os/records?perPage=500', { headers: { 'Authorization': token } });
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.os != null) osToId[item.os] = item.id; }); if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.os != null) { osToId[item.os] = item.id; osToId[item.os.toLowerCase()] = item.id; } });
} catch (e) { console.warn('z_ref_os:', e.message); } } catch (e) { console.warn('z_ref_os:', e.message); }
try { try {
const res = await request(apiBase + '/collections/z_ref_os_version/records?perPage=500&expand=os', { headers: { 'Authorization': token } }); const res = await request(apiBase + '/collections/z_ref_os_version/records?perPage=500&expand=os', { headers: { 'Authorization': token } });
@@ -154,7 +162,7 @@ jobs:
name: data.name, name: data.name,
slug: data.slug, slug: data.slug,
script_created: data.date_created || data.script_created, script_created: data.date_created || data.script_created,
script_updated: data.date_created || data.script_updated, script_updated: new Date().toISOString().split('T')[0],
updateable: data.updateable, updateable: data.updateable,
privileged: data.privileged, privileged: data.privileged,
port: data.interface_port != null ? data.interface_port : data.port, port: data.interface_port != null ? data.interface_port : data.port,
@@ -163,8 +171,8 @@ jobs:
logo: data.logo, logo: data.logo,
description: data.description, description: data.description,
config_path: data.config_path, config_path: data.config_path,
default_user: (data.default_credentials && data.default_credentials.username) || data.default_user, default_user: (data.default_credentials && data.default_credentials.username) || data.default_user || null,
default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd, default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd || null,
is_dev: false is_dev: false
}; };
var resolvedType = typeValueToId[data.type]; var resolvedType = typeValueToId[data.type];
@@ -190,7 +198,7 @@ jobs:
var postRes = await request(notesCollUrl, { var postRes = await request(notesCollUrl, {
method: 'POST', method: 'POST',
headers: { 'Authorization': token, 'Content-Type': 'application/json' }, headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ text: note.text || '', type: typeId }) body: JSON.stringify({ text: note.text || '', type: typeId, script: scriptId })
}); });
if (postRes.ok) noteIds.push(JSON.parse(postRes.body).id); if (postRes.ok) noteIds.push(JSON.parse(postRes.body).id);
} }

View File

@@ -83,7 +83,8 @@ jobs:
const http = require('http'); const http = require('http');
const url = require('url'); const url = require('url');
function request(fullUrl, opts) { function request(fullUrl, opts, redirectCount) {
redirectCount = redirectCount || 0;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
const u = url.parse(fullUrl); const u = url.parse(fullUrl);
const isHttps = u.protocol === 'https:'; const isHttps = u.protocol === 'https:';
@@ -98,6 +99,13 @@ jobs:
if (body) options.headers['Content-Length'] = Buffer.byteLength(body); if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
const lib = isHttps ? https : http; const lib = isHttps ? https : http;
const req = lib.request(options, function(res) { const req = lib.request(options, function(res) {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
const redirectUrl = url.resolve(fullUrl, res.headers.location);
res.resume();
resolve(request(redirectUrl, opts, redirectCount + 1));
return;
}
let data = ''; let data = '';
res.on('data', function(chunk) { data += chunk; }); res.on('data', function(chunk) { data += chunk; });
res.on('end', function() { res.on('end', function() {
@@ -151,7 +159,7 @@ jobs:
method: 'PATCH', method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' }, headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
name: record.name || record.slug, script_updated: new Date().toISOString().split('T')[0],
last_update_commit: process.env.PR_URL || process.env.COMMIT_URL || '' last_update_commit: process.env.PR_URL || process.env.COMMIT_URL || ''
}) })
}); });

View File

@@ -423,6 +423,86 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details> </details>
## 2026-03-19
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- core: reorder hwaccel setup and adjust GPU group usermod [@MickLesk](https://github.com/MickLesk) ([#13072](https://github.com/community-scripts/ProxmoxVE/pull/13072))
## 2026-03-18
### 🆕 New Scripts
- Alpine-Ntfy [@MickLesk](https://github.com/MickLesk) ([#13048](https://github.com/community-scripts/ProxmoxVE/pull/13048))
- Split-Pro ([#12975](https://github.com/community-scripts/ProxmoxVE/pull/12975))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Tdarr: use curl_with_retry and correct exit code [@MickLesk](https://github.com/MickLesk) ([#13060](https://github.com/community-scripts/ProxmoxVE/pull/13060))
- reitti: fix: v4 [@CrazyWolf13](https://github.com/CrazyWolf13) ([#13039](https://github.com/community-scripts/ProxmoxVE/pull/13039))
- Paperless-NGX: increase default RAM to 3GB [@MickLesk](https://github.com/MickLesk) ([#13018](https://github.com/community-scripts/ProxmoxVE/pull/13018))
- Plex: restart service after update to apply new version [@MickLesk](https://github.com/MickLesk) ([#13017](https://github.com/community-scripts/ProxmoxVE/pull/13017))
- #### ✨ New Features
- tools: centralize GPU group setup via setup_hwaccel [@MickLesk](https://github.com/MickLesk) ([#13044](https://github.com/community-scripts/ProxmoxVE/pull/13044))
- Termix: add guacd build and systemd integration [@MickLesk](https://github.com/MickLesk) ([#12999](https://github.com/community-scripts/ProxmoxVE/pull/12999))
- #### 🔧 Refactor
- Podman: replace deprecated commands with Quadlets [@MickLesk](https://github.com/MickLesk) ([#13052](https://github.com/community-scripts/ProxmoxVE/pull/13052))
- Refactor: Jellyfin repo, ffmpeg package and symlinks [@MickLesk](https://github.com/MickLesk) ([#13045](https://github.com/community-scripts/ProxmoxVE/pull/13045))
- pve-scripts-local: Increase default disk size from 4GB to 10GB [@MickLesk](https://github.com/MickLesk) ([#13009](https://github.com/community-scripts/ProxmoxVE/pull/13009))
### 💾 Core
- #### ✨ New Features
- tools.func Implement pg_cron setup for setup_postgresql [@MickLesk](https://github.com/MickLesk) ([#13053](https://github.com/community-scripts/ProxmoxVE/pull/13053))
- tools.func: Implement check_for_gh_tag function [@MickLesk](https://github.com/MickLesk) ([#12998](https://github.com/community-scripts/ProxmoxVE/pull/12998))
- tools.func: Implement fetch_and_deploy_gh_tag function [@MickLesk](https://github.com/MickLesk) ([#13000](https://github.com/community-scripts/ProxmoxVE/pull/13000))
## 2026-03-17
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Gluetun: add OpenVPN process user and cleanup stale config [@MickLesk](https://github.com/MickLesk) ([#13016](https://github.com/community-scripts/ProxmoxVE/pull/13016))
- Frigate: check OpenVino model files exist before configuring detector and use curl_with_retry instead of default wget [@MickLesk](https://github.com/MickLesk) ([#13019](https://github.com/community-scripts/ProxmoxVE/pull/13019))
### 💾 Core
- #### 🔧 Refactor
- tools.func: Update `create_self_signed_cert()` [@tremor021](https://github.com/tremor021) ([#13008](https://github.com/community-scripts/ProxmoxVE/pull/13008))
## 2026-03-16
### 🆕 New Scripts
- Gluetun ([#12976](https://github.com/community-scripts/ProxmoxVE/pull/12976))
- Anytype-Server ([#12974](https://github.com/community-scripts/ProxmoxVE/pull/12974))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Immich: use gcc-13 for compilation & add uv python pre-install with retry logic [@MickLesk](https://github.com/MickLesk) ([#12935](https://github.com/community-scripts/ProxmoxVE/pull/12935))
- Tautulli: add setuptools<81 constraint to update script [@MickLesk](https://github.com/MickLesk) ([#12959](https://github.com/community-scripts/ProxmoxVE/pull/12959))
- Seerr: add missing build deps [@MickLesk](https://github.com/MickLesk) ([#12960](https://github.com/community-scripts/ProxmoxVE/pull/12960))
- fix: yubal update [@CrazyWolf13](https://github.com/CrazyWolf13) ([#12961](https://github.com/community-scripts/ProxmoxVE/pull/12961))
### 💾 Core
- #### 🐞 Bug Fixes
- hwaccel: remove ROCm install from AMD APU setup [@MickLesk](https://github.com/MickLesk) ([#12958](https://github.com/community-scripts/ProxmoxVE/pull/12958))
## 2026-03-15 ## 2026-03-15
### 🆕 New Scripts ### 🆕 New Scripts

50
ct/alpine-ntfy.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: cobalt (cobaltgit)
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
# Source: https://ntfy.sh/
APP="Alpine-ntfy"
var_tags="${var_tags:-notification}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-256}"
var_disk="${var_disk:-2}"
var_os="${var_os:-alpine}"
var_version="${var_version:-3.23}"
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 /etc/ntfy ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
msg_info "Updating ntfy LXC"
$STD apk -U upgrade
setcap 'cap_net_bind_service=+ep' /usr/bin/ntfy
msg_ok "Updated ntfy LXC"
msg_info "Restarting ntfy"
rc-service ntfy restart
msg_ok "Restarted ntfy"
msg_ok "Updated successfully!"
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 "${TAB}${GATEWAY}${BGN}http://${IP}${CL}"

67
ct/anytype-server.sh Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://anytype.io
APP="Anytype-Server"
var_tags="${var_tags:-notes;productivity;sync}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-16}"
var_os="${var_os:-ubuntu}"
var_version="${var_version:-24.04}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /opt/anytype/any-sync-bundle ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "anytype" "grishy/any-sync-bundle"; then
msg_info "Stopping Service"
systemctl stop anytype
msg_ok "Stopped Service"
msg_info "Backing up Data"
cp -r /opt/anytype/data /opt/anytype_data_backup
msg_ok "Backed up Data"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "anytype" "grishy/any-sync-bundle" "prebuild" "latest" "/opt/anytype" "any-sync-bundle_*_linux_amd64.tar.gz"
chmod +x /opt/anytype/any-sync-bundle
msg_info "Restoring Data"
cp -r /opt/anytype_data_backup/. /opt/anytype/data
rm -rf /opt/anytype_data_backup
msg_ok "Restored Data"
msg_info "Starting Service"
systemctl start anytype
msg_ok "Started Service"
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 "${TAB}${GATEWAY}${BGN}http://${IP}:33010${CL}"
echo -e "${INFO}${YW} Client config file:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}/opt/anytype/data/client-config.yml${CL}"

61
ct/gluetun.sh Normal file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/qdm12/gluetun
APP="Gluetun"
var_tags="${var_tags:-vpn;wireguard;openvpn}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
var_tun="${var_tun:-yes}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /usr/local/bin/gluetun ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "gluetun" "qdm12/gluetun"; then
msg_info "Stopping Service"
systemctl stop gluetun
msg_ok "Stopped Service"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "gluetun" "qdm12/gluetun" "tarball"
msg_info "Building Gluetun"
cd /opt/gluetun
$STD go mod download
CGO_ENABLED=0 $STD go build -trimpath -ldflags="-s -w" -o /usr/local/bin/gluetun ./cmd/gluetun/
msg_ok "Built Gluetun"
msg_info "Starting Service"
systemctl start gluetun
msg_ok "Started Service"
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 "${TAB}${GATEWAY}${BGN}http://${IP}:8000${CL}"

6
ct/headers/alpine-ntfy Normal file
View File

@@ -0,0 +1,6 @@
___ __ _ __ ____
/ | / /___ (_)___ ___ ____ / /_/ __/_ __
/ /| | / / __ \/ / __ \/ _ \______/ __ \/ __/ /_/ / / /
/ ___ |/ / /_/ / / / / / __/_____/ / / / /_/ __/ /_/ /
/_/ |_/_/ .___/_/_/ /_/\___/ /_/ /_/\__/_/ \__, /
/_/ /____/

View File

@@ -0,0 +1,6 @@
___ __ _____
/ | ____ __ __/ /___ ______ ___ / ___/___ ______ _____ _____
/ /| | / __ \/ / / / __/ / / / __ \/ _ \______\__ \/ _ \/ ___/ | / / _ \/ ___/
/ ___ |/ / / / /_/ / /_/ /_/ / /_/ / __/_____/__/ / __/ / | |/ / __/ /
/_/ |_/_/ /_/\__, /\__/\__, / .___/\___/ /____/\___/_/ |___/\___/_/
/____/ /____/_/

6
ct/headers/gluetun Normal file
View File

@@ -0,0 +1,6 @@
________ __
/ ____/ /_ _____ / /___ ______
/ / __/ / / / / _ \/ __/ / / / __ \
/ /_/ / / /_/ / __/ /_/ /_/ / / / /
\____/_/\__,_/\___/\__/\__,_/_/ /_/

6
ct/headers/split-pro Normal file
View File

@@ -0,0 +1,6 @@
_____ ___ __ ____
/ ___/____ / (_) /_ / __ \_________
\__ \/ __ \/ / / __/_____/ /_/ / ___/ __ \
___/ / /_/ / / / /_/_____/ ____/ / / /_/ /
/____/ .___/_/_/\__/ /_/ /_/ \____/
/_/

View File

@@ -76,7 +76,7 @@ EOF
SOURCE_DIR=${STAGING_DIR}/image-source SOURCE_DIR=${STAGING_DIR}/image-source
cd /tmp cd /tmp
if [[ -f ~/.intel_version ]]; then if [[ -f ~/.intel_version ]]; then
curl -fsSLO https://raw.githubusercontent.com/immich-app/immich/refs/heads/main/machine-learning/Dockerfile curl_with_retry "https://raw.githubusercontent.com/immich-app/immich/refs/heads/main/machine-learning/Dockerfile" "Dockerfile"
readarray -t INTEL_URLS < <( readarray -t INTEL_URLS < <(
sed -n "/intel-[igc|opencl]/p" ./Dockerfile | awk '{print $3}' sed -n "/intel-[igc|opencl]/p" ./Dockerfile | awk '{print $3}'
sed -n "/libigdgmm12/p" ./Dockerfile | awk '{print $3}' sed -n "/libigdgmm12/p" ./Dockerfile | awk '{print $3}'
@@ -85,7 +85,7 @@ EOF
if [[ "$INTEL_RELEASE" != "$(cat ~/.intel_version)" ]]; then if [[ "$INTEL_RELEASE" != "$(cat ~/.intel_version)" ]]; then
msg_info "Updating Intel iGPU dependencies" msg_info "Updating Intel iGPU dependencies"
for url in "${INTEL_URLS[@]}"; do for url in "${INTEL_URLS[@]}"; do
curl -fsSLO "$url" curl_with_retry "$url" "$(basename "$url")"
done done
$STD apt-mark unhold libigdgmm12 $STD apt-mark unhold libigdgmm12
$STD apt install -y --allow-downgrades ./libigdgmm12*.deb $STD apt install -y --allow-downgrades ./libigdgmm12*.deb
@@ -133,7 +133,7 @@ EOF
$STD sudo -u postgres psql -d immich -c "REINDEX INDEX face_index;" $STD sudo -u postgres psql -d immich -c "REINDEX INDEX face_index;"
$STD sudo -u postgres psql -d immich -c "REINDEX INDEX clip_index;" $STD sudo -u postgres psql -d immich -c "REINDEX INDEX clip_index;"
fi fi
ensure_dependencies ccache ensure_dependencies ccache gcc-13 g++-13
INSTALL_DIR="/opt/${APP}" INSTALL_DIR="/opt/${APP}"
UPLOAD_DIR="$(sed -n '/^IMMICH_MEDIA_LOCATION/s/[^=]*=//p' /opt/immich/.env)" UPLOAD_DIR="$(sed -n '/^IMMICH_MEDIA_LOCATION/s/[^=]*=//p' /opt/immich/.env)"
@@ -166,7 +166,7 @@ EOF
setup_uv setup_uv
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "${RELEASE}" "$SRC_DIR" CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "${RELEASE}" "$SRC_DIR"
PNPM_VERSION="$(jq -r '.packageManager | split("@")[1]' ${SRC_DIR}/package.json)" PNPM_VERSION="$(jq -r '.packageManager | split("@")[1] | split("+")[0]' ${SRC_DIR}/package.json)"
NODE_VERSION="24" NODE_MODULE="pnpm@${PNPM_VERSION}" setup_nodejs NODE_VERSION="24" NODE_MODULE="pnpm@${PNPM_VERSION}" setup_nodejs
msg_info "Updating Immich web and microservices" msg_info "Updating Immich web and microservices"
@@ -217,15 +217,36 @@ EOF
chown -R immich:immich "$INSTALL_DIR" chown -R immich:immich "$INSTALL_DIR"
chown immich:immich ./uv.lock chown immich:immich ./uv.lock
export VIRTUAL_ENV="${ML_DIR}"/ml-venv export VIRTUAL_ENV="${ML_DIR}"/ml-venv
export UV_HTTP_TIMEOUT=300
if [[ -f ~/.openvino ]]; then if [[ -f ~/.openvino ]]; then
ML_PYTHON="python3.13"
msg_info "Pre-installing Python ${ML_PYTHON} for machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv python install "${ML_PYTHON}" && break
[[ $attempt -lt 3 ]] && msg_warn "Python download attempt $attempt failed, retrying..." && sleep 5
done
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Updating HW-accelerated machine-learning" msg_info "Updating HW-accelerated machine-learning"
$STD uv add --no-sync --optional openvino onnxruntime-openvino==1.24.1 --active -n -p python3.13 --managed-python $STD uv add --no-sync --optional openvino onnxruntime-openvino==1.24.1 --active -n -p "${ML_PYTHON}" --managed-python
$STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p python3.13 --managed-python for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT -nu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -lt 3 ]] && msg_warn "uv sync attempt $attempt failed, retrying..." && sleep 10
done
patchelf --clear-execstack "${VIRTUAL_ENV}/lib/python3.13/site-packages/onnxruntime/capi/onnxruntime_pybind11_state.cpython-313-x86_64-linux-gnu.so" patchelf --clear-execstack "${VIRTUAL_ENV}/lib/python3.13/site-packages/onnxruntime/capi/onnxruntime_pybind11_state.cpython-313-x86_64-linux-gnu.so"
msg_ok "Updated HW-accelerated machine-learning" msg_ok "Updated HW-accelerated machine-learning"
else else
ML_PYTHON="python3.11"
msg_info "Pre-installing Python ${ML_PYTHON} for machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv python install "${ML_PYTHON}" && break
[[ $attempt -lt 3 ]] && msg_warn "Python download attempt $attempt failed, retrying..." && sleep 5
done
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Updating machine-learning" msg_info "Updating machine-learning"
$STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p python3.11 --managed-python for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT -nu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -lt 3 ]] && msg_warn "uv sync attempt $attempt failed, retrying..." && sleep 10
done
msg_ok "Updated machine-learning" msg_ok "Updated machine-learning"
fi fi
cd "$SRC_DIR" cd "$SRC_DIR"

View File

@@ -39,14 +39,23 @@ function update_script() {
msg_ok "Updated Intel Dependencies" msg_ok "Updated Intel Dependencies"
fi fi
msg_info "Setting up Jellyfin Repository"
setup_deb822_repo \
"jellyfin" \
"https://repo.jellyfin.org/jellyfin_team.gpg.key" \
"https://repo.jellyfin.org/$(get_os_info id)" \
"$(get_os_info codename)"
msg_ok "Set up Jellyfin Repository"
msg_info "Updating Jellyfin" msg_info "Updating Jellyfin"
ensure_dependencies libjemalloc2 ensure_dependencies libjemalloc2
if [[ ! -f /usr/lib/libjemalloc.so ]]; then if [[ ! -f /usr/lib/libjemalloc.so ]]; then
ln -sf /usr/lib/x86_64-linux-gnu/libjemalloc.so.2 /usr/lib/libjemalloc.so ln -sf /usr/lib/x86_64-linux-gnu/libjemalloc.so.2 /usr/lib/libjemalloc.so
fi fi
$STD apt update
$STD apt -y upgrade $STD apt -y upgrade
$STD apt -y --with-new-pkgs upgrade jellyfin jellyfin-server $STD apt -y --with-new-pkgs upgrade jellyfin jellyfin-server jellyfin-ffmpeg7
ln -sf /usr/lib/jellyfin-ffmpeg/ffmpeg /usr/bin/ffmpeg
ln -sf /usr/lib/jellyfin-ffmpeg/ffprobe /usr/bin/ffprobe
msg_ok "Updated Jellyfin" msg_ok "Updated Jellyfin"
msg_ok "Updated successfully!" msg_ok "Updated successfully!"
exit exit

View File

@@ -8,7 +8,7 @@ source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxV
APP="Paperless-ngx" APP="Paperless-ngx"
var_tags="${var_tags:-document;management}" var_tags="${var_tags:-document;management}"
var_cpu="${var_cpu:-2}" var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}" var_ram="${var_ram:-3072}"
var_disk="${var_disk:-12}" var_disk="${var_disk:-12}"
var_os="${var_os:-debian}" var_os="${var_os:-debian}"
var_version="${var_version:-13}" var_version="${var_version:-13}"

View File

@@ -79,6 +79,11 @@ function update_script() {
$STD apt update $STD apt update
$STD apt install -y plexmediaserver $STD apt install -y plexmediaserver
msg_ok "Updated Plex Media Server" msg_ok "Updated Plex Media Server"
msg_info "Restarting Plex Media Server"
systemctl restart plexmediaserver
msg_ok "Restarted Plex Media Server"
msg_ok "Updated successfully!" msg_ok "Updated successfully!"
exit exit
} }

View File

@@ -23,7 +23,7 @@ function update_script() {
header_info header_info
check_container_storage check_container_storage
check_container_resources check_container_resources
if [[ ! -f /etc/systemd/system/homeassistant.service ]]; then if [[ ! -f /etc/containers/systemd/homeassistant.container ]]; then
msg_error "No ${APP} Installation Found!" msg_error "No ${APP} Installation Found!"
exit exit
fi fi

View File

@@ -9,7 +9,7 @@ APP="PVE-Scripts-Local"
var_tags="${var_tags:-pve-scripts-local}" var_tags="${var_tags:-pve-scripts-local}"
var_cpu="${var_cpu:-2}" var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}" var_ram="${var_ram:-4096}"
var_disk="${var_disk:-4}" var_disk="${var_disk:-10}"
var_os="${var_os:-debian}" var_os="${var_os:-debian}"
var_version="${var_version:-13}" var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}" var_unprivileged="${var_unprivileged:-1}"

View File

@@ -89,17 +89,49 @@ EOF
msg_ok "Started Service" msg_ok "Started Service"
msg_ok "Updated successfully!" msg_ok "Updated successfully!"
fi fi
if check_for_gh_release "photon" "komoot/photon"; then if check_for_gh_release "photon" "komoot/photon"; then
if [[ -f "$HOME/.photon" ]] && [[ "$(cat "$HOME/.photon")" == 0.7 ]]; then
CURRENT_VERSION="$(<"$HOME/.photon")"
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Photon v1 upgrade detected (breaking change)"
echo
echo "Your current version: $CURRENT_VERSION"
echo
echo "Photon v1 requires a manual migration before updating."
echo
echo "You need to:"
echo " 1. Remove existing geocoding data (not actual reitti data):"
echo " rm -rf /opt/photon_data"
echo
echo " 2. Follow the inial setup guide again:"
echo " https://github.com/community-scripts/ProxmoxVE/discussions/8737"
echo
echo " 3. Re-download and import Photon data for v1"
echo
read -rp "Do you want to continue anyway? (y/N): " CONTINUE
echo
if [[ ! "$CONTINUE" =~ ^[Yy]$ ]]; then
msg_info "Migration required. Update cancelled."
exit 0
fi
msg_warn "Continuing without migration may break Photon in the future!"
fi
msg_info "Stopping Service" msg_info "Stopping Service"
systemctl stop photon systemctl stop photon
msg_ok "Stopped Service" msg_ok "Stopped Service"
rm -f /opt/photon/photon.jar rm -f /opt/photon/photon.jar
USE_ORIGINAL_FILENAME="true" fetch_and_deploy_gh_release "photon" "komoot/photon" "singlefile" "latest" "/opt/photon" "photon-0*.jar" USE_ORIGINAL_FILENAME="true" fetch_and_deploy_gh_release "photon" "komoot/photon" "singlefile" "latest" "/opt/photon" "photon-*.jar"
mv /opt/photon/photon-*.jar /opt/photon/photon.jar mv /opt/photon/photon-*.jar /opt/photon/photon.jar
msg_info "Starting Service" msg_info "Starting Service"
systemctl start photon systemctl start photon
systemctl restart nginx
msg_ok "Started Service" msg_ok "Started Service"
msg_ok "Updated successfully!" msg_ok "Updated successfully!"
fi fi

View File

@@ -128,6 +128,8 @@ EOF
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "seerr" "seerr-team/seerr" "tarball" CLEAN_INSTALL=1 fetch_and_deploy_gh_release "seerr" "seerr-team/seerr" "tarball"
ensure_dependencies build-essential python3-setuptools
msg_info "Updating PNPM Version" msg_info "Updating PNPM Version"
pnpm_desired=$(grep -Po '"pnpm":\s*"\K[^"]+' /opt/seerr/package.json) pnpm_desired=$(grep -Po '"pnpm":\s*"\K[^"]+' /opt/seerr/package.json)
NODE_VERSION="22" NODE_MODULE="pnpm@$pnpm_desired" setup_nodejs NODE_VERSION="22" NODE_MODULE="pnpm@$pnpm_desired" setup_nodejs

View File

@@ -51,7 +51,7 @@ function update_script() {
msg_info "Updating Sparky Fitness Backend" msg_info "Updating Sparky Fitness Backend"
cd /opt/sparkyfitness/SparkyFitnessServer cd /opt/sparkyfitness/SparkyFitnessServer
$STD npm install --legacy-peer-deps $STD pnpm install
msg_ok "Updated Sparky Fitness Backend" msg_ok "Updated Sparky Fitness Backend"
msg_info "Updating Sparky Fitness Frontend (Patience)" msg_info "Updating Sparky Fitness Frontend (Patience)"

68
ct/split-pro.sh Normal file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: johanngrobe
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/oss-apps/split-pro
APP="Split-Pro"
var_tags="${var_tags:-finance;expense-sharing}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-6}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/split-pro ]]; then
msg_error "No Split Pro Installation Found!"
exit
fi
if check_for_gh_release "split-pro" "oss-apps/split-pro"; then
msg_info "Stopping Service"
systemctl stop split-pro
msg_ok "Stopped Service"
msg_info "Backing up Data"
cp /opt/split-pro/.env /opt/split-pro.env
msg_ok "Backed up Data"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "split-pro" "oss-apps/split-pro" "tarball"
msg_info "Building Application"
cd /opt/split-pro
$STD pnpm install --frozen-lockfile
$STD pnpm build
cp /opt/split-pro.env /opt/split-pro/.env
rm -f /opt/split-pro.env
ln -sf /opt/split-pro_data/uploads /opt/split-pro/uploads
$STD pnpm exec prisma migrate deploy
msg_ok "Built Application"
msg_info "Starting Service"
systemctl start split-pro
msg_ok "Started Service"
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 "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"

View File

@@ -51,6 +51,7 @@ function update_script() {
$STD source /opt/Tautulli/.venv/bin/activate $STD source /opt/Tautulli/.venv/bin/activate
$STD uv pip install -r requirements.txt $STD uv pip install -r requirements.txt
$STD uv pip install pyopenssl $STD uv pip install pyopenssl
$STD uv pip install "setuptools<81"
msg_ok "Updated Tautulli" msg_ok "Updated Tautulli"
msg_info "Restoring config and database" msg_info "Restoring config and database"

View File

@@ -33,12 +33,16 @@ function update_script() {
$STD apt upgrade -y $STD apt upgrade -y
rm -rf /opt/tdarr/Tdarr_Updater rm -rf /opt/tdarr/Tdarr_Updater
cd /opt/tdarr cd /opt/tdarr
RELEASE=$(curl -fsSL https://f000.backblazeb2.com/file/tdarrs/versions.json | grep -oP '(?<="Tdarr_Updater": ")[^"]+' | grep linux_x64 | head -n 1) RELEASE=$(curl_with_retry "https://f000.backblazeb2.com/file/tdarrs/versions.json" "-" | grep -oP '(?<="Tdarr_Updater": ")[^"]+' | grep linux_x64 | head -n 1)
curl -fsSL "$RELEASE" -o Tdarr_Updater.zip curl_with_retry "$RELEASE" "Tdarr_Updater.zip"
$STD unzip Tdarr_Updater.zip $STD unzip Tdarr_Updater.zip
chmod +x Tdarr_Updater chmod +x Tdarr_Updater
$STD ./Tdarr_Updater $STD ./Tdarr_Updater
rm -rf /opt/tdarr/Tdarr_Updater.zip rm -rf /opt/tdarr/Tdarr_Updater.zip
[[ -f /opt/tdarr/Tdarr_Server/Tdarr_Server ]] || {
msg_error "Tdarr_Updater failed — tdarr.io may be blocked by local DNS"
exit 250
}
msg_ok "Updated Tdarr" msg_ok "Updated Tdarr"
msg_ok "Updated successfully!" msg_ok "Updated successfully!"
exit exit

View File

@@ -29,10 +29,116 @@ function update_script() {
exit exit
fi fi
if check_for_gh_tag "guacd" "apache/guacamole-server"; then
msg_info "Stopping guacd"
systemctl stop guacd 2>/dev/null || true
msg_ok "Stopped guacd"
ensure_dependencies \
libcairo2-dev \
libjpeg62-turbo-dev \
libpng-dev \
libtool-bin \
uuid-dev \
libvncserver-dev \
freerdp3-dev \
libssh2-1-dev \
libtelnet-dev \
libwebsockets-dev \
libpulse-dev \
libvorbis-dev \
libwebp-dev \
libssl-dev \
libpango1.0-dev \
libswscale-dev \
libavcodec-dev \
libavutil-dev \
libavformat-dev
msg_info "Updating Guacamole Server (guacd)"
fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server" "${CHECK_UPDATE_RELEASE}" "/opt/guacamole-server"
cd /opt/guacamole-server
export CPPFLAGS="-Wno-error=deprecated-declarations"
$STD autoreconf -fi
$STD ./configure --with-init-dir=/etc/init.d --enable-allow-freerdp-snapshots
$STD make
$STD make install
$STD ldconfig
cd /opt
rm -rf /opt/guacamole-server
msg_ok "Updated Guacamole Server (guacd) to ${CHECK_UPDATE_RELEASE}"
if [[ ! -f /etc/guacamole/guacd.conf ]]; then
mkdir -p /etc/guacamole
cat <<EOF >/etc/guacamole/guacd.conf
[server]
bind_host = 127.0.0.1
bind_port = 4822
EOF
fi
if [[ ! -f /etc/systemd/system/guacd.service ]] || grep -q "Type=forking" /etc/systemd/system/guacd.service 2>/dev/null; then
cat <<EOF >/etc/systemd/system/guacd.service
[Unit]
Description=Guacamole Proxy Daemon (guacd)
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/sbin/guacd -f -b 127.0.0.1 -l 4822
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
fi
if ! grep -q "guacd.service" /etc/systemd/system/termix.service 2>/dev/null; then
sed -i '/^After=network.target/s/$/ guacd.service/' /etc/systemd/system/termix.service
sed -i '/^\[Unit\]/a Wants=guacd.service' /etc/systemd/system/termix.service
fi
systemctl daemon-reload
systemctl enable -q --now guacd
fi
if check_for_gh_release "termix" "Termix-SSH/Termix"; then if check_for_gh_release "termix" "Termix-SSH/Termix"; then
msg_info "Stopping Service" msg_info "Stopping Termix"
systemctl stop termix systemctl stop termix
msg_ok "Stopped Service" msg_ok "Stopped Termix"
msg_info "Migrating Configuration"
if [[ ! -f /opt/termix/.env ]]; then
cat <<EOF >/opt/termix/.env
NODE_ENV=production
DATA_DIR=/opt/termix/data
GUACD_HOST=127.0.0.1
GUACD_PORT=4822
EOF
fi
if ! grep -q "EnvironmentFile" /etc/systemd/system/termix.service 2>/dev/null; then
cat <<EOF >/etc/systemd/system/termix.service
[Unit]
Description=Termix Backend
After=network.target guacd.service
Wants=guacd.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/termix
EnvironmentFile=/opt/termix/.env
ExecStart=/usr/bin/node /opt/termix/dist/backend/backend/starter.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
fi
msg_ok "Migrated Configuration"
msg_info "Backing up Data" msg_info "Backing up Data"
cp -r /opt/termix/data /opt/termix_data_backup cp -r /opt/termix/data /opt/termix_data_backup
@@ -95,16 +201,16 @@ function update_script() {
sed -i 's|/app/html|/opt/termix/html|g' /etc/nginx/nginx.conf sed -i 's|/app/html|/opt/termix/html|g' /etc/nginx/nginx.conf
sed -i 's|/app/nginx|/opt/termix/nginx|g' /etc/nginx/nginx.conf sed -i 's|/app/nginx|/opt/termix/nginx|g' /etc/nginx/nginx.conf
sed -i 's|listen ${PORT};|listen 80;|g' /etc/nginx/nginx.conf sed -i 's|listen ${PORT};|listen 80;|g' /etc/nginx/nginx.conf
nginx -t && systemctl reload nginx nginx -t && systemctl reload nginx
msg_ok "Updated Nginx Configuration" msg_ok "Updated Nginx Configuration"
else else
msg_warn "Nginx configuration not updated. If Termix doesn't work, restore from backup or update manually." msg_warn "Nginx configuration not updated. If Termix doesn't work, restore from backup or update manually."
fi fi
msg_info "Starting Service" msg_info "Starting Termix"
systemctl start termix systemctl start termix
msg_ok "Started Service" msg_ok "Started Termix"
msg_ok "Updated successfully!" msg_ok "Updated successfully!"
fi fi
exit exit

View File

@@ -29,7 +29,6 @@ function update_script() {
fi fi
NODE_VERSION="22" setup_nodejs NODE_VERSION="22" setup_nodejs
hash -r 2>/dev/null || true
if check_for_gh_release "tududi" "chrisvel/tududi"; then if check_for_gh_release "tududi" "chrisvel/tududi"; then
msg_info "Stopping Service" msg_info "Stopping Service"

View File

@@ -47,7 +47,7 @@ function update_script() {
msg_info "Installing Python Dependencies" msg_info "Installing Python Dependencies"
cd /opt/yubal cd /opt/yubal
$STD uv sync --no-dev --frozen $STD uv sync --package yubal-api --no-dev --frozen
msg_ok "Installed Python Dependencies" msg_ok "Installed Python Dependencies"
msg_info "Starting Services" msg_info "Starting Services"

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: cobalt (cobaltgit)
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
# Source: https://ntfy.sh/
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing ntfy"
$STD apk add --no-cache ntfy ntfy-openrc libcap
sed -i '/^listen-http/s/^\(.*\)$/#\1\n/' /etc/ntfy/server.yml
setcap 'cap_net_bind_service=+ep' /usr/bin/ntfy
$STD rc-update add ntfy default
$STD service ntfy start
msg_ok "Installed ntfy"
motd_ssh
customize

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://anytype.io
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
setup_mongodb
msg_info "Configuring MongoDB Replica Set"
cat <<EOF >>/etc/mongod.conf
replication:
replSetName: "rs0"
EOF
systemctl restart mongod
sleep 3
$STD mongosh --eval 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "127.0.0.1:27017"}]})'
msg_ok "Configured MongoDB Replica Set"
msg_info "Installing Redis Stack"
setup_deb822_repo \
"redis-stack" \
"https://packages.redis.io/gpg" \
"https://packages.redis.io/deb" \
"jammy" \
"main"
$STD apt install -y redis-stack-server
systemctl enable -q --now redis-stack-server
msg_ok "Installed Redis Stack"
fetch_and_deploy_gh_release "anytype" "grishy/any-sync-bundle" "prebuild" "latest" "/opt/anytype" "any-sync-bundle_*_linux_amd64.tar.gz"
chmod +x /opt/anytype/any-sync-bundle
msg_info "Configuring Anytype"
mkdir -p /opt/anytype/data/storage
cat <<EOF >/opt/anytype/.env
ANY_SYNC_BUNDLE_CONFIG=/opt/anytype/data/bundle-config.yml
ANY_SYNC_BUNDLE_CLIENT_CONFIG=/opt/anytype/data/client-config.yml
ANY_SYNC_BUNDLE_INIT_STORAGE=/opt/anytype/data/storage/
ANY_SYNC_BUNDLE_INIT_EXTERNAL_ADDRS=${LOCAL_IP}
ANY_SYNC_BUNDLE_INIT_MONGO_URI=mongodb://127.0.0.1:27017/
ANY_SYNC_BUNDLE_INIT_REDIS_URI=redis://127.0.0.1:6379/
ANY_SYNC_BUNDLE_LOG_LEVEL=info
EOF
msg_ok "Configured Anytype"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/anytype.service
[Unit]
Description=Anytype Sync Server (any-sync-bundle)
After=network-online.target mongod.service redis-stack-server.service
Wants=network-online.target
Requires=mongod.service redis-stack-server.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/anytype
EnvironmentFile=/opt/anytype/.env
ExecStart=/opt/anytype/any-sync-bundle start-bundle
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now anytype
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc

View File

@@ -35,7 +35,6 @@ setup_hwaccel
msg_info "Installing Channels DVR Server (Patience)" msg_info "Installing Channels DVR Server (Patience)"
cd /opt cd /opt
$STD bash <(curl -fsSL https://getchannels.com/dvr/setup.sh) $STD bash <(curl -fsSL https://getchannels.com/dvr/setup.sh)
sed -i -e 's/^sgx:x:104:$/render:x:104:root/' -e 's/^render:x:106:root$/sgx:x:106:/' /etc/group
msg_ok "Installed Channels DVR Server" msg_ok "Installed Channels DVR Server"
motd_ssh motd_ssh

View File

@@ -13,17 +13,9 @@ setting_up_container
network_check network_check
update_os update_os
setup_hwaccel
fetch_and_deploy_gh_release "emby" "MediaBrowser/Emby.Releases" "binary" fetch_and_deploy_gh_release "emby" "MediaBrowser/Emby.Releases" "binary"
msg_info "Configuring Emby" setup_hwaccel "emby"
if [[ "$CTTYPE" == "0" ]]; then
sed -i -e 's/^ssl-cert:x:104:$/render:x:104:root,emby/' -e 's/^render:x:108:root,emby$/ssl-cert:x:108:/' /etc/group
else
sed -i -e 's/^ssl-cert:x:104:$/render:x:104:emby/' -e 's/^render:x:108:emby$/ssl-cert:x:108:/' /etc/group
fi
msg_ok "Configured Emby"
motd_ssh motd_ssh
customize customize

View File

@@ -146,7 +146,7 @@ ldconfig
msg_ok "Built libUSB" msg_ok "Built libUSB"
msg_info "Bootstrapping pip" msg_info "Bootstrapping pip"
wget -q https://bootstrap.pypa.io/get-pip.py -O /tmp/get-pip.py curl_with_retry "https://bootstrap.pypa.io/get-pip.py" "/tmp/get-pip.py"
sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' /tmp/get-pip.py sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' /tmp/get-pip.py
$STD python3 /tmp/get-pip.py "pip" $STD python3 /tmp/get-pip.py "pip"
rm -f /tmp/get-pip.py rm -f /tmp/get-pip.py
@@ -169,13 +169,13 @@ NODE_VERSION="20" setup_nodejs
msg_info "Downloading Inference Models" msg_info "Downloading Inference Models"
mkdir -p /models /openvino-model mkdir -p /models /openvino-model
wget -q -O /edgetpu_model.tflite https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite curl_with_retry "https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite" "/edgetpu_model.tflite"
wget -q -O /models/cpu_model.tflite https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite curl_with_retry "https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite" "/models/cpu_model.tflite"
cp /opt/frigate/labelmap.txt /labelmap.txt cp /opt/frigate/labelmap.txt /labelmap.txt
msg_ok "Downloaded Inference Models" msg_ok "Downloaded Inference Models"
msg_info "Downloading Audio Model" msg_info "Downloading Audio Model"
wget -q -O /tmp/yamnet.tar.gz https://www.kaggle.com/api/v1/models/google/yamnet/tfLite/classification-tflite/1/download curl_with_retry "https://www.kaggle.com/api/v1/models/google/yamnet/tfLite/classification-tflite/1/download" "/tmp/yamnet.tar.gz"
$STD tar xzf /tmp/yamnet.tar.gz -C / $STD tar xzf /tmp/yamnet.tar.gz -C /
mv /1.tflite /cpu_audio_model.tflite mv /1.tflite /cpu_audio_model.tflite
cp /opt/frigate/audio-labelmap.txt /audio-labelmap.txt cp /opt/frigate/audio-labelmap.txt /audio-labelmap.txt
@@ -205,7 +205,7 @@ msg_ok "Installed OpenVino"
msg_info "Building OpenVino Model" msg_info "Building OpenVino Model"
cd /models cd /models
wget -q http://download.tensorflow.org/models/object_detection/ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz curl_with_retry "http://download.tensorflow.org/models/object_detection/ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz" "ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz"
$STD tar -zxf ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz --no-same-owner $STD tar -zxf ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz --no-same-owner
if python3 /opt/frigate/docker/main/build_ov_model.py &>/dev/null; then if python3 /opt/frigate/docker/main/build_ov_model.py &>/dev/null; then
mkdir -p /openvino-model mkdir -p /openvino-model
@@ -219,7 +219,7 @@ if python3 /opt/frigate/docker/main/build_ov_model.py &>/dev/null; then
if [[ -n "$OV_LABELS" ]]; then if [[ -n "$OV_LABELS" ]]; then
ln -sf "$OV_LABELS" /openvino-model/coco_91cl_bkgr.txt ln -sf "$OV_LABELS" /openvino-model/coco_91cl_bkgr.txt
else else
wget -q "https://raw.githubusercontent.com/openvinotoolkit/open_model_zoo/master/data/dataset_classes/coco_91cl_bkgr.txt" -O /openvino-model/coco_91cl_bkgr.txt curl_with_retry "https://raw.githubusercontent.com/openvinotoolkit/open_model_zoo/master/data/dataset_classes/coco_91cl_bkgr.txt" "/openvino-model/coco_91cl_bkgr.txt"
fi fi
fi fi
sed -i 's/truck/car/g' /openvino-model/coco_91cl_bkgr.txt sed -i 's/truck/car/g' /openvino-model/coco_91cl_bkgr.txt
@@ -246,7 +246,7 @@ msg_info "Configuring Frigate"
mkdir -p /config /media/frigate mkdir -p /config /media/frigate
cp -r /opt/frigate/config/. /config cp -r /opt/frigate/config/. /config
curl -fsSL "https://github.com/intel-iot-devkit/sample-videos/raw/master/person-bicycle-car-detection.mp4" -o "/media/frigate/person-bicycle-car-detection.mp4" curl_with_retry "https://github.com/intel-iot-devkit/sample-videos/raw/master/person-bicycle-car-detection.mp4" "/media/frigate/person-bicycle-car-detection.mp4"
echo "tmpfs /tmp/cache tmpfs defaults 0 0" >>/etc/fstab echo "tmpfs /tmp/cache tmpfs defaults 0 0" >>/etc/fstab
@@ -289,7 +289,7 @@ detect:
enabled: false enabled: false
EOF EOF
if grep -q -o -m1 -E 'avx[^ ]*|sse4_2' /proc/cpuinfo; then if grep -q -o -m1 -E 'avx[^ ]*|sse4_2' /proc/cpuinfo && [[ -f /openvino-model/ssdlite_mobilenet_v2.xml ]] && [[ -f /openvino-model/coco_91cl_bkgr.txt ]]; then
cat <<EOF >>/config/config.yml cat <<EOF >>/config/config.yml
ffmpeg: ffmpeg:
hwaccel_args: auto hwaccel_args: auto

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/qdm12/gluetun
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 \
openvpn \
wireguard-tools \
iptables
msg_ok "Installed Dependencies"
msg_info "Configuring iptables"
$STD update-alternatives --set iptables /usr/sbin/iptables-legacy
$STD update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
ln -sf /usr/sbin/openvpn /usr/sbin/openvpn2.6
msg_ok "Configured iptables"
setup_go
fetch_and_deploy_gh_release "gluetun" "qdm12/gluetun" "tarball"
msg_info "Building Gluetun"
cd /opt/gluetun
$STD go mod download
CGO_ENABLED=0 $STD go build -trimpath -ldflags="-s -w" -o /usr/local/bin/gluetun ./cmd/gluetun/
msg_ok "Built Gluetun"
msg_info "Configuring Gluetun"
mkdir -p /opt/gluetun-data
touch /etc/alpine-release
ln -sf /opt/gluetun-data /gluetun
cat <<EOF >/opt/gluetun-data/.env
VPN_SERVICE_PROVIDER=custom
VPN_TYPE=openvpn
OPENVPN_CUSTOM_CONFIG=/opt/gluetun-data/custom.ovpn
OPENVPN_USER=
OPENVPN_PASSWORD=
OPENVPN_PROCESS_USER=root
PUID=0
PGID=0
HTTP_CONTROL_SERVER_ADDRESS=:8000
HTTPPROXY=off
SHADOWSOCKS=off
PPROF_ENABLED=no
PPROF_BLOCK_PROFILE_RATE=0
PPROF_MUTEX_PROFILE_RATE=0
PPROF_HTTP_SERVER_ADDRESS=:6060
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on
HEALTH_SERVER_ADDRESS=127.0.0.1:9999
DNS_UPSTREAM_RESOLVERS=cloudflare
LOG_LEVEL=info
STORAGE_FILEPATH=/gluetun/servers.json
PUBLICIP_FILE=/gluetun/ip
VPN_PORT_FORWARDING_STATUS_FILE=/gluetun/forwarded_port
TZ=UTC
EOF
msg_ok "Configured Gluetun"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/gluetun.service
[Unit]
Description=Gluetun VPN Client
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/gluetun-data
EnvironmentFile=/opt/gluetun-data/.env
UnsetEnvironment=USER
ExecStartPre=/bin/sh -c 'rm -f /etc/openvpn/target.ovpn'
ExecStart=/usr/local/bin/gluetun
Restart=on-failure
RestartSec=5
AmbientCapabilities=CAP_NET_ADMIN
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now gluetun
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc

View File

@@ -32,13 +32,13 @@ if [ -d /dev/dri ]; then
$STD apt install -y --no-install-recommends patchelf $STD apt install -y --no-install-recommends patchelf
tmp_dir=$(mktemp -d) tmp_dir=$(mktemp -d)
$STD pushd "$tmp_dir" $STD pushd "$tmp_dir"
curl -fsSLO https://raw.githubusercontent.com/immich-app/immich/refs/heads/main/machine-learning/Dockerfile curl_with_retry "https://raw.githubusercontent.com/immich-app/immich/refs/heads/main/machine-learning/Dockerfile" "Dockerfile"
readarray -t INTEL_URLS < <( readarray -t INTEL_URLS < <(
sed -n "/intel-[igc|opencl]/p" ./Dockerfile | awk '{print $3}' sed -n "/intel-[igc|opencl]/p" ./Dockerfile | awk '{print $3}'
sed -n "/libigdgmm12/p" ./Dockerfile | awk '{print $3}' sed -n "/libigdgmm12/p" ./Dockerfile | awk '{print $3}'
) )
for url in "${INTEL_URLS[@]}"; do for url in "${INTEL_URLS[@]}"; do
curl -fsSLO "$url" curl_with_retry "$url" "$(basename "$url")"
done done
$STD apt install -y ./libigdgmm12*.deb $STD apt install -y ./libigdgmm12*.deb
rm ./libigdgmm12*.deb rm ./libigdgmm12*.deb
@@ -154,6 +154,10 @@ sed -i "s/^#shared_preload.*/shared_preload_libraries = 'vchord.so'/" /etc/postg
systemctl restart postgresql.service systemctl restart postgresql.service
PG_DB_NAME="immich" PG_DB_USER="immich" PG_DB_GRANT_SUPERUSER="true" PG_DB_SKIP_ALTER_ROLE="true" setup_postgresql_db PG_DB_NAME="immich" PG_DB_USER="immich" PG_DB_GRANT_SUPERUSER="true" PG_DB_SKIP_ALTER_ROLE="true" setup_postgresql_db
msg_info "Installing GCC-13 (available as fallback compiler)"
$STD apt install -y gcc-13 g++-13
msg_ok "Installed GCC-13"
msg_warn "Compiling Custom Photo-processing Libraries (can take anywhere from 15min to 2h)" msg_warn "Compiling Custom Photo-processing Libraries (can take anywhere from 15min to 2h)"
LD_LIBRARY_PATH=/usr/local/lib LD_LIBRARY_PATH=/usr/local/lib
export LD_RUN_PATH=/usr/local/lib export LD_RUN_PATH=/usr/local/lib
@@ -290,7 +294,7 @@ GEO_DIR="${INSTALL_DIR}/geodata"
mkdir -p {"${APP_DIR}","${UPLOAD_DIR}","${GEO_DIR}","${INSTALL_DIR}"/cache} mkdir -p {"${APP_DIR}","${UPLOAD_DIR}","${GEO_DIR}","${INSTALL_DIR}"/cache}
fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "v2.5.6" "$SRC_DIR" fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "v2.5.6" "$SRC_DIR"
PNPM_VERSION="$(jq -r '.packageManager | split("@")[1]' ${SRC_DIR}/package.json)" PNPM_VERSION="$(jq -r '.packageManager | split("@")[1] | split("+")[0]' ${SRC_DIR}/package.json)"
NODE_VERSION="24" NODE_MODULE="pnpm@${PNPM_VERSION}" setup_nodejs NODE_VERSION="24" NODE_MODULE="pnpm@${PNPM_VERSION}" setup_nodejs
msg_info "Installing Immich (patience)" msg_info "Installing Immich (patience)"
@@ -340,15 +344,36 @@ cd "$SRC_DIR"/machine-learning
$STD useradd -U -s /usr/sbin/nologin -r -M -d "$INSTALL_DIR" immich $STD useradd -U -s /usr/sbin/nologin -r -M -d "$INSTALL_DIR" immich
mkdir -p "$ML_DIR" && chown -R immich:immich "$INSTALL_DIR" mkdir -p "$ML_DIR" && chown -R immich:immich "$INSTALL_DIR"
export VIRTUAL_ENV="${ML_DIR}/ml-venv" export VIRTUAL_ENV="${ML_DIR}/ml-venv"
export UV_HTTP_TIMEOUT=300
if [[ -f ~/.openvino ]]; then if [[ -f ~/.openvino ]]; then
ML_PYTHON="python3.13"
msg_info "Pre-installing Python ${ML_PYTHON} for machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv python install "${ML_PYTHON}" && break
[[ $attempt -lt 3 ]] && msg_warn "Python download attempt $attempt failed, retrying..." && sleep 5
done
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Installing HW-accelerated machine-learning" msg_info "Installing HW-accelerated machine-learning"
$STD uv add --no-sync --optional openvino onnxruntime-openvino==1.24.1 --active -n -p python3.13 --managed-python $STD uv add --no-sync --optional openvino onnxruntime-openvino==1.24.1 --active -n -p "${ML_PYTHON}" --managed-python
$STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p python3.13 --managed-python for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT -nu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -lt 3 ]] && msg_warn "uv sync attempt $attempt failed, retrying..." && sleep 10
done
patchelf --clear-execstack "${VIRTUAL_ENV}/lib/python3.13/site-packages/onnxruntime/capi/onnxruntime_pybind11_state.cpython-313-x86_64-linux-gnu.so" patchelf --clear-execstack "${VIRTUAL_ENV}/lib/python3.13/site-packages/onnxruntime/capi/onnxruntime_pybind11_state.cpython-313-x86_64-linux-gnu.so"
msg_ok "Installed HW-accelerated machine-learning" msg_ok "Installed HW-accelerated machine-learning"
else else
ML_PYTHON="python3.11"
msg_info "Pre-installing Python ${ML_PYTHON} for machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv python install "${ML_PYTHON}" && break
[[ $attempt -lt 3 ]] && msg_warn "Python download attempt $attempt failed, retrying..." && sleep 5
done
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Installing machine-learning" msg_info "Installing machine-learning"
$STD sudo --preserve-env=VIRTUAL_ENV -nu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p python3.11 --managed-python for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT -nu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -lt 3 ]] && msg_warn "uv sync attempt $attempt failed, retrying..." && sleep 10
done
msg_ok "Installed machine-learning" msg_ok "Installed machine-learning"
fi fi
cd "$SRC_DIR" cd "$SRC_DIR"
@@ -365,10 +390,10 @@ ln -s "$UPLOAD_DIR" "$ML_DIR"/upload
msg_info "Installing GeoNames data" msg_info "Installing GeoNames data"
cd "$GEO_DIR" cd "$GEO_DIR"
curl -fsSLZ -O "https://download.geonames.org/export/dump/admin1CodesASCII.txt" \ curl_with_retry "https://download.geonames.org/export/dump/admin1CodesASCII.txt" "admin1CodesASCII.txt"
-O "https://download.geonames.org/export/dump/admin2Codes.txt" \ curl_with_retry "https://download.geonames.org/export/dump/admin2Codes.txt" "admin2Codes.txt"
-O "https://download.geonames.org/export/dump/cities500.zip" \ curl_with_retry "https://download.geonames.org/export/dump/cities500.zip" "cities500.zip"
-O "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_10m_admin_0_countries.geojson" curl_with_retry "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_10m_admin_0_countries.geojson" "ne_10m_admin_0_countries.geojson"
unzip -q cities500.zip unzip -q cities500.zip
date --iso-8601=seconds | tr -d "\n" >geodata-date.txt date --iso-8601=seconds | tr -d "\n" >geodata-date.txt
rm cities500.zip rm cities500.zip

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Copyright (c) 2021-2026 tteck # Copyright (c) 2021-2026 community-scripts ORG
# Author: tteck (tteckster) # Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://jellyfin.org/ # Source: https://jellyfin.org/
@@ -14,31 +14,31 @@ network_check
update_os update_os
msg_custom "" "${GN}" "If NVIDIA GPU passthrough is detected, you'll be asked whether to install drivers in the container" msg_custom "" "${GN}" "If NVIDIA GPU passthrough is detected, you'll be asked whether to install drivers in the container"
setup_hwaccel
msg_info "Installing Jellyfin" msg_info "Installing Dependencies"
VERSION="$(awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release)" ensure_dependencies libjemalloc2
if ! dpkg -s libjemalloc2 >/dev/null 2>&1; then
$STD apt install -y libjemalloc2
fi
if [[ ! -f /usr/lib/libjemalloc.so ]]; then if [[ ! -f /usr/lib/libjemalloc.so ]]; then
ln -sf /usr/lib/x86_64-linux-gnu/libjemalloc.so.2 /usr/lib/libjemalloc.so ln -sf /usr/lib/x86_64-linux-gnu/libjemalloc.so.2 /usr/lib/libjemalloc.so
fi fi
if [[ ! -d /etc/apt/keyrings ]]; then msg_ok "Installed Dependencies"
mkdir -p /etc/apt/keyrings
fi
curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor --yes --output /etc/apt/keyrings/jellyfin.gpg
cat <<EOF >/etc/apt/sources.list.d/jellyfin.sources
Types: deb
URIs: https://repo.jellyfin.org/${PCT_OSTYPE}
Suites: ${VERSION}
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/jellyfin.gpg
EOF
$STD apt update msg_info "Setting up Jellyfin Repository"
$STD apt install -y jellyfin setup_deb822_repo \
"jellyfin" \
"https://repo.jellyfin.org/jellyfin_team.gpg.key" \
"https://repo.jellyfin.org/$(get_os_info id)" \
"$(get_os_info codename)"
msg_ok "Set up Jellyfin Repository"
msg_info "Installing Jellyfin"
$STD apt install -y jellyfin jellyfin-ffmpeg7
ln -sf /usr/lib/jellyfin-ffmpeg/ffmpeg /usr/bin/ffmpeg
ln -sf /usr/lib/jellyfin-ffmpeg/ffprobe /usr/bin/ffprobe
msg_ok "Installed Jellyfin"
setup_hwaccel "jellyfin"
msg_info "Configuring Jellyfin"
# Configure log rotation to prevent disk fill (keeps fail2ban compatibility) (PR: #1690 / Issue: #11224) # Configure log rotation to prevent disk fill (keeps fail2ban compatibility) (PR: #1690 / Issue: #11224)
cat <<EOF >/etc/logrotate.d/jellyfin cat <<EOF >/etc/logrotate.d/jellyfin
/var/log/jellyfin/*.log { /var/log/jellyfin/*.log {
@@ -55,12 +55,7 @@ EOF
chown -R jellyfin:adm /etc/jellyfin chown -R jellyfin:adm /etc/jellyfin
sleep 10 sleep 10
systemctl restart jellyfin systemctl restart jellyfin
if [[ "$CTTYPE" == "0" ]]; then msg_ok "Configured Jellyfin"
sed -i -e 's/^ssl-cert:x:104:$/render:x:104:root,jellyfin/' -e 's/^render:x:108:root,jellyfin$/ssl-cert:x:108:/' /etc/group
else
sed -i -e 's/^ssl-cert:x:104:$/render:x:104:jellyfin/' -e 's/^render:x:108:jellyfin$/ssl-cert:x:108:/' /etc/group
fi
msg_ok "Installed Jellyfin"
motd_ssh motd_ssh
customize customize

View File

@@ -42,8 +42,6 @@ EOF
$STD apt update $STD apt update
msg_ok "Set up Intel® Repositories" msg_ok "Set up Intel® Repositories"
setup_hwaccel
msg_info "Installing Intel® Level Zero" msg_info "Installing Intel® Level Zero"
# Debian 13+ has newer Level Zero packages in system repos that conflict with Intel repo packages # Debian 13+ has newer Level Zero packages in system repos that conflict with Intel repo packages
if is_debian && [[ "$(get_os_version_major)" -ge 13 ]]; then if is_debian && [[ "$(get_os_version_major)" -ge 13 ]]; then
@@ -89,11 +87,11 @@ msg_info "Creating ollama User and Group"
if ! id ollama >/dev/null 2>&1; then if ! id ollama >/dev/null 2>&1; then
useradd -r -s /usr/sbin/nologin -U -m -d /usr/share/ollama ollama useradd -r -s /usr/sbin/nologin -U -m -d /usr/share/ollama ollama
fi fi
$STD usermod -aG render ollama || true
$STD usermod -aG video ollama || true
$STD usermod -aG ollama $(id -u -n) $STD usermod -aG ollama $(id -u -n)
msg_ok "Created ollama User and adjusted Groups" msg_ok "Created ollama User and adjusted Groups"
setup_hwaccel "ollama"
msg_info "Creating Service" msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/ollama.service cat <<EOF >/etc/systemd/system/ollama.service
[Unit] [Unit]

View File

@@ -13,8 +13,6 @@ setting_up_container
network_check network_check
update_os update_os
setup_hwaccel
msg_info "Setting Up Plex Media Server Repository" msg_info "Setting Up Plex Media Server Repository"
setup_deb822_repo \ setup_deb822_repo \
"plexmediaserver" \ "plexmediaserver" \
@@ -26,13 +24,10 @@ msg_ok "Set Up Plex Media Server Repository"
msg_info "Installing Plex Media Server" msg_info "Installing Plex Media Server"
$STD apt install -y plexmediaserver $STD apt install -y plexmediaserver
if [[ "$CTTYPE" == "0" ]]; then
sed -i -e 's/^ssl-cert:x:104:plex$/render:x:104:root,plex/' -e 's/^render:x:108:root$/ssl-cert:x:108:plex/' /etc/group
else
sed -i -e 's/^ssl-cert:x:104:plex$/render:x:104:plex/' -e 's/^render:x:108:$/ssl-cert:x:108:/' /etc/group
fi
msg_ok "Installed Plex Media Server" msg_ok "Installed Plex Media Server"
setup_hwaccel "plex"
motd_ssh motd_ssh
customize customize
cleanup_lxc cleanup_lxc

View File

@@ -45,32 +45,58 @@ systemctl enable -q --now podman.socket
echo -e 'unqualified-search-registries=["docker.io"]' >>/etc/containers/registries.conf echo -e 'unqualified-search-registries=["docker.io"]' >>/etc/containers/registries.conf
msg_ok "Installed Podman" msg_ok "Installed Podman"
mkdir -p /etc/containers/systemd
read -r -p "${TAB3}Would you like to add Portainer? <y/N> " prompt read -r -p "${TAB3}Would you like to add Portainer? <y/N> " prompt
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
msg_info "Installing Portainer $PORTAINER_LATEST_VERSION" msg_info "Installing Portainer $PORTAINER_LATEST_VERSION"
podman volume create portainer_data >/dev/null podman volume create portainer_data >/dev/null
$STD podman run -d \ cat <<EOF >/etc/containers/systemd/portainer.container
-p 8000:8000 \ [Unit]
-p 9443:9443 \ Description=Portainer Container
--name=portainer \ After=network-online.target
--restart=always \
-v /run/podman/podman.sock:/var/run/docker.sock \ [Container]
-v portainer_data:/data \ Image=docker.io/portainer/portainer-ce:latest
portainer/portainer-ce:latest ContainerName=portainer
PublishPort=8000:8000
PublishPort=9443:9443
Volume=/run/podman/podman.sock:/var/run/docker.sock
Volume=portainer_data:/data
[Service]
Restart=always
[Install]
WantedBy=default.target multi-user.target
EOF
systemctl daemon-reload
$STD systemctl start portainer
msg_ok "Installed Portainer $PORTAINER_LATEST_VERSION" msg_ok "Installed Portainer $PORTAINER_LATEST_VERSION"
else else
read -r -p "${TAB3}Would you like to add the Portainer Agent? <y/N> " prompt read -r -p "${TAB3}Would you like to add the Portainer Agent? <y/N> " prompt
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
msg_info "Installing Portainer agent $PORTAINER_AGENT_LATEST_VERSION" msg_info "Installing Portainer agent $PORTAINER_AGENT_LATEST_VERSION"
podman volume create temp >/dev/null cat <<EOF >/etc/containers/systemd/portainer-agent.container
podman volume remove temp >/dev/null [Unit]
$STD podman run -d \ Description=Portainer Agent Container
-p 9001:9001 \ After=network-online.target
--name portainer_agent \
--restart=always \ [Container]
-v /run/podman/podman.sock:/var/run/docker.sock \ Image=docker.io/portainer/agent:latest
-v /var/lib/containers/storage/volumes:/var/lib/docker/volumes \ ContainerName=portainer_agent
portainer/agent PublishPort=9001:9001
Volume=/run/podman/podman.sock:/var/run/docker.sock
Volume=/var/lib/containers/storage/volumes:/var/lib/docker/volumes
[Service]
Restart=always
[Install]
WantedBy=default.target multi-user.target
EOF
systemctl daemon-reload
$STD systemctl start portainer-agent
msg_ok "Installed Portainer Agent $PORTAINER_AGENT_LATEST_VERSION" msg_ok "Installed Portainer Agent $PORTAINER_AGENT_LATEST_VERSION"
fi fi
fi fi
@@ -81,19 +107,29 @@ msg_ok "Pulled Home Assistant Image"
msg_info "Installing Home Assistant" msg_info "Installing Home Assistant"
$STD podman volume create hass_config $STD podman volume create hass_config
$STD podman run -d \ cat <<EOF >/etc/containers/systemd/homeassistant.container
--name homeassistant \ [Unit]
--restart unless-stopped \ Description=Home Assistant Container
-v /dev:/dev \ After=network-online.target
-v hass_config:/config \
-v /etc/localtime:/etc/localtime:ro \ [Container]
-v /etc/timezone:/etc/timezone:ro \ Image=docker.io/homeassistant/home-assistant:stable
--net=host \ ContainerName=homeassistant
homeassistant/home-assistant:stable Volume=/dev:/dev
podman generate systemd \ Volume=hass_config:/config
--new --name homeassistant \ Volume=/etc/localtime:/etc/localtime:ro
>/etc/systemd/system/homeassistant.service Volume=/etc/timezone:/etc/timezone:ro
systemctl enable -q --now homeassistant Network=host
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target multi-user.target
EOF
systemctl daemon-reload
$STD systemctl start homeassistant
msg_ok "Installed Home Assistant" msg_ok "Installed Home Assistant"
motd_ssh motd_ssh

View File

@@ -45,32 +45,58 @@ systemctl enable -q --now podman.socket
echo -e 'unqualified-search-registries=["docker.io"]' >>/etc/containers/registries.conf echo -e 'unqualified-search-registries=["docker.io"]' >>/etc/containers/registries.conf
msg_ok "Installed Podman" msg_ok "Installed Podman"
mkdir -p /etc/containers/systemd
read -r -p "${TAB3}Would you like to add Portainer? <y/N> " prompt read -r -p "${TAB3}Would you like to add Portainer? <y/N> " prompt
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
msg_info "Installing Portainer $PORTAINER_LATEST_VERSION" msg_info "Installing Portainer $PORTAINER_LATEST_VERSION"
podman volume create portainer_data >/dev/null podman volume create portainer_data >/dev/null
$STD podman run -d \ cat <<EOF >/etc/containers/systemd/portainer.container
-p 8000:8000 \ [Unit]
-p 9443:9443 \ Description=Portainer Container
--name=portainer \ After=network-online.target
--restart=always \
-v /run/podman/podman.sock:/var/run/docker.sock \ [Container]
-v portainer_data:/data \ Image=docker.io/portainer/portainer-ce:latest
portainer/portainer-ce:latest ContainerName=portainer
PublishPort=8000:8000
PublishPort=9443:9443
Volume=/run/podman/podman.sock:/var/run/docker.sock
Volume=portainer_data:/data
[Service]
Restart=always
[Install]
WantedBy=default.target multi-user.target
EOF
systemctl daemon-reload
$STD systemctl start portainer
msg_ok "Installed Portainer $PORTAINER_LATEST_VERSION" msg_ok "Installed Portainer $PORTAINER_LATEST_VERSION"
else else
read -r -p "${TAB3}Would you like to add the Portainer Agent? <y/N> " prompt read -r -p "${TAB3}Would you like to add the Portainer Agent? <y/N> " prompt
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
msg_info "Installing Portainer agent $PORTAINER_AGENT_LATEST_VERSION" msg_info "Installing Portainer agent $PORTAINER_AGENT_LATEST_VERSION"
podman volume create temp >/dev/null cat <<EOF >/etc/containers/systemd/portainer-agent.container
podman volume remove temp >/dev/null [Unit]
$STD podman run -d \ Description=Portainer Agent Container
-p 9001:9001 \ After=network-online.target
--name portainer_agent \
--restart=always \ [Container]
-v /run/podman/podman.sock:/var/run/docker.sock \ Image=docker.io/portainer/agent:latest
-v /var/lib/containers/storage/volumes:/var/lib/docker/volumes \ ContainerName=portainer_agent
portainer/agent PublishPort=9001:9001
Volume=/run/podman/podman.sock:/var/run/docker.sock
Volume=/var/lib/containers/storage/volumes:/var/lib/docker/volumes
[Service]
Restart=always
[Install]
WantedBy=default.target multi-user.target
EOF
systemctl daemon-reload
$STD systemctl start portainer-agent
msg_ok "Installed Portainer Agent $PORTAINER_AGENT_LATEST_VERSION" msg_ok "Installed Portainer Agent $PORTAINER_AGENT_LATEST_VERSION"
fi fi
fi fi

View File

@@ -44,7 +44,7 @@ msg_ok "Configured RabbitMQ"
USE_ORIGINAL_FILENAME="true" fetch_and_deploy_gh_release "reitti" "dedicatedcode/reitti" "singlefile" "latest" "/opt/reitti" "reitti-app.jar" USE_ORIGINAL_FILENAME="true" fetch_and_deploy_gh_release "reitti" "dedicatedcode/reitti" "singlefile" "latest" "/opt/reitti" "reitti-app.jar"
mv /opt/reitti/reitti-*.jar /opt/reitti/reitti.jar mv /opt/reitti/reitti-*.jar /opt/reitti/reitti.jar
USE_ORIGINAL_FILENAME="true" fetch_and_deploy_gh_release "photon" "komoot/photon" "singlefile" "latest" "/opt/photon" "photon-0*.jar" USE_ORIGINAL_FILENAME="true" fetch_and_deploy_gh_release "photon" "komoot/photon" "singlefile" "latest" "/opt/photon" "photon-*.jar"
mv /opt/photon/photon-*.jar /opt/photon/photon.jar mv /opt/photon/photon-*.jar /opt/photon/photon.jar
msg_info "Installing Nginx Tile Cache" msg_info "Installing Nginx Tile Cache"

View File

@@ -14,7 +14,9 @@ network_check
update_os update_os
msg_info "Installing Dependencies" msg_info "Installing Dependencies"
$STD apt-get install -y build-essential $STD apt install -y \
build-essential \
python3-setuptools
msg_ok "Installed Dependencies" msg_ok "Installed Dependencies"
fetch_and_deploy_gh_release "seerr" "seerr-team/seerr" "tarball" fetch_and_deploy_gh_release "seerr" "seerr-team/seerr" "tarball"

View File

@@ -47,7 +47,7 @@ msg_ok "Configured Sparky Fitness"
msg_info "Building Backend" msg_info "Building Backend"
cd /opt/sparkyfitness/SparkyFitnessServer cd /opt/sparkyfitness/SparkyFitnessServer
$STD npm install --legacy-peer-deps $STD pnpm install
msg_ok "Built Backend" msg_ok "Built Backend"
msg_info "Building Frontend (Patience)" msg_info "Building Frontend (Patience)"

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: johanngrobe
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/oss-apps/split-pro
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
NODE_VERSION="22" NODE_MODULE="pnpm" setup_nodejs
PG_VERSION="17" PG_MODULES="cron" setup_postgresql
msg_info "Installing Dependencies"
$STD apt install -y openssl
msg_ok "Installed Dependencies"
PG_DB_NAME="splitpro" PG_DB_USER="splitpro" PG_DB_EXTENSIONS="pg_cron" setup_postgresql_db
fetch_and_deploy_gh_release "split-pro" "oss-apps/split-pro" "tarball"
msg_info "Installing Dependencies"
cd /opt/split-pro
$STD pnpm install --frozen-lockfile
msg_ok "Installed Dependencies"
msg_info "Building Split Pro"
cd /opt/split-pro
mkdir -p /opt/split-pro_data/uploads
ln -sf /opt/split-pro_data/uploads /opt/split-pro/uploads
NEXTAUTH_SECRET=$(openssl rand -base64 32)
cp .env.example .env
sed -i "s|^DATABASE_URL=.*|DATABASE_URL=\"postgresql://${PG_DB_USER}:${PG_DB_PASS}@localhost:5432/${PG_DB_NAME}\"|" .env
sed -i "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=\"${NEXTAUTH_SECRET}\"|" .env
sed -i "s|^NEXTAUTH_URL=.*|NEXTAUTH_URL=\"http://${LOCAL_IP}:3000\"|" .env
sed -i "s|^NEXTAUTH_URL_INTERNAL=.*|NEXTAUTH_URL_INTERNAL=\"http://localhost:3000\"|" .env
sed -i "/^POSTGRES_CONTAINER_NAME=/d" .env
sed -i "/^POSTGRES_USER=/d" .env
sed -i "/^POSTGRES_PASSWORD=/d" .env
sed -i "/^POSTGRES_DB=/d" .env
sed -i "/^POSTGRES_PORT=/d" .env
$STD pnpm build
$STD pnpm exec prisma migrate deploy
msg_ok "Built Split Pro"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/split-pro.service
[Unit]
Description=Split Pro
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/split-pro
EnvironmentFile=/opt/split-pro/.env
ExecStart=/usr/bin/pnpm start
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now split-pro
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc

View File

@@ -20,12 +20,16 @@ msg_ok "Installed Dependencies"
msg_info "Installing Tdarr" msg_info "Installing Tdarr"
mkdir -p /opt/tdarr mkdir -p /opt/tdarr
cd /opt/tdarr cd /opt/tdarr
RELEASE=$(curl -fsSL https://f000.backblazeb2.com/file/tdarrs/versions.json | grep -oP '(?<="Tdarr_Updater": ")[^"]+' | grep linux_x64 | head -n 1) RELEASE=$(curl_with_retry "https://f000.backblazeb2.com/file/tdarrs/versions.json" "-" | grep -oP '(?<="Tdarr_Updater": ")[^"]+' | grep linux_x64 | head -n 1)
curl -fsSL "$RELEASE" -o Tdarr_Updater.zip curl_with_retry "$RELEASE" "Tdarr_Updater.zip"
$STD unzip Tdarr_Updater.zip $STD unzip Tdarr_Updater.zip
chmod +x Tdarr_Updater chmod +x Tdarr_Updater
$STD ./Tdarr_Updater $STD ./Tdarr_Updater
rm -rf /opt/tdarr/Tdarr_Updater.zip rm -rf /opt/tdarr/Tdarr_Updater.zip
[[ -f /opt/tdarr/Tdarr_Server/Tdarr_Server ]] || {
msg_error "Tdarr_Updater failed — tdarr.io may be blocked by local DNS"
exit 250
}
msg_ok "Installed Tdarr" msg_ok "Installed Tdarr"
setup_hwaccel setup_hwaccel

View File

@@ -19,9 +19,41 @@ $STD apt install -y \
python3 \ python3 \
nginx \ nginx \
openssl \ openssl \
gettext-base gettext-base \
libcairo2-dev \
libjpeg62-turbo-dev \
libpng-dev \
libtool-bin \
uuid-dev \
libvncserver-dev \
freerdp3-dev \
libssh2-1-dev \
libtelnet-dev \
libwebsockets-dev \
libpulse-dev \
libvorbis-dev \
libwebp-dev \
libssl-dev \
libpango1.0-dev \
libswscale-dev \
libavcodec-dev \
libavutil-dev \
libavformat-dev
msg_ok "Installed Dependencies" msg_ok "Installed Dependencies"
msg_info "Building Guacamole Server (guacd)"
fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server" "latest" "/opt/guacamole-server"
cd /opt/guacamole-server
export CPPFLAGS="-Wno-error=deprecated-declarations"
$STD autoreconf -fi
$STD ./configure --with-init-dir=/etc/init.d --enable-allow-freerdp-snapshots
$STD make
$STD make install
$STD ldconfig
cd /opt
rm -rf /opt/guacamole-server
msg_ok "Built Guacamole Server (guacd)"
NODE_VERSION="22" setup_nodejs NODE_VERSION="22" setup_nodejs
fetch_and_deploy_gh_release "termix" "Termix-SSH/Termix" fetch_and_deploy_gh_release "termix" "Termix-SSH/Termix"
@@ -74,17 +106,46 @@ systemctl reload nginx
msg_ok "Configured Nginx" msg_ok "Configured Nginx"
msg_info "Creating Service" msg_info "Creating Service"
mkdir -p /etc/guacamole
cat <<EOF >/etc/guacamole/guacd.conf
[server]
bind_host = 127.0.0.1
bind_port = 4822
EOF
cat <<EOF >/opt/termix/.env
NODE_ENV=production
DATA_DIR=/opt/termix/data
GUACD_HOST=127.0.0.1
GUACD_PORT=4822
EOF
cat <<EOF >/etc/systemd/system/guacd.service
[Unit]
Description=Guacamole Proxy Daemon (guacd)
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/sbin/guacd -f -b 127.0.0.1 -l 4822
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
cat <<EOF >/etc/systemd/system/termix.service cat <<EOF >/etc/systemd/system/termix.service
[Unit] [Unit]
Description=Termix Backend Description=Termix Backend
After=network.target After=network.target guacd.service
Wants=guacd.service
[Service] [Service]
Type=simple Type=simple
User=root User=root
WorkingDirectory=/opt/termix WorkingDirectory=/opt/termix
Environment=NODE_ENV=production EnvironmentFile=/opt/termix/.env
Environment=DATA_DIR=/opt/termix/data
ExecStart=/usr/bin/node /opt/termix/dist/backend/backend/starter.js ExecStart=/usr/bin/node /opt/termix/dist/backend/backend/starter.js
Restart=on-failure Restart=on-failure
RestartSec=5 RestartSec=5
@@ -92,7 +153,7 @@ RestartSec=5
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
systemctl enable -q --now termix systemctl enable -q --now guacd termix
msg_ok "Created Service" msg_ok "Created Service"
motd_ssh motd_ssh

View File

@@ -1979,6 +1979,47 @@ extract_version_from_json() {
fi fi
} }
# ------------------------------------------------------------------------------
# Get latest GitHub tag (for repos that only publish tags, not releases).
#
# Usage:
# get_latest_gh_tag "owner/repo" [prefix]
#
# Arguments:
# $1 - GitHub repo (owner/repo)
# $2 - Optional prefix filter (e.g., "v" to only match tags starting with "v")
#
# Returns:
# Latest tag name (stdout), or returns 1 on failure
# ------------------------------------------------------------------------------
get_latest_gh_tag() {
local repo="$1"
local prefix="${2:-}"
local temp_file
temp_file=$(mktemp)
if ! github_api_call "https://api.github.com/repos/${repo}/tags?per_page=50" "$temp_file"; then
rm -f "$temp_file"
return 1
fi
local tag=""
if [[ -n "$prefix" ]]; then
tag=$(jq -r --arg p "$prefix" '[.[] | select(.name | startswith($p))][0].name // empty' "$temp_file")
else
tag=$(jq -r '.[0].name // empty' "$temp_file")
fi
rm -f "$temp_file"
if [[ -z "$tag" ]]; then
msg_error "No tags found for ${repo}"
return 1
fi
echo "$tag"
}
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Get latest GitHub release version with fallback to tags # Get latest GitHub release version with fallback to tags
# Usage: get_latest_github_release "owner/repo" [strip_v] [include_prerelease] # Usage: get_latest_github_release "owner/repo" [strip_v] [include_prerelease]
@@ -2077,103 +2118,131 @@ verify_gpg_fingerprint() {
} }
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Get latest GitHub tag for a repository. # Fetches and deploys a GitHub tag-based source tarball.
# #
# Description: # Description:
# - Queries the GitHub API for tags (not releases) # - Downloads the source tarball for a given tag from GitHub
# - Useful for repos that only create tags, not full releases # - Extracts to the target directory
# - Supports optional prefix filter and version-only extraction # - Writes the version to ~/.<app>
# - Returns the latest tag name (printed to stdout)
# #
# Usage: # Usage:
# MONGO_VERSION=$(get_latest_gh_tag "mongodb/mongo-tools") # fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server"
# LATEST=$(get_latest_gh_tag "owner/repo" "v") # only tags starting with "v" # fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server" "latest" "/opt/guacamole-server"
# LATEST=$(get_latest_gh_tag "owner/repo" "" "true") # strip leading "v"
# #
# Arguments: # Arguments:
# $1 - GitHub repo (owner/repo) # $1 - App name (used for version file ~/.<app>)
# $2 - Tag prefix filter (optional, e.g. "v" or "100.") # $2 - GitHub repo (owner/repo)
# $3 - Strip prefix from result (optional, "true" to strip $2 prefix) # $3 - Tag version (default: "latest" → auto-detect via get_latest_gh_tag)
# # $4 - Target directory (default: /opt/$app)
# Returns:
# 0 on success (tag printed to stdout), 1 on failure
# #
# Notes: # Notes:
# - Skips tags containing "rc", "alpha", "beta", "dev", "test" # - Supports CLEAN_INSTALL=1 to wipe target before extracting
# - Sorts by version number (sort -V) to find the latest # - For repos that only publish tags, not GitHub Releases
# - Respects GITHUB_TOKEN for rate limiting
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
get_latest_gh_tag() { fetch_and_deploy_gh_tag() {
local repo="$1" local app="$1"
local prefix="${2:-}" local repo="$2"
local strip_prefix="${3:-false}" local version="${3:-latest}"
local target="${4:-/opt/$app}"
local app_lc=""
app_lc="$(echo "${app,,}" | tr -d ' ')"
local version_file="$HOME/.${app_lc}"
local header_args=() if [[ "$version" == "latest" ]]; then
[[ -n "${GITHUB_TOKEN:-}" ]] && header_args=(-H "Authorization: Bearer $GITHUB_TOKEN") version=$(get_latest_gh_tag "$repo") || {
msg_error "Failed to determine latest tag for ${repo}"
return 1
}
fi
local http_code="" local current_version=""
http_code=$(curl -sSL --max-time 20 -w "%{http_code}" -o /tmp/gh_tags.json \ [[ -f "$version_file" ]] && current_version=$(<"$version_file")
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"${header_args[@]}" \
"https://api.github.com/repos/${repo}/tags?per_page=100" 2>/dev/null) || true
if [[ "$http_code" == "401" ]]; then if [[ "$current_version" == "$version" ]]; then
msg_error "GitHub API authentication failed (HTTP 401)." msg_ok "$app is already up-to-date ($version)"
if [[ -n "${GITHUB_TOKEN:-}" ]]; then return 0
msg_error "Your GITHUB_TOKEN appears to be invalid or expired." fi
else
msg_error "The repository may require authentication. Try: export GITHUB_TOKEN=\"ghp_your_token\"" local tmpdir
fi tmpdir=$(mktemp -d) || return 1
rm -f /tmp/gh_tags.json local tarball_url="https://github.com/${repo}/archive/refs/tags/${version}.tar.gz"
local filename="${app_lc}-${version}.tar.gz"
msg_info "Fetching GitHub tag: ${app} (${version})"
download_file "$tarball_url" "$tmpdir/$filename" || {
msg_error "Download failed: $tarball_url"
rm -rf "$tmpdir"
return 1 return 1
}
mkdir -p "$target"
if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then
rm -rf "${target:?}/"*
fi fi
if [[ "$http_code" == "403" ]]; then tar --no-same-owner -xzf "$tmpdir/$filename" -C "$tmpdir" || {
msg_error "GitHub API rate limit exceeded (HTTP 403)." msg_error "Failed to extract tarball"
msg_error "To increase the limit, export a GitHub token before running the script:" rm -rf "$tmpdir"
msg_error " export GITHUB_TOKEN=\"ghp_your_token_here\""
rm -f /tmp/gh_tags.json
return 1 return 1
fi }
if [[ "$http_code" == "000" || -z "$http_code" ]]; then local unpack_dir
msg_error "GitHub API connection failed (no response)." unpack_dir=$(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | head -n1)
msg_error "Check your network/DNS: curl -sSL https://api.github.com/rate_limit"
rm -f /tmp/gh_tags.json
return 1
fi
if [[ "$http_code" != "200" ]] || [[ ! -s /tmp/gh_tags.json ]]; then shopt -s dotglob nullglob
msg_error "Unable to fetch tags for ${repo} (HTTP ${http_code})" cp -r "$unpack_dir"/* "$target/"
rm -f /tmp/gh_tags.json shopt -u dotglob nullglob
return 1
fi
local tags_json rm -rf "$tmpdir"
tags_json=$(</tmp/gh_tags.json) echo "$version" >"$version_file"
rm -f /tmp/gh_tags.json msg_ok "Deployed ${app} ${version} to ${target}"
# Extract tag names, filter by prefix, exclude pre-release patterns, sort by version
local latest=""
latest=$(echo "$tags_json" | grep -oP '"name":\s*"\K[^"]+' |
{ [[ -n "$prefix" ]] && grep "^${prefix}" || cat; } |
grep -viE '(rc|alpha|beta|dev|test|preview|snapshot)' |
sort -V | tail -n1)
if [[ -z "$latest" ]]; then
msg_warn "No matching tags found for ${repo}${prefix:+ (prefix: $prefix)}"
return 1
fi
if [[ "$strip_prefix" == "true" && -n "$prefix" ]]; then
latest="${latest#"$prefix"}"
fi
echo "$latest"
return 0 return 0
} }
# ------------------------------------------------------------------------------
# Checks for new GitHub tag (for repos without releases).
#
# Description:
# - Uses get_latest_gh_tag to fetch the latest tag
# - Compares it to a local cached version (~/.<app>)
# - If newer, sets global CHECK_UPDATE_RELEASE and returns 0
#
# Usage:
# if check_for_gh_tag "guacd" "apache/guacamole-server"; then
# fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server" "/opt/guacamole-server"
# fi
#
# Notes:
# - For repos that only publish tags, not GitHub Releases
# - Same interface as check_for_gh_release
# ------------------------------------------------------------------------------
check_for_gh_tag() {
local app="$1"
local repo="$2"
local prefix="${3:-}"
local app_lc=""
app_lc="$(echo "${app,,}" | tr -d ' ')"
local current_file="$HOME/.${app_lc}"
msg_info "Checking for update: ${app}"
local latest=""
latest=$(get_latest_gh_tag "$repo" "$prefix") || return 1
local current=""
[[ -f "$current_file" ]] && current="$(<"$current_file")"
if [[ -z "$current" || "$current" != "$latest" ]]; then
CHECK_UPDATE_RELEASE="$latest"
msg_ok "Update available: ${app} ${current:-not installed}${latest}"
return 0
fi
msg_ok "No update available: ${app} (${latest})"
return 1
}
# ============================================================================== # ==============================================================================
# INSTALL FUNCTIONS # INSTALL FUNCTIONS
# ============================================================================== # ==============================================================================
@@ -2519,6 +2588,8 @@ check_for_codeberg_release() {
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
create_self_signed_cert() { create_self_signed_cert() {
local APP_NAME="${1:-${APPLICATION}}" local APP_NAME="${1:-${APPLICATION}}"
local HOSTNAME="$(hostname -f)"
local IP="$(hostname -I | awk '{print $1}')"
local APP_NAME_LC=$(echo "${APP_NAME,,}" | tr -d ' ') local APP_NAME_LC=$(echo "${APP_NAME,,}" | tr -d ' ')
local CERT_DIR="/etc/ssl/${APP_NAME_LC}" local CERT_DIR="/etc/ssl/${APP_NAME_LC}"
local CERT_KEY="${CERT_DIR}/${APP_NAME_LC}.key" local CERT_KEY="${CERT_DIR}/${APP_NAME_LC}.key"
@@ -2536,8 +2607,8 @@ create_self_signed_cert() {
mkdir -p "$CERT_DIR" mkdir -p "$CERT_DIR"
$STD openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \ $STD openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \
-subj "/CN=${APP_NAME}" \ -subj "/CN=${HOSTNAME}" \
-addext "subjectAltName=DNS:${APP_NAME}" \ -addext "subjectAltName=DNS:${HOSTNAME},DNS:localhost,IP:${IP},IP:127.0.0.1" \
-keyout "$CERT_KEY" \ -keyout "$CERT_KEY" \
-out "$CERT_CRT" || { -out "$CERT_CRT" || {
msg_error "Failed to create self-signed certificate" msg_error "Failed to create self-signed certificate"
@@ -4185,6 +4256,8 @@ function setup_gs() {
# - NVIDIA requires matching host driver version # - NVIDIA requires matching host driver version
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
function setup_hwaccel() { function setup_hwaccel() {
local service_user="${1:-}"
# Check if user explicitly disabled GPU in advanced settings # Check if user explicitly disabled GPU in advanced settings
# ENABLE_GPU is exported from build.func # ENABLE_GPU is exported from build.func
if [[ "${ENABLE_GPU:-no}" == "no" ]]; then if [[ "${ENABLE_GPU:-no}" == "no" ]]; then
@@ -4436,7 +4509,7 @@ function setup_hwaccel() {
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# Device Permissions # Device Permissions
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
_setup_gpu_permissions "$in_ct" _setup_gpu_permissions "$in_ct" "$service_user"
cache_installed_version "hwaccel" "1.0" cache_installed_version "hwaccel" "1.0"
msg_ok "Setup Hardware Acceleration" msg_ok "Setup Hardware Acceleration"
@@ -4629,9 +4702,6 @@ _setup_amd_apu() {
$STD apt -y install firmware-amd-graphics 2>/dev/null || true $STD apt -y install firmware-amd-graphics 2>/dev/null || true
fi fi
# ROCm compute stack (OpenCL + HIP) - also works for many APUs
_setup_rocm "$os_id" "$os_codename"
msg_ok "AMD APU configured" msg_ok "AMD APU configured"
} }
@@ -4679,16 +4749,9 @@ _setup_rocm() {
return 0 return 0
} }
# AMDGPU driver repository (append to same keyring) # Note: The amdgpu/latest/ubuntu repo (kernel driver packages) is intentionally
{ # omitted — kernel drivers are managed by the Proxmox host, not the LXC container.
echo "" # Only the ROCm userspace compute stack is needed inside the container.
echo "Types: deb"
echo "URIs: https://repo.radeon.com/amdgpu/latest/ubuntu"
echo "Suites: ${ROCM_REPO_CODENAME}"
echo "Components: main"
echo "Architectures: amd64"
echo "Signed-By: /etc/apt/keyrings/rocm.gpg"
} >>/etc/apt/sources.list.d/rocm.sources
# Pin ROCm packages to prefer radeon repo # Pin ROCm packages to prefer radeon repo
cat <<EOF >/etc/apt/preferences.d/rocm-pin-600 cat <<EOF >/etc/apt/preferences.d/rocm-pin-600
@@ -4697,7 +4760,26 @@ Pin: release o=repo.radeon.com
Pin-Priority: 600 Pin-Priority: 600
EOF EOF
$STD apt update || msg_warn "apt update failed (AMD repo may be temporarily unavailable) — continuing anyway" # apt update with retry — repo.radeon.com CDN can be mid-sync (transient size mismatches).
# Run with ERR trap disabled so a transient failure does not abort the entire install.
local _apt_ok=0
for _attempt in 1 2 3; do
if (
set +e
apt-get update -qq 2>&1
exit $?
) 2>/dev/null; then
_apt_ok=1
break
fi
msg_warn "apt update failed (attempt ${_attempt}/3) — AMD repo may be temporarily unavailable, retrying in 30s…"
sleep 30
done
if [[ $_apt_ok -eq 0 ]]; then
msg_warn "apt update still failing after 3 attempts — skipping ROCm install"
return 0
fi
# Install only runtime packages — full 'rocm' meta-package includes 15GB+ dev tools # Install only runtime packages — full 'rocm' meta-package includes 15GB+ dev tools
$STD apt install -y rocm-opencl-runtime rocm-hip-runtime rocm-smi-lib 2>/dev/null || { $STD apt install -y rocm-opencl-runtime rocm-hip-runtime rocm-smi-lib 2>/dev/null || {
msg_warn "ROCm runtime install failed — trying minimal set" msg_warn "ROCm runtime install failed — trying minimal set"
@@ -5061,6 +5143,7 @@ EOF
# ══════════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════════
_setup_gpu_permissions() { _setup_gpu_permissions() {
local in_ct="$1" local in_ct="$1"
local service_user="${2:-}"
# /dev/dri permissions (Intel/AMD) # /dev/dri permissions (Intel/AMD)
if [[ "$in_ct" == "0" && -d /dev/dri ]]; then if [[ "$in_ct" == "0" && -d /dev/dri ]]; then
@@ -5127,6 +5210,12 @@ _setup_gpu_permissions() {
chmod 666 /dev/kfd 2>/dev/null || true chmod 666 /dev/kfd 2>/dev/null || true
msg_info "AMD ROCm compute device configured" msg_info "AMD ROCm compute device configured"
fi fi
# Add service user to render and video groups for GPU hardware acceleration
if [[ -n "$service_user" ]]; then
usermod -aG render "$service_user" 2>/dev/null || true
usermod -aG video "$service_user" 2>/dev/null || true
fi
} }
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@@ -6609,22 +6698,38 @@ EOF
# - Optionally uses official PGDG repository for specific versions # - Optionally uses official PGDG repository for specific versions
# - Detects existing PostgreSQL version # - Detects existing PostgreSQL version
# - Dumps all databases before upgrade # - Dumps all databases before upgrade
# - Installs optional PG_MODULES (e.g. postgis, contrib) # - Installs optional PG_MODULES (e.g. postgis, contrib, cron)
# - Restores dumped data post-upgrade # - Restores dumped data post-upgrade
# #
# Variables: # Variables:
# USE_PGDG_REPO - Use official PGDG repository (default: true) # USE_PGDG_REPO - Use official PGDG repository (default: true)
# Set to "false" to use distro packages instead # Set to "false" to use distro packages instead
# PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16) # PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16)
# PG_MODULES - Comma-separated list of modules (e.g. "postgis,contrib") # PG_MODULES - Comma-separated list of modules (e.g. "postgis,contrib,cron")
# #
# Examples: # Examples:
# setup_postgresql # Uses PGDG repo, PG 16 # setup_postgresql # Uses PGDG repo, PG 16
# PG_VERSION="17" setup_postgresql # Specific version from PGDG # PG_VERSION="17" setup_postgresql # Specific version from PGDG
# USE_PGDG_REPO=false setup_postgresql # Uses distro package instead # USE_PGDG_REPO=false setup_postgresql # Uses distro package instead
# PG_VERSION="17" PG_MODULES="cron" setup_postgresql # With pg_cron module
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
function setup_postgresql() { # Internal helper: Configure shared_preload_libraries for pg_cron
_configure_pg_cron_preload() {
local modules="${1:-}"
[[ -z "$modules" ]] && return 0
if [[ ",$modules," == *",cron,"* ]]; then
local current_libs
current_libs=$(sudo -u postgres psql -tAc "SHOW shared_preload_libraries;" 2>/dev/null || echo "")
if [[ "$current_libs" != *"pg_cron"* ]]; then
local new_libs="${current_libs:+${current_libs},}pg_cron"
$STD sudo -u postgres psql -c "ALTER SYSTEM SET shared_preload_libraries = '${new_libs}';"
$STD systemctl restart postgresql
fi
fi
}
setup_postgresql() {
local PG_VERSION="${PG_VERSION:-16}" local PG_VERSION="${PG_VERSION:-16}"
local PG_MODULES="${PG_MODULES:-}" local PG_MODULES="${PG_MODULES:-}"
local USE_PGDG_REPO="${USE_PGDG_REPO:-true}" local USE_PGDG_REPO="${USE_PGDG_REPO:-true}"
@@ -6662,6 +6767,7 @@ function setup_postgresql() {
$STD apt install -y "postgresql-${CURRENT_PG_VERSION}-${module}" 2>/dev/null || true $STD apt install -y "postgresql-${CURRENT_PG_VERSION}-${module}" 2>/dev/null || true
done done
fi fi
_configure_pg_cron_preload "$PG_MODULES"
return 0 return 0
fi fi
@@ -6697,6 +6803,7 @@ function setup_postgresql() {
$STD apt install -y "postgresql-${INSTALLED_VERSION}-${module}" 2>/dev/null || true $STD apt install -y "postgresql-${INSTALLED_VERSION}-${module}" 2>/dev/null || true
done done
fi fi
_configure_pg_cron_preload "$PG_MODULES"
return 0 return 0
fi fi
@@ -6718,6 +6825,7 @@ function setup_postgresql() {
$STD apt install -y "postgresql-${PG_VERSION}-${module}" 2>/dev/null || true $STD apt install -y "postgresql-${PG_VERSION}-${module}" 2>/dev/null || true
done done
fi fi
_configure_pg_cron_preload "$PG_MODULES"
return 0 return 0
fi fi
@@ -6745,13 +6853,16 @@ function setup_postgresql() {
local SUITE local SUITE
case "$DISTRO_CODENAME" in case "$DISTRO_CODENAME" in
trixie | forky | sid) trixie | forky | sid)
if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then
SUITE="trixie-pgdg" SUITE="trixie-pgdg"
else else
msg_warn "PGDG repo not available for ${DISTRO_CODENAME}, falling back to distro packages" msg_warn "PGDG repo not available for ${DISTRO_CODENAME}, falling back to distro packages"
USE_PGDG_REPO=false setup_postgresql USE_PGDG_REPO=false setup_postgresql
return $? return $?
fi fi
;; ;;
*) *)
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt") SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt")
@@ -6835,6 +6946,7 @@ function setup_postgresql() {
} }
done done
fi fi
_configure_pg_cron_preload "$PG_MODULES"
} }
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@@ -6853,6 +6965,7 @@ function setup_postgresql() {
# PG_DB_NAME="immich" PG_DB_USER="immich" PG_DB_EXTENSIONS="pgvector" setup_postgresql_db # PG_DB_NAME="immich" PG_DB_USER="immich" PG_DB_EXTENSIONS="pgvector" setup_postgresql_db
# PG_DB_NAME="ghostfolio" PG_DB_USER="ghostfolio" PG_DB_GRANT_SUPERUSER="true" setup_postgresql_db # PG_DB_NAME="ghostfolio" PG_DB_USER="ghostfolio" PG_DB_GRANT_SUPERUSER="true" setup_postgresql_db
# PG_DB_NAME="adventurelog" PG_DB_USER="adventurelog" PG_DB_EXTENSIONS="postgis" setup_postgresql_db # PG_DB_NAME="adventurelog" PG_DB_USER="adventurelog" PG_DB_EXTENSIONS="postgis" setup_postgresql_db
# PG_DB_NAME="splitpro" PG_DB_USER="splitpro" PG_DB_EXTENSIONS="pg_cron" setup_postgresql_db
# #
# Variables: # Variables:
# PG_DB_NAME - Database name (required) # PG_DB_NAME - Database name (required)
@@ -6884,6 +6997,13 @@ function setup_postgresql_db() {
$STD sudo -u postgres psql -c "CREATE ROLE $PG_DB_USER WITH LOGIN PASSWORD '$PG_DB_PASS';" $STD sudo -u postgres psql -c "CREATE ROLE $PG_DB_USER WITH LOGIN PASSWORD '$PG_DB_PASS';"
$STD sudo -u postgres psql -c "CREATE DATABASE $PG_DB_NAME WITH OWNER $PG_DB_USER ENCODING 'UTF8' TEMPLATE template0;" $STD sudo -u postgres psql -c "CREATE DATABASE $PG_DB_NAME WITH OWNER $PG_DB_USER ENCODING 'UTF8' TEMPLATE template0;"
# Configure pg_cron database BEFORE creating the extension (must be set before pg_cron loads)
if [[ -n "${PG_DB_EXTENSIONS:-}" ]] && [[ ",${PG_DB_EXTENSIONS//[[:space:]]/}," == *",pg_cron,"* ]]; then
$STD sudo -u postgres psql -c "ALTER SYSTEM SET cron.database_name = '${PG_DB_NAME}';"
$STD sudo -u postgres psql -c "ALTER SYSTEM SET cron.timezone = 'UTC';"
$STD systemctl restart postgresql
fi
# Install extensions (comma-separated) # Install extensions (comma-separated)
if [[ -n "${PG_DB_EXTENSIONS:-}" ]]; then if [[ -n "${PG_DB_EXTENSIONS:-}" ]]; then
IFS=',' read -ra EXT_LIST <<<"${PG_DB_EXTENSIONS:-}" IFS=',' read -ra EXT_LIST <<<"${PG_DB_EXTENSIONS:-}"
@@ -6893,6 +7013,12 @@ function setup_postgresql_db() {
done done
fi fi
# Grant pg_cron schema permissions to DB user
if [[ -n "${PG_DB_EXTENSIONS:-}" ]] && [[ ",${PG_DB_EXTENSIONS//[[:space:]]/}," == *",pg_cron,"* ]]; then
$STD sudo -u postgres psql -d "$PG_DB_NAME" -c "GRANT USAGE ON SCHEMA cron TO ${PG_DB_USER};"
$STD sudo -u postgres psql -d "$PG_DB_NAME" -c "GRANT ALL ON ALL TABLES IN SCHEMA cron TO ${PG_DB_USER};"
fi
# ALTER ROLE settings for Django/Rails compatibility (unless skipped) # ALTER ROLE settings for Django/Rails compatibility (unless skipped)
if [[ "${PG_DB_SKIP_ALTER_ROLE:-}" != "true" ]]; then if [[ "${PG_DB_SKIP_ALTER_ROLE:-}" != "true" ]]; then
$STD sudo -u postgres psql -c "ALTER ROLE $PG_DB_USER SET client_encoding TO 'utf8';" $STD sudo -u postgres psql -c "ALTER ROLE $PG_DB_USER SET client_encoding TO 'utf8';"
@@ -6934,7 +7060,6 @@ function setup_postgresql_db() {
export PG_DB_USER export PG_DB_USER
export PG_DB_PASS export PG_DB_PASS
} }
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Installs rbenv and ruby-build, installs Ruby and optionally Rails. # Installs rbenv and ruby-build, installs Ruby and optionally Rails.
# #