Compare commits

..

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
fc33543d4b Merge remote-tracking branch 'origin/master' into create-pull-request/patch-1788655917
# Conflicts:
#	.claude/skills/hassio-addon-workflow/scripts/pr_review.sh

Co-authored-by: alexbelgium <44178713+alexbelgium@users.noreply.github.com>
2026-09-07 11:26:55 +00:00
alexbelgium
df62d357d8 fix: auto-fix linting issues 2026-09-06 00:51:57 +00:00
303 changed files with 2683 additions and 2670 deletions

View File

@@ -30,8 +30,14 @@ VAR="${1:?usage: env_trace.sh <VAR> [process-name-or-pid]}"
TARGET="${2:-}" TARGET="${2:-}"
# VAR is interpolated into grep/sed patterns below — restrict it to a valid env var name # VAR is interpolated into grep/sed patterns below — restrict it to a valid env var name
case "$VAR" in case "$VAR" in
[A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || { echo "invalid env var name: $VAR" >&2; exit 1; } ;; [A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || {
*) echo "invalid env var name: $VAR" >&2; exit 1 ;; echo "invalid env var name: $VAR" >&2
exit 1
} ;;
*)
echo "invalid env var name: $VAR" >&2
exit 1
;;
esac esac
echo "== tracing ${VAR} ==" echo "== tracing ${VAR} =="
@@ -39,7 +45,7 @@ echo
echo "1. /data/options.json (the user's saved options)" echo "1. /data/options.json (the user's saved options)"
if [ -f /data/options.json ]; then if [ -f /data/options.json ]; then
python3 - "$VAR" <<'PY' python3 - "$VAR" << 'PY'
import json, sys import json, sys
var = sys.argv[1] var = sys.argv[1]
try: try:
@@ -80,7 +86,8 @@ echo "3. s6 container_environment (only read by services using #!/usr/bin/with-c
seen3=0 seen3=0
for d in /var/run/s6/container_environment /run/s6/container_environment; do for d in /var/run/s6/container_environment /run/s6/container_environment; do
if [ -f "$d/$VAR" ]; then if [ -f "$d/$VAR" ]; then
echo " $d/$VAR = [$(cat "$d/$VAR")]"; seen3=1 echo " $d/$VAR = [$(cat "$d/$VAR")]"
seen3=1
fi fi
done done
[ "$seen3" -eq 0 ] && echo " not present in either envdir" [ "$seen3" -eq 0 ] && echo " not present in either envdir"
@@ -122,8 +129,8 @@ else
done done
fi fi
# What it was actually launched with beats any theory about its environment. # What it was actually launched with beats any theory about its environment.
tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 | tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 \
grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /' | grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /'
done done
fi fi
fi fi

View File

@@ -31,7 +31,7 @@ awk '{t+=$2} END{printf " threads=%d\n", t}' <(ps -eo pid,nlwp --no-headers 2>
echo echo
echo "== per-process memory (top 20 by PSS) ==" echo "== per-process memory (top 20 by PSS) =="
printf ' %-28s %8s %8s %8s\n' COMMAND RSS PSS PRIVATE printf ' %-28s %8s %8s %8s\n' COMMAND RSS PSS PRIVATE
python3 - "$OUT" <<'PY' python3 - "$OUT" << 'PY'
import os, sys import os, sys
rows = [] rows = []
for pid in filter(str.isdigit, os.listdir('/proc')): for pid in filter(str.isdigit, os.listdir('/proc')):
@@ -62,17 +62,17 @@ PY
echo echo
echo "== reserved-but-not-resident (lazy allocations, NOT leaks) ==" echo "== reserved-but-not-resident (lazy allocations, NOT leaks) =="
ipcs -m 2>/dev/null | awk 'NR>3 && $5 ~ /^[0-9]+$/ && $5 > 50000000 {printf " SysV shm %.0f MB (owner %s) — check Rss in /proc/<pid>/smaps before calling it used\n", $5/1048576, $3}' ipcs -m 2> /dev/null | awk 'NR>3 && $5 ~ /^[0-9]+$/ && $5 > 50000000 {printf " SysV shm %.0f MB (owner %s) — check Rss in /proc/<pid>/smaps before calling it used\n", $5/1048576, $3}'
echo echo
echo "== CPU over ${SAMPLE}s (idle unless you are driving the UI) ==" echo "== CPU over ${SAMPLE}s (idle unless you are driving the UI) =="
# utime+stime. Parsed after the LAST ')' because field 2 is (comm) and may contain spaces — # utime+stime. Parsed after the LAST ')' because field 2 is (comm) and may contain spaces —
# a plain $14+$15 is wrong for anything like 'npm exec @foo' and silently reports a fabricated # a plain $14+$15 is wrong for anything like 'npm exec @foo' and silently reports a fabricated
# number rather than failing. # number rather than failing.
jiffies() { awk -F') ' '{n=split($NF,a," "); print a[12]+a[13]}' "/proc/$1/stat" 2>/dev/null; } jiffies() { awk -F') ' '{n=split($NF,a," "); print a[12]+a[13]}' "/proc/$1/stat" 2> /dev/null; }
# jiffies are USER_HZ units — almost always 100, but read it rather than assume it # jiffies are USER_HZ units — almost always 100, but read it rather than assume it
HZ=$(getconf CLK_TCK 2>/dev/null) && [ "$HZ" -gt 0 ] 2>/dev/null || HZ=100 HZ=$(getconf CLK_TCK 2> /dev/null) && [ "$HZ" -gt 0 ] 2> /dev/null || HZ=100
# Sample EVERY readable process, not the top-N of ps.txt: that list is sorted by RSS, # Sample EVERY readable process, not the top-N of ps.txt: that list is sorted by RSS,
# and the busiest process is not necessarily a big one. # and the busiest process is not necessarily a big one.
@@ -86,14 +86,14 @@ for pid in "${!t0[@]}"; do
[ -r "/proc/$pid/stat" ] || continue [ -r "/proc/$pid/stat" ] || continue
t1=$(jiffies "$pid") || continue t1=$(jiffies "$pid") || continue
[ -n "$t1" ] && [ -n "${t0[$pid]}" ] || continue [ -n "$t1" ] && [ -n "${t0[$pid]}" ] || continue
delta=$(( t1 - ${t0[$pid]} )) delta=$((t1 - ${t0[$pid]}))
[ "$delta" -gt 0 ] || continue [ "$delta" -gt 0 ] || continue
pct=$(awk -v d="$delta" -v s="$SAMPLE" -v hz="$HZ" 'BEGIN{printf "%.2f", d*100/(hz*s)}') pct=$(awk -v d="$delta" -v s="$SAMPLE" -v hz="$HZ" 'BEGIN{printf "%.2f", d*100/(hz*s)}')
comm=$(tr -d '\0' < "/proc/$pid/comm" 2>/dev/null) comm=$(tr -d '\0' < "/proc/$pid/comm" 2> /dev/null)
echo "$pct $pid $comm" echo "$pct $pid $comm"
done | sort -rn | head -12 | awk '{printf " %6s%% %-8s %s\n", $1, $2, $3}' done | sort -rn | head -12 | awk '{printf " %6s%% %-8s %s\n", $1, $2, $3}'
echo echo
echo " established conns on :8082/:3000/:3001 = $(ss -tn 2>/dev/null | grep -cE 'ESTAB.*:(8082|3000|3001)')" echo " established conns on :8082/:3000/:3001 = $(ss -tn 2> /dev/null | grep -cE 'ESTAB.*:(8082|3000|3001)')"
echo " (those are claude_desktop/webtop viewer ports; 0 here means CPU above is idle burn)" echo " (those are claude_desktop/webtop viewer ports; 0 here means CPU above is idle burn)"
echo " raw ps: $OUT/ps.txt" echo " raw ps: $OUT/ps.txt"

View File

@@ -47,7 +47,10 @@ if [ -n "${BUILD_VERSION:-}" ]; then
if [ -z "$SLUG" ] && [ -n "${HOSTNAME:-}" ]; then if [ -z "$SLUG" ] && [ -n "${HOSTNAME:-}" ]; then
base=$(printf '%s' "$HOSTNAME" | sed 's/^[0-9a-f]\{8\}-//') base=$(printf '%s' "$HOSTNAME" | sed 's/^[0-9a-f]\{8\}-//')
for cand in "$(printf '%s' "$base" | tr '-' '_')" "$base"; do for cand in "$(printf '%s' "$base" | tr '-' '_')" "$base"; do
[ -f "$REPO/$cand/config.yaml" ] && { SLUG="$cand"; break; } [ -f "$REPO/$cand/config.yaml" ] && {
SLUG="$cand"
break
}
done done
[ -z "$SLUG" ] && SLUG="$base" [ -z "$SLUG" ] && SLUG="$base"
fi fi
@@ -69,8 +72,8 @@ if [ -n "${BUILD_VERSION:-}" ]; then
else else
echo " MISMATCH — this branch is NOT what is running." echo " MISMATCH — this branch is NOT what is running."
git fetch origin master --quiet 2> /dev/null git fetch origin master --quiet 2> /dev/null
master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null | master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null \
grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}') | grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}')
echo " origin/master version = ${master:-unknown}" echo " origin/master version = ${master:-unknown}"
echo " -> work from origin/master; analysing this branch will mislead you." echo " -> work from origin/master; analysing this branch will mislead you."
echo echo

View File

@@ -13,22 +13,31 @@ set -uo pipefail
if root=$(git rev-parse --show-toplevel 2> /dev/null); then cd "$root" || exit 1; fi if root=$(git rev-parse --show-toplevel 2> /dev/null); then cd "$root" || exit 1; fi
ADDON="${1:-}" ADDON="${1:-}"
[ "${ADDON:-}" = "--vs-master" ] && { ADDON=""; set -- --vs-master; } [ "${ADDON:-}" = "--vs-master" ] && {
ADDON=""
set -- --vs-master
}
VS_MASTER=false VS_MASTER=false
for a in "$@"; do [ "$a" = "--vs-master" ] && VS_MASTER=true; done for a in "$@"; do [ "$a" = "--vs-master" ] && VS_MASTER=true; done
if [ -z "$ADDON" ]; then if [ -z "$ADDON" ]; then
mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null | mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null \
cut -d/ -f1 | sort -u | grep -vE '^\.' ) | cut -d/ -f1 | sort -u | grep -vE '^\.')
if [ "${#_dirs[@]}" -gt 1 ]; then if [ "${#_dirs[@]}" -gt 1 ]; then
echo "several changed dirs: ${_dirs[*]}" echo "several changed dirs: ${_dirs[*]}"
echo "pass one explicitly: validate.sh <addon-dir>"; exit 1 echo "pass one explicitly: validate.sh <addon-dir>"
exit 1
fi fi
ADDON="${_dirs[0]:-}" ADDON="${_dirs[0]:-}"
fi fi
[ -z "$ADDON" ] && { echo "usage: validate.sh <addon-dir> [--vs-master]"; exit 1; } [ -z "$ADDON" ] && {
echo "usage: validate.sh <addon-dir> [--vs-master]"
exit 1
}
git rev-parse --verify origin/master > /dev/null 2>&1 || { git rev-parse --verify origin/master > /dev/null 2>&1 || {
echo "origin/master missing — run: git fetch origin master"; exit 1; } echo "origin/master missing — run: git fetch origin master"
exit 1
}
export PYTHONDONTWRITEBYTECODE=1 export PYTHONDONTWRITEBYTECODE=1
echo "== validating $ADDON ==" echo "== validating $ADDON =="
@@ -36,14 +45,22 @@ fail=0
note() { printf ' %-13s %s\n' "$1" "$2"; } note() { printf ' %-13s %s\n' "$1" "$2"; }
# execline `run`/`finish` files are not shell (25 of them here, across 21 add-ons). Neither # execline `run`/`finish` files are not shell (25 of them here, across 21 add-ons). Neither
# linter below can read one, so anything either says about it is noise. # linter below can read one, so anything either says about it is noise.
is_execline() { local l; IFS= read -r l < "$1" 2> /dev/null; [[ $l == '#!'*execlineb* ]]; } is_execline() {
local l
IFS= read -r l < "$1" 2> /dev/null
[[ $l == '#!'*execlineb* ]]
}
# Shell: bash -n then shellcheck -x (follows sourced files, as CI does). One list for both. # Shell: bash -n then shellcheck -x (follows sourced files, as CI does). One list for both.
files=() files=()
while IFS= read -r f; do is_execline "$f" || files+=("$f"); done \ while IFS= read -r f; do is_execline "$f" || files+=("$f"); done \
< <(find "$ADDON" -type f \( -name '*.sh' -o -name 'run' -o -name 'finish' -o -name 'autostart' \) 2> /dev/null) < <(find "$ADDON" -type f \( -name '*.sh' -o -name 'run' -o -name 'finish' -o -name 'autostart' \) 2> /dev/null)
for f in "${files[@]}"; do for f in "${files[@]}"; do
if ! out=$(bash -n "$f" 2>&1); then note "bash -n" "FAIL $f"; echo "$out" | sed 's/^/ /'; fail=1; fi if ! out=$(bash -n "$f" 2>&1); then
note "bash -n" "FAIL $f"
echo "$out" | sed 's/^/ /'
fail=1
fi
done done
[ "$fail" -eq 0 ] && note "bash -n" "${#files[@]} file(s) checked" [ "$fail" -eq 0 ] && note "bash -n" "${#files[@]} file(s) checked"
@@ -65,13 +82,16 @@ command -v hadolint > /dev/null 2>&1 && [ -f "$ADDON/Dockerfile" ] && {
if [ -f "$ADDON/config.yaml" ]; then if [ -f "$ADDON/config.yaml" ]; then
# path passed as argv, never interpolated into Python source # path passed as argv, never interpolated into Python source
python3 - "$ADDON/config.yaml" <<'PY' || { note "config.yaml" "FAIL parse"; fail=1; } python3 - "$ADDON/config.yaml" << 'PY' || {
import yaml,sys import yaml,sys
d=yaml.safe_load(open(sys.argv[1])) d=yaml.safe_load(open(sys.argv[1]))
print(' %-13s ok (version=%s, %d options)' % ('config.yaml', d.get('version'), len(d.get('options') or {}))) print(' %-13s ok (version=%s, %d options)' % ('config.yaml', d.get('version'), len(d.get('options') or {})))
missing=[k for k in (d.get('options') or {}) if k not in (d.get('schema') or {})] missing=[k for k in (d.get('options') or {}) if k not in (d.get('schema') or {})]
if missing: print(' %-13s options with no schema entry: %s' % ('WARN', missing)); sys.exit(0) if missing: print(' %-13s options with no schema entry: %s' % ('WARN', missing)); sys.exit(0)
PY PY
note "config.yaml" "FAIL parse"
fail=1
}
command -v yamllint > /dev/null 2>&1 && { command -v yamllint > /dev/null 2>&1 && {
yl=$(yamllint -f parsable "$ADDON/config.yaml" 2>&1 | grep -c .) yl=$(yamllint -f parsable "$ADDON/config.yaml" 2>&1 | grep -c .)
note "yamllint" "$yl finding(s) (compare with --vs-master)" note "yamllint" "$yl finding(s) (compare with --vs-master)"
@@ -79,7 +99,10 @@ PY
fi fi
while IFS= read -r f; do while IFS= read -r f; do
python3 -m py_compile "$f" 2> /dev/null || { note "py_compile" "FAIL $f"; fail=1; } python3 -m py_compile "$f" 2> /dev/null || {
note "py_compile" "FAIL $f"
fail=1
}
done < <(find "$ADDON" -type f -name '*.py' 2> /dev/null) done < <(find "$ADDON" -type f -name '*.py' 2> /dev/null)
$VS_MASTER && command -v npx > /dev/null 2>&1 && [ -f "$ADDON/CHANGELOG.md" ] && { $VS_MASTER && command -v npx > /dev/null 2>&1 && [ -f "$ADDON/CHANGELOG.md" ] && {
@@ -96,7 +119,8 @@ if git diff --name-only origin/master...HEAD 2> /dev/null | grep -Fxq "$ADDON/CH
note "CHANGELOG" "updated" note "CHANGELOG" "updated"
else else
# This one IS gated: onpr_check-pr.yaml exits 1 without it. # This one IS gated: onpr_check-pr.yaml exits 1 without it.
note "CHANGELOG" "NOT UPDATED for $ADDON — CI hard-gates this"; fail=1 note "CHANGELOG" "NOT UPDATED for $ADDON — CI hard-gates this"
fail=1
fi fi
if git diff origin/master...HEAD -- "$ADDON/config.yaml" 2> /dev/null | grep -q '^+version:'; then if git diff origin/master...HEAD -- "$ADDON/config.yaml" 2> /dev/null | grep -q '^+version:'; then
note "version" "bumped" note "version" "bumped"
@@ -110,7 +134,8 @@ note "docker build" "NOT tested locally (dockerd unavailable) — CI is the only
if $VS_MASTER; then if $VS_MASTER; then
echo echo
echo "== findings ADDED by this diff (pre-existing ones filtered out) ==" echo "== findings ADDED by this diff (pre-existing ones filtered out) =="
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
added=0 added=0
# Fed by process substitution, not a pipe: a pipeline runs this in a subshell, where the # Fed by process substitution, not a pipe: a pipeline runs this in a subshell, where the
# findings below could never reach $fail and the verdict would contradict the list. # findings below could never reach $fail and the verdict would contradict the list.
@@ -125,14 +150,26 @@ if $VS_MASTER; then
# cancel it out and report a false clean. # cancel it out and report a false clean.
case "$f" in case "$f" in
*.sh | *autostart | */run | */finish) *.sh | *autostart | */run | */finish)
command -v shellcheck > /dev/null 2>&1 || { echo " $f: SKIPPED (shellcheck not installed)"; continue; } command -v shellcheck > /dev/null 2>&1 || {
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; } ;; echo " $f: SKIPPED (shellcheck not installed)"
continue
}
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; }
;;
*.yaml | *.yml) *.yaml | *.yml)
command -v yamllint > /dev/null 2>&1 || { echo " $f: SKIPPED (yamllint not installed)"; continue; } command -v yamllint > /dev/null 2>&1 || {
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; } ;; echo " $f: SKIPPED (yamllint not installed)"
continue
}
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; }
;;
*Dockerfile) *Dockerfile)
command -v hadolint > /dev/null 2>&1 || { echo " $f: SKIPPED (hadolint not installed)"; continue; } command -v hadolint > /dev/null 2>&1 || {
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; } ;; echo " $f: SKIPPED (hadolint not installed)"
continue
}
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; }
;;
*) continue ;; *) continue ;;
esac esac
b=$(cmd "$tmp/base" | sort) b=$(cmd "$tmp/base" | sort)
@@ -141,7 +178,8 @@ if $VS_MASTER; then
[ -n "$new" ] && { [ -n "$new" ] && {
echo " $f: $(printf '%s\n' "$new" | grep -c .) NEW finding(s)" echo " $f: $(printf '%s\n' "$new" | grep -c .) NEW finding(s)"
printf '%s\n' "$new" | sed 's/^/ /' | head -5 printf '%s\n' "$new" | sed 's/^/ /' | head -5
added=1; fail=1 added=1
fail=1
} }
done < <(git diff --name-only origin/master...HEAD -- "$ADDON" 2> /dev/null) done < <(git diff --name-only origin/master...HEAD -- "$ADDON" 2> /dev/null)
[ "$added" -eq 0 ] && echo " (none — this diff introduced no new lint findings)" [ "$added" -eq 0 ] && echo " (none — this diff introduced no new lint findings)"

View File

@@ -40,7 +40,7 @@ the check is a guard, not a common case.)
## Rule 1 — pick exactly one verdict ## Rule 1 — pick exactly one verdict
| verdict | when | | verdict | when |
|---|---| | --- | --- |
| `duplicate` | An existing open or closed issue reports the same thing. Set `duplicate_of`. | | `duplicate` | An existing open or closed issue reports the same thing. Set `duplicate_of`. |
| `needs-info` | You cannot tell what is wrong without the add-on version, HA version, architecture, config, or the actual log output. | | `needs-info` | You cannot tell what is wrong without the add-on version, HA version, architecture, config, or the actual log output. |
| `question` | A usage question answerable from `DOCS.md`, the wiki, or the add-on config. Not a defect. | | `question` | A usage question answerable from `DOCS.md`, the wiki, or the add-on config. Not a defect. |

View File

@@ -39,7 +39,7 @@ rest are on you: nothing checks them, so breaking one ships silently.
that value, and compare: that value, and compare:
| `version` vs `U` | what to do | example | | `version` vs `U` | what to do | example |
|---|---|---| | --- | --- | --- |
| identical | **append** `.1` | sonarr `4.0.19.3001` -> `4.0.19.3001.1` | | identical | **append** `.1` | sonarr `4.0.19.3001` -> `4.0.19.3001.1` |
| `U` + `.` + digits | **increment** those digits | radarr `6.3.0.10514.1` -> `6.3.0.10514.2` | | `U` + `.` + digits | **increment** those digits | radarr `6.3.0.10514.1` -> `6.3.0.10514.2` |
| anything else | **leave it alone** | plex, readarr, joal | | anything else | **leave it alone** | plex, readarr, joal |

View File

@@ -44,7 +44,7 @@ for pr in $PRS; do
echo "$BAD" echo "$BAD"
# Ensure the label exists before adding it — with set -e a missing # Ensure the label exists before adding it — with set -e a missing
# label would abort the whole loop and skip any PRs behind this one. # label would abort the whole loop and skip any PRs behind this one.
gh label create "ai:blocked" --repo "$REPO" --color ededed >/dev/null 2>&1 || true gh label create "ai:blocked" --repo "$REPO" --color ededed > /dev/null 2>&1 || true
gh pr ready "$pr" --repo "$REPO" --undo || true gh pr ready "$pr" --repo "$REPO" --undo || true
gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked" gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked"
gh pr comment "$pr" --repo "$REPO" --body \ gh pr comment "$pr" --repo "$REPO" --body \

View File

@@ -15,110 +15,116 @@ CTX="$OUT/context.md"
: > "$CTX" : > "$CTX"
gh issue view "$ISSUE_NUMBER" --repo "$REPO" \ gh issue view "$ISSUE_NUMBER" --repo "$REPO" \
--json number,title,body,author,labels,createdAt,comments > "$OUT/issue.json" --json number,title,body,author,labels,createdAt,comments > "$OUT/issue.json"
TITLE=$(jq -r '.title' "$OUT/issue.json") TITLE=$(jq -r '.title' "$OUT/issue.json")
# ---------------------------------------------------------------- addon slug # ---------------------------------------------------------------- addon slug
# Titles follow "🐛 [Immich Frame] ENV_VARS arent being picked up". # Titles follow "🐛 [Immich Frame] ENV_VARS arent being picked up".
RAW=$(sed -n 's/.*\[\([^]]*\)\].*/\1/p' <<<"$TITLE" | head -n1) RAW=$(sed -n 's/.*\[\([^]]*\)\].*/\1/p' <<< "$TITLE" | head -n1)
ADDON="" ADDON=""
if [ -n "$RAW" ]; then if [ -n "$RAW" ]; then
CAND=$(tr '[:upper:] ' '[:lower:]_' <<<"$RAW") CAND=$(tr '[:upper:] ' '[:lower:]_' <<< "$RAW")
# Directory list without checking out any of them. # Directory list without checking out any of them.
git ls-tree -d --name-only HEAD > "$OUT/dirs.txt" git ls-tree -d --name-only HEAD > "$OUT/dirs.txt"
for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do
if grep -qxF "$guess" "$OUT/dirs.txt"; then ADDON="$guess"; break; fi if grep -qxF "$guess" "$OUT/dirs.txt"; then
done ADDON="$guess"
# Separator-insensitive exact match: a title like "[Calibre-web]" (hyphen) break
# against a directory named calibre_web (underscore) matches neither exact fi
# guess above, and would otherwise fall through to the substring fallback done
# below, which picks the shorter "calibre" instead — the wrong add-on. # Separator-insensitive exact match: a title like "[Calibre-web]" (hyphen)
# Stripping -, _, . from both sides before comparing catches this case. # against a directory named calibre_web (underscore) matches neither exact
if [ -z "$ADDON" ]; then # guess above, and would otherwise fall through to the substring fallback
CAND_STRIPPED=$(tr -d '_.-' <<<"$CAND") # below, which picks the shorter "calibre" instead — the wrong add-on.
while IFS= read -r dir; do # Stripping -, _, . from both sides before comparing catches this case.
if [ "$(tr -d '_.-' <<<"$dir")" = "$CAND_STRIPPED" ]; then ADDON="$dir"; break; fi if [ -z "$ADDON" ]; then
done < "$OUT/dirs.txt" CAND_STRIPPED=$(tr -d '_.-' <<< "$CAND")
fi while IFS= read -r dir; do
# Last resort: longest directory name contained in the candidate. if [ "$(tr -d '_.-' <<< "$dir")" = "$CAND_STRIPPED" ]; then
if [ -z "$ADDON" ]; then ADDON="$dir"
ADDON=$(awk -v c="$CAND" 'length($0)>2 && index(c,$0){print length($0)"\t"$0}' \ break
fi
done < "$OUT/dirs.txt"
fi
# Last resort: longest directory name contained in the candidate.
if [ -z "$ADDON" ]; then
ADDON=$(awk -v c="$CAND" 'length($0)>2 && index(c,$0){print length($0)"\t"$0}' \
"$OUT/dirs.txt" | sort -rn | head -n1 | cut -f2) "$OUT/dirs.txt" | sort -rn | head -n1 | cut -f2)
fi fi
fi fi
{ {
echo "# Issue #${ISSUE_NUMBER}" echo "# Issue #${ISSUE_NUMBER}"
echo echo
echo "Repo: ${REPO}" echo "Repo: ${REPO}"
echo "Addon resolved from title: ${ADDON:-UNRESOLVED}" echo "Addon resolved from title: ${ADDON:-UNRESOLVED}"
echo echo
echo "## Title" echo "## Title"
echo "$TITLE" echo "$TITLE"
echo echo
echo "## Author" echo "## Author"
jq -r '.author.login' "$OUT/issue.json" jq -r '.author.login' "$OUT/issue.json"
echo echo
echo "## Body" echo "## Body"
echo '```' echo '```'
jq -r '.body // "(empty)"' "$OUT/issue.json" jq -r '.body // "(empty)"' "$OUT/issue.json"
echo '```' echo '```'
echo echo
echo "## Existing comments (in order)" echo "## Existing comments (in order)"
jq -r '.comments[]? | "### @\(.author.login)\n\(.body)\n"' "$OUT/issue.json" jq -r '.comments[]? | "### @\(.author.login)\n\(.body)\n"' "$OUT/issue.json"
echo echo
echo "## Existing labels" echo "## Existing labels"
jq -r '[.labels[]?.name] | join(", ")' "$OUT/issue.json" jq -r '[.labels[]?.name] | join(", ")' "$OUT/issue.json"
} >> "$CTX" } >> "$CTX"
# ------------------------------------------------------------- addon sources # ------------------------------------------------------------- addon sources
if [ -n "$ADDON" ]; then if [ -n "$ADDON" ]; then
{ {
echo
echo "## Addon files: ${ADDON}/"
# `set` REPLACES the checkout list, so .templates has to be repeated here
# or the workflow's sparse-checkout of it is silently undone at this point
# — which is exactly the state that starved #2949 of its turn budget.
if ! git sparse-checkout set --no-cone .github/prompts .github/scripts .templates "$ADDON" 2>&1; then
# Swallowing this used to leave ADDON resolved with no files behind it,
# so the classifier could still reach high confidence off the addon
# name alone. Say so explicitly, in the same word Rule 2 already keys
# its low-confidence check on.
echo
echo "**Could not check out this add-on's source. Treat as UNRESOLVED for confidence purposes.**"
else
for f in config.yaml config.json Dockerfile CHANGELOG.md DOCS.md README.md; do
[ -f "$ADDON/$f" ] || continue
echo echo
echo "### ${ADDON}/${f}" echo "## Addon files: ${ADDON}/"
echo '```' # `set` REPLACES the checkout list, so .templates has to be repeated here
head -c 8000 "$ADDON/$f" # or the workflow's sparse-checkout of it is silently undone at this point
echo '```' # — which is exactly the state that starved #2949 of its turn budget.
done if ! git sparse-checkout set --no-cone .github/prompts .github/scripts .templates "$ADDON" 2>&1; then
# Swallowing this used to leave ADDON resolved with no files behind it,
# so the classifier could still reach high confidence off the addon
# name alone. Say so explicitly, in the same word Rule 2 already keys
# its low-confidence check on.
echo
echo "**Could not check out this add-on's source. Treat as UNRESOLVED for confidence purposes.**"
else
for f in config.yaml config.json Dockerfile CHANGELOG.md DOCS.md README.md; do
[ -f "$ADDON/$f" ] || continue
echo
echo "### ${ADDON}/${f}"
echo '```'
head -c 8000 "$ADDON/$f"
echo '```'
done
echo echo
echo "## Recent commits touching ${ADDON}/" echo "## Recent commits touching ${ADDON}/"
git log -n 15 --date=short --pretty='- %ad %h %s' -- "$ADDON" 2>/dev/null || true git log -n 15 --date=short --pretty='- %ad %h %s' -- "$ADDON" 2> /dev/null || true
fi fi
} >> "$CTX" } >> "$CTX"
fi fi
# -------------------------------------------------------- possible duplicates # -------------------------------------------------------- possible duplicates
{ {
echo echo
echo "## Similar existing issues (candidate duplicates)" echo "## Similar existing issues (candidate duplicates)"
KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<<"$TITLE" \ KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<< "$TITLE" \
| tr '[:upper:]' '[:lower:]' \ | tr '[:upper:]' '[:lower:]' \
| tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ') | tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ')
# Excludes the issue being triaged: if it's already indexed by GitHub search # Excludes the issue being triaged: if it's already indexed by GitHub search
# by the time this runs, keyword overlap with its own title would otherwise # by the time this runs, keyword overlap with its own title would otherwise
# list it as a "candidate duplicate" of itself. # list it as a "candidate duplicate" of itself.
gh search issues --repo "$REPO" --limit 15 \ gh search issues --repo "$REPO" --limit 15 \
--json number,title,state,url -- "$KEYWORDS" 2>/dev/null \ --json number,title,state,url -- "$KEYWORDS" 2> /dev/null \
| jq -r --argjson self "$ISSUE_NUMBER" \ | jq -r --argjson self "$ISSUE_NUMBER" \
'.[] | select(.number != $self) | "- #\(.number) [\(.state)] \(.title)"' \ '.[] | select(.number != $self) | "- #\(.number) [\(.state)] \(.title)"' \
|| echo "(search unavailable)" || echo "(search unavailable)"
} >> "$CTX" } >> "$CTX"
echo "context bundle: $(wc -c < "$CTX") bytes, addon=${ADDON:-none}" echo "context bundle: $(wc -c < "$CTX") bytes, addon=${ADDON:-none}"

