Compare commits

..

2 Commits

Author SHA1 Message Date
CanbiZ (MickLesk)
e13464e2ed Fix source URL for build functions in alpine-ntfy.sh 2026-03-18 13:49:29 +01:00
CanbiZ (MickLesk)
77e717a6a0 add Alpine Based install of ntfy (alpine-ntfy) 2026-03-18 13:48:42 +01:00
23 changed files with 88 additions and 958 deletions

View File

@@ -75,8 +75,7 @@ jobs:
const http = require('http'); const http = require('http');
const url = require('url'); const url = require('url');
function request(fullUrl, opts, redirectCount) { function request(fullUrl, opts) {
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:';
@@ -91,13 +90,6 @@ 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
View File

@@ -1,675 +0,0 @@
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,8 +48,7 @@ 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, redirectCount) { function request(fullUrl, opts) {
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:';
@@ -64,13 +63,6 @@ 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() {
@@ -133,15 +125,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; noteTypeToId[item.type.toLowerCase()] = item.id; } }); if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) noteTypeToId[item.type] = 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; installMethodTypeToId[item.type.toLowerCase()] = item.id; } }); if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) installMethodTypeToId[item.type] = 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; osToId[item.os.toLowerCase()] = item.id; } }); if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.os != null) osToId[item.os] = 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 } });
@@ -162,7 +154,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: new Date().toISOString().split('T')[0], script_updated: data.date_created || data.script_updated,
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,
@@ -171,8 +163,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 || null, default_user: (data.default_credentials && data.default_credentials.username) || data.default_user,
default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd || null, default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd,
is_dev: false is_dev: false
}; };
var resolvedType = typeValueToId[data.type]; var resolvedType = typeValueToId[data.type];
@@ -198,7 +190,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, script: scriptId }) body: JSON.stringify({ text: note.text || '', type: typeId })
}); });
if (postRes.ok) noteIds.push(JSON.parse(postRes.body).id); if (postRes.ok) noteIds.push(JSON.parse(postRes.body).id);
} }

View File