View File

@@ -125,7 +125,7 @@ jobs:
- name: Analyse and fix - name: Analyse and fix
if: steps.batch.outputs.count != '0' if: steps.batch.outputs.count != '0'
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1 uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange. The scheduled path # Skip the OIDC -> Claude App token exchange. The scheduled path

View File

@@ -64,7 +64,7 @@ jobs:
fetch-depth: 1 fetch-depth: 1
- name: Run Claude Code - name: Run Claude Code
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1 uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI. # AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.

View File

@@ -135,7 +135,7 @@ jobs:
- name: Execute the plan - name: Execute the plan
if: steps.bundle.outputs.has_plan == 'true' if: steps.bundle.outputs.has_plan == 'true'
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1 uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange, which 401s whenever # Skip the OIDC -> Claude App token exchange, which 401s whenever

View File

@@ -166,7 +166,7 @@ jobs:
id: classify id: classify
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true' if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
continue-on-error: true continue-on-error: true
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1 uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Without this the action falls back to the OIDC -> Claude App token # Without this the action falls back to the OIDC -> Claude App token

View File

@@ -79,7 +79,7 @@ jobs:
- name: Address CodeRabbit comments - name: Address CodeRabbit comments
if: steps.claim.outputs.go == 'true' if: steps.claim.outputs.go == 'true'
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1 uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange, which 401s whenever # Skip the OIDC -> Claude App token exchange, which 401s whenever

View File

@@ -6,7 +6,7 @@ set -e
# Banner # Banner
# ====================================================================== # ======================================================================
if ! bashio::supervisor.ping 2>/dev/null; then if ! bashio::supervisor.ping 2> /dev/null; then
# Source standalone bashio first to provide function definitions # Source standalone bashio first to provide function definitions
if [ -f /usr/local/lib/bashio-standalone.sh ]; then if [ -f /usr/local/lib/bashio-standalone.sh ]; then
source /usr/local/lib/bashio-standalone.sh source /usr/local/lib/bashio-standalone.sh
@@ -19,11 +19,11 @@ if ! bashio::supervisor.ping 2>/dev/null; then
if [ ! -f /usr/bin/bashio ]; then if [ ! -f /usr/bin/bashio ]; then
cp -rf /usr/local/lib/bashio-standalone.sh /usr/bin/bashio cp -rf /usr/local/lib/bashio-standalone.sh /usr/bin/bashio
fi fi
grep -rl "^#!.*bashio" /etc | grep -rl "^#!.*bashio" /etc \
while IFS= read -r f; do | while IFS= read -r f; do
grep -qF "source /usr/local/lib/bashio-standalone.sh" "$f" && continue grep -qF "source /usr/local/lib/bashio-standalone.sh" "$f" && continue
sed -i '1a source /usr/local/lib/bashio-standalone.sh' "$f" sed -i '1a source /usr/local/lib/bashio-standalone.sh' "$f"
done done
else else
bashio::log.blue '-----------------------------------------------------------' bashio::log.blue '-----------------------------------------------------------'
bashio::log.blue " Add-on: $(bashio::addon.name)" bashio::log.blue " Add-on: $(bashio::addon.name)"
@@ -49,7 +49,7 @@ bashio::log.green ' Provided by: https://github.com/alexbelgium/hassio-addons '
bashio::log.blue '-----------------------------------------------------------' bashio::log.blue '-----------------------------------------------------------'
# Adapt user abc # Adapt user abc
if command -v id &>/dev/null && id abc &>/dev/null; then if command -v id &> /dev/null && id abc &> /dev/null; then
if bashio::config.has_value "PUID" && bashio::config.has_value "PGID"; then if bashio::config.has_value "PUID" && bashio::config.has_value "PGID"; then
PUID="$(bashio::config "PUID")" PUID="$(bashio::config "PUID")"
PGID="$(bashio::config "PGID")" PGID="$(bashio::config "PGID")"

View File

@@ -152,15 +152,15 @@ if [[ "${1:-}" == "--self-test" ]]; then
done done
fi fi
[[ "$self_test_rc" -eq 0 ]] && [[ "$self_test_rc" -eq 0 ]] \
echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)" && echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
exit "$self_test_rc" exit "$self_test_rc"
fi fi
################################################################################ ################################################################################
# Guard: only run inside Supervisor-managed add-ons # Guard: only run inside Supervisor-managed add-ons
################################################################################ ################################################################################
if ! bashio::supervisor.ping 2>/dev/null; then if ! bashio::supervisor.ping 2> /dev/null; then
echo "..." echo "..."
exit 0 exit 0
fi fi
@@ -179,7 +179,7 @@ ENV_FILE="/.env"
ETC_ENV_FILE="/etc/environment" ETC_ENV_FILE="/etc/environment"
[[ -f "$JSONSOURCE" ]] || bashio::exit.nok "Missing $JSONSOURCE" [[ -f "$JSONSOURCE" ]] || bashio::exit.nok "Missing $JSONSOURCE"
command -v jq >/dev/null || bashio::exit.nok "jq is required" command -v jq > /dev/null || bashio::exit.nok "jq is required"
mkdir -p /etc mkdir -p /etc
touch "$ETC_ENV_FILE" touch "$ETC_ENV_FILE"
@@ -193,7 +193,10 @@ SECRETSOURCE=""
resolve_secret() { resolve_secret() {
local v="$1" name line local v="$1" name line
[[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)$ ]] || { printf '%s' "$v"; return; } [[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)$ ]] || {
printf '%s' "$v"
return
}
name="${BASH_REMATCH[1]}" name="${BASH_REMATCH[1]}"
[[ -n "$SECRETSOURCE" ]] || bashio::exit.nok "Secrets not mounted" [[ -n "$SECRETSOURCE" ]] || bashio::exit.nok "Secrets not mounted"

View File

@@ -4,9 +4,9 @@
set -e set -e
if ! bashio::supervisor.ping 2>/dev/null; then if ! bashio::supervisor.ping 2> /dev/null; then
bashio::log.blue "Disabled : please use another method" bashio::log.blue "Disabled : please use another method"
exit 0 exit 0
fi fi
bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Instructions here : https://github.com/alexbelgium/hassio-addons/wiki/Mounting-remote-shares-in-Addons" bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Instructions here : https://github.com/alexbelgium/hassio-addons/wiki/Mounting-remote-shares-in-Addons"
@@ -16,167 +16,167 @@ bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Ins
#################### ####################
cleanup_cred() { cleanup_cred() {
if [[ -n "${CRED_FILE:-}" && -f "${CRED_FILE:-}" ]]; then if [[ -n "${CRED_FILE:-}" && -f "${CRED_FILE:-}" ]]; then
rm -f "$CRED_FILE" || true rm -f "$CRED_FILE" || true
fi fi
CRED_FILE="" CRED_FILE=""
} }
test_mount() { test_mount() {
MOUNTED=false
ERROR_MOUNT=false
mountpoint="/mnt/$diskname"
if ! mountpoint -q "$mountpoint"; then
return 0
fi
_test_write() {
local testfile="$mountpoint/.writetest_$$"
if : >"$testfile" 2>/dev/null; then
rm -f "$testfile" 2>/dev/null || true
return 0
else
rm -f "$testfile" 2>/dev/null || true
return 1
fi
}
if _test_write 2>/dev/null; then
MOUNTED=true
return 0
fi
local orig_mountoptions="$MOUNTOPTIONS"
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noserverino"* ]]; then
echo "... retrying mount with noserverino"
MOUNTOPTIONS="${orig_mountoptions},noserverino"
umount "$mountpoint" 2>/dev/null || true
if mount_drive "$MOUNTOPTIONS"; then
if _test_write 2>/dev/null; then
MOUNTED=true
return 0
fi
fi
fi
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noperm"* ]]; then
echo "... retrying mount with noperm"
MOUNTOPTIONS="${orig_mountoptions},noperm"
umount "$mountpoint" 2>/dev/null || true
if mount_drive "$MOUNTOPTIONS"; then
if _test_write 2>/dev/null; then
MOUNTED=true
return 0
fi
fi
fi
if mountpoint -q "$mountpoint"; then
MOUNTED="readonly"
else
MOUNTED=false MOUNTED=false
fi ERROR_MOUNT=false
return 0 mountpoint="/mnt/$diskname"
if ! mountpoint -q "$mountpoint"; then
return 0
fi
_test_write() {
local testfile="$mountpoint/.writetest_$$"
if : > "$testfile" 2> /dev/null; then
rm -f "$testfile" 2> /dev/null || true
return 0
else
rm -f "$testfile" 2> /dev/null || true
return 1
fi
}
if _test_write 2> /dev/null; then
MOUNTED=true
return 0
fi
local orig_mountoptions="$MOUNTOPTIONS"
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noserverino"* ]]; then
echo "... retrying mount with noserverino"
MOUNTOPTIONS="${orig_mountoptions},noserverino"
umount "$mountpoint" 2> /dev/null || true
if mount_drive "$MOUNTOPTIONS"; then
if _test_write 2> /dev/null; then
MOUNTED=true
return 0
fi
fi
fi
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noperm"* ]]; then
echo "... retrying mount with noperm"
MOUNTOPTIONS="${orig_mountoptions},noperm"
umount "$mountpoint" 2> /dev/null || true
if mount_drive "$MOUNTOPTIONS"; then
if _test_write 2> /dev/null; then
MOUNTED=true
return 0
fi
fi
fi
if mountpoint -q "$mountpoint"; then
MOUNTED="readonly"
else
MOUNTED=false
fi
return 0
} }
mount_drive() { mount_drive() {
MOUNTED=true MOUNTED=true
MOUNTOPTIONS="$1" MOUNTOPTIONS="$1"
if [[ "$FSTYPE" == "cifs" ]]; then if [[ "$FSTYPE" == "cifs" ]]; then
mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2> "$ERRORCODE_FILE" || MOUNTED=false
elif [[ "$FSTYPE" == "nfs" ]]; then elif [[ "$FSTYPE" == "nfs" ]]; then
mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2> "$ERRORCODE_FILE" || MOUNTED=false
fi fi
if [[ "$MOUNTED" == "true" ]]; then if [[ "$MOUNTED" == "true" ]]; then
test_mount test_mount
fi fi
} }
# Retry ladder: SMB3 -> SMB2 when mount fails due to dialect/negotiation issues # Retry ladder: SMB3 -> SMB2 when mount fails due to dialect/negotiation issues
retry_cifs_with_vers_ladder_on_dialect_failure() { retry_cifs_with_vers_ladder_on_dialect_failure() {
[[ "${FSTYPE:-}" == "cifs" ]] || return 0 [[ "${FSTYPE:-}" == "cifs" ]] || return 0
[[ "${MOUNTED:-false}" == "false" ]] || return 0 [[ "${MOUNTED:-false}" == "false" ]] || return 0
[[ "${CIFS_LADDER_ATTEMPTED:-false}" == "false" ]] || return 0 [[ "${CIFS_LADDER_ATTEMPTED:-false}" == "false" ]] || return 0
local err mountpoint local err mountpoint
mountpoint="/mnt/$diskname" mountpoint="/mnt/$diskname"
err="$(cat "$ERRORCODE_FILE" 2>/dev/null || true)" err="$(cat "$ERRORCODE_FILE" 2> /dev/null || true)"
if echo "$err" | grep -Eq 'mount error\(13\)|Permission denied|NT_STATUS_(LOGON_FAILURE|ACCESS_DENIED)|STATUS_(LOGON_FAILURE|ACCESS_DENIED)'; then if echo "$err" | grep -Eq 'mount error\(13\)|Permission denied|NT_STATUS_(LOGON_FAILURE|ACCESS_DENIED)|STATUS_(LOGON_FAILURE|ACCESS_DENIED)'; then
return 0 return 0
fi fi
if ! echo "$err" | grep -Eq 'mount error\(22\)|mount error\(95\)|mount error\(112\)|Server abruptly closed the connection|does not support the SMB version|Protocol negotiation|NT_STATUS_CONNECTION_DISCONNECTED'; then if ! echo "$err" | grep -Eq 'mount error\(22\)|mount error\(95\)|mount error\(112\)|Server abruptly closed the connection|does not support the SMB version|Protocol negotiation|NT_STATUS_CONNECTION_DISCONNECTED'; then
return 0 return 0
fi fi
CIFS_LADDER_ATTEMPTED=true CIFS_LADDER_ATTEMPTED=true
bashio::log.warning "...... CIFS negotiation/dialect failure: trying SMB dialect ladder (3.x -> 2.x -> 1.0)." bashio::log.warning "...... CIFS negotiation/dialect failure: trying SMB dialect ladder (3.x -> 2.x -> 1.0)."
local base_opts try_opts vers vopt sectry local base_opts try_opts vers vopt sectry
base_opts="$MOUNTOPTIONS"
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
local -a opt_variants=("" ",nounix" ",noserverino" ",nounix,noserverino")
local -a sec_variants=("" ",sec=ntlmssp" ",sec=ntlmv2" ",sec=ntlm")
local -a vers_variants=("3.1.1" "3.02" "3.0" "2.1" "2.0" "1.0")
for vopt in "${opt_variants[@]}"; do
for vers in "${vers_variants[@]}"; do
for sectry in "${sec_variants[@]}"; do
[[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2>/dev/null || true
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
mount_drive "$try_opts"
done
[[ "$MOUNTED" == "false" ]] || break
done
[[ "$MOUNTED" == "false" ]] || break
done
if [[ "$MOUNTED" == "false" ]]; then
bashio::log.warning "...... still failing after vers ladder; retrying with reduced CIFS options."
base_opts="$MOUNTOPTIONS" base_opts="$MOUNTOPTIONS"
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')" base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
base_opts="${base_opts//,mfsymlinks/}"
base_opts="${base_opts//,nobrl/}"
base_opts="$(echo "$base_opts" | sed -E 's/,iocharset=[^,]+//g')"
local -a vers_variants2=("2.1" "2.0" "1.0") local -a opt_variants=("" ",nounix" ",noserverino" ",nounix,noserverino")
local -a sec_variants=("" ",sec=ntlmssp" ",sec=ntlmv2" ",sec=ntlm")
local -a vers_variants=("3.1.1" "3.02" "3.0" "2.1" "2.0" "1.0")
for vopt in "${opt_variants[@]}"; do for vopt in "${opt_variants[@]}"; do
for vers in "${vers_variants2[@]}"; do for vers in "${vers_variants[@]}"; do
for sectry in "${sec_variants[@]}"; do for sectry in "${sec_variants[@]}"; do
[[ "$MOUNTED" == "false" ]] || break [[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2>/dev/null || true umount "$mountpoint" 2> /dev/null || true
try_opts="${base_opts}${vopt},vers=${vers}${sectry}" try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
mount_drive "$try_opts" mount_drive "$try_opts"
done
[[ "$MOUNTED" == "false" ]] || break
done done
[[ "$MOUNTED" == "false" ]] || break [[ "$MOUNTED" == "false" ]] || break
done
[[ "$MOUNTED" == "false" ]] || break
done done
if [[ "$MOUNTED" == "false" ]]; then if [[ "$MOUNTED" == "false" ]]; then
for vopt in "${opt_variants[@]}"; do bashio::log.warning "...... still failing after vers ladder; retrying with reduced CIFS options."
for vers in "${vers_variants2[@]}"; do base_opts="$MOUNTOPTIONS"
[[ "$MOUNTED" == "false" ]] || break base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
umount "$mountpoint" 2>/dev/null || true base_opts="${base_opts//,mfsymlinks/}"
try_opts="${base_opts}${vopt},vers=${vers},sec=ntlmssp" base_opts="${base_opts//,nobrl/}"
mount_drive "$try_opts" base_opts="$(echo "$base_opts" | sed -E 's/,iocharset=[^,]+//g')"
done
[[ "$MOUNTED" == "false" ]] || break
done
fi
fi
return 0 local -a vers_variants2=("2.1" "2.0" "1.0")
for vopt in "${opt_variants[@]}"; do
for vers in "${vers_variants2[@]}"; do
for sectry in "${sec_variants[@]}"; do
[[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2> /dev/null || true
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
mount_drive "$try_opts"
done
[[ "$MOUNTED" == "false" ]] || break
done
[[ "$MOUNTED" == "false" ]] || break
done
if [[ "$MOUNTED" == "false" ]]; then
for vopt in "${opt_variants[@]}"; do
for vers in "${vers_variants2[@]}"; do
[[ "$MOUNTED" == "false" ]] || break
umount "$mountpoint" 2> /dev/null || true
try_opts="${base_opts}${vopt},vers=${vers},sec=ntlmssp"
mount_drive "$try_opts"
done
[[ "$MOUNTED" == "false" ]] || break
done
fi
fi
return 0
} }
######################## ########################
@@ -185,289 +185,289 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
if bashio::config.has_value 'networkdisks'; then if bashio::config.has_value 'networkdisks'; then
echo "Mounting network share(s)..." echo "Mounting network share(s)..."
MOREDISKS="$(bashio::config 'networkdisks')" MOREDISKS="$(bashio::config 'networkdisks')"
USERNAME="$(bashio::config 'cifsusername')" USERNAME="$(bashio::config 'cifsusername')"
PASSWORD="$(bashio::config 'cifspassword')" PASSWORD="$(bashio::config 'cifspassword')"
SMBVERS=""
SECVERS=""
CHARSET=",iocharset=utf8"
MOREDISKS="${MOREDISKS//$'\r'/}"
MOREDISKS="${MOREDISKS//$'\n'/,}"
MOREDISKS="$(echo "$MOREDISKS" | sed -E 's/[[:space:]]*,[[:space:]]*/,/g; s/^[[:space:]]+//; s/[[:space:]]+$//')"
DOMAINCLIENT=""
CIFSDOMAIN=""
if bashio::config.has_value 'cifsdomain'; then
CIFSDOMAIN="$(bashio::config 'cifsdomain')"
echo "... using domain $CIFSDOMAIN"
DOMAINCLIENT="--workgroup=$CIFSDOMAIN"
fi
PUID=",uid=$(id -u)"
PGID=",gid=$(id -g)"
if bashio::config.has_value 'PUID' && bashio::config.has_value 'PGID'; then
echo "... using PUID $(bashio::config 'PUID') and PGID $(bashio::config 'PGID')"
PUID=",uid=$(bashio::config 'PUID')"
PGID=",gid=$(bashio::config 'PGID')"
fi
IFS=',' read -r -a DISK_LIST <<< "$MOREDISKS"
for disk in "${DISK_LIST[@]}"; do
CRED_FILE=""
cleanup_cred
disk="${disk//$'\r'/}"
disk="$(echo "$disk" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
[[ -z "$disk" ]] && continue
disk="$(echo "$disk" | sed 's,/$,,')"
disk="${disk//"\040"/ }"
FSTYPE="cifs"
if [[ "$disk" =~ ^nfs:// ]]; then
FSTYPE="nfs"
disk="${disk#nfs://}"
elif [[ "$disk" =~ ^[^/:]+:/.+ ]]; then
FSTYPE="nfs"
fi
if [[ "$FSTYPE" == "cifs" ]]; then
server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -n 1)"
else
server="${disk%%:*}"
fi
diskname="$disk"
diskname="${diskname//\\//}"
diskname="${diskname##*/}"
if mountpoint -q "/mnt/$diskname" 2>/dev/null; then
bashio::log.warning "...... mount point /mnt/$diskname already in use (name collision for $disk). Skipping this share."
continue
fi
ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log"
: >"$ERRORCODE_FILE" || true
MOUNTED=false
CIFS_LADDER_ATTEMPTED=false
SMBVERS_FORCE=""
SECVERS_FORCE=""
SMBVERS="" SMBVERS=""
SECVERS="" SECVERS=""
CHARSET=",iocharset=utf8"
echo "... mounting ($FSTYPE) $disk" MOREDISKS="${MOREDISKS//$'\r'/}"
MOREDISKS="${MOREDISKS//$'\n'/,}"
MOREDISKS="$(echo "$MOREDISKS" | sed -E 's/[[:space:]]*,[[:space:]]*/,/g; s/^[[:space:]]+//; s/[[:space:]]+$//')"
if [[ "$FSTYPE" == "cifs" ]]; then DOMAINCLIENT=""
if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; then CIFSDOMAIN=""
bashio::log.fatal "...... invalid CIFS path \"$disk\". Use //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2" if bashio::config.has_value 'cifsdomain'; then
echo "Invalid CIFS path structure: $disk" >"$ERRORCODE_FILE" || true CIFSDOMAIN="$(bashio::config 'cifsdomain')"
continue echo "... using domain $CIFSDOMAIN"
fi DOMAINCLIENT="--workgroup=$CIFSDOMAIN"
else
if [[ ! "$disk" =~ ^[^/:]+:/.+ ]]; then
bashio::log.fatal "...... invalid NFS path \"$disk\". Use server:/export/path or 123.12.12.12:/export/path"
echo "Invalid NFS path structure: $disk" >"$ERRORCODE_FILE" || true
continue
fi
fi fi
mkdir -p "/mnt/$diskname" PUID=",uid=$(id -u)"
chown root:root "/mnt/$diskname" PGID=",gid=$(id -g)"
if bashio::config.has_value 'PUID' && bashio::config.has_value 'PGID'; then
echo "... using PUID $(bashio::config 'PUID') and PGID $(bashio::config 'PGID')"
PUID=",uid=$(bashio::config 'PUID')"
PGID=",gid=$(bashio::config 'PGID')"
fi
if [[ "$FSTYPE" == "cifs" ]]; then IFS=',' read -r -a DISK_LIST <<< "$MOREDISKS"
CRED_FILE="$(mktemp /tmp/cifs-cred.XXXXXX)"
chmod 600 "$CRED_FILE" for disk in "${DISK_LIST[@]}"; do
{ CRED_FILE=""
printf 'username=%s\n' "$USERNAME" cleanup_cred
printf 'password=%s\n' "$PASSWORD"
if [[ -n "${CIFSDOMAIN:-}" ]]; then disk="${disk//$'\r'/}"
printf 'domain=%s\n' "$CIFSDOMAIN" disk="$(echo "$disk" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
[[ -z "$disk" ]] && continue
disk="$(echo "$disk" | sed 's,/$,,')"
disk="${disk//"\040"/ }"
FSTYPE="cifs"
if [[ "$disk" =~ ^nfs:// ]]; then
FSTYPE="nfs"
disk="${disk#nfs://}"
elif [[ "$disk" =~ ^[^/:]+:/.+ ]]; then
FSTYPE="nfs"
fi fi
} >"$CRED_FILE"
fi
if [[ "$FSTYPE" == "cifs" ]]; then if [[ "$FSTYPE" == "cifs" ]]; then
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}" server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -n 1)"
if [[ "$MOUNTED" == "false" ]]; then else
retry_cifs_with_vers_ladder_on_dialect_failure server="${disk%%:*}"
fi fi
else
mount_drive "rw,nfsvers=4.2,proto=tcp,hard,timeo=600,retrans=2"
fi
if [[ "$MOUNTED" == "false" ]]; then diskname="$disk"
if [[ "$FSTYPE" == "cifs" ]]; then diskname="${diskname//\\//}"
if command -v nmap >/dev/null 2>&1; then diskname="${diskname##*/}"
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
if ! echo "$output" | grep -q "445/open"; then if mountpoint -q "/mnt/$diskname" 2> /dev/null; then
if echo "$output" | grep -q "/open"; then bashio::log.warning "...... mount point /mnt/$diskname already in use (name collision for $disk). Skipping this share."
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
else
bashio::log.fatal "...... fatal : $server not reachable, is it correct"
fi
cleanup_cred
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
continue continue
fi
echo "...... $server is confirmed reachable"
else
bashio::log.warning "...... nmap not available; skipping SMB port reachability test"
fi fi
if command -v smbclient >/dev/null 2>&1; then ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log"
OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)" : > "$ERRORCODE_FILE" || true
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
cleanup_cred
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
bashio::addon.stop
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
echo "... using SMBv1"
bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options."
SMBVERS_FORCE=",vers=1.0"
SECVERS_FORCE=",sec=ntlm"
elif ! echo "$OUTPUT" | grep -q "Disk"; then
bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?"
else
echo "...... credentials are valid"
fi
else
bashio::log.warning "...... smbclient not available; skipping SMB credential test"
fi
SMBRAW=""
SMB1_DETECTED=false
if command -v nmap >/dev/null 2>&1; then
NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2>/dev/null || true)"
SMBRAW="$(
echo "$NMAP_OUTPUT" \
| awk '/SMB2_DIALECT_/ {print $NF}' \
| sed 's/SMB2_DIALECT_//' \
| tr -d '_' \
| sort -V | tail -n 1 || true
)"
if [[ -z "$SMBRAW" ]] && echo "$NMAP_OUTPUT" | grep -Eiq 'NT LM 0\.12|SMBv1|NT1'; then
SMB1_DETECTED=true
fi
fi
MOUNTED=false
CIFS_LADDER_ATTEMPTED=false
SMBVERS_FORCE=""
SECVERS_FORCE=""
SMBVERS="" SMBVERS=""
case "$SMBRAW" in SECVERS=""
311) SMBVERS=",vers=3.1.1" ;;
302) SMBVERS=",vers=3.02" ;;
300) SMBVERS=",vers=3.0" ;;
210) SMBVERS=",vers=2.1" ;;
202|200) SMBVERS=",vers=2.0" ;;
*) SMBVERS="" ;;
esac
if [[ -z "$SMBVERS" && "$SMB1_DETECTED" == "true" ]]; then echo "... mounting ($FSTYPE) $disk"
echo "...... SMB version detected via nmap : SMBv1 (NT LM 0.12)"
SMBVERS=",vers=1.0"
SECVERS=",sec=ntlm"
fi
if [[ -n "$SMBVERS" ]]; then if [[ "$FSTYPE" == "cifs" ]]; then
echo "...... SMB version detected : ${SMBVERS#,vers=}" if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; then
elif command -v smbclient >/dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -U "$USERNAME%$PASSWORD" $DOMAINCLIENT -c "exit" &>/dev/null; then bashio::log.fatal "...... invalid CIFS path \"$disk\". Use //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2"
echo "...... SMB version : only SMBv1 is supported, this can lead to issues" echo "Invalid CIFS path structure: $disk" > "$ERRORCODE_FILE" || true
SECVERS=",sec=ntlm" continue
SMBVERS=",vers=1.0" fi
else else
echo "...... SMB version : couldn't detect, falling back to SMB3->SMB2->SMB1 ladder on negotiation/dialect failure" if [[ ! "$disk" =~ ^[^/:]+:/.+ ]]; then
SMBVERS=",vers=3.1.1" bashio::log.fatal "...... invalid NFS path \"$disk\". Use server:/export/path or 123.12.12.12:/export/path"
echo "Invalid NFS path structure: $disk" > "$ERRORCODE_FILE" || true
continue
fi
fi fi
if [[ -n "$SMBVERS_FORCE" ]]; then mkdir -p "/mnt/$diskname"
if [[ -n "$SMBVERS" && "$SMBVERS" != "$SMBVERS_FORCE" ]]; then chown root:root "/mnt/$diskname"
bashio::log.warning "...... overriding detected SMB version ${SMBVERS#,vers=} with forced ${SMBVERS_FORCE#,vers=} (server requires legacy protocol)"
fi if [[ "$FSTYPE" == "cifs" ]]; then
SMBVERS="$SMBVERS_FORCE" CRED_FILE="$(mktemp /tmp/cifs-cred.XXXXXX)"
[[ -z "$SECVERS" ]] && SECVERS="$SECVERS_FORCE" chmod 600 "$CRED_FILE"
{
printf 'username=%s\n' "$USERNAME"
printf 'password=%s\n' "$PASSWORD"
if [[ -n "${CIFSDOMAIN:-}" ]]; then
printf 'domain=%s\n' "$CIFSDOMAIN"
fi
} > "$CRED_FILE"
fi fi
SECVERS_BASE="$SECVERS" if [[ "$FSTYPE" == "cifs" ]]; then
for SECTRY in "$SECVERS_BASE" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}"
if [[ "$MOUNTED" == "false" ]]; then if [[ "$MOUNTED" == "false" ]]; then
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECTRY}${PUID}${PGID}${CHARSET}" retry_cifs_with_vers_ladder_on_dialect_failure
fi fi
done else
mount_drive "rw,nfsvers=4.2,proto=tcp,hard,timeo=600,retrans=2"
fi
if [[ "$MOUNTED" == "false" ]]; then if [[ "$MOUNTED" == "false" ]]; then
retry_cifs_with_vers_ladder_on_dialect_failure if [[ "$FSTYPE" == "cifs" ]]; then
if command -v nmap > /dev/null 2>&1; then
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)"
if ! echo "$output" | grep -q "445/open"; then
if echo "$output" | grep -q "/open"; then
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
else
bashio::log.fatal "...... fatal : $server not reachable, is it correct"
fi
cleanup_cred
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
continue
fi
echo "...... $server is confirmed reachable"
else
bashio::log.warning "...... nmap not available; skipping SMB port reachability test"
fi
if command -v smbclient > /dev/null 2>&1; then
OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)"
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
cleanup_cred
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
bashio::addon.stop
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
echo "... using SMBv1"
bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options."
SMBVERS_FORCE=",vers=1.0"
SECVERS_FORCE=",sec=ntlm"
elif ! echo "$OUTPUT" | grep -q "Disk"; then
bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?"
else
echo "...... credentials are valid"
fi
else
bashio::log.warning "...... smbclient not available; skipping SMB credential test"
fi
SMBRAW=""
SMB1_DETECTED=false
if command -v nmap > /dev/null 2>&1; then
NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2> /dev/null || true)"
SMBRAW="$(
echo "$NMAP_OUTPUT" \
| awk '/SMB2_DIALECT_/ {print $NF}' \
| sed 's/SMB2_DIALECT_//' \
| tr -d '_' \
| sort -V | tail -n 1 || true
)"
if [[ -z "$SMBRAW" ]] && echo "$NMAP_OUTPUT" | grep -Eiq 'NT LM 0\.12|SMBv1|NT1'; then
SMB1_DETECTED=true
fi
fi
SMBVERS=""
case "$SMBRAW" in
311) SMBVERS=",vers=3.1.1" ;;
302) SMBVERS=",vers=3.02" ;;
300) SMBVERS=",vers=3.0" ;;
210) SMBVERS=",vers=2.1" ;;
202 | 200) SMBVERS=",vers=2.0" ;;
*) SMBVERS="" ;;
esac
if [[ -z "$SMBVERS" && "$SMB1_DETECTED" == "true" ]]; then
echo "...... SMB version detected via nmap : SMBv1 (NT LM 0.12)"
SMBVERS=",vers=1.0"
SECVERS=",sec=ntlm"
fi
if [[ -n "$SMBVERS" ]]; then
echo "...... SMB version detected : ${SMBVERS#,vers=}"
elif command -v smbclient > /dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -U "$USERNAME%$PASSWORD" $DOMAINCLIENT -c "exit" &> /dev/null; then
echo "...... SMB version : only SMBv1 is supported, this can lead to issues"
SECVERS=",sec=ntlm"
SMBVERS=",vers=1.0"
else
echo "...... SMB version : couldn't detect, falling back to SMB3->SMB2->SMB1 ladder on negotiation/dialect failure"
SMBVERS=",vers=3.1.1"
fi
if [[ -n "$SMBVERS_FORCE" ]]; then
if [[ -n "$SMBVERS" && "$SMBVERS" != "$SMBVERS_FORCE" ]]; then
bashio::log.warning "...... overriding detected SMB version ${SMBVERS#,vers=} with forced ${SMBVERS_FORCE#,vers=} (server requires legacy protocol)"
fi
SMBVERS="$SMBVERS_FORCE"
[[ -z "$SECVERS" ]] && SECVERS="$SECVERS_FORCE"
fi
SECVERS_BASE="$SECVERS"
for SECTRY in "$SECVERS_BASE" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do
if [[ "$MOUNTED" == "false" ]]; then
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECTRY}${PUID}${PGID}${CHARSET}"
fi
done
if [[ "$MOUNTED" == "false" ]]; then
retry_cifs_with_vers_ladder_on_dialect_failure
fi
else
if command -v nmap > /dev/null 2>&1; then
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)"
if ! echo "$output" | grep -Eq '(2049|111)/open'; then
bashio::log.fatal "...... $server is reachable but NFS ports not open"
fi
else
bashio::log.warning "...... nmap not available; skipping NFS port reachability test"
fi
for NFVER in 4.2 4.1 4 3; do
if [[ "$MOUNTED" == "false" ]]; then
mount_drive "rw,nfsvers=${NFVER},proto=tcp"
fi
done
fi
fi fi
else if [[ "$MOUNTED" == "true" ]]; then
if command -v nmap >/dev/null 2>&1; then bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}"
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)" rm -f "$ERRORCODE_FILE" 2> /dev/null || true
if ! echo "$output" | grep -Eq '(2049|111)/open'; then
bashio::log.fatal "...... $server is reachable but NFS ports not open" if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then
fi bashio::log.warning ""
bashio::log.warning "Your SMB system requires SMBv1. This is an obsolete protocol. Please correct this to prevent issues."
bashio::log.warning ""
fi
cleanup_cred
elif [[ "$MOUNTED" == "readonly" ]]; then
bashio::log.warning "...... $disk mounted to /mnt/$diskname but is READ-ONLY or not writable by UID/GID ${PUID#,uid=}:${PGID#,gid=}."
bashio::log.warning "...... Check Samba share permissions, or try setting PUID/PGID to 0/0 (root), or adjust server ACLs."
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
cleanup_cred
else else
bashio::log.warning "...... nmap not available; skipping NFS port reachability test" if [[ "$FSTYPE" == "cifs" ]]; then
bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME. Please check share path, username/password/domain; try UID/GID 0."
bashio::log.fatal "Here is some debugging info :"
if command -v smbclient > /dev/null 2>&1; then
smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true
else
bashio::log.warning "smbclient not available; cannot print SMB debugging info"
fi
mount_drive "rw,credentials=${CRED_FILE}${PUID}${PGID}"
if [[ "$MOUNTED" == "false" ]]; then
retry_cifs_with_vers_ladder_on_dialect_failure
fi
else
bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check export path and allowlist for this client."
mount_drive "rw"
fi
ERR_READ="$(cat "$ERRORCODE_FILE" 2> /dev/null || true)"
bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min"
umount "/mnt/$diskname" 2> /dev/null || true
rmdir "/mnt/$diskname" 2> /dev/null || true
cleanup_cred
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
bashio::addon.stop
fi fi
done
for NFVER in 4.2 4.1 4 3; do
if [[ "$MOUNTED" == "false" ]]; then
mount_drive "rw,nfsvers=${NFVER},proto=tcp"
fi
done
fi
fi
if [[ "$MOUNTED" == "true" ]]; then
bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}"
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then
bashio::log.warning ""
bashio::log.warning "Your SMB system requires SMBv1. This is an obsolete protocol. Please correct this to prevent issues."
bashio::log.warning ""
fi
cleanup_cred
elif [[ "$MOUNTED" == "readonly" ]]; then
bashio::log.warning "...... $disk mounted to /mnt/$diskname but is READ-ONLY or not writable by UID/GID ${PUID#,uid=}:${PGID#,gid=}."
bashio::log.warning "...... Check Samba share permissions, or try setting PUID/PGID to 0/0 (root), or adjust server ACLs."
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
cleanup_cred
else
if [[ "$FSTYPE" == "cifs" ]]; then
bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME. Please check share path, username/password/domain; try UID/GID 0."
bashio::log.fatal "Here is some debugging info :"
if command -v smbclient >/dev/null 2>&1; then
smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true
else
bashio::log.warning "smbclient not available; cannot print SMB debugging info"
fi
mount_drive "rw,credentials=${CRED_FILE}${PUID}${PGID}"
if [[ "$MOUNTED" == "false" ]]; then
retry_cifs_with_vers_ladder_on_dialect_failure
fi
else
bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check export path and allowlist for this client."
mount_drive "rw"
fi
ERR_READ="$(cat "$ERRORCODE_FILE" 2>/dev/null || true)"
bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min"
umount "/mnt/$diskname" 2>/dev/null || true
rmdir "/mnt/$diskname" 2>/dev/null || true
cleanup_cred
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
bashio::addon.stop
fi
done
fi fi

View File

@@ -138,9 +138,9 @@ append_unique_line() {
# $1=file, $2=line # $1=file, $2=line
local _file="$1" local _file="$1"
local _line="$2" local _line="$2"
mkdir -p "$(dirname "$_file")" 2>/dev/null || true mkdir -p "$(dirname "$_file")" 2> /dev/null || true
touch "$_file" 2>/dev/null || true touch "$_file" 2> /dev/null || true
grep -qxF -- "$_line" "$_file" 2>/dev/null || echo "$_line" >> "$_file" grep -qxF -- "$_line" "$_file" 2> /dev/null || echo "$_line" >> "$_file"
} }
while IFS= read -r line; do while IFS= read -r line; do

View File

@@ -7,27 +7,33 @@ set -e
########################################## ##########################################
pick_exec_dir() { pick_exec_dir() {
local d local d
for d in /dev/shm /run /var/run /mnt /root /; do for d in /dev/shm /run /var/run /mnt /root /; do
if [ -d "$d" ] && [ -w "$d" ]; then if [ -d "$d" ] && [ -w "$d" ]; then
local t="${d%/}/.exec_test_$$" local t="${d%/}/.exec_test_$$"
printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; } printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || {
chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; } rm -f "$t" 2> /dev/null || true
if "$t" >/dev/null 2>&1; then continue
rm -f "$t" 2>/dev/null || true }
echo "$d" chmod 700 "$t" 2> /dev/null || {
return 0 rm -f "$t" 2> /dev/null || true
fi continue
rm -f "$t" 2>/dev/null || true }
fi if "$t" > /dev/null 2>&1; then
done rm -f "$t" 2> /dev/null || true
return 1 echo "$d"
return 0
fi
rm -f "$t" 2> /dev/null || true
fi
done
return 1
} }
EXEC_DIR="$(pick_exec_dir || true)" EXEC_DIR="$(pick_exec_dir || true)"
if [ -z "${EXEC_DIR:-}" ]; then if [ -z "${EXEC_DIR:-}" ]; then
echo "ERROR: Could not find an exec-capable writable directory." echo "ERROR: Could not find an exec-capable writable directory."
exit 1 exit 1
fi fi
###################### ######################
@@ -35,14 +41,14 @@ fi
###################### ######################
candidate_shebangs=( candidate_shebangs=(
"/command/with-contenv bashio" "/command/with-contenv bashio"
"/usr/bin/with-contenv bashio" "/usr/bin/with-contenv bashio"
"/usr/bin/env bashio" "/usr/bin/env bashio"
"/usr/bin/bashio" "/usr/bin/bashio"
"/usr/bin/bash" "/usr/bin/bash"
"/bin/bash" "/bin/bash"
"/usr/bin/sh" "/usr/bin/sh"
"/bin/sh" "/bin/sh"
) )
SHEBANG_ERRORS=() SHEBANG_ERRORS=()
@@ -85,76 +91,76 @@ echo "${_bv:-PROBE_OK}"
' '
validate_shebang() { validate_shebang() {
local candidate="$1" local candidate="$1"
local tmp out rc local tmp out rc
local errfile msg local errfile msg
# shellcheck disable=SC2206 # shellcheck disable=SC2206
local cmd=( $candidate ) local cmd=($candidate)
local exe="${cmd[0]}" local exe="${cmd[0]}"
if [ ! -x "$exe" ]; then if [ ! -x "$exe" ]; then
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate") SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
return 1
fi
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
{
printf '#!%s\n' "$candidate"
printf '%s\n' "$probe_script_content"
} > "$tmp"
chmod 700 "$tmp" 2> /dev/null || true
set +e
out="$("$tmp" 2> "$errfile")"
rc=$?
set -e
rm -f "$tmp" 2> /dev/null || true
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
rm -f "$errfile" 2> /dev/null || true
return 0
fi
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
if [ -s "$errfile" ]; then
msg+=$' stderr:\n'
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
else
msg+=$' stderr: <empty>\n'
fi
SHEBANG_ERRORS+=("$msg")
rm -f "$errfile" 2> /dev/null || true
return 1 return 1
fi
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
{
printf '#!%s\n' "$candidate"
printf '%s\n' "$probe_script_content"
} >"$tmp"
chmod 700 "$tmp" 2>/dev/null || true
set +e
out="$("$tmp" 2>"$errfile")"
rc=$?
set -e
rm -f "$tmp" 2>/dev/null || true
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
rm -f "$errfile" 2>/dev/null || true
return 0
fi
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
if [ -s "$errfile" ]; then
msg+=$' stderr:\n'
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
else
msg+=$' stderr: <empty>\n'
fi
SHEBANG_ERRORS+=("$msg")
rm -f "$errfile" 2>/dev/null || true
return 1
} }
shebang="" shebang=""
for candidate in "${candidate_shebangs[@]}"; do for candidate in "${candidate_shebangs[@]}"; do
if validate_shebang "$candidate"; then if validate_shebang "$candidate"; then
shebang="$candidate" shebang="$candidate"
break break
fi fi
done done
if [ -z "$shebang" ]; then if [ -z "$shebang" ]; then
echo "ERROR: No valid shebang found." >&2 echo "ERROR: No valid shebang found." >&2
printf ' - %s\n' "${candidate_shebangs[@]}" >&2 printf ' - %s\n' "${candidate_shebangs[@]}" >&2
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2 printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
fi fi
exit 1 exit 1
fi fi
if ! command -v bashio::addon.version >/dev/null 2>&1; then if ! command -v bashio::addon.version > /dev/null 2>&1; then
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh /usr/local/lib/bashio-standalone.sh; do for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh /usr/local/lib/bashio-standalone.sh; do
if [ -f "$f" ]; then if [ -f "$f" ]; then
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "$f" . "$f"
break break
fi fi
done done
fi fi
################## ##################
@@ -162,9 +168,9 @@ fi
################## ##################
# Exit if /config is not mounted or HA not used # Exit if /config is not mounted or HA not used
if [ ! -d /config ] || ! bashio::supervisor.ping 2>/dev/null; then if [ ! -d /config ] || ! bashio::supervisor.ping 2> /dev/null; then
echo "..." echo "..."
exit 0 exit 0
fi fi
# Define slug # Define slug
@@ -173,11 +179,11 @@ slug="${slug#*_}"
# Check type of config folder # Check type of config folder
if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then
CONFIGLOCATION="/config" CONFIGLOCATION="/config"
CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/$slug.sh" CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/$slug.sh"
else else
CONFIGLOCATION="/config/addons_autoscripts" CONFIGLOCATION="/config/addons_autoscripts"
CONFIGFILEBROWSER="/homeassistant/addons_autoscripts/$slug.sh" CONFIGFILEBROWSER="/homeassistant/addons_autoscripts/$slug.sh"
fi fi
# Default location # Default location
@@ -189,28 +195,28 @@ bashio::log.green "Execute $CONFIGFILEBROWSER if existing"
# Download template if no script found and exit # Download template if no script found and exit
if [ ! -f "$CONFIGSOURCE" ]; then if [ ! -f "$CONFIGSOURCE" ]; then
TEMPLATESOURCE="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/script.template" TEMPLATESOURCE="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/script.template"
curl -f -L -s -S "$TEMPLATESOURCE" --output "$CONFIGSOURCE" || true curl -f -L -s -S "$TEMPLATESOURCE" --output "$CONFIGSOURCE" || true
exit 0 exit 0
fi fi
# Convert scripts to linux # Convert scripts to linux
dos2unix "$CONFIGSOURCE" >/dev/null 2>&1 || true dos2unix "$CONFIGSOURCE" > /dev/null 2>&1 || true
chmod +x "$CONFIGSOURCE" chmod +x "$CONFIGSOURCE"
if grep -q 'bashio' "$CONFIGSOURCE"; then if grep -q 'bashio' "$CONFIGSOURCE"; then
sed -i "1s|^.*|#!$shebang|" "$CONFIGSOURCE" sed -i "1s|^.*|#!$shebang|" "$CONFIGSOURCE"
else else
sed -i '1s|^.*|#!/bin/bash|' "$CONFIGSOURCE" sed -i '1s|^.*|#!/bin/bash|' "$CONFIGSOURCE"
fi fi
# Check if there is actual commands # Check if there is actual commands
while IFS= read -r line; do while IFS= read -r line; do
line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
if [[ -n "$line" ]] && [[ ! "$line" =~ ^# ]]; then if [[ -n "$line" ]] && [[ ! "$line" =~ ^# ]]; then
bashio::log.green "... script found, executing" bashio::log.green "... script found, executing"
"$CONFIGSOURCE" "$CONFIGSOURCE"
break break
fi fi
done < "$CONFIGSOURCE" done < "$CONFIGSOURCE"

View File

@@ -9,12 +9,12 @@ set -e
# Avoid usage of local dns such as adguard home or pihole\n" # Avoid usage of local dns such as adguard home or pihole\n"
if bashio::config.has_value 'DNS_server'; then if bashio::config.has_value 'DNS_server'; then
if ! bashio::config.true "openvpn_enabled" && ! bashio::config.true "wireguard_enabled"; then if ! bashio::config.true "openvpn_enabled" && ! bashio::config.true "wireguard_enabled"; then
# Define variables # Define variables
DNSSERVER=$(bashio::config 'DNS_server') DNSSERVER=$(bashio::config 'DNS_server')
DNS="" DNS=""
DNSLIST="" DNSLIST=""
# Get DNS servers # Get DNS servers
# shellcheck disable=SC2086 # shellcheck disable=SC2086
for server in ${DNSSERVER//,/ }; do # Separate comma separated values for server in ${DNSSERVER//,/ }; do # Separate comma separated values
@@ -26,7 +26,7 @@ if bashio::config.has_value 'DNS_server'; then
bashio::log.warning "DNS $server was requested but can't be pinged. It won't be used" bashio::log.warning "DNS $server was requested but can't be pinged. It won't be used"
fi fi
done done
# Only add DNS if there are DNS set # Only add DNS if there are DNS set
# shellcheck disable=SC2236 # shellcheck disable=SC2236
if [[ -n "${DNS:-}" ]]; then if [[ -n "${DNS:-}" ]]; then

View File

@@ -33,293 +33,297 @@ _BASHIO_COLOR=1
[ "${TERM:-}" = "dumb" ] && _BASHIO_COLOR=0 [ "${TERM:-}" = "dumb" ] && _BASHIO_COLOR=0
_bashio_color() { _bashio_color() {
[ "$_BASHIO_COLOR" = "1" ] || return 0 [ "$_BASHIO_COLOR" = "1" ] || return 0
case "${1:-}" in case "${1:-}" in
blue) printf '\033[34m' ;; blue) printf '\033[34m' ;;
green) printf '\033[32m' ;; green) printf '\033[32m' ;;
yellow) printf '\033[33m' ;; yellow) printf '\033[33m' ;;
red) printf '\033[31m' ;; red) printf '\033[31m' ;;
magenta) printf '\033[35m' ;; magenta) printf '\033[35m' ;;
reset) printf '\033[0m' ;; reset) printf '\033[0m' ;;
*) printf '' ;; *) printf '' ;;
esac esac
} }
_bashio_log() { _bashio_log() {
local c="${1:-}"; shift || true local c="${1:-}"
printf '%s%s%s\n' "$(_bashio_color "$c")" "$*" "$(_bashio_color reset)" shift || true
printf '%s%s%s\n' "$(_bashio_color "$c")" "$*" "$(_bashio_color reset)"
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Helpers # Helpers
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
_bashio_is_true() { _bashio_is_true() {
case "${1:-}" in case "${1:-}" in
1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;; 1 | true | TRUE | True | yes | YES | Yes | on | ON | On) return 0 ;;
*) return 1 ;; *) return 1 ;;
esac esac
} }
# ENV mapping helper: # ENV mapping helper:
# tries variants + prefixes and prints the value if env var is defined (even empty), # tries variants + prefixes and prints the value if env var is defined (even empty),
# returning 0 when found, 1 when not found. # returning 0 when found, 1 when not found.
_bashio_env_get() { _bashio_env_get() {
local key="${1:-}" local key="${1:-}"
[ -n "$key" ] || return 1 [ -n "$key" ] || return 1
local norm norm_uc raw_uc local norm norm_uc raw_uc
norm="$(printf '%s' "$key" | tr '.-' '__')" norm="$(printf '%s' "$key" | tr '.-' '__')"
norm_uc="$(printf '%s' "$norm" | tr '[:lower:]' '[:upper:]')" norm_uc="$(printf '%s' "$norm" | tr '[:lower:]' '[:upper:]')"
raw_uc="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')" raw_uc="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
local variants=( local variants=(
"$key" "$key"
"$raw_uc" "$raw_uc"
"$norm" "$norm"
"$norm_uc" "$norm_uc"
) )
local prefixes=("" "CFG_" "CONFIG_" "ADDON_" "OPTION_" "OPT_") local prefixes=("" "CFG_" "CONFIG_" "ADDON_" "OPTION_" "OPT_")
local v p name local v p name
for v in "${variants[@]}"; do for v in "${variants[@]}"; do
for p in "${prefixes[@]}"; do for p in "${prefixes[@]}"; do
name="${p}${v}" name="${p}${v}"
if [ -n "${!name+x}" ]; then if [ -n "${!name+x}" ]; then
printf '%s' "${!name}" printf '%s' "${!name}"
return 0 return 0
fi fi
done
done done
done
return 1 return 1
} }
# env presence (even if empty) used by config.exists # env presence (even if empty) used by config.exists
_bashio_env_has() { _bashio_env_has() {
local key="${1:-}" local key="${1:-}"
[ -n "$key" ] || return 1 [ -n "$key" ] || return 1
_bashio_env_get "$key" >/dev/null 2>&1 _bashio_env_get "$key" > /dev/null 2>&1
} }
# JSON options source (jq required). Prints value or empty; returns 0 always. # JSON options source (jq required). Prints value or empty; returns 0 always.
_bashio_json_get() { _bashio_json_get() {
local key="${1:-}" local key="${1:-}"
local file="${STANDALONE_OPTIONS_JSON:-}" local file="${STANDALONE_OPTIONS_JSON:-}"
[ -n "$key" ] || return 0 [ -n "$key" ] || return 0
[ -n "$file" ] || return 0 [ -n "$file" ] || return 0
[ -f "$file" ] || return 0 [ -f "$file" ] || return 0
command -v jq >/dev/null 2>&1 || return 0 command -v jq > /dev/null 2>&1 || return 0
# getpath(split(".")) supports nested access; missing => empty # getpath(split(".")) supports nested access; missing => empty
jq -er --arg k "$key" 'getpath(($k|split("."))) // empty' "$file" 2>/dev/null || true jq -er --arg k "$key" 'getpath(($k|split("."))) // empty' "$file" 2> /dev/null || true
} }
# Net wait using /dev/tcp with a timeout # Net wait using /dev/tcp with a timeout
_bashio_tcp_wait() { _bashio_tcp_wait() {
local host="${1:-}" port="${2:-}" to="${3:-30}" local host="${1:-}" port="${2:-}" to="${3:-30}"
[ -n "$host" ] && [ -n "$port" ] || return 1 [ -n "$host" ] && [ -n "$port" ] || return 1
local start now local start now
start="$(date +%s)" start="$(date +%s)"
while :; do while :; do
if exec 3<>"/dev/tcp/${host}/${port}" 2>/dev/null; then if exec 3<> "/dev/tcp/${host}/${port}" 2> /dev/null; then
exec 3>&- 3<&- exec 3>&- 3<&-
return 0 return 0
fi fi
now="$(date +%s)" now="$(date +%s)"
if [ $((now - start)) -ge "$to" ]; then if [ $((now - start)) -ge "$to" ]; then
return 1 return 1
fi fi
sleep 1 sleep 1
done done
} }
# Prefer nc if present, fallback to /dev/tcp # Prefer nc if present, fallback to /dev/tcp
_bashio_tcp_wait_nc() { _bashio_tcp_wait_nc() {
command -v nc >/dev/null 2>&1 || return 1 command -v nc > /dev/null 2>&1 || return 1
local host="${1:-}" port="${2:-}" to="${3:-30}" local host="${1:-}" port="${2:-}" to="${3:-30}"
# BusyBox and OpenBSD nc differ; cover both styles # BusyBox and OpenBSD nc differ; cover both styles
nc -z -w "$to" "$host" "$port" 2>/dev/null || nc -z "$host" "$port" 2>/dev/null nc -z -w "$to" "$host" "$port" 2> /dev/null || nc -z "$host" "$port" 2> /dev/null
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Logging API # Logging API
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::log.blue() { _bashio_log blue "$*"; } bashio::log.blue() { _bashio_log blue "$*"; }
bashio::log.green() { _bashio_log green "$*"; } bashio::log.green() { _bashio_log green "$*"; }
bashio::log.yellow() { _bashio_log yellow "$*"; } bashio::log.yellow() { _bashio_log yellow "$*"; }
bashio::log.red() { _bashio_log red "$*"; } bashio::log.red() { _bashio_log red "$*"; }
bashio::log.magenta() { _bashio_log magenta "$*"; } bashio::log.magenta() { _bashio_log magenta "$*"; }
# Common aliases # Common aliases
bashio::log.info() { bashio::log.blue "$@"; } bashio::log.info() { bashio::log.blue "$@"; }
bashio::log.warning() { bashio::log.yellow "$@"; } bashio::log.warning() { bashio::log.yellow "$@"; }
bashio::log.error() { bashio::log.red "$@"; } bashio::log.error() { bashio::log.red "$@"; }
bashio::log.debug() { printf '%s\n' "$*"; } bashio::log.debug() { printf '%s\n' "$*"; }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Supervisor shim # Supervisor shim
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::supervisor.ping() { bashio::supervisor.ping() {
_bashio_is_true "${STANDALONE_FORCE_SUPERVISOR_PING:-}" && return 0 _bashio_is_true "${STANDALONE_FORCE_SUPERVISOR_PING:-}" && return 0
return 1 return 1
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Add-on metadata # Add-on metadata
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::addon.name() { printf '%s' "${ADDON_NAME:-Standalone container}"; } bashio::addon.name() { printf '%s' "${ADDON_NAME:-Standalone container}"; }
bashio::addon.description() { printf '%s' "${ADDON_DESCRIPTION:-Running without Home Assistant Supervisor}"; } bashio::addon.description() { printf '%s' "${ADDON_DESCRIPTION:-Running without Home Assistant Supervisor}"; }
bashio::addon.version() { printf '%s' "${BUILD_VERSION:-1.0}"; } bashio::addon.version() { printf '%s' "${BUILD_VERSION:-1.0}"; }
bashio::addon.version_latest(){ printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; } bashio::addon.version_latest() { printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; }
bashio::addon.update_available() { bashio::addon.update_available() {
if [ -n "${ADDON_VERSION_LATEST:-}" ] && [ "${ADDON_VERSION_LATEST:-}" != "${BUILD_VERSION:-}" ]; then if [ -n "${ADDON_VERSION_LATEST:-}" ] && [ "${ADDON_VERSION_LATEST:-}" != "${BUILD_VERSION:-}" ]; then
printf '%s' "true" printf '%s' "true"
else else
printf '%s' "false" printf '%s' "false"
fi fi
} }
bashio::addon.ingress_port() { printf '%s' "${ADDON_INGRESS_PORT:-}"; } bashio::addon.ingress_port() { printf '%s' "${ADDON_INGRESS_PORT:-}"; }
bashio::addon.ingress_entry() { printf '%s' "${ADDON_INGRESS_ENTRY:-}"; } bashio::addon.ingress_entry() { printf '%s' "${ADDON_INGRESS_ENTRY:-}"; }
bashio::addon.ip_address() { printf '%s' "${ADDON_IP_ADDRESS:-}"; } bashio::addon.ip_address() { printf '%s' "${ADDON_IP_ADDRESS:-}"; }
# Ports: # Ports:
# - numeric arg "8080" -> env PORT_8080 or ADDON_PORT_8080, fallback to the number # - numeric arg "8080" -> env PORT_8080 or ADDON_PORT_8080, fallback to the number
# - non-numeric "WEB_PORT" -> resolve as config/env key # - non-numeric "WEB_PORT" -> resolve as config/env key
bashio::addon.port() { bashio::addon.port() {
local arg="${1:-}" local arg="${1:-}"
if [[ "$arg" =~ ^[0-9]+$ ]]; then if [[ "$arg" =~ ^[0-9]+$ ]]; then
local v="" local v=""
v="$(_bashio_env_get "PORT_${arg}" 2>/dev/null || true)" v="$(_bashio_env_get "PORT_${arg}" 2> /dev/null || true)"
[ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2>/dev/null || true)" [ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2> /dev/null || true)"
printf '%s' "${v:-$arg}" printf '%s' "${v:-$arg}"
else else
printf '%s' "$(_bashio_env_get "$arg" 2>/dev/null || true)" printf '%s' "$(_bashio_env_get "$arg" 2> /dev/null || true)"
fi fi
} }
# addon.option : write/delete option in JSON when possible; fallback export env # addon.option : write/delete option in JSON when possible; fallback export env
bashio::addon.option() { bashio::addon.option() {
local key="${1:-}" value="${2-__BASHIO_UNSET__}" file="${STANDALONE_OPTIONS_JSON:-}" local key="${1:-}" value="${2-__BASHIO_UNSET__}" file="${STANDALONE_OPTIONS_JSON:-}"
[ -n "$key" ] || return 0 [ -n "$key" ] || return 0
if [ -n "$file" ] && [ -f "$file" ] && command -v jq >/dev/null 2>&1; then if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then
local tmp local tmp
tmp="$(mktemp)" tmp="$(mktemp)"
if [ "$value" = "__BASHIO_UNSET__" ]; then if [ "$value" = "__BASHIO_UNSET__" ]; then
jq --arg k "$key" 'delpath(($k|split(".")))' "$file" >"$tmp" && mv "$tmp" "$file" jq --arg k "$key" 'delpath(($k|split(".")))' "$file" > "$tmp" && mv "$tmp" "$file"
else else
jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" >"$tmp" && mv "$tmp" "$file" jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" > "$tmp" && mv "$tmp" "$file"
fi
return 0
fi fi
return 0
fi
# Fallback: export as env (dot/dash -> underscore). Delete becomes no-op. # Fallback: export as env (dot/dash -> underscore). Delete becomes no-op.
if [ "$value" != "__BASHIO_UNSET__" ]; then if [ "$value" != "__BASHIO_UNSET__" ]; then
export "$(printf '%s' "$key" | tr '.-' '__')"="$value" export "$(printf '%s' "$key" | tr '.-' '__')"="$value"
fi fi
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# System info # System info
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::info.operating_system() { bashio::info.operating_system() {
if [ -r /etc/os-release ]; then if [ -r /etc/os-release ]; then
# shellcheck disable=SC1091 # shellcheck disable=SC1091
. /etc/os-release . /etc/os-release
printf '%s' "${PRETTY_NAME:-${NAME:-Linux}}" printf '%s' "${PRETTY_NAME:-${NAME:-Linux}}"
else else
printf '%s' "Linux" printf '%s' "Linux"
fi fi
} }
bashio::info.arch() { uname -m; } bashio::info.arch() { uname -m; }
bashio::info.machine() { uname -m; } bashio::info.machine() { uname -m; }
bashio::info.homeassistant(){ printf '%s' "standalone"; } bashio::info.homeassistant() { printf '%s' "standalone"; }
bashio::info.supervisor() { printf '%s' "standalone"; } bashio::info.supervisor() { printf '%s' "standalone"; }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Config API # Config API
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::config() { bashio::config() {
local key="${1:-}" local key="${1:-}"
[ -n "$key" ] || { printf '%s' ""; return 0; } [ -n "$key" ] || {
printf '%s' ""
return 0
}
local v="" local v=""
if _bashio_env_get "$key" >/dev/null 2>&1; then if _bashio_env_get "$key" > /dev/null 2>&1; then
v="$(_bashio_env_get "$key" 2>/dev/null || true)" v="$(_bashio_env_get "$key" 2> /dev/null || true)"
fi fi
[ -z "$v" ] && v="$(_bashio_json_get "$key")" [ -z "$v" ] && v="$(_bashio_json_get "$key")"
printf '%s' "${v:-}" printf '%s' "${v:-}"
} }
bashio::config.has_value() { [ -n "$(bashio::config "$1")" ]; } bashio::config.has_value() { [ -n "$(bashio::config "$1")" ]; }
bashio::config.true() { bashio::config.true() {
_bashio_is_true "$(bashio::config "$1")" _bashio_is_true "$(bashio::config "$1")"
} }
# config.exists : key is present (env or JSON), even if value is empty # config.exists : key is present (env or JSON), even if value is empty
bashio::config.exists() { bashio::config.exists() {
local key="${1:-}" file="${STANDALONE_OPTIONS_JSON:-}" local key="${1:-}" file="${STANDALONE_OPTIONS_JSON:-}"
[ -n "$key" ] || return 1 [ -n "$key" ] || return 1
if _bashio_env_has "$key"; then if _bashio_env_has "$key"; then
return 0 return 0
fi fi
if [ -n "$file" ] && [ -f "$file" ] && command -v jq >/dev/null 2>&1; then if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then
jq -e --arg k "$key" 'haspath(($k|split(".")))' "$file" >/dev/null 2>&1 jq -e --arg k "$key" 'haspath(($k|split(".")))' "$file" > /dev/null 2>&1
return $? return $?
fi fi
return 1 return 1
} }
# Common "require.*" shims (advisory/no-op in standalone) # Common "require.*" shims (advisory/no-op in standalone)
bashio::config.require.ssl() { printf '%s' "${REQUIRE_SSL:-true}"; } bashio::config.require.ssl() { printf '%s' "${REQUIRE_SSL:-true}"; }
bashio::config.require.username() { :; } bashio::config.require.username() { :; }
bashio::config.require.password() { :; } bashio::config.require.password() { :; }
bashio::config.require.port() { :; } bashio::config.require.port() { :; }
# config.array: # config.array:
# Accepts CSV ("a,b,c"), space/newline-separated text, or JSON array ["a","b"]. # Accepts CSV ("a,b,c"), space/newline-separated text, or JSON array ["a","b"].
# Prints one item per line. # Prints one item per line.
bashio::config.array() { bashio::config.array() {
local key="${1:-}" raw local key="${1:-}" raw
raw="$(bashio::config "$key")" raw="$(bashio::config "$key")"
[ -n "$raw" ] || return 0 [ -n "$raw" ] || return 0
if command -v jq >/dev/null 2>&1 && printf '%s' "$raw" | jq -e . >/dev/null 2>&1; then if command -v jq > /dev/null 2>&1 && printf '%s' "$raw" | jq -e . > /dev/null 2>&1; then
printf '%s' "$raw" | jq -r '.[]' 2>/dev/null && return 0 printf '%s' "$raw" | jq -r '.[]' 2> /dev/null && return 0
fi fi
if printf '%s' "$raw" | grep -q ','; then if printf '%s' "$raw" | grep -q ','; then
printf '%s' "$raw" | tr ',' '\n' printf '%s' "$raw" | tr ',' '\n'
return 0 return 0
fi fi
printf '%s\n' "$raw" printf '%s\n' "$raw"
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# var helpers # var helpers
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::var.true() { _bashio_is_true "${1:-}"; } bashio::var.true() { _bashio_is_true "${1:-}"; }
bashio::var.false() { ! _bashio_is_true "${1:-}"; } bashio::var.false() { ! _bashio_is_true "${1:-}"; }
bashio::var.has_value() { [ -n "${1:-}" ]; } bashio::var.has_value() { [ -n "${1:-}" ]; }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Filesystem helpers # Filesystem helpers
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::fs.file_exists() { [ -f "${1:-}" ]; } bashio::fs.file_exists() { [ -f "${1:-}" ]; }
bashio::fs.directory_exists() { [ -d "${1:-}" ]; } bashio::fs.directory_exists() { [ -d "${1:-}" ]; }
bashio::fs.file_contains() { bashio::fs.file_contains() {
local f="${1:-}" p="${2:-}" local f="${1:-}" p="${2:-}"
[ -f "$f" ] && grep -q -- "$p" "$f" 2>/dev/null [ -f "$f" ] && grep -q -- "$p" "$f" 2> /dev/null
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -327,25 +331,25 @@ bashio::fs.file_contains() {
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Wait for TCP service: bashio::net.wait_for host port [timeout] # Wait for TCP service: bashio::net.wait_for host port [timeout]
bashio::net.wait_for() { bashio::net.wait_for() {
local host="${1:-}" port="${2:-}" to="${3:-30}" local host="${1:-}" port="${2:-}" to="${3:-30}"
_bashio_tcp_wait_nc "$host" "$port" "$to" && return 0 _bashio_tcp_wait_nc "$host" "$port" "$to" && return 0
_bashio_tcp_wait "$host" "$port" "$to" _bashio_tcp_wait "$host" "$port" "$to"
} }
# DNS helper: bashio::dns.host <hostname> -> prints an IP (or empty) # DNS helper: bashio::dns.host <hostname> -> prints an IP (or empty)
bashio::dns.host() { bashio::dns.host() {
local h="${1:-}" local h="${1:-}"
[ -n "$h" ] || return 1 [ -n "$h" ] || return 1
if command -v getent >/dev/null 2>&1; then if command -v getent > /dev/null 2>&1; then
getent ahostsv4 "$h" | awk '{print $1; exit}' getent ahostsv4 "$h" | awk '{print $1; exit}'
else else
nslookup "$h" 2>/dev/null | awk '/^Address: /{print $2; exit}' nslookup "$h" 2> /dev/null | awk '/^Address: /{print $2; exit}'
fi fi
} }
# Hostname # Hostname
bashio::host.hostname() { bashio::host.hostname() {
command -v hostname >/dev/null 2>&1 && hostname || printf '%s' "${HOSTNAME:-unknown}" command -v hostname > /dev/null 2>&1 && hostname || printf '%s' "${HOSTNAME:-unknown}"
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -355,40 +359,43 @@ bashio::host.hostname() {
# bashio::services "mqtt" "host" # bashio::services "mqtt" "host"
# bashio::services.available "mqtt" # bashio::services.available "mqtt"
bashio::services() { bashio::services() {
local svc="${1:-}" key="${2:-}" local svc="${1:-}" key="${2:-}"
[ -n "$svc" ] && [ -n "$key" ] || { printf '%s' ""; return 0; } [ -n "$svc" ] && [ -n "$key" ] || {
printf '%s' ""
return 0
}
local upper svc_upper var v="" local upper svc_upper var v=""
upper="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')" upper="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
svc_upper="$(printf '%s' "$svc" | tr '[:lower:]' '[:upper:]')" svc_upper="$(printf '%s' "$svc" | tr '[:lower:]' '[:upper:]')"
# Common mappings # Common mappings
case "$svc_upper:$upper" in case "$svc_upper:$upper" in
MQTT:HOST) var="MQTT_HOST" ;; MQTT:HOST) var="MQTT_HOST" ;;
MQTT:PORT) var="MQTT_PORT" ;; MQTT:PORT) var="MQTT_PORT" ;;
MQTT:USERNAME) var="MQTT_USER" ;; MQTT:USERNAME) var="MQTT_USER" ;;
MQTT:PASSWORD) var="MQTT_PASSWORD" ;; MQTT:PASSWORD) var="MQTT_PASSWORD" ;;
MQTT:TLS) var="MQTT_TLS" ;; MQTT:TLS) var="MQTT_TLS" ;;
MYSQL:HOST|MARIADB:HOST) var="DB_HOST" ;; MYSQL:HOST | MARIADB:HOST) var="DB_HOST" ;;
MYSQL:PORT|MARIADB:PORT) var="DB_PORT" ;; MYSQL:PORT | MARIADB:PORT) var="DB_PORT" ;;
MYSQL:USERNAME|MARIADB:USERNAME) var="DB_USER" ;; MYSQL:USERNAME | MARIADB:USERNAME) var="DB_USER" ;;
MYSQL:PASSWORD|MARIADB:PASSWORD) var="DB_PASSWORD" ;; MYSQL:PASSWORD | MARIADB:PASSWORD) var="DB_PASSWORD" ;;
MYSQL:DATABASE|MARIADB:DATABASE) var="DB_NAME" ;; MYSQL:DATABASE | MARIADB:DATABASE) var="DB_NAME" ;;
*) var="${svc_upper}_${upper}" ;; *) var="${svc_upper}_${upper}" ;;
esac esac
v="$(_bashio_env_get "$var" 2>/dev/null || true)" v="$(_bashio_env_get "$var" 2> /dev/null || true)"
if [ -z "$v" ]; then if [ -z "$v" ]; then
v="$(_bashio_json_get "services.${svc}.${key}")" v="$(_bashio_json_get "services.${svc}.${key}")"
[ -z "$v" ] && v="$(_bashio_json_get "${svc}.${key}")" [ -z "$v" ] && v="$(_bashio_json_get "${svc}.${key}")"
fi fi
printf '%s' "${v:-}" printf '%s' "${v:-}"
} }
bashio::services.available() { bashio::services.available() {
local svc="${1:-}" host local svc="${1:-}" host
host="$(bashio::services "$svc" "host")" host="$(bashio::services "$svc" "host")"
[ -n "$host" ] [ -n "$host" ]
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -396,39 +403,43 @@ bashio::services.available() {
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
mkdir -p "$BASHIO_CACHE_DIR" mkdir -p "$BASHIO_CACHE_DIR"
bashio::cache.exists() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ]; } bashio::cache.exists() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ]; }
bashio::cache.get() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ] && cat "$BASHIO_CACHE_DIR/${1}.cache"; } bashio::cache.get() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ] && cat "$BASHIO_CACHE_DIR/${1}.cache"; }
bashio::cache.set() { printf '%s' "${2:-}" > "$BASHIO_CACHE_DIR/${1}.cache"; } bashio::cache.set() { printf '%s' "${2:-}" > "$BASHIO_CACHE_DIR/${1}.cache"; }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# jq wrapper (some add-ons call bashio::jq) # jq wrapper (some add-ons call bashio::jq)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::jq() { command -v jq >/dev/null 2>&1 && jq "$@"; } bashio::jq() { command -v jq > /dev/null 2>&1 && jq "$@"; }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Home Assistant token # Home Assistant token
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::homeassistant.token() { bashio::homeassistant.token() {
local t="${HOMEASSISTANT_TOKEN:-${HASS_TOKEN:-}}" local t="${HOMEASSISTANT_TOKEN:-${HASS_TOKEN:-}}"
if [ -z "$t" ] && [ -n "${STANDALONE_OPTIONS_JSON:-}" ] && [ -f "${STANDALONE_OPTIONS_JSON:-}" ] && command -v jq >/dev/null 2>&1; then if [ -z "$t" ] && [ -n "${STANDALONE_OPTIONS_JSON:-}" ] && [ -f "${STANDALONE_OPTIONS_JSON:-}" ] && command -v jq > /dev/null 2>&1; then
t="$(jq -er '.homeassistant.token // empty' "$STANDALONE_OPTIONS_JSON" 2>/dev/null || true)" t="$(jq -er '.homeassistant.token // empty' "$STANDALONE_OPTIONS_JSON" 2> /dev/null || true)"
fi fi
printf '%s' "${t:-}" printf '%s' "${t:-}"
} }
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Exit helpers # Exit helpers
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bashio::exit.ok() { exit 0; } bashio::exit.ok() { exit 0; }
bashio::exit.nok() { local m="${1:-}"; [ -n "$m" ] && bashio::log.red "$m"; exit 1; } bashio::exit.nok() {
local m="${1:-}"
[ -n "$m" ] && bashio::log.red "$m"
exit 1
}
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Core config check shim # Core config check shim
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Set STANDALONE_CORE_CHECK_CMD="hass --script check_config -c /config" to enable # Set STANDALONE_CORE_CHECK_CMD="hass --script check_config -c /config" to enable
bashio::core.check() { bashio::core.check() {
if [ -n "${STANDALONE_CORE_CHECK_CMD:-}" ]; then if [ -n "${STANDALONE_CORE_CHECK_CMD:-}" ]; then
eval "$STANDALONE_CORE_CHECK_CMD" eval "$STANDALONE_CORE_CHECK_CMD"
else else
return 0 return 0
fi fi
} }