@@ -83,8 +83,7 @@ jobs:
const http = require('http'); const http = require('http');
const url = require('url'); const url = require('url');
function request(fullUrl, opts, redirectCount) { function request(fullUrl, opts) {
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:';
@@ -99,13 +98,6 @@ 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() {
@@ -159,7 +151,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({
script_updated: new Date().toISOString().split('T')[0], name: record.name || record.slug,
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,36 +423,16 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details> </details>
## 2026-03-19
### 🚀 Updated Scripts
- Owncast: increase default disk size from 2GB to 10GB [@Copilot](https://github.com/Copilot) ([#13079](https://github.com/community-scripts/ProxmoxVE/pull/13079))
- #### 🐞 Bug Fixes
- Increase Tracearr RAM; derive APP_VERSION [@MickLesk](https://github.com/MickLesk) ([#13087](https://github.com/community-scripts/ProxmoxVE/pull/13087))
- ProjectSend: Update application access URL [@tremor021](https://github.com/tremor021) ([#13078](https://github.com/community-scripts/ProxmoxVE/pull/13078))
- Dispatcharr: use npm install --no-audit --progress=false [@MickLesk](https://github.com/MickLesk) ([#13074](https://github.com/community-scripts/ProxmoxVE/pull/13074))
- core: reorder hwaccel setup and adjust GPU group usermod [@MickLesk](https://github.com/MickLesk) ([#13072](https://github.com/community-scripts/ProxmoxVE/pull/13072))
### 📚 Documentation
- github: add PocketBase bot workflow [@MickLesk](https://github.com/MickLesk) ([#13075](https://github.com/community-scripts/ProxmoxVE/pull/13075))
## 2026-03-18 ## 2026-03-18
### 🆕 New Scripts ### 🆕 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))
- Split-Pro ([#12975](https://github.com/community-scripts/ProxmoxVE/pull/12975))
### 🚀 Updated Scripts ### 🚀 Updated Scripts
- #### 🐞 Bug Fixes - #### 🐞 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)) - 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)) - Plex: restart service after update to apply new version [@MickLesk](https://github.com/MickLesk) ([#13017](https://github.com/community-scripts/ProxmoxVE/pull/13017))
@@ -463,7 +443,6 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
- #### 🔧 Refactor - #### 🔧 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)) - 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)) - 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))
@@ -471,7 +450,6 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
- #### ✨ New Features - #### ✨ 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 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)) - tools.func: Implement fetch_and_deploy_gh_tag function [@MickLesk](https://github.com/MickLesk) ([#13000](https://github.com/community-scripts/ProxmoxVE/pull/13000))

View File

@@ -110,9 +110,7 @@ function update_script() {
msg_info "Building Frontend" msg_info "Building Frontend"
cd /opt/dispatcharr/frontend cd /opt/dispatcharr/frontend
node -e "const p=require('./package.json');p.overrides=p.overrides||{};p.overrides['webworkify-webpack']='2.1.3';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2));" $STD npm install --legacy-peer-deps
rm -f package-lock.json
$STD npm install --no-audit --progress=false
$STD npm run build $STD npm run build
msg_ok "Built Frontend" msg_ok "Built Frontend"

View File

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

View File

@@ -9,7 +9,7 @@ APP="Owncast"
var_tags="${var_tags:-broadcasting}" var_tags="${var_tags:-broadcasting}"
var_cpu="${var_cpu:-2}" var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}" var_ram="${var_ram:-2048}"
var_disk="${var_disk:-10}" var_disk="${var_disk:-2}"
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

@@ -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/containers/systemd/homeassistant.container ]]; then if [[ ! -f /etc/systemd/system/homeassistant.service ]]; then
msg_error "No ${APP} Installation Found!" msg_error "No ${APP} Installation Found!"
exit exit
fi fi

View File

@@ -60,4 +60,4 @@ description
msg_ok "Completed successfully!\n" msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}" echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}/install for the initial setup${CL}" echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}"

View File

@@ -89,49 +89,17 @@ 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-*.jar" USE_ORIGINAL_FILENAME="true" fetch_and_deploy_gh_release "photon" "komoot/photon" "singlefile" "latest" "/opt/photon" "photon-0*.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

@@ -33,16 +33,12 @@ 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_with_retry "https://f000.backblazeb2.com/file/tdarrs/versions.json" "-" | grep -oP '(?<="Tdarr_Updater": ")[^"]+' | grep linux_x64 | head -n 1) RELEASE=$(curl -fsSL https://f000.backblazeb2.com/file/tdarrs/versions.json | grep -oP '(?<="Tdarr_Updater": ")[^"]+' | grep linux_x64 | head -n 1)
curl_with_retry "$RELEASE" "Tdarr_Updater.zip" curl -fsSL "$RELEASE" -o 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

@@ -8,7 +8,7 @@ source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxV
APP="Tracearr" APP="Tracearr"
var_tags="${var_tags:-media}" var_tags="${var_tags:-media}"
var_cpu="${var_cpu:-2}" var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}" var_ram="${var_ram:-2048}"
var_disk="${var_disk:-10}" 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}"
@@ -140,7 +140,7 @@ EOF
msg_ok "Built Tracearr" msg_ok "Built Tracearr"
msg_info "Configuring Tracearr" msg_info "Configuring Tracearr"
sed -i "s|^APP_VERSION=.*|APP_VERSION=${CHECK_UPDATE_RELEASE#v}|" /data/tracearr/.env sed -i "s/^APP_VERSION=.*/APP_VERSION=$(cat /root/.tracearr)/" /data/tracearr/.env
chmod 600 /data/tracearr/.env chmod 600 /data/tracearr/.env
chown -R tracearr:tracearr /data/tracearr chown -R tracearr:tracearr /data/tracearr
mkdir -p /data/backup mkdir -p /data/backup

View File

@@ -66,9 +66,7 @@ CELERY_BROKER_URL=redis://localhost:6379/0
DJANGO_SECRET_KEY=$DJANGO_SECRET DJANGO_SECRET_KEY=$DJANGO_SECRET
EOF EOF
cd /opt/dispatcharr/frontend cd /opt/dispatcharr/frontend
node -e "const p=require('./package.json');p.overrides=p.overrides||{};p.overrides['webworkify-webpack']='2.1.3';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2));" $STD npm install --legacy-peer-deps
rm -f package-lock.json
$STD npm install --no-audit --progress=false
$STD npm run build $STD npm run build
msg_ok "Configured Dispatcharr" msg_ok "Configured Dispatcharr"

View File

@@ -13,10 +13,10 @@ setting_up_container
network_check network_check
update_os update_os
fetch_and_deploy_gh_release "emby" "MediaBrowser/Emby.Releases" "binary"
setup_hwaccel "emby" setup_hwaccel "emby"
fetch_and_deploy_gh_release "emby" "MediaBrowser/Emby.Releases" "binary"
motd_ssh motd_ssh
customize customize
cleanup_lxc cleanup_lxc

View File

@@ -14,6 +14,7 @@ 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 "jellyfin"
msg_info "Installing Dependencies" msg_info "Installing Dependencies"
ensure_dependencies libjemalloc2 ensure_dependencies libjemalloc2
@@ -36,8 +37,6 @@ ln -sf /usr/lib/jellyfin-ffmpeg/ffmpeg /usr/bin/ffmpeg
ln -sf /usr/lib/jellyfin-ffmpeg/ffprobe /usr/bin/ffprobe ln -sf /usr/lib/jellyfin-ffmpeg/ffprobe /usr/bin/ffprobe
msg_ok "Installed Jellyfin" msg_ok "Installed Jellyfin"
setup_hwaccel "jellyfin"
msg_info "Configuring 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

View File

@@ -42,6 +42,8 @@ EOF
$STD apt update $STD apt update
msg_ok "Set up Intel® Repositories" msg_ok "Set up Intel® Repositories"
setup_hwaccel "ollama"
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
@@ -90,8 +92,6 @@ fi
$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,6 +13,8 @@ setting_up_container
network_check network_check
update_os update_os
setup_hwaccel "plex"
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,8 +28,6 @@ msg_info "Installing Plex Media Server"
$STD apt install -y plexmediaserver $STD apt install -y plexmediaserver
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,58 +45,32 @@ 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
cat <<EOF >/etc/containers/systemd/portainer.container $STD podman run -d \
[Unit] -p 8000:8000 \
Description=Portainer Container -p 9443:9443 \
After=network-online.target --name=portainer \
--restart=always \
[Container] -v /run/podman/podman.sock:/var/run/docker.sock \
Image=docker.io/portainer/portainer-ce:latest -v portainer_data:/data \
ContainerName=portainer portainer/portainer-ce:latest
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"
cat <<EOF >/etc/containers/systemd/portainer-agent.container podman volume create temp >/dev/null
[Unit] podman volume remove temp >/dev/null
Description=Portainer Agent Container $STD podman run -d \
After=network-online.target -p 9001:9001 \
--name portainer_agent \
[Container] --restart=always \
Image=docker.io/portainer/agent:latest -v /run/podman/podman.sock:/var/run/docker.sock \
ContainerName=portainer_agent -v /var/lib/containers/storage/volumes:/var/lib/docker/volumes \
PublishPort=9001:9001 portainer/agent
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
@@ -107,29 +81,19 @@ 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
cat <<EOF >/etc/containers/systemd/homeassistant.container $STD podman run -d \
[Unit] --name homeassistant \
Description=Home Assistant Container --restart unless-stopped \
After=network-online.target -v /dev:/dev \
-v hass_config:/config \
[Container] -v /etc/localtime:/etc/localtime:ro \
Image=docker.io/homeassistant/home-assistant:stable -v /etc/timezone:/etc/timezone:ro \
ContainerName=homeassistant --net=host \
Volume=/dev:/dev homeassistant/home-assistant:stable
Volume=hass_config:/config podman generate systemd \
Volume=/etc/localtime:/etc/localtime:ro --new --name homeassistant \
Volume=/etc/timezone:/etc/timezone:ro >/etc/systemd/system/homeassistant.service
Network=host systemctl enable -q --now homeassistant
[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,58 +45,32 @@ 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
cat <<EOF >/etc/containers/systemd/portainer.container $STD podman run -d \
[Unit] -p 8000:8000 \
Description=Portainer Container -p 9443:9443 \
After=network-online.target --name=portainer \
--restart=always \
[Container] -v /run/podman/podman.sock:/var/run/docker.sock \
Image=docker.io/portainer/portainer-ce:latest -v portainer_data:/data \
ContainerName=portainer portainer/portainer-ce:latest
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"
cat <<EOF >/etc/containers/systemd/portainer-agent.container podman volume create temp >/dev/null
[Unit] podman volume remove temp >/dev/null
Description=Portainer Agent Container $STD podman run -d \
After=network-online.target -p 9001:9001 \
--name portainer_agent \
[Container] --restart=always \
Image=docker.io/portainer/agent:latest -v /run/podman/podman.sock:/var/run/docker.sock \
ContainerName=portainer_agent -v /var/lib/containers/storage/volumes:/var/lib/docker/volumes \
PublishPort=9001:9001 portainer/agent
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-*.jar" USE_ORIGINAL_FILENAME="true" fetch_and_deploy_gh_release "photon" "komoot/photon" "singlefile" "latest" "/opt/photon" "photon-0*.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

@@ -20,16 +20,12 @@ 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_with_retry "https://f000.backblazeb2.com/file/tdarrs/versions.json" "-" | grep -oP '(?<="Tdarr_Updater": ")[^"]+' | grep linux_x64 | head -n 1) RELEASE=$(curl -fsSL https://f000.backblazeb2.com/file/tdarrs/versions.json | grep -oP '(?<="Tdarr_Updater": ")[^"]+' | grep linux_x64 | head -n 1)
curl_with_retry "$RELEASE" "Tdarr_Updater.zip" curl -fsSL "$RELEASE" -o 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

@@ -5213,8 +5213,8 @@ _setup_gpu_permissions() {
# Add service user to render and video groups for GPU hardware acceleration # Add service user to render and video groups for GPU hardware acceleration
if [[ -n "$service_user" ]]; then if [[ -n "$service_user" ]]; then
usermod -aG render "$service_user" 2>/dev/null || true $STD usermod -aG render "$service_user" 2>/dev/null || true
usermod -aG video "$service_user" 2>/dev/null || true $STD usermod -aG video "$service_user" 2>/dev/null || true
fi fi
} }
@@ -6698,38 +6698,22 @@ 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, cron) # - Installs optional PG_MODULES (e.g. postgis, contrib)
# - 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,cron") # PG_MODULES - Comma-separated list of modules (e.g. "postgis,contrib")
# #
# 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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Internal helper: Configure shared_preload_libraries for pg_cron function setup_postgresql() {
_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}"
@@ -6767,7 +6751,6 @@ 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
@@ -6803,7 +6786,6 @@ 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
@@ -6825,7 +6807,6 @@ 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
@@ -6853,16 +6834,13 @@ 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")
@@ -6946,7 +6924,6 @@ setup_postgresql() {
} }
done done
fi fi
_configure_pg_cron_preload "$PG_MODULES"
} }
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@@ -6965,7 +6942,6 @@ 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)
@@ -6997,13 +6973,6 @@ 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:-}"
@@ -7013,12 +6982,6 @@ 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';"
@@ -7060,6 +7023,7 @@ 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.
# #