View File

@@ -41,14 +41,14 @@ restore_apt_sources_if_missing() {
mirror="http://ports.ubuntu.com/ubuntu-ports" mirror="http://ports.ubuntu.com/ubuntu-ports"
security_mirror="$mirror" security_mirror="$mirror"
fi fi
cat > /etc/apt/sources.list <<EOF2 cat > /etc/apt/sources.list << EOF2
deb ${mirror} ${codename} main restricted universe multiverse deb ${mirror} ${codename} main restricted universe multiverse
deb ${mirror} ${codename}-updates main restricted universe multiverse deb ${mirror} ${codename}-updates main restricted universe multiverse
deb ${security_mirror} ${codename}-security main restricted universe multiverse deb ${security_mirror} ${codename}-security main restricted universe multiverse
EOF2 EOF2
;; ;;
debian) debian)
cat > /etc/apt/sources.list <<EOF2 cat > /etc/apt/sources.list << EOF2
deb http://deb.debian.org/debian ${codename} main contrib non-free non-free-firmware deb http://deb.debian.org/debian ${codename} main contrib non-free non-free-firmware
deb http://deb.debian.org/debian ${codename}-updates main contrib non-free non-free-firmware deb http://deb.debian.org/debian ${codename}-updates main contrib non-free non-free-firmware
deb http://deb.debian.org/debian-security ${codename}-security main contrib non-free non-free-firmware deb http://deb.debian.org/debian-security ${codename}-security main contrib non-free non-free-firmware

View File

@@ -184,7 +184,7 @@ done
if [ "$PACKMANAGER" = "apt" ]; then apt-get update > /dev/null; fi if [ "$PACKMANAGER" = "apt" ]; then apt-get update > /dev/null; fi
if [ "$PACKMANAGER" = "pacman" ]; then pacman -Sy > /dev/null; fi if [ "$PACKMANAGER" = "pacman" ]; then pacman -Sy > /dev/null; fi
if [ "$PACKMANAGER" = "apk" ] && [ -f /etc/apk/repositories ] && ! grep -q "community" /etc/apk/repositories; then if [ "$PACKMANAGER" = "apk" ] && [ -f /etc/apk/repositories ] && ! grep -q "community" /etc/apk/repositories; then
ALPINE_VER=$(cat /etc/alpine-release 2>/dev/null | cut -d. -f1,2) ALPINE_VER=$(cat /etc/alpine-release 2> /dev/null | cut -d. -f1,2)
if [ -n "$ALPINE_VER" ]; then if [ -n "$ALPINE_VER" ]; then
echo "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VER}/community" >> /etc/apk/repositories echo "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VER}/community" >> /etc/apk/repositories
apk update > /dev/null apk update > /dev/null
@@ -228,7 +228,7 @@ for files in "/etc/services.d" "/etc/cont-init.d"; do
if ! ls $files 1> /dev/null 2>&1; then continue; fi if ! ls $files 1> /dev/null 2>&1; then continue; fi
# Bashio # Bashio
if grep -q -rnw "${files}" -e 'bashio' 2>/dev/null && [ ! -f "/usr/bin/bashio" ]; then if grep -q -rnw "${files}" -e 'bashio' 2> /dev/null && [ ! -f "/usr/bin/bashio" ]; then
[ "$VERBOSE" = true ] && echo "install bashio" [ "$VERBOSE" = true ] && echo "install bashio"
BASHIO_VERSION="v0.17.5" BASHIO_VERSION="v0.17.5"
mkdir -p /tmp/bashio mkdir -p /tmp/bashio

View File

@@ -7,13 +7,13 @@
PID1=false PID1=false
if [ "$$" -eq 1 ]; then if [ "$$" -eq 1 ]; then
PID1=true PID1=true
echo "Starting as entrypoint" echo "Starting as entrypoint"
if [ -d /command ]; then if [ -d /command ]; then
ln -sf /command/* /usr/bin/ 2>/dev/null || true ln -sf /command/* /usr/bin/ 2> /dev/null || true
fi fi
else else
echo "Starting custom scripts" echo "Starting custom scripts"
fi fi
########################################## ##########################################
@@ -21,31 +21,37 @@ fi
########################################## ##########################################
pick_exec_dir() { pick_exec_dir() {
# Prefer locations that are commonly exec-capable in containers # Prefer locations that are commonly exec-capable in containers
# and writable. Avoid /tmp because it may be mounted noexec. # and writable. Avoid /tmp because it may be mounted noexec.
local d local d
for d in /dev/shm /run /var/run /mnt /root /; do for d in /dev/shm /run /var/run /mnt /root /; do
if [ -d "$d" ] && [ -w "$d" ]; then if [ -d "$d" ] && [ -w "$d" ]; then
# Create a tiny test executable to confirm "exec" works # Create a tiny test executable to confirm "exec" works
local t="${d%/}/.exec_test_$$" local t="${d%/}/.exec_test_$$"
printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; } printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || {
chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; } rm -f "$t" 2> /dev/null || true
if "$t" >/dev/null 2>&1; then continue
rm -f "$t" 2>/dev/null || true }
echo "$d" chmod 700 "$t" 2> /dev/null || {
return 0 rm -f "$t" 2> /dev/null || true
fi continue
rm -f "$t" 2>/dev/null || true }
fi if "$t" > /dev/null 2>&1; then
done rm -f "$t" 2> /dev/null || true
return 1 echo "$d"
return 0
fi
rm -f "$t" 2> /dev/null || true
fi
done
return 1
} }
EXEC_DIR="$(pick_exec_dir || true)" EXEC_DIR="$(pick_exec_dir || true)"
if [ -z "${EXEC_DIR:-}" ]; then if [ -z "${EXEC_DIR:-}" ]; then
echo "ERROR: Could not find an exec-capable writable directory (e.g., /dev/shm,/run)." echo "ERROR: Could not find an exec-capable writable directory (e.g., /dev/shm,/run)."
echo "Your environment likely mounts all writable dirs as noexec; shebang validation cannot run safely." echo "Your environment likely mounts all writable dirs as noexec; shebang validation cannot run safely."
exit 1 exit 1
fi fi
###################### ######################
@@ -53,14 +59,14 @@ fi
###################### ######################
candidate_shebangs=( candidate_shebangs=(
"/command/with-contenv bashio" "/command/with-contenv bashio"
"/usr/bin/with-contenv bashio" "/usr/bin/with-contenv bashio"
"/usr/bin/env bashio" "/usr/bin/env bashio"
"/usr/bin/bashio" "/usr/bin/bashio"
"/usr/bin/bash" "/usr/bin/bash"
"/bin/bash" "/bin/bash"
"/usr/bin/sh" "/usr/bin/sh"
"/bin/sh" "/bin/sh"
) )
SHEBANG_ERRORS=() SHEBANG_ERRORS=()
@@ -104,68 +110,68 @@ echo "${_bv:-PROBE_OK}"
' '
validate_shebang() { validate_shebang() {
local candidate="$1" local candidate="$1"
local tmp out rc local tmp out rc
local errfile msg local errfile msg
# shellcheck disable=SC2206 # shellcheck disable=SC2206
local cmd=( $candidate ) local cmd=($candidate)
local exe="${cmd[0]}" local exe="${cmd[0]}"
if [ ! -x "$exe" ]; then if [ ! -x "$exe" ]; then
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate") SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
return 1
fi
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
{
printf '#!%s\n' "$candidate"
printf '%s\n' "$probe_script_content"
} > "$tmp"
chmod 700 "$tmp" 2> /dev/null || true
set +e
out="$("$tmp" 2> "$errfile")"
rc=$?
set -e
rm -f "$tmp" 2> /dev/null || true
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
rm -f "$errfile" 2> /dev/null || true
return 0
fi
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
if [ -s "$errfile" ]; then
msg+=$' stderr:\n'
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
else
msg+=$' stderr: <empty>\n'
fi
SHEBANG_ERRORS+=("$msg")
rm -f "$errfile" 2> /dev/null || true
return 1 return 1
fi
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
{
printf '#!%s\n' "$candidate"
printf '%s\n' "$probe_script_content"
} >"$tmp"
chmod 700 "$tmp" 2>/dev/null || true
set +e
out="$("$tmp" 2>"$errfile")"
rc=$?
set -e
rm -f "$tmp" 2>/dev/null || true
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
rm -f "$errfile" 2>/dev/null || true
return 0
fi
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
if [ -s "$errfile" ]; then
msg+=$' stderr:\n'
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
else
msg+=$' stderr: <empty>\n'
fi
SHEBANG_ERRORS+=("$msg")
rm -f "$errfile" 2>/dev/null || true
return 1
} }
shebang="" shebang=""
for candidate in "${candidate_shebangs[@]}"; do for candidate in "${candidate_shebangs[@]}"; do
if validate_shebang "$candidate"; then if validate_shebang "$candidate"; then
shebang="$candidate" shebang="$candidate"
break break
fi fi
done done
if [ -z "$shebang" ]; then if [ -z "$shebang" ]; then
echo "ERROR: No valid shebang found (unable to execute bashio::addon.version via candidates)." >&2 echo "ERROR: No valid shebang found (unable to execute bashio::addon.version via candidates)." >&2
echo "Tried:" >&2 echo "Tried:" >&2
printf ' - %s\n' "${candidate_shebangs[@]}" >&2 printf ' - %s\n' "${candidate_shebangs[@]}" >&2
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
echo "Probe failures:" >&2 echo "Probe failures:" >&2
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2 printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
fi fi
exit 1 exit 1
fi fi
##################################### #####################################
@@ -197,28 +203,28 @@ S6_CONTAINER_ENV="/run/s6/container_environment"
# Only when this script is PID 1 -- under /init it is the stage-2 hook and stage 1 has already # Only when this script is PID 1 -- under /init it is the stage-2 hook and stage 1 has already
# written the directory -- and only where with-contenv exists to care. # written the directory -- and only where with-contenv exists to care.
if $PID1 && { [ -x /command/with-contenv ] || [ -x /usr/bin/with-contenv ]; }; then if $PID1 && { [ -x /command/with-contenv ] || [ -x /usr/bin/with-contenv ]; }; then
# Filled in a sibling and renamed into place, never written to live. A half-populated envdir is # Filled in a sibling and renamed into place, never written to live. A half-populated envdir is
# worse than an absent one: s6-envdir accepts it, so a with-contenv script starts and runs # worse than an absent one: s6-envdir accepts it, so a with-contenv script starts and runs
# against an environment quietly missing SUPERVISOR_TOKEN, where an absent one stops it at its # against an environment quietly missing SUPERVISOR_TOKEN, where an absent one stops it at its
# shebang. rename(2) means a concurrent reader -- a HEALTHCHECK can run alongside PID 1 -- sees # shebang. rename(2) means a concurrent reader -- a HEALTHCHECK can run alongside PID 1 -- sees
# the directory either absent or complete, never mid-dump. # the directory either absent or complete, never mid-dump.
# #
# Cleared rather than written over: /run is not a tmpfs here, so an image layer can persist # Cleared rather than written over: /run is not a tmpfs here, so an image layer can persist
# entries, and writing name by name would merge into them and leave variables PID 1 does not # entries, and writing name by name would merge into them and leave variables PID 1 does not
# have, a stale SUPERVISOR_TOKEN among them. A failed rm has to abort the chain, because mkdir -p # have, a stale SUPERVISOR_TOKEN among them. A failed rm has to abort the chain, because mkdir -p
# accepts a surviving symlink-to-directory and would let the dump follow it. rm does not traverse # accepts a surviving symlink-to-directory and would let the dump follow it. rm does not traverse
# a symlink, but it would empty anything bind-mounted at this exact path -- not a configuration # a symlink, but it would empty anything bind-mounted at this exact path -- not a configuration
# any add-on uses, and not one s6 would tolerate either. # any add-on uses, and not one s6 would tolerate either.
if rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" && mkdir -p "$S6_CONTAINER_ENV.tmp" && if rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" && mkdir -p "$S6_CONTAINER_ENV.tmp" \
s6-dumpenv -- "$S6_CONTAINER_ENV.tmp" && mv "$S6_CONTAINER_ENV.tmp" "$S6_CONTAINER_ENV"; then && s6-dumpenv -- "$S6_CONTAINER_ENV.tmp" && mv "$S6_CONTAINER_ENV.tmp" "$S6_CONTAINER_ENV"; then
echo "Populated $S6_CONTAINER_ENV for with-contenv" echo "Populated $S6_CONTAINER_ENV for with-contenv"
else else
# Leaves the directory absent, which is exactly how this fails today -- so the failure mode is # Leaves the directory absent, which is exactly how this fails today -- so the failure mode is
# unchanged, not newly degraded. Never fatal, a read-only /run must still let the add-on boot, # unchanged, not newly degraded. Never fatal, a read-only /run must still let the add-on boot,
# but never silent either, since the shebang failure it leaves behind says nothing on its own. # but never silent either, since the shebang failure it leaves behind says nothing on its own.
rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" 2>/dev/null || true rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" 2> /dev/null || true
echo -e "\e[38;5;214m$(date) WARNING: could not populate $S6_CONTAINER_ENV; scripts with a with-contenv shebang will fail at their shebang, as they did before this was attempted\e[0m" echo -e "\e[38;5;214m$(date) WARNING: could not populate $S6_CONTAINER_ENV; scripts with a with-contenv shebang will fail at their shebang, as they did before this was attempted\e[0m"
fi fi
fi fi
#################################### ####################################
@@ -228,21 +234,21 @@ fi
BASHIO_LIB="" BASHIO_LIB=""
BASHIO_LIB_FULL=false BASHIO_LIB_FULL=false
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh; do for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh; do
if [ -f "$f" ]; then if [ -f "$f" ]; then
BASHIO_LIB="$f" BASHIO_LIB="$f"
# The real library, which talks to the Supervisor. The standalone shim below only reads # The real library, which talks to the Supervisor. The standalone shim below only reads
# environment variables, which matters to wait_for_supervisor(). # environment variables, which matters to wait_for_supervisor().
BASHIO_LIB_FULL=true BASHIO_LIB_FULL=true
break break
fi fi
done done
if [ -z "$BASHIO_LIB" ]; then if [ -z "$BASHIO_LIB" ]; then
for f in /usr/local/lib/bashio-standalone.sh /.bashio-standalone.sh; do for f in /usr/local/lib/bashio-standalone.sh /.bashio-standalone.sh; do
if [ -f "$f" ]; then if [ -f "$f" ]; then
BASHIO_LIB="$f" BASHIO_LIB="$f"
break break
fi fi
done done
fi fi
############################## ##############################
@@ -268,56 +274,56 @@ fi
# skips the wait. When the Supervisor is already up -- the normal case -- this costs one request. # skips the wait. When the Supervisor is already up -- the normal case -- this costs one request.
wait_for_supervisor() { wait_for_supervisor() {
local max="${HA_SUPERVISOR_WAIT:-30}" local max="${HA_SUPERVISOR_WAIT:-30}"
local started deadline remaining attempt announced=0 local started deadline remaining attempt announced=0
# Nothing to wait for without a token. The standalone shim is excluded too: it answers these # Nothing to wait for without a token. The standalone shim is excluded too: it answers these
# calls from environment variables and never contacts the Supervisor, so it can never satisfy # calls from environment variables and never contacts the Supervisor, so it can never satisfy
# the probe and would burn the whole ceiling on every boot. # the probe and would burn the whole ceiling on every boot.
[ -n "${SUPERVISOR_TOKEN:-}" ] || return 0 [ -n "${SUPERVISOR_TOKEN:-}" ] || return 0
[ "${BASHIO_LIB_FULL:-false}" = "true" ] || return 0 [ "${BASHIO_LIB_FULL:-false}" = "true" ] || return 0
# bashio's own curl carries no --max-time, so each attempt is bounded from the outside. # bashio's own curl carries no --max-time, so each attempt is bounded from the outside.
command -v timeout >/dev/null 2>&1 || return 0 command -v timeout > /dev/null 2>&1 || return 0
# Digits only, then forced to base 10: `test -gt` accepts a zero-padded override like 08, but # Digits only, then forced to base 10: `test -gt` accepts a zero-padded override like 08, but
# arithmetic expansion reads it as octal and fails, which would leave the deadline empty and # arithmetic expansion reads it as octal and fails, which would leave the deadline empty and
# spin the loop below forever. # spin the loop below forever.
case "$max" in '' | *[!0-9]*) return 0 ;; esac case "$max" in '' | *[!0-9]*) return 0 ;; esac
max=$((10#$max)) max=$((10#$max))
[ "$max" -gt 0 ] || return 0 [ "$max" -gt 0 ] || return 0
started=$SECONDS started=$SECONDS
deadline=$((started + max)) deadline=$((started + max))
while :; do while :; do
remaining=$((deadline - SECONDS)) remaining=$((deadline - SECONDS))
if [ "$remaining" -le 0 ]; then if [ "$remaining" -le 0 ]; then
echo -e "\e[38;5;214m$(date) WARNING: Supervisor API did not report this add-on's network details within ${max}s, continuing anyway\e[0m" echo -e "\e[38;5;214m$(date) WARNING: Supervisor API did not report this add-on's network details within ${max}s, continuing anyway\e[0m"
return 0 return 0
fi fi
# No single attempt may outlive the ceiling it is bounded by. # No single attempt may outlive the ceiling it is bounded by.
attempt=5 attempt=5
[ "$remaining" -lt "$attempt" ] && attempt="$remaining" [ "$remaining" -lt "$attempt" ] && attempt="$remaining"
# One call is enough to settle all of them: bashio fetches the whole /addons/self/info object # One call is enough to settle all of them: bashio fetches the whole /addons/self/info object
# and caches it, so a populated ip_address means ingress_port and the rest are cached too. # and caches it, so a populated ip_address means ingress_port and the rest are cached too.
# Run in a child shell so bashio's globals and traps stay out of the entrypoint; its own error # Run in a child shell so bashio's globals and traps stay out of the entrypoint; its own error
# logging is dropped because a failed attempt here is expected, not news. # logging is dropped because a failed attempt here is expected, not news.
# shellcheck disable=SC2016 # shellcheck disable=SC2016
if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \ if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \
_ "$BASHIO_LIB" >/dev/null 2>&1; then _ "$BASHIO_LIB" > /dev/null 2>&1; then
[ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s" [ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s"
return 0 return 0
fi fi
if [ "$announced" -eq 0 ]; then if [ "$announced" -eq 0 ]; then
echo "Waiting for the Supervisor API to report this add-on's network details..." echo "Waiting for the Supervisor API to report this add-on's network details..."
announced=1 announced=1
fi fi
# Skipped when the attempt already consumed what was left, so the sleep cannot overshoot. # Skipped when the attempt already consumed what was left, so the sleep cannot overshoot.
[ "$((deadline - SECONDS))" -gt 0 ] && sleep 1 [ "$((deadline - SECONDS))" -gt 0 ] && sleep 1
done done
} }
wait_for_supervisor wait_for_supervisor
@@ -327,97 +333,97 @@ wait_for_supervisor
#################### ####################
run_one_script() { run_one_script() {
local script="$1" local script="$1"
echo "$script: executing" echo "$script: executing"
if [ "$(id -u)" -eq 0 ]; then if [ "$(id -u)" -eq 0 ]; then
chown "$(id -u)":"$(id -g)" "$script" || true chown "$(id -u)":"$(id -g)" "$script" || true
chmod a+x "$script" || true chmod a+x "$script" || true
else else
echo -e "\e[38;5;214m$(date) WARNING: Script executed with user $(id -u):$(id -g), things can break and chown won't work\e[0m" echo -e "\e[38;5;214m$(date) WARNING: Script executed with user $(id -u):$(id -g), things can break and chown won't work\e[0m"
sed -i "s/^[[:space:]]*chown /true # chown /g" "$script" sed -i "s/^[[:space:]]*chown /true # chown /g" "$script"
sed -i "s/^[[:space:]]*chmod /true # chmod /g" "$script" sed -i "s/^[[:space:]]*chmod /true # chmod /g" "$script"
fi
sed -i "1s|^.*|#!$shebang|" "$script"
chmod +x "$script"
if [ "${ha_entry_source:-null}" = "true" ]; then
sed -i -E 's/^[[:space:]]*exit ([0-9]+)/return \1 \|\| exit \1/g' "$script"
sed -i 's/bashio::exit\.nok/return 1/g' "$script"
sed -i 's/bashio::exit\.ok/return 0/g' "$script"
# shellcheck disable=SC1090
source "$script" || echo -e "\033[0;31mError\033[0m : $script exiting $?"
else
_run_rc=0
"$script" || _run_rc=$?
if [ "$_run_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
echo "Direct exec failed (rc=126, likely E2BIG), retrying via source in subshell..."
_run_rc=0
(
# shellcheck disable=SC1090
. "$BASHIO_LIB" 2>/dev/null || true
# shellcheck disable=SC1090
. "$script"
) || _run_rc=$?
if [ "$_run_rc" -ne 0 ]; then
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
fi
elif [ "$_run_rc" -ne 0 ]; then
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
fi fi
fi
sed -i '1a exit 0' "$script" sed -i "1s|^.*|#!$shebang|" "$script"
chmod +x "$script"
if [ "${ha_entry_source:-null}" = "true" ]; then
sed -i -E 's/^[[:space:]]*exit ([0-9]+)/return \1 \|\| exit \1/g' "$script"
sed -i 's/bashio::exit\.nok/return 1/g' "$script"
sed -i 's/bashio::exit\.ok/return 0/g' "$script"
# shellcheck disable=SC1090
source "$script" || echo -e "\033[0;31mError\033[0m : $script exiting $?"
else
_run_rc=0
"$script" || _run_rc=$?
if [ "$_run_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
echo "Direct exec failed (rc=126, likely E2BIG), retrying via source in subshell..."
_run_rc=0
(
# shellcheck disable=SC1090
. "$BASHIO_LIB" 2> /dev/null || true
# shellcheck disable=SC1090
. "$script"
) || _run_rc=$?
if [ "$_run_rc" -ne 0 ]; then
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
fi
elif [ "$_run_rc" -ne 0 ]; then
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
fi
fi
sed -i '1a exit 0' "$script"
} }
if [ -d /etc/cont-init.d ]; then if [ -d /etc/cont-init.d ]; then
for SCRIPTS in /etc/cont-init.d/*; do for SCRIPTS in /etc/cont-init.d/*; do
[ -e "$SCRIPTS" ] || continue [ -e "$SCRIPTS" ] || continue
run_one_script "$SCRIPTS" run_one_script "$SCRIPTS"
done done
fi fi
if $PID1; then if $PID1; then
shopt -s nullglob shopt -s nullglob
for runfile in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run; do for runfile in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run; do
[ -f "$runfile" ] || continue [ -f "$runfile" ] || continue
echo "Starting: $runfile" echo "Starting: $runfile"
sed -i "1s|^.*|#!$shebang|" "$runfile" sed -i "1s|^.*|#!$shebang|" "$runfile"
chmod +x "$runfile" chmod +x "$runfile"
( (
restart_count=0 restart_count=0
max_restarts=5 max_restarts=5
while true; do while true; do
_svc_rc=0 _svc_rc=0
"$runfile" || _svc_rc=$? "$runfile" || _svc_rc=$?
if [ "$_svc_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then if [ "$_svc_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
echo "Direct exec of $runfile failed (rc=126, likely E2BIG), retrying via source..." echo "Direct exec of $runfile failed (rc=126, likely E2BIG), retrying via source..."
_svc_rc=0 _svc_rc=0
( (
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "$BASHIO_LIB" 2>/dev/null || true . "$BASHIO_LIB" 2> /dev/null || true
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "$runfile" . "$runfile"
) || _svc_rc=$? ) || _svc_rc=$?
fi fi
rc=$_svc_rc rc=$_svc_rc
if [ "$rc" -eq 0 ]; then if [ "$rc" -eq 0 ]; then
echo "$runfile exited cleanly (exit 0), not restarting." echo "$runfile exited cleanly (exit 0), not restarting."
break break
fi fi
restart_count=$((restart_count + 1)) restart_count=$((restart_count + 1))
if [ "$restart_count" -ge "$max_restarts" ]; then if [ "$restart_count" -ge "$max_restarts" ]; then
echo -e "\033[0;31mERROR: $runfile has crashed $restart_count times (last exit code: $rc), giving up.\033[0m" echo -e "\033[0;31mERROR: $runfile has crashed $restart_count times (last exit code: $rc), giving up.\033[0m"
break break
fi fi
echo -e "\e[38;5;214m$(date) WARNING: $runfile exited (code $rc), restarting (#${restart_count}/${max_restarts}) in 5s...\e[0m" echo -e "\e[38;5;214m$(date) WARNING: $runfile exited (code $rc), restarting (#${restart_count}/${max_restarts}) in 5s...\e[0m"
sleep 5 sleep 5
done done
) & ) &
done done
shopt -u nullglob shopt -u nullglob
fi fi
###################### ######################
@@ -425,43 +431,43 @@ fi
###################### ######################
if $PID1; then if $PID1; then
echo " " echo " "
echo -e "\033[0;32mEverything started!\033[0m" echo -e "\033[0;32mEverything started!\033[0m"
terminate() { terminate() {
local local_pid local local_pid
echo "Termination signal received, forwarding to subprocesses..." echo "Termination signal received, forwarding to subprocesses..."
if command -v pgrep >/dev/null 2>&1; then if command -v pgrep > /dev/null 2>&1; then
while read -r pid; do while read -r pid; do
[ -n "$pid" ] || continue [ -n "$pid" ] || continue
echo "Terminating child PID $pid" echo "Terminating child PID $pid"
kill -TERM "$pid" 2>/dev/null || echo "Failed to terminate PID $pid" kill -TERM "$pid" 2> /dev/null || echo "Failed to terminate PID $pid"
done < <(pgrep -P "$$" || true) done < <(pgrep -P "$$" || true)
else else
for p in /proc/[0-9]*/; do for p in /proc/[0-9]*/; do
local_pid="${p#/proc/}" local_pid="${p#/proc/}"
local_pid="${local_pid%/}" local_pid="${local_pid%/}"
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2>/dev/null; then if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2> /dev/null; then
echo "Terminating child PID $local_pid" echo "Terminating child PID $local_pid"
kill -TERM "$local_pid" 2>/dev/null || echo "Failed to terminate PID $local_pid" kill -TERM "$local_pid" 2> /dev/null || echo "Failed to terminate PID $local_pid"
fi
done
fi fi
done wait || true
fi echo "All subprocesses terminated. Exiting."
wait || true exit 0
echo "All subprocesses terminated. Exiting." }
exit 0
}
trap terminate SIGTERM SIGINT trap terminate SIGTERM SIGINT
while :; do while :; do
sleep infinity & sleep infinity &
wait $! wait $!
done done
else else
echo " " echo " "
echo -e "\033[0;32mStarting the upstream container\033[0m" echo -e "\033[0;32mStarting the upstream container\033[0m"
echo " " echo " "
if [ -f /docker-mods ]; then if [ -f /docker-mods ]; then
exec /docker-mods exec /docker-mods
fi fi
fi fi

View File

@@ -147,7 +147,7 @@ manual actions with precedence. Prompts live in `.github/prompts/`, shared shell
in `.github/scripts/`. in `.github/scripts/`.
| Workflow | Model | Trigger | Role | | Workflow | Model | Trigger | Role |
|---|---|---|---| | --- | --- | --- | --- |
| `on_issues_ai_triage.yaml` | Sonnet-low | issue opened (+ author reply, daily catch-up) | Tier 1: classify, dedupe, answer, ask for info; label `ai-triage` for real add-on bugs | | `on_issues_ai_triage.yaml` | Sonnet-low | issue opened (+ author reply, daily catch-up) | Tier 1: classify, dedupe, answer, ask for info; label `ai-triage` for real add-on bugs |
| `daily_ai_fix.yaml` | Opus 5-xhigh | daily 03:00 | Tier 2: diagnose the `ai-triage` batch; small+confident → ready PR (`ai:fixed`); else write a plan (`ai:plan-pending`) | | `daily_ai_fix.yaml` | Opus 5-xhigh | daily 03:00 | Tier 2: diagnose the `ai-triage` batch; small+confident → ready PR (`ai:fixed`); else write a plan (`ai:plan-pending`) |
| `on_issue_approved.yaml` | Opus 5-high | maintainer adds `ai:approved` | Tier 3: execute the approved plan → ready PR | | `on_issue_approved.yaml` | Opus 5-high | maintainer adds `ai:approved` | Tier 3: execute the approved plan → ready PR |
@@ -176,7 +176,7 @@ prompt-only, not machine-enforced.
## Linting Rules ## Linting Rules
| Tool | Config | Key ignores | | Tool | Config | Key ignores |
|------|--------|------------| | ------ | -------- | ------------ |
| Hadolint | `.hadolint.yaml` | DL3002, DL3006-9, DL3018 (no pinning required) | | Hadolint | `.hadolint.yaml` | DL3002, DL3006-9, DL3018 (no pinning required) |
| ShellCheck | `.shellcheckrc` | SC2002 | | ShellCheck | `.shellcheckrc` | SC2002 |
| Markdownlint | `.markdownlint.yaml` | MD013 (line length), MD025, MD033, MD041 | | Markdownlint | `.markdownlint.yaml` | MD013 (line length), MD025, MD033, MD041 |

View File

@@ -75,7 +75,7 @@ You can add the following tags in the file :
### Addon version numbering ### Addon version numbering
The `version` written in the addon `config.yaml` is the one Home Assistant compares to decide whether an update is available. Home Assistant hides the update when it can order both versions and the new one is not strictly newer (`1.2.3` -> `1.2.3-2` is a semver pre-release, so it is *older*), and it cannot order tags such as `version-bf9e0b4f` or `ubuntu-2026-06-01` at all. The `version` written in the addon `config.yaml` is the one Home Assistant compares to decide whether an update is available. Home Assistant hides the update when it can order both versions and the new one is not strictly newer (`1.2.3` -> `1.2.3-2` is a semver pre-release, so it is _older_), and it cannot order tags such as `version-bf9e0b4f` or `ubuntu-2026-06-01` at all.
The addon version is therefore derived from the upstream tag: The addon version is therefore derived from the upstream tag:

View File

@@ -1,31 +1,31 @@
## 2.8.0 (2026-09-05) ## 2.8.0 (2026-09-05)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases) - Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.7.0 (2026-08-29) ## 2.7.0 (2026-08-29)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases) - Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.5.1 (2026-08-22) ## 2.5.1 (2026-08-22)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases) - Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.4.0 (2026-08-13) ## 2.4.0 (2026-08-13)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases) - Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.1.0 (2026-08-08) ## 2.1.0 (2026-08-08)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases) - Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.0.3 (2026-08-01) ## 2.0.3 (2026-08-01)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases) - Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.0.2 (2026-08-01) ## 2.0.2 (2026-08-01)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases) - Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 2.0.0 (2026-07-22) ## 2.0.0 (2026-07-22)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases) - Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 1.76.52 (2026-07-11) ## 1.76.52 (2026-07-11)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases) - Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 1.76.51 (2026-06-17) ## 1.76.51 (2026-06-17)
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases) - Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
## 1.76.49 (2026-06-05) ## 1.76.49 (2026-06-05)

View File

@@ -6,7 +6,7 @@ This addon is based on the docker image <https://github.com/lklynet/aurral>
## Configuration ## Configuration
| Option | Default | Description | | Option | Default | Description |
|---|---|---| | --- | --- | --- |
| `download_folder` | `/share/aurral/downloads` | Path where Aurral writes flow downloads. Must be under `/share`. | | `download_folder` | `/share/aurral/downloads` | Path where Aurral writes flow downloads. Must be under `/share`. |
| `weekly_flow_folder` | `weekly-flow` | Subfolder name appended to `download_folder` for weekly flow files. The full path will be `download_folder/weekly_flow_folder`. | | `weekly_flow_folder` | `weekly-flow` | Subfolder name appended to `download_folder` for weekly flow files. The full path will be `download_folder/weekly_flow_folder`. |

View File

@@ -1,16 +1,16 @@
## 1.85.0 (2026-08-29) ## 1.85.0 (2026-08-29)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases) - Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.84.0 (2026-08-15) ## 1.84.0 (2026-08-15)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases) - Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.83.0 (2026-08-01) ## 1.83.0 (2026-08-01)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases) - Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.82.1 (2026-07-16) ## 1.82.1 (2026-07-16)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases) - Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.81.0 (2026-07-04) ## 1.81.0 (2026-07-04)
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases) - Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
## 1.80.0 (2026-06-05) ## 1.80.0 (2026-06-05)

View File

@@ -67,7 +67,7 @@ Default credentials: `admin` / `password` (change after first login).
### Options ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| | -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions | | `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions | | `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) | | `TZ` | str | | Timezone (e.g., `Europe/London`) |

View File

@@ -44,7 +44,7 @@ Configurations can be done through the app webUI, except for the following optio
### Options ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| | -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions | | `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions | | `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) | | `TZ` | str | | Timezone (e.g., `Europe/London`) |

0
bazarr/rootfs/etc/services.d/nginx/run Normal file → Executable file
View File

View File

@@ -17,7 +17,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Organize & Edit ### Organize & Edit
| Tool | Tool | Tool | | Tool | Tool | Tool |
|------|------|------| | ------ | ------ | ------ |
| Merge PDF | Split PDF | Organize PDF | | Merge PDF | Split PDF | Organize PDF |
| Delete Pages | Extract Pages | Reverse Pages | | Delete Pages | Extract Pages | Reverse Pages |
| Rotate PDF | Rotate Custom | Crop PDF | | Rotate PDF | Rotate Custom | Crop PDF |
@@ -28,7 +28,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Convert TO PDF ### Convert TO PDF
| Tool | Tool | Tool | | Tool | Tool | Tool |
|------|------|------| | ------ | ------ | ------ |
| Word to PDF | Excel to PDF | PowerPoint to PDF | | Word to PDF | Excel to PDF | PowerPoint to PDF |
| Image to PDF | JPG to PDF | PNG to PDF | | Image to PDF | JPG to PDF | PNG to PDF |
| BMP to PDF | TIFF to PDF | WEBP to PDF | | BMP to PDF | TIFF to PDF | WEBP to PDF |
@@ -44,7 +44,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Convert FROM PDF ### Convert FROM PDF
| Tool | Tool | Tool | | Tool | Tool | Tool |
|------|------|------| | ------ | ------ | ------ |
| PDF to DOCX | PDF to Excel | PDF to JPG | | PDF to DOCX | PDF to Excel | PDF to JPG |
| PDF to PNG | PDF to BMP | PDF to TIFF | | PDF to PNG | PDF to BMP | PDF to TIFF |
| PDF to WEBP | PDF to SVG | PDF to Text | | PDF to WEBP | PDF to SVG | PDF to Text |
@@ -54,7 +54,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Security & Metadata ### Security & Metadata
| Tool | Tool | Tool | | Tool | Tool | Tool |
|------|------|------| | ------ | ------ | ------ |
| Encrypt PDF | Decrypt PDF | Change Permissions | | Encrypt PDF | Decrypt PDF | Change Permissions |
| Remove Restrictions | Sign PDF | Digital Sign PDF | | Remove Restrictions | Sign PDF | Digital Sign PDF |
| Validate Signature | Edit Metadata | View Metadata | | Validate Signature | Edit Metadata | View Metadata |
@@ -64,7 +64,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Enhance & Process ### Enhance & Process
| Tool | Tool | Tool | | Tool | Tool | Tool |
|------|------|------| | ------ | ------ | ------ |
| Compress PDF | OCR PDF | Deskew PDF | | Compress PDF | OCR PDF | Deskew PDF |
| Rasterize PDF | Linearize PDF | PDF to PDF/A | | Rasterize PDF | Linearize PDF | PDF to PDF/A |
| Adjust Colors | Invert Colors | Text Color | | Adjust Colors | Invert Colors | Text Color |
@@ -76,7 +76,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
### Forms & More ### Forms & More
| Tool | Tool | Tool | | Tool | Tool | Tool |
|------|------|------| | ------ | ------ | ------ |
| Form Creator | Form Filler | Table of Contents | | Form Creator | Form Filler | Table of Contents |
| Bookmark | PDF Editor | Extract Images | | Bookmark | PDF Editor | Extract Images |
| Extract Tables | Extract Attachments | Edit Attachments | | Extract Tables | Extract Attachments | Edit Attachments |

0
bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/run Normal file → Executable file
View File

View File

@@ -1,21 +1,3 @@
## 20260909.5 (09-09-2026)
- Minor bugs fixed
## 20260909.4 (09-09-2026)
- Minor bugs fixed
## 20260909.3 (09-09-2026)
- Minor bugs fixed
## 20260909.2 (09-09-2026)
- Minor bugs fixed
## 20260909 (09-09-2026)
- Minor bugs fixed
## 20260908.2 (08-09-2026)
- Minor bugs fixed
## 20260908.1 (08-09-2026)
- Synced with upstream birdnet-go (4 commits); re-merges the open fork PRs, adding fork PR #62 (reanalyze a clip with every loaded model + one-click correction)
## 20260908 (08-09-2026)
- Synced with upstream birdnet-go (7 commits); re-merges the open fork PRs
## 20260907 (07-09-2026)
- Rebuild: re-merges the open fork PRs, picking up the updated fork PR #57
## 20260901.4 (01-09-2026) ## 20260901.4 (01-09-2026)
- Minor bugs fixed - Minor bugs fixed
## 20260901.3 (01-09-2026) ## 20260901.3 (01-09-2026)

View File

@@ -127,5 +127,5 @@ slug: birdnet-go-dev
udev: true udev: true
url: https://github.com/alexbelgium/hassio-addons url: https://github.com/alexbelgium/hassio-addons
usb: true usb: true
version: "20260909.5" version: "20260901.4"
video: true video: true

View File

@@ -11,15 +11,6 @@
# stamping) is written to the directory given as $1 so the Docker build can # stamping) is written to the directory given as $1 so the Docker build can
# compile it. # compile it.
# #
# With --check the script does not build anything: it performs the exact same
# merge sequence, skips (instead of failing on) every conflicting PR, and prints
# one "!!! CONFLICT pr=#N conflicts-with=... files=... " line per offender before
# exiting 2. Use it to find conflicts *before* a build burns on them. This is a
# different question from GitHub's `mergeable` field, which compares a PR against
# its own base ref - for a stacked PR that base is another feature branch (often
# stale, sometimes belonging to a closed PR), so GitHub can report CLEAN for a PR
# that does not merge onto main at all.
#
# Environment: # Environment:
# BIRDNET_FORK owner/repo of the fork (default alexbelgium/birdnet-go) # BIRDNET_FORK owner/repo of the fork (default alexbelgium/birdnet-go)
# BIRDNET_UPSTREAM owner/repo of the upstream (default tphakala/birdnet-go) # BIRDNET_UPSTREAM owner/repo of the upstream (default tphakala/birdnet-go)
@@ -28,26 +19,7 @@
# #
set -euo pipefail set -euo pipefail
CHECK_ONLY="${MERGE_PRS_CHECK:-0}" TARGET_DIR="${1:?usage: merge-prs.sh <target-dir>}"
TARGET_DIR=""
while [ "$#" -gt 0 ]; do
case "$1" in
--check) CHECK_ONLY=1 ;;
-*) echo "unknown option: $1" >&2; exit 64 ;;
*)
# Last-one-wins would silently clone into the wrong directory if a caller ever
# appended an argument; the pre-flag script used "${1}", so refuse rather than
# quietly change which operand counts.
if [ -n "${TARGET_DIR}" ]; then
echo "usage: merge-prs.sh [--check] <target-dir>" >&2
exit 64
fi
TARGET_DIR="$1"
;;
esac
shift
done
: "${TARGET_DIR:?usage: merge-prs.sh [--check] <target-dir>}"
FORK="${BIRDNET_FORK:-alexbelgium/birdnet-go}" FORK="${BIRDNET_FORK:-alexbelgium/birdnet-go}"
UPSTREAM="${BIRDNET_UPSTREAM:-tphakala/birdnet-go}" UPSTREAM="${BIRDNET_UPSTREAM:-tphakala/birdnet-go}"
@@ -61,52 +33,6 @@ GH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
log() { echo ">>> $*"; } log() { echo ">>> $*"; }
# Conflicting PRs collected in --check mode: "number|scope|files|title".
conflicting=()
LOCKFILE="frontend/package-lock.json"
# The single place that decides whether a conflicted merge is still acceptable.
# package-lock.json is generated content and stacked PRs can carry an older copy even when
# their source changes merge cleanly, so keep the lockfile already assembled on the base side
# — but only when it is the sole conflict. Any source conflict stays fatal.
# Returns 0 when it resolved and committed such a merge, 1 when the conflict is real.
# BOTH the real merge and the --check probe must go through here: when only the real merge
# applied the policy, the probe called a PR "conflicts-with=main" that the build would have
# merged fine, and printed the opposite remediation to the true one.
resolve_sole_lockfile() {
local dir="$1"
local -a conflicted
mapfile -t conflicted < <(git -C "${dir}" diff --name-only --diff-filter=U)
if [ "${#conflicted[@]}" -ne 1 ] || [ "${conflicted[0]}" != "${LOCKFILE}" ]; then
return 1
fi
log "Resolving generated ${LOCKFILE} conflict using the base tree"
git -C "${dir}" checkout --ours -- "${LOCKFILE}" || return 1
git -C "${dir}" add "${LOCKFILE}" || return 1
git -C "${dir}" commit --no-edit > /dev/null || return 1
}
# Does ${1} merge cleanly onto the pristine upstream-synced main? Probed in a
# throwaway worktree so the accumulated tree is left untouched. Tells apart a PR
# that is simply stale against main (fixable inside that PR's own branch) from
# one that only clashes with another open PR (needs a cross-PR decision).
merges_onto_main() {
local sha="$1" tmpdir probe rc=0
tmpdir="$(mktemp -d)"
probe="${tmpdir}/probe"
git worktree add --quiet --detach "${probe}" "${MAIN_SYNCED}"
if ! git -C "${probe}" merge --no-edit --no-ff -m probe "${sha}" > /dev/null 2>&1; then
# Same policy as the real merge, or this misclassifies a lockfile-only clash.
resolve_sole_lockfile "${probe}" > /dev/null 2>&1 || rc=1
fi
git worktree remove --force "${probe}" > /dev/null 2>&1 || true
# worktree remove only takes the child back; without this the mktemp parent is left behind
# on every checked conflict.
rmdir "${tmpdir}" > /dev/null 2>&1 || true
return "${rc}"
}
git config --global user.email "addon-builder@users.noreply.github.com" git config --global user.email "addon-builder@users.noreply.github.com"
git config --global user.name "BirdNET-Go Addon Builder" git config --global user.name "BirdNET-Go Addon Builder"
git config --global advice.detachedHead false git config --global advice.detachedHead false
@@ -121,7 +47,6 @@ git remote add upstream "${UPSTREAM_URL}"
git fetch --no-tags upstream main git fetch --no-tags upstream main
# --no-ff keeps an explicit sync commit; a no-op when main is already current. # --no-ff keeps an explicit sync commit; a no-op when main is already current.
git merge --no-edit --no-ff upstream/main git merge --no-edit --no-ff upstream/main
MAIN_SYNCED="$(git rev-parse HEAD)"
log "Querying open non-draft PRs from ${FORK}" log "Querying open non-draft PRs from ${FORK}"
auth_header=() auth_header=()
@@ -154,43 +79,27 @@ for entry in "${prs[@]}"; do
if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then
mapfile -t conflicted_files < <(git diff --name-only --diff-filter=U) mapfile -t conflicted_files < <(git diff --name-only --diff-filter=U)
if resolve_sole_lockfile .; then # package-lock.json is generated content and stacked PRs can carry an
: # generated lockfile only - the merge is committed and the build continues # older copy even when their source changes merge cleanly. Keep the
# lockfile already assembled from upstream and earlier PRs, but only
# when it is the sole conflict. Any source conflict remains fatal.
if [ "${#conflicted_files[@]}" -eq 1 ] \
&& [ "${conflicted_files[0]}" = "frontend/package-lock.json" ]; then
log "Resolving generated frontend/package-lock.json conflict using the accumulated tree"
git checkout --ours -- frontend/package-lock.json
git add frontend/package-lock.json
git commit --no-edit
else else
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2 echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
if [ "${#conflicted_files[@]}" -gt 0 ]; then if [ "${#conflicted_files[@]}" -gt 0 ]; then
printf '!!! Conflicting file: %s\n' "${conflicted_files[@]}" >&2 printf '!!! Conflicting file: %s\n' "${conflicted_files[@]}" >&2
fi fi
git merge --abort || true
if [ "${CHECK_ONLY}" = "1" ]; then
scope="accumulated"
merges_onto_main "${sha}" || scope="main"
conflicting+=("${number}|${scope}|${conflicted_files[*]:-}|${title}")
log "check mode: skipping PR #${number}, continuing with the rest"
continue
fi
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2 echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
git merge --abort || true
exit 1 exit 1
fi fi
fi fi
done done
if [ "${CHECK_ONLY}" = "1" ]; then
if [ "${#conflicting[@]}" -eq 0 ]; then
log "CHECK OK: every open non-draft PR merges into the combined build tree"
exit 0
fi
echo "!!! CHECK FAILED: ${#conflicting[@]} PR(s) would break the add-on build" >&2
for entry in "${conflicting[@]}"; do
IFS='|' read -r number scope files title <<<"${entry}"
echo "!!! CONFLICT pr=#${number} conflicts-with=${scope} files=${files} title=${title}" >&2
done
echo "!!! conflicts-with=main -> the PR is stale against main; merge main into its branch and resolve there." >&2
echo "!!! conflicts-with=accumulated -> the PR only clashes with another open PR; decide which one owns the hunk." >&2
exit 2
fi
log "Merged HEAD: $(git rev-parse --short HEAD)" log "Merged HEAD: $(git rev-parse --short HEAD)"
log "Source tree ready at ${TARGET_DIR}" log "Source tree ready at ${TARGET_DIR}"

View File

@@ -43,8 +43,8 @@ if [ ! -f "$CONFIG_LOCATION" ]; then
# config. (We can't remove the file and continue — subsequent yq calls # config. (We can't remove the file and continue — subsequent yq calls
# under set -e would abort the init script.) # under set -e would abort the init script.)
if ! curl -fL -s -S \ if ! curl -fL -s -S \
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \ https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
-o "$CONFIG_LOCATION"; then -o "$CONFIG_LOCATION"; then
bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it" bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it"
echo '{}' > "$CONFIG_LOCATION" echo '{}' > "$CONFIG_LOCATION"
fi fi
@@ -190,5 +190,5 @@ LOG_DIR="/config/logs"
if [ -d "$LOG_DIR" ]; then if [ -d "$LOG_DIR" ]; then
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}" bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
ln -sf "$LOG_DIR" /logs ln -sf "$LOG_DIR" /logs
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2>/dev/null || true find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2> /dev/null || true
fi fi

View File

@@ -44,8 +44,8 @@ if ! bashio::config.true 'mariadb_auto_config'; then
# Only revert if config.yaml points at the HA MariaDB host we would have # Only revert if config.yaml points at the HA MariaDB host we would have
# written — a mysql block pointing at a different host was set manually. # written — a mysql block pointing at a different host was set manually.
# shellcheck disable=SC2016 # shellcheck disable=SC2016
CURRENT_MYSQL_HOST="$(yq -r '.output.mysql.host // empty' "$CONFIG_LOCATION" 2>/dev/null || true)" CURRENT_MYSQL_HOST="$(yq -r '.output.mysql.host // empty' "$CONFIG_LOCATION" 2> /dev/null || true)"
if yq -e '.output.mysql.enabled == true' "$CONFIG_LOCATION" >/dev/null 2>&1 \ if yq -e '.output.mysql.enabled == true' "$CONFIG_LOCATION" > /dev/null 2>&1 \
&& [ "${CURRENT_MYSQL_HOST}" = "${MYSQL_HOST}" ]; then && [ "${CURRENT_MYSQL_HOST}" = "${MYSQL_HOST}" ]; then
yq -i -y \ yq -i -y \
'.output.mysql.enabled = false '.output.mysql.enabled = false
@@ -71,7 +71,7 @@ bashio::log.green "---"
# Resolve MariaDB hostname to IPv4: on HAOS >=17.3 the Supervisor network # Resolve MariaDB hostname to IPv4: on HAOS >=17.3 the Supervisor network
# gained IPv6, but the MariaDB addon only grants its user from the IPv4 # gained IPv6, but the MariaDB addon only grants its user from the IPv4
# subnet. Fall back to the raw hostname if resolution fails. # subnet. Fall back to the raw hostname if resolution fails.
MYSQL_HOST_RESOLVED="$(getent ahostsv4 "${MYSQL_HOST}" 2>/dev/null | awk '{print $1; exit}')" MYSQL_HOST_RESOLVED="$(getent ahostsv4 "${MYSQL_HOST}" 2> /dev/null | awk '{print $1; exit}')"
MYSQL_HOST_RESOLVED="${MYSQL_HOST_RESOLVED:-${MYSQL_HOST}}" MYSQL_HOST_RESOLVED="${MYSQL_HOST_RESOLVED:-${MYSQL_HOST}}"
if [ "${MYSQL_HOST_RESOLVED}" != "${MYSQL_HOST}" ]; then if [ "${MYSQL_HOST_RESOLVED}" != "${MYSQL_HOST}" ]; then
bashio::log.blue "Resolved ${MYSQL_HOST} -> ${MYSQL_HOST_RESOLVED} (forcing IPv4)" bashio::log.blue "Resolved ${MYSQL_HOST} -> ${MYSQL_HOST_RESOLVED} (forcing IPv4)"

View File

@@ -1,4 +1,4 @@
## 20260827 (2026-08-29) ## 20260827 (2026-08-29)
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases) - Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
- Upstream tag : 20260823 - Upstream tag : 20260823
@@ -6,13 +6,13 @@
- Minor bugs fixed - Minor bugs fixed
## 20260716.1 (26-08-2026) ## 20260716.1 (26-08-2026)
- Minor bugs fixed - Minor bugs fixed
## 20260716.1 (2026-08-26) ## 20260716.1 (2026-08-26)
- Rebuild after syncing the alexbelgium/birdnet-go fork with upstream tphakala/birdnet-go and resolving conflicts on all open pull requests - Rebuild after syncing the alexbelgium/birdnet-go fork with upstream tphakala/birdnet-go and resolving conflicts on all open pull requests
## 20260716 (2026-07-16) ## 20260716 (2026-07-16)
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases) - Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
## 20260712 (2026-07-13) ## 20260712 (2026-07-13)
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases) - Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
## nightly-20260615-4 (09-07-2026) ## nightly-20260615-4 (09-07-2026)
@@ -22,8 +22,8 @@
## nightly-20260615-2 (05-07-2026) ## nightly-20260615-2 (05-07-2026)
- Minor bugs fixed - Minor bugs fixed
- Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774) - Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774)
- MQTT auto-config now also enables BirdNET-Go's native Home Assistant MQTT auto-discovery: detection sensors appear in Home Assistant automatically with no manual YAML (existing UI/config.yaml edits are preserved) - MQTT auto-config now also enables BirdNET-Go's native Home Assistant MQTT auto-discovery: detection sensors appear in Home Assistant automatically with no manual YAML (existing UI/config.yaml edits are preserved)
- MQTT auto-config seeds `realtime.mqtt.retain: true` (only when unset) so sensor states survive Home Assistant restarts - MQTT auto-config seeds `realtime.mqtt.retain: true` (only when unset) so sensor states survive Home Assistant restarts
- Added supervisor watchdog (tcp://[HOST]:[PORT:8080]) so the add-on is automatically restarted if BirdNET-Go stops responding - Added supervisor watchdog (tcp://[HOST]:[PORT:8080]) so the add-on is automatically restarted if BirdNET-Go stops responding
- Added backup_exclude for rotated logs, making Home Assistant backups smaller (SQLite journals are kept so hot backups stay consistent) - Added backup_exclude for rotated logs, making Home Assistant backups smaller (SQLite journals are kept so hot backups stay consistent)

View File

@@ -43,8 +43,8 @@ if [ ! -f "$CONFIG_LOCATION" ]; then
# config. (We can't remove the file and continue — subsequent yq calls # config. (We can't remove the file and continue — subsequent yq calls
# under set -e would abort the init script.) # under set -e would abort the init script.)
if ! curl -fL -s -S \ if ! curl -fL -s -S \
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \ https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
-o "$CONFIG_LOCATION"; then -o "$CONFIG_LOCATION"; then
bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it" bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it"
echo '{}' > "$CONFIG_LOCATION" echo '{}' > "$CONFIG_LOCATION"
fi fi
@@ -186,5 +186,5 @@ LOG_DIR="/config/logs"
if [ -d "$LOG_DIR" ]; then if [ -d "$LOG_DIR" ]; then
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}" bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
ln -sf "$LOG_DIR" /logs ln -sf "$LOG_DIR" /logs
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2>/dev/null || true find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2> /dev/null || true
fi fi

View File

@@ -9,9 +9,9 @@ Here are some example tests I did (the whole threads are really interesting also
**My recommendation:** **My recommendation:**
- **Best entry system (< 50€):** Boya By-lm40 (30€) + deadcat (10€) - **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
- **Best middle end system (< 150€):** Clippy EM272 TRS/TRRS (55€) + Rode AI micro trs/trrs to usb (70€) + Rycote deadcat (27€) - **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
- **Best high end system (<400€):** Clippy EM272 XLR (85€) or LOM Ucho Pro (75€) + Focusrite Scarlet 2i2 4th Gen (200€) + Bubblebee Pro Extreme deadcat (45€) - **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
**Sources for high end microphones in Europe:** **Sources for high end microphones in Europe:**

View File

@@ -112,6 +112,7 @@ def automatic_mqtt_publish(file, detection, path):
mqttc.publish(mqtt_topic, json_bird, 1) mqttc.publish(mqtt_topic, json_bird, 1)
log.info("Posted to MQTT: ok") log.info("Posted to MQTT: ok")
# Create MQTT client using legacy callback API when available for # Create MQTT client using legacy callback API when available for
# compatibility with paho-mqtt >= 2.0 # compatibility with paho-mqtt >= 2.0
callback_api = getattr(mqtt, "CallbackAPIVersion", None) callback_api = getattr(mqtt, "CallbackAPIVersion", None)

View File

@@ -9,9 +9,9 @@ Here are some example tests I did (the whole threads are really interesting also
**My recommendation:** **My recommendation:**
- **Best entry system (< 50€):** Boya By-lm40 (30€) + deadcat (10€) - **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
- **Best middle end system (< 150€):** Clippy EM272 TRS/TRRS (55€) + Rode AI micro trs/trrs to usb (70€) + Rycote deadcat (27€) - **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
- **Best high end system (<400€):** Clippy EM272 XLR (85€) or LOM Ucho Pro (75€) + Focusrite Scarlet 2i2 4th Gen (200€) + Bubblebee Pro Extreme deadcat (45€) - **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
**Sources for high end microphones in Europe:** **Sources for high end microphones in Europe:**

View File

@@ -112,6 +112,7 @@ def automatic_mqtt_publish(file, detection, path):
mqttc.publish(mqtt_topic, json_bird, 1) mqttc.publish(mqtt_topic, json_bird, 1)
log.info("Posted to MQTT: ok") log.info("Posted to MQTT: ok")
# Create MQTT client using legacy callback API when available for # Create MQTT client using legacy callback API when available for
# compatibility with paho-mqtt >= 2.0 # compatibility with paho-mqtt >= 2.0
callback_api = getattr(mqtt, "CallbackAPIVersion", None) callback_api = getattr(mqtt, "CallbackAPIVersion", None)

View File

@@ -1,19 +1,19 @@
## 0.8.8 (2026-08-22) ## 0.8.8 (2026-08-22)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases) - Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.6 (2026-08-13) ## 0.8.6 (2026-08-13)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases) - Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.5 (2026-08-01) ## 0.8.5 (2026-08-01)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases) - Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.4 (2026-07-13) ## 0.8.4 (2026-07-13)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases) - Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.3 (2026-07-11) ## 0.8.3 (2026-07-11)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases) - Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.8.2.1 (2026-07-05) ## 0.8.2.1 (2026-07-05)
- Re-tag of 0.8.2-1 with no content change. Home Assistant compares add-on versions with semver semantics, where a `-N` suffix counts as a *pre-release* and sorts **below** the base version — so users already on 0.8.2 saw the 0.8.2-1 nginx fix as "Up-to-date" with the Update button disabled. Four-segment `0.8.2.1` sorts above both `0.8.2` and `0.8.2-1` (and below the next upstream `0.8.3`), so the update becomes installable everywhere. - Re-tag of 0.8.2-1 with no content change. Home Assistant compares add-on versions with semver semantics, where a `-N` suffix counts as a *pre-release* and sorts **below** the base version — so users already on 0.8.2 saw the 0.8.2-1 nginx fix as "Up-to-date" with the Update button disabled. Four-segment `0.8.2.1` sorts above both `0.8.2` and `0.8.2-1` (and below the next upstream `0.8.3`), so the update becomes installable everywhere.
@@ -23,7 +23,7 @@
## 0.8.2 (2026-07-04) ## 0.8.2 (2026-07-04)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases) - Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.7.5 (2026-06-23) ## 0.7.5 (2026-06-23)
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases) - Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
## 0.7.4 (2026-06-01) ## 0.7.4 (2026-06-01)

View File

@@ -73,7 +73,7 @@ map:
name: BirdNET-PiPy name: BirdNET-PiPy
options: options:
env_vars: [] env_vars: []
ICECAST_PASSWORD: '' ICECAST_PASSWORD: ""
data_location: /config/data data_location: /config/data
panel_icon: mdi:bird panel_icon: mdi:bird
ports: ports:

View File

@@ -7,11 +7,11 @@ DATA_LOCATION="$(bashio::config 'data_location' || true)"
DATA_LOCATION="${DATA_LOCATION:-$DEFAULT_LOCATION}" DATA_LOCATION="${DATA_LOCATION:-$DEFAULT_LOCATION}"
case "${DATA_LOCATION}" in case "${DATA_LOCATION}" in
/config/*|/share/*|/data/*) ;; /config/* | /share/* | /data/*) ;;
*) *)
bashio::log.warning "Invalid data_location '${DATA_LOCATION}', falling back to ${DEFAULT_LOCATION}" bashio::log.warning "Invalid data_location '${DATA_LOCATION}', falling back to ${DEFAULT_LOCATION}"
DATA_LOCATION="${DEFAULT_LOCATION}" DATA_LOCATION="${DEFAULT_LOCATION}"
;; ;;
esac esac
LEGACY1="/config/birdnet-pipy/data" LEGACY1="/config/birdnet-pipy/data"
@@ -19,14 +19,14 @@ LEGACY2="/data"
mkdir -p "${DATA_LOCATION}" mkdir -p "${DATA_LOCATION}"
if [ -z "$(ls -A "${DATA_LOCATION}" 2>/dev/null || true)" ]; then if [ -z "$(ls -A "${DATA_LOCATION}" 2> /dev/null || true)" ]; then
if [ -d "${LEGACY1}" ] && [ -n "$(ls -A "${LEGACY1}" 2>/dev/null || true)" ]; then if [ -d "${LEGACY1}" ] && [ -n "$(ls -A "${LEGACY1}" 2> /dev/null || true)" ]; then
bashio::log.notice "Migrating legacy data from ${LEGACY1} to ${DATA_LOCATION}" bashio::log.notice "Migrating legacy data from ${LEGACY1} to ${DATA_LOCATION}"
cp -a "${LEGACY1}/." "${DATA_LOCATION}/" || true cp -a "${LEGACY1}/." "${DATA_LOCATION}/" || true
elif [ -d "${LEGACY2}" ] && [ "${LEGACY2}" != "${DATA_LOCATION}" ] && [ -n "$(ls -A "${LEGACY2}" 2>/dev/null || true)" ]; then elif [ -d "${LEGACY2}" ] && [ "${LEGACY2}" != "${DATA_LOCATION}" ] && [ -n "$(ls -A "${LEGACY2}" 2> /dev/null || true)" ]; then
bashio::log.notice "Migrating legacy data from ${LEGACY2} to ${DATA_LOCATION}" bashio::log.notice "Migrating legacy data from ${LEGACY2} to ${DATA_LOCATION}"
cp -a "${LEGACY2}/." "${DATA_LOCATION}/" || true cp -a "${LEGACY2}/." "${DATA_LOCATION}/" || true
fi fi
fi fi
mkdir -p "${DATA_LOCATION}/config" "${DATA_LOCATION}/clips" "${DATA_LOCATION}/logs" "${DATA_LOCATION}/cache" || true mkdir -p "${DATA_LOCATION}/config" "${DATA_LOCATION}/clips" "${DATA_LOCATION}/logs" "${DATA_LOCATION}/cache" || true

View File

@@ -7,10 +7,10 @@ CFG_DIR="${DATA_DIR}/config"
SETTINGS="${CFG_DIR}/user_settings.json" SETTINGS="${CFG_DIR}/user_settings.json"
if [ -f "$SETTINGS" ]; then if [ -f "$SETTINGS" ]; then
if ! grep -q 'detection' "$SETTINGS"; then if ! grep -q 'detection' "$SETTINGS"; then
bak="${SETTINGS}.bak" bak="${SETTINGS}.bak"
[ -e "$bak" ] && bak="${SETTINGS}.bak.$(date -u +%Y%m%dT%H%M%SZ)" [ -e "$bak" ] && bak="${SETTINGS}.bak.$(date -u +%Y%m%dT%H%M%SZ)"
mv -f -- "$SETTINGS" "$bak" mv -f -- "$SETTINGS" "$bak"
echo "WARNING: Erroneous file detected: '$SETTINGS' did not contain 'detection' and was renamed to '$bak'." >&2 echo "WARNING: Erroneous file detected: '$SETTINGS' did not contain 'detection' and was renamed to '$bak'." >&2
fi fi
fi fi

View File

@@ -3,21 +3,21 @@
set -euo pipefail set -euo pipefail
SND_GID="" SND_GID=""
if [ -e /dev/snd ] && command -v stat >/dev/null 2>&1; then if [ -e /dev/snd ] && command -v stat > /dev/null 2>&1; then
SND_GID="$(stat -c '%g' /dev/snd 2>/dev/null || true)" SND_GID="$(stat -c '%g' /dev/snd 2> /dev/null || true)"
fi fi
if [ -n "${SND_GID}" ] && getent group audio >/dev/null 2>&1; then if [ -n "${SND_GID}" ] && getent group audio > /dev/null 2>&1; then
current_gid="$(getent group audio | cut -d: -f3 || true)" current_gid="$(getent group audio | cut -d: -f3 || true)"
if [ -n "${current_gid}" ] && [ "${current_gid}" != "${SND_GID}" ]; then if [ -n "${current_gid}" ] && [ "${current_gid}" != "${SND_GID}" ]; then
groupmod -g "${SND_GID}" audio 2>/dev/null || true groupmod -g "${SND_GID}" audio 2> /dev/null || true
fi fi
fi fi
for u in root nginx www-data icecast2; do for u in root nginx www-data icecast2; do
if id "${u}" >/dev/null 2>&1; then if id "${u}" > /dev/null 2>&1; then
usermod -aG audio "${u}" 2>/dev/null || true usermod -aG audio "${u}" 2> /dev/null || true
fi fi
done done
# Create /run/pulse/native symlink if the PulseAudio socket is elsewhere # Create /run/pulse/native symlink if the PulseAudio socket is elsewhere
@@ -25,21 +25,21 @@ done
PULSE_SOCK="${PULSE_SERVER:-}" PULSE_SOCK="${PULSE_SERVER:-}"
PULSE_SOCK="${PULSE_SOCK#unix:}" PULSE_SOCK="${PULSE_SOCK#unix:}"
if [ -n "${PULSE_SOCK}" ] && [ -S "${PULSE_SOCK}" ] && [ ! -S /run/pulse/native ]; then if [ -n "${PULSE_SOCK}" ] && [ -S "${PULSE_SOCK}" ] && [ ! -S /run/pulse/native ]; then
mkdir -p /run/pulse mkdir -p /run/pulse
ln -sf "${PULSE_SOCK}" /run/pulse/native ln -sf "${PULSE_SOCK}" /run/pulse/native
fi fi
# Copy PulseAudio cookie for the icecast2 user so it can authenticate # Copy PulseAudio cookie for the icecast2 user so it can authenticate
if id icecast2 >/dev/null 2>&1; then if id icecast2 > /dev/null 2>&1; then
ICECAST_HOME="$(getent passwd icecast2 | cut -d: -f6)" ICECAST_HOME="$(getent passwd icecast2 | cut -d: -f6)"
if [ -n "${ICECAST_HOME}" ]; then if [ -n "${ICECAST_HOME}" ]; then
for cookie in /config/.config/pulse/cookie /root/.config/pulse/cookie; do for cookie in /config/.config/pulse/cookie /root/.config/pulse/cookie; do
if [ -f "${cookie}" ]; then if [ -f "${cookie}" ]; then
mkdir -p "${ICECAST_HOME}/.config/pulse" mkdir -p "${ICECAST_HOME}/.config/pulse"
cp "${cookie}" "${ICECAST_HOME}/.config/pulse/cookie" cp "${cookie}" "${ICECAST_HOME}/.config/pulse/cookie"
chown icecast2 "${ICECAST_HOME}/.config/pulse" "${ICECAST_HOME}/.config/pulse/cookie" chown icecast2 "${ICECAST_HOME}/.config/pulse" "${ICECAST_HOME}/.config/pulse/cookie"
break break
fi fi
done done
fi fi
fi fi

14
birdnet-pipy/rootfs/etc/services.d/api/run Normal file → Executable file
View File

@@ -10,10 +10,10 @@ cd /app
# and core.main reach the API locally, satisfying the wait_for in nginx/run + # and core.main reach the API locally, satisfying the wait_for in nginx/run +
# main/run). Heavy admin jobs cooperatively yield to keep this worker live. # main/run). Heavy admin jobs cooperatively yield to keep this worker live.
exec gunicorn wsgi:application \ exec gunicorn wsgi:application \
--worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker \ --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker \
--workers 1 \ --workers 1 \
--bind 127.0.0.1:5002 \ --bind 127.0.0.1:5002 \
--timeout 120 \ --timeout 120 \
--graceful-timeout 30 \ --graceful-timeout 30 \
--keep-alive 65 \ --keep-alive 65 \
--error-logfile - --error-logfile -

0
birdnet-pipy/rootfs/etc/services.d/icecast/run Normal file → Executable file
View File

0
birdnet-pipy/rootfs/etc/services.d/main/run Normal file → Executable file
View File

0
birdnet-pipy/rootfs/etc/services.d/model/run Normal file → Executable file
View File

0
birdnet-pipy/rootfs/etc/services.d/nginx/run Normal file → Executable file
View File

View File

@@ -1,10 +1,10 @@
## 1.37.2 (2026-08-29) ## 1.37.2 (2026-08-29)
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases) - Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
## 1.37.1 (2026-08-01) ## 1.37.1 (2026-08-01)
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases) - Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
## 1.37.0 (2026-07-25) ## 1.37.0 (2026-07-25)
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases) - Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)

0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run Normal file → Executable file
View File

0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/nginx/run Normal file → Executable file
View File

View File

@@ -21,19 +21,19 @@ export ROCKET_SECRET_KEY="${secret_key}"
# Find the matching log level # Find the matching log level
if bashio::config.has_value 'log_level'; then if bashio::config.has_value 'log_level'; then
case "$(bashio::string.lower "$(bashio::config 'log_level')")" in case "$(bashio::string.lower "$(bashio::config 'log_level')")" in
all|trace) all | trace)
log_level="trace" log_level="trace"
;; ;;
debug) debug)
log_level="debug" log_level="debug"
;; ;;
info|notice) info | notice)
log_level="info" log_level="info"
;; ;;
warning) warning)
log_level="warn" log_level="warn"
;; ;;
error|fatal) error | fatal)
log_level="error" log_level="error"
;; ;;
off) off)

View File

@@ -64,7 +64,7 @@ Default credentials are shown in the startup logs.
### Options ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| | -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions | | `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions | | `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) | | `TZ` | str | | Timezone (e.g., `Europe/London`) |

View File

@@ -1,46 +1,46 @@
## 1.94.121-ls127 (2026-09-05) ## 1.94.121-ls127 (2026-09-05)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.94.117-ls125 (2026-08-29) ## 1.94.117-ls125 (2026-08-29)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.93.138-ls124 (2026-08-22) ## 1.93.138-ls124 (2026-08-22)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.93.136-ls122 (2026-08-13) ## 1.93.136-ls122 (2026-08-13)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.93.134-ls121 (2026-08-08) ## 1.93.134-ls121 (2026-08-08)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.93.129-ls119 (2026-08-01) ## 1.93.129-ls119 (2026-08-01)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.144-ls117 (2026-07-25) ## 1.92.144-ls117 (2026-07-25)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.141-ls115 (2026-07-18) ## 1.92.141-ls115 (2026-07-18)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.140-ls113 (2026-07-16) ## 1.92.140-ls113 (2026-07-16)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.139-ls112 (2026-07-11) ## 1.92.139-ls112 (2026-07-11)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.92.134-ls109 (2026-07-04) ## 1.92.134-ls109 (2026-07-04)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.91.180-ls108 (2026-06-27) ## 1.91.180-ls108 (2026-06-27)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.91.175-ls106 (2026-06-20) ## 1.91.175-ls106 (2026-06-20)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.91.172-ls105 (2026-06-13) ## 1.91.172-ls105 (2026-06-13)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.91.171-ls104 (2026-06-11) ## 1.91.171-ls104 (2026-06-11)
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases) - Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
## 1.91.168-ls103 (2026-06-05) ## 1.91.168-ls103 (2026-06-05)

View File

@@ -1,12 +1,12 @@
## 2026.09.05 (2026-09-05) ## 2026.09.05 (2026-09-05)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases) - Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
- Upstream tag : version-d39975b5 - Upstream tag : version-d39975b5
## 2026.08.13 (2026-08-13) ## 2026.08.13 (2026-08-13)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases) - Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
- Upstream tag : version-e8713bf7 - Upstream tag : version-e8713bf7
## 2026.08.01.1 (2026-08-01) ## 2026.08.01.1 (2026-08-01)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases) - Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
- Upstream tag : version-e2e1ec9c - Upstream tag : version-e2e1ec9c
@@ -14,25 +14,25 @@
- Version renamed from `version-bf9e0b4f`, which Home Assistant could not order and therefore could not reliably offer as an update: the previous version held no number that could order it, so the date of this change is used. The addon itself and the upstream version it tracks are unchanged - Version renamed from `version-bf9e0b4f`, which Home Assistant could not order and therefore could not reliably offer as an update: the previous version held no number that could order it, so the date of this change is used. The addon itself and the upstream version it tracks are unchanged
## version-bf9e0b4f (2026-07-25) ## version-bf9e0b4f (2026-07-25)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases) - Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-ae9a7fca (2026-07-16) ## version-ae9a7fca (2026-07-16)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases) - Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-30a7c401 (2026-07-11) ## version-30a7c401 (2026-07-11)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases) - Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-7148c2a3 (2026-07-04) ## version-7148c2a3 (2026-07-04)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases) - Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-ec868334 (2026-06-27) ## version-ec868334 (2026-06-27)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases) - Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-b9b82d3e (2026-06-20) ## version-b9b82d3e (2026-06-20)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases) - Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-cca5154d (2026-06-11) ## version-cca5154d (2026-06-11)
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases) - Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
## version-89f88890 (2026-06-05) ## version-89f88890 (2026-06-05)

1
browser_chromium/rootfs/etc/cont-init.d/20-folders.sh Executable file → Normal file
View File

@@ -7,7 +7,6 @@ set -e
PUID=$(bashio::config "PUID") PUID=$(bashio::config "PUID")
PGID=$(bashio::config "PGID") PGID=$(bashio::config "PGID")
# Create cache # Create cache
#mkdir -p /.cache #mkdir -p /.cache
#chmod 755 /.cache #chmod 755 /.cache

View File

@@ -5,12 +5,12 @@ set -Eeuo pipefail
# Resolve chromium binary (LSIO uses this path) # Resolve chromium binary (LSIO uses this path)
if [[ -x /usr/lib/chromium/chromium ]]; then if [[ -x /usr/lib/chromium/chromium ]]; then
BIN=/usr/lib/chromium/chromium BIN=/usr/lib/chromium/chromium
elif command -v chromium >/dev/null 2>&1; then elif command -v chromium >/dev/null 2>&1; then
BIN="$(command -v chromium)" BIN="$(command -v chromium)"
else else
echo "ERROR: chromium binary not found" >&2 echo "ERROR: chromium binary not found" >&2
exit 127 exit 127
fi fi
# Fixed profile dir as requested # Fixed profile dir as requested
@@ -19,7 +19,7 @@ mkdir -p "${USER_DATA_DIR}"
# Cleanup stale locks if chromium is not already running # Cleanup stale locks if chromium is not already running
if ! pgrep -f 'chromium' >/dev/null 2>&1; then if ! pgrep -f 'chromium' >/dev/null 2>&1; then
rm -f "${USER_DATA_DIR}/Singleton"* || true rm -f "${USER_DATA_DIR}/Singleton"* || true
fi fi
# Detect seccomp mode (0 = disabled) # Detect seccomp mode (0 = disabled)
@@ -27,22 +27,22 @@ seccomp_mode="$(awk -F':' '/^Seccomp:/{gsub(/[[:space:]]/,"",$2); print $2}' /pr
# Base arguments (exactly what you requested + required safety flags) # Base arguments (exactly what you requested + required safety flags)
args=( args=(
--remote-debugging-address=0.0.0.0 --remote-debugging-address=0.0.0.0
--remote-debugging-port=9221 --remote-debugging-port=9221
--user-data-dir="${USER_DATA_DIR}" --user-data-dir="${USER_DATA_DIR}"
--disable-dev-shm-usage --disable-dev-shm-usage
--no-first-run --no-first-run
--no-default-browser-check --no-default-browser-check
--disable-background-networking --disable-background-networking
--disable-sync --disable-sync
--password-store=basic --password-store=basic
"--simulate-outdated-no-au=Tue, 31 Dec 2099 23:59:59 GMT" "--simulate-outdated-no-au=Tue, 31 Dec 2099 23:59:59 GMT"
--start-maximized --start-maximized
) )
# Container reality: sandbox usually breaks unless seccomp is fully disabled # Container reality: sandbox usually breaks unless seccomp is fully disabled
if [[ "${seccomp_mode}" != "0" ]]; then if [[ "${seccomp_mode}" != "0" ]]; then
args+=( --no-sandbox --test-type ) args+=(--no-sandbox --test-type)
fi fi
# IMPORTANT: # IMPORTANT:

View File

@@ -1,37 +1,37 @@
## 2.56.3 (2026-09-05) ## 2.56.3 (2026-09-05)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.56.0 (2026-08-22) ## 2.56.0 (2026-08-22)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.55.4 (2026-08-13) ## 2.55.4 (2026-08-13)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.55.3 (2026-08-08) ## 2.55.3 (2026-08-08)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.55.2 (2026-08-01) ## 2.55.2 (2026-08-01)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.55.0 (2026-07-16) ## 2.55.0 (2026-07-16)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.54.2 (2026-07-04) ## 2.54.2 (2026-07-04)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.54.1 (2026-06-23) ## 2.54.1 (2026-06-23)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.54.0 (2026-06-20) ## 2.54.0 (2026-06-20)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.52.2 (2026-06-17) ## 2.52.2 (2026-06-17)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.52.1 (2026-06-13) ## 2.52.1 (2026-06-13)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.52.0 (2026-06-11) ## 2.52.0 (2026-06-11)
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases) - Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
## 2.51.0 (2026-06-05) ## 2.51.0 (2026-06-05)

View File

@@ -1,16 +1,16 @@
## 9.14.0 (2026-08-29) ## 9.14.0 (2026-08-29)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases) - Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.13.0 (2026-08-08) ## 9.13.0 (2026-08-08)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases) - Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.12.0 (2026-08-01) ## 9.12.0 (2026-08-01)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases) - Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.11.0 (2026-07-04) ## 9.11.0 (2026-07-04)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases) - Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
## 9.10.0 (2026-06-27) ## 9.10.0 (2026-06-27)
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases) - Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)

View File

@@ -62,7 +62,7 @@ Please read the upstream container documentation for further info: https://githu
### Options ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| | -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions | | `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions | | `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) | | `TZ` | str | | Timezone (e.g., `Europe/London`) |

View File

@@ -1,4 +1,4 @@
## 0.6.27.4 (2026-09-04) ## 0.6.27.4 (2026-09-04)
- Fix: Kobo sync could not be enabled, failing with "Kepubify binary not found" even when the path was set by hand. The LinuxServer base image installs the converter as `/usr/bin/kepubify` with `curl -o`, which leaves it mode 0644 and gives it a name calibre-web does not accept : `binary_helper.py` only takes `kepubify-linux-64bit` or `kepubify-linux-32bit`, and only when `os.access(X_OK)` passes. The addon now makes the binary executable and publishes it as `/opt/kepubify/kepubify-linux-64bit`, the directory calibre-web's own autodetection already probes, so the path is filled in without any manual step (https://github.com/alexbelgium/hassio-addons/issues/3040) - Fix: Kobo sync could not be enabled, failing with "Kepubify binary not found" even when the path was set by hand. The LinuxServer base image installs the converter as `/usr/bin/kepubify` with `curl -o`, which leaves it mode 0644 and gives it a name calibre-web does not accept : `binary_helper.py` only takes `kepubify-linux-64bit` or `kepubify-linux-32bit`, and only when `os.access(X_OK)` passes. The addon now makes the binary executable and publishes it as `/opt/kepubify/kepubify-linux-64bit`, the directory calibre-web's own autodetection already probes, so the path is filled in without any manual step (https://github.com/alexbelgium/hassio-addons/issues/3040)
- Fix: on installs created before that change, calibre-web had already run its autodetection once, found nothing usable and stored an empty path, and it never retries. An empty path is now reset so calibre-web detects the converter itself at the next start. A path set by hand is left alone - Fix: on installs created before that change, calibre-web had already run its autodetection once, found nothing usable and stored an empty path, and it never retries. An empty path is now reset so calibre-web detects the converter itself at the next start. A path set by hand is left alone

View File

@@ -62,7 +62,7 @@ Default password: admin123
### Options ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| | -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions | | `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions | | `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) | | `TZ` | str | | Timezone (e.g., `Europe/London`) |
@@ -99,7 +99,7 @@ This addon supports mounting both local drives and remote SMB shares:
### Optional Calibre-Web features ### Optional Calibre-Web features
Calibre-Web documents optional extras that a manual installation adds with `pip install calibreweb[metadata]` and similar. **You do not need to install anything here**: the LinuxServer base image this add-on builds on installs Calibre-Web's `requirements.txt` *and* its full `optional-requirements.txt` into the application's virtualenv, so the gdrive, gmail, goodreads, ldap, oauth, metadata, comics and kobo dependencies are all present already. Running `pip install calibreweb[...]` inside the container is not a supported way to enable them: it installs the PyPI distribution of Calibre-Web over an installation that already has those dependencies, and it can disturb the versions the base image pinned. It is also thrown away, because the Supervisor recreates the add-on container on restart. Calibre-Web documents optional extras that a manual installation adds with `pip install calibreweb[metadata]` and similar. **You do not need to install anything here**: the LinuxServer base image this add-on builds on installs Calibre-Web's `requirements.txt` _and_ its full `optional-requirements.txt` into the application's virtualenv, so the gdrive, gmail, goodreads, ldap, oauth, metadata, comics and kobo dependencies are all present already. Running `pip install calibreweb[...]` inside the container is not a supported way to enable them: it installs the PyPI distribution of Calibre-Web over an installation that already has those dependencies, and it can disturb the versions the base image pinned. It is also thrown away, because the Supervisor recreates the add-on container on restart.
Optional features are switched on in the Calibre-Web web interface, not in the add-on options, under `Admin` -> `Basic Configuration` -> `Feature Configuration` (for example `Enable Uploads`, `Enable Kobo sync`, `Use Goodreads`). Optional features are switched on in the Calibre-Web web interface, not in the add-on options, under `Admin` -> `Basic Configuration` -> `Feature Configuration` (for example `Enable Uploads`, `Enable Kobo sync`, `Use Goodreads`).

View File

@@ -30,16 +30,16 @@ else
# duplicates it can leave behind are entries calibre-web skips or already trusts. # duplicates it can leave behind are entries calibre-web skips or already trusts.
# The column only exists once calibre-web 0.6.27+ has migrated app.db and cont-init runs # The column only exists once calibre-web 0.6.27+ has migrated app.db and cont-init runs
# before calibre-web, so a failure here is not fatal : the next start applies it. # before calibre-web, so a failure here is not fatal : the next start applies it.
trusted_ips_error=$(sqlite3 /config/app.db "update settings set config_reverse_proxy_trusted_ips='127.0.0.1,::1,::ffff:127.0.0.1,172.30.32.0/23,::ffff:172.30.32.0/119,'||coalesce(config_reverse_proxy_trusted_ips,'') where coalesce(config_reverse_proxy_trusted_ips,'') not like '%::ffff:172.30.32.0/119%'" 2>&1) || trusted_ips_error=$(sqlite3 /config/app.db "update settings set config_reverse_proxy_trusted_ips='127.0.0.1,::1,::ffff:127.0.0.1,172.30.32.0/23,::ffff:172.30.32.0/119,'||coalesce(config_reverse_proxy_trusted_ips,'') where coalesce(config_reverse_proxy_trusted_ips,'') not like '%::ffff:172.30.32.0/119%'" 2>&1) \
bashio::log.warning "Could not set the ingress trusted ip list, it will be applied at next start (${trusted_ips_error})" || bashio::log.warning "Could not set the ingress trusted ip list, it will be applied at next start (${trusted_ips_error})"
# Calibre-web autodetects kepubify only while this setting is still NULL. On every install # Calibre-web autodetects kepubify only while this setting is still NULL. On every install
# that predates the Dockerfile fix above, that detection already ran, found nothing usable and # that predates the Dockerfile fix above, that detection already ran, found nothing usable and
# stored an empty string, so it is never retried. Put an empty value back to NULL and # stored an empty string, so it is never retried. Put an empty value back to NULL and
# calibre-web detects /opt/kepubify itself when it starts, a few seconds after this runs. # calibre-web detects /opt/kepubify itself when it starts, a few seconds after this runs.
# A path the user set by hand is not empty and is left alone. # A path the user set by hand is not empty and is left alone.
kepubify_error=$(sqlite3 /config/app.db "update settings set config_kepubifypath = NULL where config_kepubifypath = ''" 2>&1) || kepubify_error=$(sqlite3 /config/app.db "update settings set config_kepubifypath = NULL where config_kepubifypath = ''" 2>&1) \
bashio::log.warning "Could not reset the kepubify path, it will be applied at next start (${kepubify_error})" || bashio::log.warning "Could not reset the kepubify path, it will be applied at next start (${kepubify_error})"
fi fi
bashio::log.info "Default username:password is admin:admin123" bashio::log.info "Default username:password is admin:admin123"

View File

@@ -1,7 +1,7 @@
## 0.60.3 (2026-09-05) ## 0.60.3 (2026-09-05)
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases) - Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
## 0.55.8 (2026-07-16) ## 0.55.8 (2026-07-16)
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases) - Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)

View File

@@ -52,7 +52,7 @@ You can add a shortcut pointing to your Changedetection.io instance with the fol
### Options ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| | -------- | ------ | --------- | ------------- |
| `PGID` | int | `0` | Group ID for file permissions | | `PGID` | int | `0` | Group ID for file permissions |
| `PUID` | int | `0` | User ID for file permissions | | `PUID` | int | `0` | User ID for file permissions |
| `TZ` | str | | Timezone (e.g., `Europe/London`) | | `TZ` | str | | Timezone (e.g., `Europe/London`) |

0
changedetection.io/rootfs/etc/services.d/nginx/run Normal file → Executable file
View File

View File

@@ -1,12 +1,12 @@
## 07308545.6 (2026-09-05) ## 07308545.6 (2026-09-05)
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases) - Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
- Upstream tag : v3.2.3+claude1.40609.1 - Upstream tag : v3.2.3+claude1.40609.1
## 07308545.5 (2026-08-29) ## 07308545.5 (2026-08-29)
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases) - Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
- Upstream tag : v3.2.2+claude1.37937.3 - Upstream tag : v3.2.2+claude1.37937.3
## 07308545.4 (2026-08-22) ## 07308545.4 (2026-08-22)
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases) - Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
- Upstream tag : v3.2.2+claude1.32885.1 - Upstream tag : v3.2.2+claude1.32885.1
@@ -71,7 +71,7 @@
## 07308545.1 (17-08-2026) ## 07308545.1 (17-08-2026)
- Minor bugs fixed - Minor bugs fixed
## 07308543.1 (17-08-2026) ## 07308543.1 (17-08-2026)
- Fix the "For your security, sign in again" prompt recurring on every restart again. The v1.37 - Fix the "For your security, sign in again" prompt recurring on every restart again. The v1.37
`safeStorage` patch (`86-claude_safestorage.sh` / `claude-safestorage-patch.js`) only knew how `safeStorage` patch (`86-claude_safestorage.sh` / `claude-safestorage-patch.js`) only knew how
@@ -94,11 +94,11 @@
## 07308545 (2026-08-15) ## 07308545 (2026-08-15)
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases) - Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
- Upstream tag : v3.2.2+claude1.30096.1 - Upstream tag : v3.2.2+claude1.30096.1
## 07308544 (2026-08-13) ## 07308544 (2026-08-13)
- Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases) - Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases)
- Upstream tag : v3.2.2+claude1.28929.0 - Upstream tag : v3.2.2+claude1.28929.0
## 07308543 (2026-08-08) ## 07308543 (2026-08-08)
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases) - Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
- Upstream tag : debiantrixie-version-07308543 - Upstream tag : debiantrixie-version-07308543
@@ -185,10 +185,10 @@
## 2026.08.02 (02-08-2026) ## 2026.08.02 (02-08-2026)
- Minor bugs fixed - Minor bugs fixed
## ubunturesolute-version-3a10bef7 (2026-08-01) ## ubunturesolute-version-3a10bef7 (2026-08-01)
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases) - Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
## kali-version-e963b19b (2026-08-01) ## kali-version-e963b19b (2026-08-01)
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases) - Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
## 1.37 (28-07-2026) ## 1.37 (28-07-2026)
@@ -199,7 +199,7 @@
- **`gnome-keyring` stays out of the image.** Re-adding it would reintroduce the first-boot keyring password prompt that blocks Claude Desktop from launching at all. This route needs no keyring, no D-Bus Secret Service, no daemon and no password, so no prompt can appear. The trade-off is unchanged from v1.35 and is inherent to the `basic` backend: its key is fixed rather than gated by a keyring, so any process that can read the persistent `$HOME/.config/Claude` profile can recover the stored credentials. - **`gnome-keyring` stays out of the image.** Re-adding it would reintroduce the first-boot keyring password prompt that blocks Claude Desktop from launching at all. This route needs no keyring, no D-Bus Secret Service, no daemon and no password, so no prompt can appear. The trade-off is unchanged from v1.35 and is inherent to the `basic` backend: its key is fixed rather than gated by a keyring, so any process that can read the persistent `$HOME/.config/Claude` profile can recover the stored credentials.
- **One-time step after upgrading.** The previously stored session is already stale, so a single sign-in is still needed once after this update; it then persists across restarts. - **One-time step after upgrading.** The previously stored session is already stale, so a single sign-in is still needed once after this update; it then persists across restarts.
## 1.36.4 (28-07-2026) ## 1.36.4 (28-07-2026)
- Fix Selkies dying with a Rust `RuntimeDirNotSet` unwrap panic just after `Data WebSocket Server listening on port 8081`, and the data websocket then being proxied to the wrong port. Upstream relies on s6-rc ordering: `init-selkies-config` publishes `XDG_RUNTIME_DIR` and `CUSTOM_WS_PORT` into the s6 envdir and `svc-selkies` starts afterwards. The add-on entrypoint replaces s6-overlay and starts every `s6-rc.d` run script in parallel with no dependency graph, so Selkies can snapshot the envdir before that oneshot has written to it -- which is why it bound port 8081 (its own default) instead of the 8082 nginx proxies to, and why its Wayland compositor found no runtime directory to bind a socket in. `20-folders.sh` now exports both variables inside each run script, where no start ordering can lose them, and corrects the base image's `$HOME/.XDG` override where that write happens instead of appending a correction after the `exit 0` that the oneshot-tolerance block adds -- which meant the correction never ran on any boot after the first. - Fix Selkies dying with a Rust `RuntimeDirNotSet` unwrap panic just after `Data WebSocket Server listening on port 8081`, and the data websocket then being proxied to the wrong port. Upstream relies on s6-rc ordering: `init-selkies-config` publishes `XDG_RUNTIME_DIR` and `CUSTOM_WS_PORT` into the s6 envdir and `svc-selkies` starts afterwards. The add-on entrypoint replaces s6-overlay and starts every `s6-rc.d` run script in parallel with no dependency graph, so Selkies can snapshot the envdir before that oneshot has written to it -- which is why it bound port 8081 (its own default) instead of the 8082 nginx proxies to, and why its Wayland compositor found no runtime directory to bind a socket in. `20-folders.sh` now exports both variables inside each run script, where no start ordering can lose them, and corrects the base image's `$HOME/.XDG` override where that write happens instead of appending a correction after the `exit 0` that the oneshot-tolerance block adds -- which meant the correction never ran on any boot after the first.
@@ -210,7 +210,7 @@
## 1.36.2 (28-07-2026) ## 1.36.2 (28-07-2026)
- Minor bugs fixed - Minor bugs fixed
## 1.36.1 (27-07-2026) ## 1.36.1 (27-07-2026)
- Fix the Codex CLI install failing on every boot with `Verified Codex <version> installation failed; Codex is unavailable this boot`, leaving `install_codex_cli` permanently non-functional. The download, its SHA-256 verification, and the extraction all succeeded; the chain broke at the final step, which validates the candidate binary by running `--version` as the `abc` runtime user. `mktemp -d` creates its directory `0700 root:root`, and `abc` cannot traverse a root-only directory, so executing the staged binary failed with `unable to exec: Permission denied` (exit 126) before it could be moved into place. Reproduced and fixed by making the staging directory traversable (`chmod 0755`) immediately after `mktemp`; verified on a live add-on container, where the same probe goes from exit 126 to success once the mode is widened. Nothing secret is staged there — the public release archive and the extracted binary, both world-readable upstream artifacts — and the existing `cleanup()` trap still removes the directory on exit. The validation deliberately keeps running as `abc` rather than root, so the binary is exercised as the identity that will actually run it. - Fix the Codex CLI install failing on every boot with `Verified Codex <version> installation failed; Codex is unavailable this boot`, leaving `install_codex_cli` permanently non-functional. The download, its SHA-256 verification, and the extraction all succeeded; the chain broke at the final step, which validates the candidate binary by running `--version` as the `abc` runtime user. `mktemp -d` creates its directory `0700 root:root`, and `abc` cannot traverse a root-only directory, so executing the staged binary failed with `unable to exec: Permission denied` (exit 126) before it could be moved into place. Reproduced and fixed by making the staging directory traversable (`chmod 0755`) immediately after `mktemp`; verified on a live add-on container, where the same probe goes from exit 126 to success once the mode is widened. Nothing secret is staged there — the public release archive and the extracted binary, both world-readable upstream artifacts — and the existing `cleanup()` trap still removes the directory on exit. The validation deliberately keeps running as `abc` rather than root, so the binary is exercised as the identity that will actually run it.
@@ -240,10 +240,10 @@
## 1.33 (22-07-2026) ## 1.33 (22-07-2026)
- Add cowork virtualization support: `qemu-system-x86` and `ovmf` (Bookworm main, installed via apt) plus `virtiofsd` for sharing the workspace into the sandbox microVM. `virtiofsd` is only packaged for Debian trixie/sid, not Bookworm or bookworm-backports, and its trixie `.deb` links a newer GLIBC than this add-on's Bookworm runtime — so it now gets built from the pinned crates.io release (`1.14.0`) in a dedicated `virtiofsd-builder` stage, the same GLIBC-safe pattern already used for `rtk` and `tokensave`. Its build deps (`libseccomp-dev`, `libcap-ng-dev`, `pkg-config`, `clang`, `libclang-dev`) live only in that builder stage; only the runtime shared libs (`libseccomp2`, `libcap-ng0`) ship in the final image. The built binary is validated with `--version` at build time alongside `rtk`/`tokensave`, so a GLIBC/ABI mismatch fails the image build instead of surfacing at container start. Docker itself is deliberately not installed: this base image already ships Docker-in-Docker (`docker-ce`/`containerd.io` from Docker's own apt repo, started via the pre-existing `START_DOCKER` env var) — an initial attempt to also `apt-get install docker.io` broke the build, since Debian's package pulls in `containerd`/`runc`, which apt refuses to install alongside the base image's already-installed `containerd.io` (`Conflicts`). - Add cowork virtualization support: `qemu-system-x86` and `ovmf` (Bookworm main, installed via apt) plus `virtiofsd` for sharing the workspace into the sandbox microVM. `virtiofsd` is only packaged for Debian trixie/sid, not Bookworm or bookworm-backports, and its trixie `.deb` links a newer GLIBC than this add-on's Bookworm runtime — so it now gets built from the pinned crates.io release (`1.14.0`) in a dedicated `virtiofsd-builder` stage, the same GLIBC-safe pattern already used for `rtk` and `tokensave`. Its build deps (`libseccomp-dev`, `libcap-ng-dev`, `pkg-config`, `clang`, `libclang-dev`) live only in that builder stage; only the runtime shared libs (`libseccomp2`, `libcap-ng0`) ship in the final image. The built binary is validated with `--version` at build time alongside `rtk`/`tokensave`, so a GLIBC/ABI mismatch fails the image build instead of surfacing at container start. Docker itself is deliberately not installed: this base image already ships Docker-in-Docker (`docker-ce`/`containerd.io` from Docker's own apt repo, started via the pre-existing `START_DOCKER` env var) — an initial attempt to also `apt-get install docker.io` broke the build, since Debian's package pulls in `containerd`/`runc`, which apt refuses to install alongside the base image's already-installed `containerd.io` (`Conflicts`).
## ubunturesolute-version-8208e985 (2026-07-21) ## ubunturesolute-version-8208e985 (2026-07-21)
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases) - Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
## kali-version-9ad48e7a (2026-07-18) ## kali-version-9ad48e7a (2026-07-18)
- Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases) - Update to latest version from linuxserver/docker-baseimage-selkies (changelog : https://github.com/linuxserver/docker-baseimage-selkies/releases)
## 1.32 (17-07-2026) ## 1.32 (17-07-2026)

View File

@@ -55,9 +55,9 @@
# shared memory into ordinary files for no benefit. If the size cannot be determined, keep the # shared memory into ordinary files for no benefit. If the size cannot be determined, keep the
# flag — the crash it prevents is worse than the overhead it costs. # flag — the crash it prevents is worse than the overhead it costs.
SHM_FLAGS="--disable-dev-shm-usage" SHM_FLAGS="--disable-dev-shm-usage"
SHM_KB="$(df -k /dev/shm 2> /dev/null | awk 'NR==2 {print $2}')" SHM_KB="$(df -k /dev/shm 2>/dev/null | awk 'NR==2 {print $2}')"
if [ -n "$SHM_KB" ] && [ "$SHM_KB" -ge 262144 ]; then if [ -n "$SHM_KB" ] && [ "$SHM_KB" -ge 262144 ]; then
SHM_FLAGS="" SHM_FLAGS=""
fi fi
# SHM_FLAGS must stay unquoted so it expands to a separate argument, or to nothing at all. # SHM_FLAGS must stay unquoted so it expands to a separate argument, or to nothing at all.

View File

@@ -141,7 +141,7 @@ release_info=""
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 30 \ if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 30 \
-o "$release_metadata" "$CODEX_RELEASE_API"; then -o "$release_metadata" "$CODEX_RELEASE_API"; then
release_info="$( release_info="$(
CODEX_ASSET="$CODEX_ASSET" python3 - "$release_metadata" <<'PY' 2> /dev/null || true CODEX_ASSET="$CODEX_ASSET" python3 - "$release_metadata" << 'PY' 2> /dev/null || true
import json import json
import os import os
import re import re
@@ -249,7 +249,7 @@ fi
{ {
printf '#!/usr/bin/env bash\n' printf '#!/usr/bin/env bash\n'
printf 'CODEX_REAL=%q\n' "$CODEX_REAL" printf 'CODEX_REAL=%q\n' "$CODEX_REAL"
cat <<'SH' cat << 'SH'
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)" RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
if [ -z "$RUNTIME_HOME" ]; then if [ -z "$RUNTIME_HOME" ]; then
echo "codex: unable to resolve the abc runtime home" >&2 echo "codex: unable to resolve the abc runtime home" >&2
@@ -338,8 +338,7 @@ ln -sfn "$CODEX_BIN" "$CODEX_LINK"
CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'danger-full-access')" CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'danger-full-access')"
run_as_runtime_user mkdir -p "$RUNTIME_HOME/.codex" run_as_runtime_user mkdir -p "$RUNTIME_HOME/.codex"
CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" RUNTIME_HOME="$RUNTIME_HOME" \ CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" RUNTIME_HOME="$RUNTIME_HOME" \
run_as_runtime_user python3 - <<'PY' \ run_as_runtime_user python3 - << 'PY' || bashio::log.warning "Unable to update the managed Codex configuration block"
|| bashio::log.warning "Unable to update the managed Codex configuration block"
import os import os
import re import re
import tomllib import tomllib

View File

@@ -30,7 +30,7 @@ manage_claude_md_block() {
fi fi
elif [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; then elif [ -f "$CLAUDE_MD" ] && grep -qF "$begin" "$CLAUDE_MD"; then
bashio::log.info "Removing ${name} guidance from CLAUDE.md" bashio::log.info "Removing ${name} guidance from CLAUDE.md"
CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - <<'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically" CLAUDE_MD="$CLAUDE_MD" BLOCK_NAME="$name" python3 - << 'PY' || bashio::log.warning "Unable to remove the ${name} guidance automatically"
import os import os
import re import re
from pathlib import Path from pathlib import Path
@@ -58,7 +58,7 @@ PY
manage_settings_hook() { manage_settings_hook() {
# manage_settings_hook <event> <matcher> <command> <add|remove> # manage_settings_hook <event> <matcher> <command> <add|remove>
HOOK_EVENT="$1" HOOK_MATCHER="$2" HOOK_COMMAND="$3" HOOK_ACTION="$4" \ HOOK_EVENT="$1" HOOK_MATCHER="$2" HOOK_COMMAND="$3" HOOK_ACTION="$4" \
python3 - <<'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'" python3 - << 'PY' || bashio::log.warning "Unable to update the $1 hook for '$3'"
import json import json
import os import os
from pathlib import Path from pathlib import Path
@@ -389,14 +389,14 @@ bashio::log.info "MCP servers for Claude Desktop: ${MCP_SERVERS_DESKTOP}"
bashio::log.info "MCP servers for Claude Code: ${MCP_SERVERS_CODE}" bashio::log.info "MCP servers for Claude Code: ${MCP_SERVERS_CODE}"
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \ HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \ HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \ TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \ CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \ HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \ MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
MCP_SERVERS_DESKTOP="$MCP_SERVERS_DESKTOP" MCP_SERVERS_CODE="$MCP_SERVERS_CODE" \ MCP_SERVERS_DESKTOP="$MCP_SERVERS_DESKTOP" MCP_SERVERS_CODE="$MCP_SERVERS_CODE" \
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \ CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
python3 - <<'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically" python3 - << 'PY' || bashio::log.warning "Unable to update the MCP server registrations automatically"
import json import json
import os import os
from pathlib import Path from pathlib import Path
@@ -607,7 +607,7 @@ PY
# Guide Claude to actually use the Headroom compression tools so the MCP integration produces # Guide Claude to actually use the Headroom compression tools so the MCP integration produces
# real savings when transparent proxying is unavailable. # real savings when transparent proxying is unavailable.
if $HEADROOM_ENABLED; then if $HEADROOM_ENABLED; then
manage_claude_md_block headroom add <<'MD' manage_claude_md_block headroom add << 'MD'
## Headroom context compression ## Headroom context compression
A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens: A local Headroom proxy (127.0.0.1:8787) backs the `headroom` MCP tools. To save context tokens:
@@ -636,7 +636,7 @@ if $HEADROOM_ENABLED && bashio::config.true 'headroom_wrap_claude_code'; then
else else
HEADROOM_ROUTE_ACTION="remove" HEADROOM_ROUTE_ACTION="remove"
fi fi
HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - <<'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env" HEADROOM_ROUTE_ACTION="$HEADROOM_ROUTE_ACTION" python3 - << 'PY' || bashio::log.warning "Unable to manage the Claude Code proxy routing env"
import json import json
import os import os
from pathlib import Path from pathlib import Path
@@ -703,7 +703,7 @@ manage_settings_hook PostToolUse "Bash|Grep|Glob|WebFetch" "$HEADROOM_HOOK_CMD"
# Tell Claude Code that it can configure Home Assistant over the Core API via the shipped # Tell Claude Code that it can configure Home Assistant over the Core API via the shipped
# `ha-cli` helper (no /config filesystem mount needed). # `ha-cli` helper (no /config filesystem mount needed).
if bashio::config.true 'enable_ha_api_helper'; then if bashio::config.true 'enable_ha_api_helper'; then
manage_claude_md_block ha-api-helper add <<'MD' manage_claude_md_block ha-api-helper add << 'MD'
## Configuring Home Assistant ## Configuring Home Assistant
You can configure this Home Assistant instance through its Core API using the `ha-cli` You can configure this Home Assistant instance through its Core API using the `ha-cli`
@@ -731,7 +731,7 @@ fi
# Registering the MCP server is not enough on its own: without guidance the model rarely reaches # Registering the MCP server is not enough on its own: without guidance the model rarely reaches
# for a second agent, the same gap the Headroom block above exists to close. # for a second agent, the same gap the Headroom block above exists to close.
if $CODEX_ENABLED; then if $CODEX_ENABLED; then
manage_claude_md_block codex add <<'MD' manage_claude_md_block codex add << 'MD'
## Delegating to ChatGPT Codex ## Delegating to ChatGPT Codex
The `codex` MCP server runs OpenAI's Codex agent locally, signed in with the user's ChatGPT The `codex` MCP server runs OpenAI's Codex agent locally, signed in with the user's ChatGPT

View File

@@ -19,7 +19,7 @@ esac
# only while it still holds one of those managed values — a defaultMode the user set by hand # only while it still holds one of those managed values — a defaultMode the user set by hand
# is never deleted. Ownership of the written file is reconciled by 84-claude_runtime_ownership.sh. # is never deleted. Ownership of the written file is reconciled by 84-claude_runtime_ownership.sh.
mkdir -p "$(dirname "$SETTINGS_PATH")" mkdir -p "$(dirname "$SETTINGS_PATH")"
PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - <<'PY' PERMISSION_MODE="$PERMISSION_MODE" SETTINGS_PATH="$SETTINGS_PATH" python3 - << 'PY'
import json import json
import os import os
from pathlib import Path from pathlib import Path

View File

@@ -36,7 +36,7 @@ fi
# archive stays unpatched, so every later boot would retry under a new pid and strand another # archive stays unpatched, so every later boot would retry under a new pid and strand another
# copy until the container runs out of space. # copy until the container runs out of space.
cleanup_tmp() { cleanup_tmp() {
find "$(dirname "$ASAR")" -maxdepth 1 -name ".$(basename "$ASAR").addon-tmp.*" -delete 2>/dev/null || true find "$(dirname "$ASAR")" -maxdepth 1 -name ".$(basename "$ASAR").addon-tmp.*" -delete 2> /dev/null || true
} }
if output=$(timeout 120 node "$PATCHER" "$ASAR" 2>&1); then if output=$(timeout 120 node "$PATCHER" "$ASAR" 2>&1); then

View File

@@ -45,7 +45,7 @@ if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
fi fi
section "Claude Code permission state" section "Claude Code permission state"
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY' RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
import json import json
import os import os
from pathlib import Path from pathlib import Path
@@ -66,7 +66,7 @@ else:
PY PY
section "MCP registrations (environment values redacted)" section "MCP registrations (environment values redacted)"
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY' RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
import json import json
import os import os
from pathlib import Path from pathlib import Path
@@ -104,7 +104,7 @@ for path in paths:
PY PY
section "Claude Code hooks" section "Claude Code hooks"
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY' RUNTIME_HOME="$RUNTIME_HOME" python3 - << 'PY'
import json import json
import os import os
from pathlib import Path from pathlib import Path

0
claude_desktop/rootfs/usr/local/bin/codex-login Executable file → Normal file
View File

14
claude_desktop/rootfs/usr/local/bin/ha-cli Executable file → Normal file
View File

@@ -96,7 +96,7 @@ def resolve_endpoint():
def _url(base, path): def _url(base, path):
path = path.lstrip("/") path = path.lstrip("/")
if path.startswith("api/"): if path.startswith("api/"):
path = path[len("api/"):] path = path[len("api/") :]
return base.rstrip("/") + "/" + path return base.rstrip("/") + "/" + path
@@ -133,7 +133,9 @@ def rest(method, base, token, path, body=None):
if body is not None: if body is not None:
data = json.dumps(body).encode("utf-8") data = json.dumps(body).encode("utf-8")
headers["Content-Type"] = "application/json" headers["Content-Type"] = "application/json"
req = urllib.request.Request(_url(base, path), data=data, method=method, headers=headers) req = urllib.request.Request(
_url(base, path), data=data, method=method, headers=headers
)
try: try:
with urllib.request.urlopen(req, timeout=30) as resp: with urllib.request.urlopen(req, timeout=30) as resp:
text = resp.read().decode("utf-8") text = resp.read().decode("utf-8")
@@ -141,7 +143,9 @@ def rest(method, base, token, path, body=None):
detail = exc.read().decode("utf-8", "replace").strip() detail = exc.read().decode("utf-8", "replace").strip()
sys.exit(f"ha-cli: HTTP {exc.code} {exc.reason} on {method} {path}\n{detail}") sys.exit(f"ha-cli: HTTP {exc.code} {exc.reason} on {method} {path}\n{detail}")
except urllib.error.URLError as exc: except urllib.error.URLError as exc:
sys.exit(f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}") sys.exit(
f"ha-cli: cannot reach Home Assistant ({exc.reason}) on {method} {path}"
)
try: try:
return json.loads(text) if text.strip() else None return json.loads(text) if text.strip() else None
except ValueError: except ValueError:
@@ -214,7 +218,9 @@ def main(argv):
sys.exit("usage: ha-cli call <domain.service> [BODY]") sys.exit("usage: ha-cli call <domain.service> [BODY]")
domain, service = args[0].split(".", 1) domain, service = args[0].split(".", 1)
body = _read_body(args[1]) if len(args) > 1 else None body = _read_body(args[1]) if len(args) > 1 else None
_print(rest("POST", rest_base, token, f"services/{domain}/{service}", body or {})) _print(
rest("POST", rest_base, token, f"services/{domain}/{service}", body or {})
)
elif cmd == "states": elif cmd == "states":
path = f"states/{args[0]}" if args else "states" path = f"states/{args[0]}" if args else "states"
_print(rest("GET", rest_base, token, path)) _print(rest("GET", rest_base, token, path))

View File

@@ -76,7 +76,11 @@ def main() -> int:
# Find big string/array fields before paying the headroom import cost. # Find big string/array fields before paying the headroom import cost.
def is_string_array(value): def is_string_array(value):
return isinstance(value, list) and len(value) > ARRAY_KEEP and all(isinstance(v, str) for v in value) return (
isinstance(value, list)
and len(value) > ARRAY_KEEP
and all(isinstance(v, str) for v in value)
)
if isinstance(response, str): if isinstance(response, str):
string_candidates = ["__whole__"] if len(response) >= MIN_CHARS else [] string_candidates = ["__whole__"] if len(response) >= MIN_CHARS else []
@@ -85,10 +89,14 @@ def main() -> int:
string_candidates = [ string_candidates = [
key key
for key, value in response.items() for key, value in response.items()
if key not in SKIP_KEYS and isinstance(value, str) and len(value) >= MIN_CHARS if key not in SKIP_KEYS
and isinstance(value, str)
and len(value) >= MIN_CHARS
] ]
array_candidates = [ array_candidates = [
key for key, value in response.items() if key not in SKIP_KEYS and is_string_array(value) key
for key, value in response.items()
if key not in SKIP_KEYS and is_string_array(value)
] ]
else: else:
string_candidates = [] string_candidates = []

View File

@@ -1,16 +1,16 @@
## 2.10.5 (2026-08-13) ## 2.10.5 (2026-08-13)
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases) - Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
## 2.10.3 (2026-08-08) ## 2.10.3 (2026-08-08)
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases) - Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
## 2.10.2 (2026-08-01) ## 2.10.2 (2026-08-01)
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases) - Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
## 2.9.16 (2026-07-11) ## 2.9.16 (2026-07-11)
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases) - Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)
## 2.9.14 (2026-06-20) ## 2.9.14 (2026-06-20)
- Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases) - Update to latest version from Cleanuparr/Cleanuparr (changelog : https://github.com/Cleanuparr/Cleanuparr/releases)

View File

@@ -23,7 +23,7 @@ Integrations supported:
## Configuration ## Configuration
| Option | Description | | Option | Description |
|--------|-------------| | -------- | ------------- |
| `TZ` | Timezone (e.g. `Europe/Paris`). Defaults to `Europe/London`. | | `TZ` | Timezone (e.g. `Europe/Paris`). Defaults to `Europe/London`. |
| `PUID` | User ID to run the process as. Defaults to `0` (root). | | `PUID` | User ID to run the process as. Defaults to `0` (root). |
| `PGID` | Group ID to run the process as. Defaults to `0` (root). | | `PGID` | Group ID to run the process as. Defaults to `0` (root). |

View File

@@ -1,8 +1,7 @@
arch: arch:
- aarch64 - aarch64
- amd64 - amd64
description: description: Automatically removes stuck and unwanted downloads from your *arr and download clients
Automatically removes stuck and unwanted downloads from your *arr and download clients
devices: devices:
- /dev/dri - /dev/dri
- /dev/dri/card0 - /dev/dri/card0

View File

@@ -33,7 +33,7 @@ mkdir -p "$HA_DATA_DIR"
# Symlink /app/config → HA persistent storage # Symlink /app/config → HA persistent storage
# The .NET app uses /app/config, NOT /config at the filesystem root # The .NET app uses /app/config, NOT /config at the filesystem root
if [ -d /app/config ] && [ ! -L /app/config ]; then if [ -d /app/config ] && [ ! -L /app/config ]; then
cp -rn /app/config/. "$HA_DATA_DIR/" 2>/dev/null || true cp -rn /app/config/. "$HA_DATA_DIR/" 2> /dev/null || true
rm -rf /app/config rm -rf /app/config
fi fi
ln -sfn "$HA_DATA_DIR" /app/config ln -sfn "$HA_DATA_DIR" /app/config

View File

@@ -1,13 +1,13 @@
## 19.20.5 (2026-09-05) ## 19.20.5 (2026-09-05)
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases) - Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
## 19.20.4 (2026-08-29) ## 19.20.4 (2026-08-29)
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases) - Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
## 19.20.0 (2026-08-08) ## 19.20.0 (2026-08-08)
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases) - Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)
## 19.19.1 (2026-06-17) ## 19.19.1 (2026-06-17)
- Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases) - Update to latest version from coderaiser/cloudcmd (changelog : https://github.com/coderaiser/cloudcmd/releases)

View File

@@ -40,7 +40,7 @@ Configurations can be done through the app webUI, except for the following optio
### Options ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| | -------- | ------ | --------- | ------------- |
| `CUSTOM_OPTIONS` | str | | Custom CLI options (e.g., `--name Homeassistant`) | | `CUSTOM_OPTIONS` | str | | Custom CLI options (e.g., `--name Homeassistant`) |
| `DROPBOX_TOKEN` | str | | Dropbox integration token (see https://cloudcmd.io/) | | `DROPBOX_TOKEN` | str | | Dropbox integration token (see https://cloudcmd.io/) |
| `localdisks` | str | | Local drives to mount (e.g., `sda1,sdb1,MYNAS`) | | `localdisks` | str | | Local drives to mount (e.g., `sda1,sdb1,MYNAS`) |

View File

@@ -103,7 +103,7 @@ for candidate in "${cloudcmd_candidates[@]}"; do
fi fi
done done
if [ -z "$CLOUDCMD_BIN" ] && command -v cloudcmd >/dev/null 2>&1; then if [ -z "$CLOUDCMD_BIN" ] && command -v cloudcmd > /dev/null 2>&1; then
CLOUDCMD_BIN=$(command -v cloudcmd) CLOUDCMD_BIN=$(command -v cloudcmd)
fi fi

View File

@@ -1,37 +1,37 @@
## 2.2.11 (2026-08-29) ## 2.2.11 (2026-08-29)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.2.8 (2026-08-22) ## 2.2.8 (2026-08-22)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.2.6 (2026-08-08) ## 2.2.6 (2026-08-08)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.2.5 (2026-08-02) ## 2.2.5 (2026-08-02)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.2.4 (2026-08-01) ## 2.2.4 (2026-08-01)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.2.2 (2026-07-25) ## 2.2.2 (2026-07-25)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.2.0 (2026-07-16) ## 2.2.0 (2026-07-16)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.1.2 (2026-07-11) ## 2.1.2 (2026-07-11)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.1.0 (2026-07-04) ## 2.1.0 (2026-07-04)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.0.3 (2026-06-23) ## 2.0.3 (2026-06-23)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 2.0.0 (2026-06-20) ## 2.0.0 (2026-06-20)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)
## 1.12.8 (2026-06-17) ## 1.12.8 (2026-06-17)
- Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases) - Update to latest version from ajslater/codex (changelog : https://github.com/ajslater/codex/releases)

View File

@@ -62,7 +62,7 @@ You can place the user folder from the theme/skeleton in /share/codex/www/user,
## Options ## Options
| Option | Description | Default | Example | | Option | Description | Default | Example |
|--------|-------------|---------|---------| | -------- | ------------- | --------- | --------- |
| `PGID` | Group ID for file permissions | `0` | `1000` | | `PGID` | Group ID for file permissions | `0` | `1000` |
| `PUID` | User ID for file permissions | `0` | `1000` | | `PUID` | User ID for file permissions | `0` | `1000` |
| `TZ` | Timezone in long format | - | `America/Los_Angeles` | | `TZ` | Timezone in long format | - | `America/Los_Angeles` |

View File

@@ -1,10 +1,10 @@
## 26.04.3.2.1 (2026-09-05) ## 26.04.3.2.1 (2026-09-05)
- Update to latest version from collabora/code - Update to latest version from collabora/code
## 26.04.3.1.1 (2026-08-13) ## 26.04.3.1.1 (2026-08-13)
- Update to latest version from collabora/code - Update to latest version from collabora/code
## 26.04.2.4.1 (2026-07-26) ## 26.04.2.4.1 (2026-07-26)
- Rebuild on a Debian base: upstream turned collabora/code into a distroless image with no shell, which broke the addon build entirely. collabora/code stays the tracked upstream image in build.json, but is now a build stage whose payload is copied onto ghcr.io/hassio-addons/debian-base, and the addon ships its own launcher in place of the removed /start-collabora-online.sh - Rebuild on a Debian base: upstream turned collabora/code into a distroless image with no shell, which broke the addon build entirely. collabora/code stays the tracked upstream image in build.json, but is now a build stage whose payload is copied onto ghcr.io/hassio-addons/debian-base, and the addon ships its own launcher in place of the removed /start-collabora-online.sh
- build.json names the architecture explicitly again (`collabora/code:latest-amd64` and `collabora/code:latest-arm64`). The builder never passes `--platform`, so the tag is the only thing that decides which binaries land in the addon - build.json names the architecture explicitly again (`collabora/code:latest-amd64` and `collabora/code:latest-arm64`). The builder never passes `--platform`, so the tag is the only thing that decides which binaries land in the addon
@@ -19,16 +19,16 @@
- Added `aliasgroup2` and `aliasgroup3` for additional Nextcloud servers - Added `aliasgroup2` and `aliasgroup3` for additional Nextcloud servers
- `cert_domain` is now a string (it is a certificate common name) and is passed to Collabora - `cert_domain` is now a string (it is a certificate common name) and is passed to Collabora
- Documented the above, and corrected the README which asked for two backslashes where Collabora expects one - Documented the above, and corrected the README which asked for two backslashes where Collabora expects one
## 1.3.0 (2026-07-16) ## 1.3.0 (2026-07-16)
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases) - Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
## 1.2.2 (2026-07-04) ## 1.2.2 (2026-07-04)
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases) - Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
## 1.2.1 (2026-06-27) ## 1.2.1 (2026-06-27)
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases) - Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
## 1.2.0 (2026-06-23) ## 1.2.0 (2026-06-23)
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases) - Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)

View File

@@ -51,7 +51,7 @@ Webui can be found at `https://homeassistant:9980/browser/dist/admin/admin.html`
### Options ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| | -------- | ------ | --------- | ------------- |
| `aliasgroup1` | str | | External address of the **Nextcloud** server allowed to use this Collabora (e.g. `https://nextcloud_domain\.com:443`) | | `aliasgroup1` | str | | External address of the **Nextcloud** server allowed to use this Collabora (e.g. `https://nextcloud_domain\.com:443`) |
| `aliasgroup2` | str | | A second Nextcloud server, same format as `aliasgroup1` | | `aliasgroup2` | str | | A second Nextcloud server, same format as `aliasgroup1` |
| `aliasgroup3` | str | | A third Nextcloud server, same format as `aliasgroup1` | | `aliasgroup3` | str | | A third Nextcloud server, same format as `aliasgroup1` |

View File

@@ -27,9 +27,9 @@ normalise_wopi_host() {
# server_name is a literal "hostname[:port]", not a regex and not a URL # server_name is a literal "hostname[:port]", not a regex and not a URL
normalise_server_name() { normalise_server_name() {
local value="$1" local value="$1"
value="${value//\\/}" # never escaped, drop backslashes if any were copied over value="${value//\\/}" # never escaped, drop backslashes if any were copied over
value="${value#*://}" # strip the scheme value="${value#*://}" # strip the scheme
value="${value%%/*}" # strip any path value="${value%%/*}" # strip any path
printf '%s' "$value" printf '%s' "$value"
} }

View File

@@ -1,7 +1,7 @@
## 0.38.7 (2026-09-05) ## 0.38.7 (2026-09-05)
- Update to latest version from frankieramirez/comicarr (changelog : https://github.com/frankieramirez/comicarr/releases) - Update to latest version from frankieramirez/comicarr (changelog : https://github.com/frankieramirez/comicarr/releases)
## 0.38.2 (2026-08-29) ## 0.38.2 (2026-08-29)
- Update to latest version from frankieramirez/comicarr (changelog : https://github.com/frankieramirez/comicarr/releases) - Update to latest version from frankieramirez/comicarr (changelog : https://github.com/frankieramirez/comicarr/releases)
## 0.34.0 (20-08-2026) ## 0.34.0 (20-08-2026)

View File

@@ -30,7 +30,7 @@ database.
## Configuration ## Configuration
| Option | Description | | Option | Description |
|--------|-------------| | -------- | ------------- |
| `PUID` / `PGID` | Ownership applied to the add-on configuration directory. Defaults to `0` (root). See the note below before changing it. | | `PUID` / `PGID` | Ownership applied to the add-on configuration directory. Defaults to `0` (root). See the note below before changing it. |
| `TZ` | Timezone, e.g. `Europe/Paris`. | | `TZ` | Timezone, e.g. `Europe/Paris`. |
| `localdisks` | Local disks to mount, e.g. `sda1` or a disk label. | | `localdisks` | Local disks to mount, e.g. `sda1` or a disk label. |

0
comicarr/rootfs/etc/services.d/comicarr/run Normal file → Executable file
View File

0
comicarr/rootfs/etc/services.d/nginx/run Normal file → Executable file
View File

View File

@@ -60,7 +60,7 @@ Initial setup requires creating an admin account through the web interface.
### Options ### Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| | -------- | ------ | --------- | ------------- |
| `TZ` | str | | Timezone (e.g., `Europe/London`) | | `TZ` | str | | Timezone (e.g., `Europe/London`) |
| `localdisks` | str | | Local drives to mount (e.g., `sda1,sdb1,MYNAS`) | | `localdisks` | str | | Local drives to mount (e.g., `sda1,sdb1,MYNAS`) |
| `networkdisks` | str | | SMB shares to mount (e.g., `//SERVER/SHARE`) | | `networkdisks` | str | | SMB shares to mount (e.g., `//SERVER/SHARE`) |

View File

@@ -1,12 +1,12 @@
## 8.19.21 (2026-09-05) ## 8.19.21 (2026-09-05)
- Update to latest version from elastic/elasticsearch (changelog : https://github.com/elastic/elasticsearch/releases) - Update to latest version from elastic/elasticsearch (changelog : https://github.com/elastic/elasticsearch/releases)
## 8.19.20 (2026-08-13) ## 8.19.20 (2026-08-13)
- Update to latest version from elastic/elasticsearch (changelog : https://github.com/elastic/elasticsearch/releases) - Update to latest version from elastic/elasticsearch (changelog : https://github.com/elastic/elasticsearch/releases)
## 8.19.19.2 (21-07-2026) ## 8.19.19.2 (21-07-2026)
- Minor bugs fixed - Minor bugs fixed
## 8.19.19 (2026-07-21) ## 8.19.19 (2026-07-21)
- Update to latest version from elastic/elasticsearch (changelog : https://github.com/elastic/elasticsearch/releases) - Update to latest version from elastic/elasticsearch (changelog : https://github.com/elastic/elasticsearch/releases)
## 8.19.18-3 (14-07-2026) ## 8.19.18-3 (14-07-2026)

Some files were not shown because too many files have changed in this diff Show More