Compare commits

..

18 Commits

Author SHA1 Message Date
github-actions
2eb4f169af GitHub bot : README updated 2026-08-20 17:22:21 +00:00
github-actions
2247172ad1 GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild] 2026-08-20 13:38:07 +00:00
Alexandre
d0ef2fec48 feat(comicarr): new add-on for Comicarr with ingress support (#3001)
* feat(comicarr): new add-on with Home Assistant ingress

Comicarr is a fork of Mylar3 with a React frontend and a FastAPI backend.
The upstream image is a plain python:3.12-slim with no s6-overlay, so
ha_entrypoint.sh runs as pid 1 and supervises both the app and nginx —
the same shape the komga add-on uses.

Ingress needs a reverse proxy because the app has no url-base support of
any kind: vite emits absolute /assets urls, the api client and the cover
img tags build absolute /api and /cache urls, and SecurityHeadersMiddleware
sends X-Frame-Options: DENY together with a CSP carrying
frame-ancestors 'none', which alone would leave the panel blank. The
bundled nginx rewrites those paths onto the ingress entry, replaces the
two framing headers with the same policy narrowed to the Home Assistant
origin, scopes the session cookie to the ingress path and drops upstream's
one-year immutable caching for the rewritten assets.

The app is started directly as root by default rather than through the
upstream /entrypoint.sh, which runs useradd -u "$PUID" under set -e and
would exit on this repo's PUID=0 default; that entrypoint is still used
when the user asks for an unprivileged uid. --port 8090 is forced because
the port is writable from the Settings page and changing it there would
silently break both the proxy and the health check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(comicarr): note that switching PUID leaves existing files root-owned

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(comicarr): drop ingress_port, the add-on linter rejects the default

8099 is the Supervisor default, and frenck/action-addon-linter fails with
"'ingress_port' should be removed, it uses a default value". komga omits it
for the same reason; nginx still binds whatever bashio::addon.ingress_port
reports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(comicarr): 0755 on the entrypoint instead of 777

The rest of the repo uses 777 here, but this add-on is the one that offers a
non-root mode: with PUID set, the app runs as an unprivileged user that could
otherwise rewrite a file docker executes as root on the next start. Nothing
writes to /ha_entrypoint.sh at runtime, so 0755 costs nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 15:37:26 +02:00
github-actions
8ddd7957f8 GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild] 2026-08-20 10:25:31 +00:00
Alexandre
d0ec6b2243 Modify 2026-08-20 12:24:37 +02:00
Alexandre
68257f926b Update updater.json 2026-08-20 12:16:41 +02:00
Alexandre
9960a04304 fix(claude_desktop): install the complete Codex package so tool calls work (#3000)
* fix(claude_desktop): install the complete Codex package, not just the executable

Since codex-cli 0.147.0 the CLI delegates every shell and file-read tool call to a
companion codex-code-mode-host binary that it looks up next to its own executable.
81-codex_cli.sh downloaded the codex-<target>.tar.gz release asset, which contains
only the codex executable, so that binary was never installed and every tool call
failed with "failed to spawn code-mode host ...: No such file or directory" while
the run still exited 0.

Download the codex-package-<target>.tar.gz asset instead — the complete package
tree upstream's own installer uses — and install all of it into the existing
/data/codex prefix, which already satisfies Codex's layout contract. Make the
"already installed" test require the code-mode host and the package manifest so
existing incomplete installs repair themselves, and report layout completeness in
claude-tools-doctor.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(claude_desktop): advertise Codex only when its package tree is complete

82-claude_tools.sh registers the Codex MCP server whenever the launcher at
/data/codex/bin/codex is executable and re-checks nothing else, while the launcher
and the package tree persist in /data independently of each other. Three paths
therefore reached that launcher next to an install that cannot run a tool call: a
boot that cannot reach the release metadata and keeps a pre-existing install missing
the code-mode host or the manifest, the same boot finding a stamp-less tree left by
an interrupted replacement, and a launcher surviving from an earlier boot after the
install was dropped. All three reproduced against the real script with stubbed
bashio/s6 and an unreachable metadata endpoint.

Define completeness once (executable, code-mode host, package manifest, version
stamp) and gate the launcher on it, removing the launcher and the /usr/local/bin
symlink when it does not hold. Nothing else is deleted, so a later boot completes the
install without another download or another login. The doctor's layout check now
includes the stamp for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(claude_desktop): install the Codex package tree by position, not by name

The whole reason for downloading the 118 MB codex-package asset instead of the
lone executable is that a helper Codex needs must not be left out — that is the
bug this branch fixes. install_codex_package still enumerated the five paths
release 0.148.0 happens to ship, so a helper added by a future release would be
downloaded, extracted and then discarded, failing exactly the way the missing
code-mode host does today. Verified against the extracted function: with a
staged tree carrying an unknown bin/ helper and an unknown top-level directory,
the previous code installed neither.

Move whatever the archive contains instead: every staged entry beside bin/ into
/data/codex, every staged bin/ entry except the entrypoint into /data/codex/bin,
then the entrypoint to codex-real last, so the ordering guarantee the stamp
relies on is unchanged. Only paths the archive actually contains are touched,
because /data/codex also holds this install's staging directory, and the
existing launcher is skipped by name while the version stamp is a dot file that
no glob matches. Removing each destination before moving onto it also drops
files an older release left behind.

Exercised with a scaffold around the extracted function: fresh install with
unknown helpers present, upgrade over an existing install with a stale helper
and a launcher to preserve, a minimal package with no optional directories, and
an unwritable prefix to confirm failure is reported rather than swallowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(claude_desktop): scope the two deletion claims to what the code does

Both overstated. install_codex_package() replaces every path the new release
ships, but does not prune a path upstream stops shipping, so "files an older
release left behind are removed with it" was wrong for exactly that case; and
"nothing is deleted beyond the launcher" read as if the /usr/local/bin/codex
symlink named in the previous sentence survived, when it is removed with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:18:40 +02:00
Alexandre
4f8c0f2ed1 feat(kapowarr): new add-on with Home Assistant ingress support (#2999)
* feat(kapowarr): new add-on with Home Assistant ingress support

Kapowarr is a comic book library manager in the *arr family. The add-on is
built on the upstream image (mrcas/kapowarr), with the repository's standard
nginx ingress scaffolding on top.

Ingress uses the pattern komga and bazarr already use here: Kapowarr renders
absolute urls from its url base, Home Assistant strips its own ingress prefix
before forwarding, so Kapowarr is started with --UrlBase /kapowarr and nginx
rewrites that fixed prefix back onto the ingress entry.

Database and logs go to the add-on configuration directory. Temporary
downloads are symlinked there rather than passed with --TempDownloadFolder,
which upstream re-applies on its own restarts and would keep overwriting a
folder chosen in Settings > Download.

* fix(kapowarr): review fixes from the codex pass

- repair a /app/temp_downloads symlink pointing at the wrong target instead of
  accepting any symlink
- exclude logs and temporary downloads from Home Assistant backups: the temp
  folder now lives in the add-on config directory and can hold gigabytes
- fix the /dev/nvme2n3p3 typo inherited from the copied device list (the
  partition is nvme2n1p3); the same typo is present in the other add-ons
- document that the url base must not be changed, and that a non-zero PUID
  only reaches folders that user can already access
- drop three dead Dockerfile lines (BASHIO_VERSION is overridden inside
  ha_automatic_packages.sh, USER root is a no-op on this image)

* fix(kapowarr): pin host and port too, not just the url base

Found by a Codex review that could read the upstream source.

Kapowarr stores host, port and url base in its database and reads the stored
value whenever the matching flag is absent. Only --UrlBase was passed, so a
host or port changed in Settings > General survived every restart and upgrade
while nginx and the healthcheck stayed pointed at 127.0.0.1:5656 -- a permanent
502 with no way back except editing the database by hand.

All three flags are startup-only upstream, so passing them re-applies the
add-on's values once per container start without fighting the self-restarts
Kapowarr performs after a settings change.
2026-08-19 21:21:55 +02:00
Alexandre
2fc1ea84be fix(immich): URI-encode DB credentials for psql connection strings (#2980)
The addon builds every psql connection as a postgres:// URI with the raw
username and password interpolated in. libpq percent-decodes the userinfo
part of a URI, so a password containing '%' (or '@', '/', '?', '#') is
decoded into different bytes before it reaches the server, and every
connection fails with "password authentication failed for user".

Encode the credentials with jq's @uri once and use the encoded copies in
the URIs only; the raw password is still what gets handed to Immich via
export_db_env and what is written by CREATE/ALTER USER. Those SQL
statements now double single quotes so a password containing a single
quote no longer breaks the statement either.

This is the same approach already used by the postgres_15 and postgres_17
addons in this repo.

Closes #1614

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:29:50 +02:00
Alexandre
48ac78c59d fix(filebrowser_quantum): repair direct access on port 8071 (#2998)
* fix(filebrowser_quantum): repair direct access on port 8071

1.5.1.1 published the port but direct access still did not work, in two ways
measured against a running instance:

1. The root redirect was absolute, so nginx built it from $server_port and
   sent the browser to :8072 — the container-internal port, not the published
   one. `absolute_redirect off` keeps the redirect relative.
2. The page served under /filebrowser_quantum/ referenced its assets under
   the app's own baseURL (the ingress entry path), which that vhost did not
   route: GET /api/hassio_ingress/<hash>/public/static/favicon.svg returned
   404 while the same file under /filebrowser_quantum/ returned 200. The page
   loaded and every asset on it failed.

Rather than translating paths, the vhost now passes requests through
unchanged and redirects only the bare root to the app's baseURL, which is
what its own links already point at. Asset, API and websocket URLs then work
without any response rewriting. Ingress is untouched.

* docs(filebrowser_quantum): describe the legacy redirects accurately

The comments, CHANGELOG and README still said only the bare root was
redirected, which stopped being true when the two /filebrowser_quantum
compatibility redirects were added. Raised by CodeRabbit and Codex.
2026-08-19 10:27:36 +02:00
Alexandre
9302fc9a51 fix(komga): keep the reader inside the ingress panel (#2995)
* fix(komga): keep the reader inside the ingress panel

Komga's ui opens the reader with window.open(url, '_blank'). The Home
Assistant companion apps hand such a popup to an external browser, which
carries no ingress session cookie, so Home Assistant answers 401 before
Komga is reached.

Nginx now injects a small script into the ui shell that turns same origin
popups into a navigation in the current tab. The OAuth2 login popup, which
passes a window name and a feature string, and cross origin links are left
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(komga): only intercept popups when resourceBaseUrl is known

Review feedback : the '/' fallback meant that if Komga ever stopped
setting window.resourceBaseUrl, every same origin _blank popup would be
captured -- and ingress shares the Home Assistant origin. Require the
base, and give it a trailing slash so a sibling path such as
<entry>/komgaX is not treated as being below <entry>/komga.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 07:30:47 +02:00
Alexandre
29ce08c162 fix(seerr): reapply the asset cache-bust reverted by the builder (#2975) (#2997)
Restores #2993 verbatim. It was merged, then reverted by the builder's
revert-on-failure job a minute later - not because of anything in it, but
because EndBug/add-and-commit's floating v11 tag had moved to a release whose
action.yml no longer loads, so prebuild-sanitize failed before running a step.
The tag is pinned back to v11.0.0 in #2996, which has to land first for the
builder to get past that job.

The change itself is unchanged and still verified against the real njs module:
the rewritten /_next paths carry the add-on version, njs strips the marker
before proxying, so a browser holding the year-cached rewritten bundle fetches
fresh URLs on the first load after the update.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 07:30:20 +02:00
Alexandre
bb1d0c6b66 ci: pin EndBug/add-and-commit to v11.0.0, the floating v11 tag is broken (#2996)
Every push to master has failed to build since 2026-08-19 05:15. The
prebuild-sanitize job dies before running a single step:

    EndBug/add-and-commit/v11/action.yml (Line: 25, Col: 18):
    Unrecognized named-value: 'github'. Located at position 1 within
    expression: github.workspace
    Failed to load EndBug/add-and-commit/v11/action.yml

Upstream's v11.1.0, published 2026-08-18 22:44 UTC, put a literal
"${{ github.workspace }}" inside the description of the `cwd` input. Action
metadata descriptions are still parsed as expressions and the `github` context
does not exist there, so the action no longer loads at all. The floating v11
tag was moved to it, which is why nothing changed in this repo and every
workflow using the action broke at once - the builder, the README and stats
refreshers, the CRLF sweep, the image compressor and the issue labeller.

v11.0.0 does not contain that line and loads normally, so pinning to it keeps
the version Dependabot moved us to in #2985 while stepping off the tag. It also
took out an unrelated add-on fix: the builder's revert-on-failure job reverted
the seerr merge (#2993) as collateral, and that is being reapplied separately.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 07:29:52 +02:00
GitHub Actions
1b1436b61a Revert "fix(seerr): version the rewritten asset paths so cached bundles expire (#2975) (#2993)"
This reverts commit 08fe5e33be.
2026-08-19 05:15:54 +00:00
Alexandre
08fe5e33be fix(seerr): version the rewritten asset paths so cached bundles expire (#2975) (#2993)
* fix(seerr): version the rewritten asset paths so cached bundles expire (#2975)

Seerr serves everything under /_next/static/ with "Cache-Control: public,
max-age=31536000, immutable", and nginx's sub_filter strips ETag,
Last-Modified and Content-Length from every response it rewrites. The HTML
is served "no-store" but keeps naming the same chunk URLs, and all three
3.4.1.x add-on versions ship the same upstream build, so a browser that had
loaded Seerr through ingress once kept replaying the JavaScript it cached
then - for up to a year, with no request to revalidate it.

That is why #2975 outlived two fixes: the reporter's https origin was still
executing the 3.4.1/3.4.1.1 bundle, whose rewritten root link makes Next
hard-navigate to /api/hassio_ingress/<token> without a trailing slash, which
Home Assistant does not route and answers with its own "404: Not Found". An
origin that had never cached it - the same instance over http://<ip>:8123 -
already showed the fixed behaviour.

The asset paths now carry the add-on version ("/ha-3-4-1-3/_next/..."), so
every release has its own URLs, a poisoned cache is bypassed on the first
load after an update, and any future change to a rewrite rule is actually
delivered. njs strips the marker again before proxying, so Seerr still
receives the paths it serves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(seerr): tighten the cache-bust comments after review

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(seerr): take the cache-bust marker straight from BUILD_VERSION

bashio::addon.version is an indirection here: bashio-standalone.sh defines it
as printf '%s' "${BUILD_VERSION:-1.0}", and the builder always passes
BUILD_VERSION from config.yaml, which the Dockerfile bakes in as an ENV. Reading
it directly drops a Supervisor round-trip and the fallback chain around it, for
the same value. The sanitiser stays: it protects the sed replacement and the
regex literal the marker lands in inside Seerr's bundle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 07:15:20 +02:00
Alexandre
dac8efba60 fix(scrutiny_original): keep real /init as PID 1 so s6 supervision starts (#2991)
* fix(scrutiny_original): keep real /init as PID 1 so s6 supervision starts

collector-once's s6-svwait -u /run/service/scrutiny fails because
ha_entrypoint.sh (not the image's own /init) was PID 1 and never runs
real s6 supervision, so /run/service/* never gets created. Same root
cause and fix as scrutiny/scrutiny_fa (#2878): keep /init as PID 1 and
let ha_entrypoint.sh act only as the S6_STAGE2_HOOK.

Verified against the actual analogj/scrutiny:latest-omnibus image
(pulled via the GHCR registry API): its collector-once run script is
byte-identical to the one that caused #2877, and it bundles the same
s6-overlay-3.1.6.2.

Closes #2989

* fix(scrutiny_original): symlink /command into /usr/bin at build time

With /init as PID 1, ha_entrypoint.sh's own PID1 branch (which creates
this same symlink at runtime, and rewrites service run-file shebangs)
never runs. nginx/run and finish use #!/usr/bin/with-contenv bashio,
which only resolves if /usr/bin/with-contenv exists — so without this,
nginx (ingress) fails to start. scrutiny/Dockerfile and
scrutiny_fa/Dockerfile already do this in their "Install apps" stage;
this was missed when porting their PID-1 fix over.

Caught by chatgpt-codex-connector's PR review.
2026-08-18 21:36:49 +02:00
Alexandre
87f69ce79b fix(scrutiny_fa_original): keep real /init as PID 1 so s6 supervision starts (#2992)
Same bug and root cause as scrutiny_original (#2991) and scrutiny/scrutiny_fa
(#2878): collector-once's `s6-svwait -u /run/service/scrutiny` fails because
ha_entrypoint.sh (not the image's own /init) was PID 1 and never runs real s6
supervision, so /run/service/* never gets created.

scrutiny_fa_original shares the exact same rootfs (byte-identical cont-init.d
and services.d/nginx scripts) as scrutiny_original and builds from the same
ghcr.io/analogj/scrutiny:latest-omnibus image, so the same fix applies:
keep /init as PID 1, patch ha_entrypoint.sh to hand off to real s6-rc
supervision, and symlink /command into /usr/bin at build time (nginx/run and
finish use #!/usr/bin/with-contenv bashio, which only resolves once that
symlink exists — a P1 finding from scrutiny_original's PR review that
applies here identically).

Keeps the existing bashio::require.unprotected guard in
/etc/cont-init.d/90-run.sh unchanged.
2026-08-18 21:36:35 +02:00
Alexandre
84dfd1b996 fix(seerr): stop rewriting the root link inside the JS bundle (#2975) (#2986)
* fix(seerr): stop rewriting the root link inside the JS bundle (#2975)

3.4.1.1 appended a trailing slash to both the server-rendered "Discover"
link and its counterpart inside Seerr's JavaScript bundle. The slash is
correct in the HTML - Home Assistant routes ingress on
"/api/hassio_ingress/{token}/{path:.*}" and rejects a slash-less entry -
but it cannot survive in the bundle: next/link resolves a pushed href
through normalizePathTrailingSlash(), which drops a trailing slash while
`trailingSlash` is false, and Next.js then hard navigates to the
slash-less URL, recreating the same 404. On the root page it instead
throws "Invariant: attempted to hard navigate to the same URL" and the
click does nothing.

Dropping the bundle rewrite leaves the link as "/", which the client
router matches against its own "/" route and transitions to in-app -
the same path every other sidebar entry already takes, none of which are
rewritten here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(seerr): drop text/html from sub_filter_types

nginx pre-seeds text/html into sub_filter_types, so listing it emits
`[warn] duplicate MIME type "text/html"` on every config load. Verified
against nginx 1.22.1 locally: with the type dropped, `nginx -t` is
warning free and an HTML response is still filtered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:29:01 +02:00
71 changed files with 1928 additions and 100 deletions

View File

@@ -228,7 +228,7 @@ jobs:
echo "... done"
- name: Commit if needed
uses: EndBug/add-and-commit@v11
uses: EndBug/add-and-commit@v11.0.0
with:
message: "GitHub bot : README updated"
default_author: github_actions

View File

@@ -237,7 +237,7 @@ jobs:
# Get stars evolution
wget -S -O .github/starsevol.svg "https://api.star-history.com/svg?repos=alexbelgium/hassio-addons&type=Date" || true
- name: Commit if needed
uses: EndBug/add-and-commit@v11
uses: EndBug/add-and-commit@v11.0.0
with:
message: "GitHub bot : graphs updated"
default_author: github_actions

View File

@@ -59,7 +59,7 @@ jobs:
# Remove issues list
rm issueslist
- name: Commit if needed
uses: EndBug/add-and-commit@v11
uses: EndBug/add-and-commit@v11.0.0
with:
message: "Github bot : issues linked to readme"
default_author: github_actions

View File

@@ -95,7 +95,7 @@ jobs:
- name: Commit sanitize changes
id: sanitize_commit
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
uses: EndBug/add-and-commit@v11
uses: EndBug/add-and-commit@v11.0.0
with:
commit: -u
message: "GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild]"
@@ -410,7 +410,7 @@ jobs:
done
- name: Commit changelog changes
uses: EndBug/add-and-commit@v11
uses: EndBug/add-and-commit@v11.0.0
with:
commit: -u
message: "GitHub bot: changelog [nobuild]"

View File

@@ -18,7 +18,7 @@ jobs:
uses: erclu/check-crlf@v1
- name: Commit if needed
uses: EndBug/add-and-commit@v11
uses: EndBug/add-and-commit@v11.0.0
with:
message: "Github bot : CRLF corrected"
default_author: github_actions
@@ -50,7 +50,7 @@ jobs:
dos2unix -k "$f"
done
- name: Commit if needed
uses: EndBug/add-and-commit@v11
uses: EndBug/add-and-commit@v11.0.0
with:
message: "Github bot : CRLF corrected"
default_author: github_actions

View File

@@ -31,7 +31,7 @@ jobs:
- name: Commit if needed
if: steps.calibre.outputs.markdown != ''
uses: EndBug/add-and-commit@v11
uses: EndBug/add-and-commit@v11.0.0
with:
message: "Github bot : image compressed"
default_author: github_actions

View File

@@ -109,7 +109,7 @@ jobs:
#TOTAL3="$(awk '{SUM+=$2}END{print SUM}' Stats)"
- name: Commit if needed
uses: EndBug/add-and-commit@v11
uses: EndBug/add-and-commit@v11.0.0
with:
default_author: github_actions
message : "Github bot : stats updated"

View File

@@ -56,7 +56,7 @@ If you want to do add the repository manually, please follow the procedure highl
### Number of addons
- In the repository : 141
- In the repository : 143
- Installed : 361103
### Top 3
@@ -309,6 +309,16 @@ If you want to do add the repository manually, please follow the procedure highl
![aarch64][aarch64-badge]
![amd64][amd64-badge]
&#10003; ![image](https://api.iconify.design/mdi/book-open-page-variant.svg) [Comicarr](comicarr/) : Automated comic book and manga downloader and library manager with a modern React UI
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fcomicarr%2Fconfig.yaml)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fcomicarr%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
![ingress][ingress-badge]
![smb][smb-badge]
![localdisks][localdisks-badge]
&#10003; ![image](https://api.iconify.design/mdi/book-open.svg) [Comixed](comixed/) : managing digital comics
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fcomixed%2Fconfig.yaml)
@@ -560,6 +570,16 @@ If you want to do add the repository manually, please follow the procedure highl
![aarch64][aarch64-badge]
![amd64][amd64-badge]
&#10003; ![image](https://api.iconify.design/mdi/book-multiple.svg) [Kapowarr](zzz_archived_kapowarr/) : Comic book library manager, fitting in the *arr suite of software
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fzzz_archived_kapowarr%2Fconfig.yaml)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fzzz_archived_kapowarr%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
![ingress][ingress-badge]
![smb][smb-badge]
![localdisks][localdisks-badge]
&#10003; [Kometa](kometa/) : Python script to update metadata information for movies, shows, and collections as well as automatically build collections
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fkometa%2Fconfig.yaml)

View File

@@ -49,7 +49,6 @@ server {
sub_filter `/asset `%%ingress_entry%%/asset;
sub_filter "'/asset" "'%%ingress_entry%%/asset";
sub_filter \"/asset \"%%ingress_entry%%/asset;
sub_filter \"/auth \"%%ingress_entry%%/auth;
sub_filter window.location.origin} window.location.origin}%%ingress_entry%%;
}
}

View File

@@ -49,7 +49,6 @@ server {
sub_filter `/asset `%%ingress_entry%%/asset;
sub_filter "'/asset" "'%%ingress_entry%%/asset";
sub_filter \"/asset \"%%ingress_entry%%/asset;
sub_filter \"/auth \"%%ingress_entry%%/auth;
sub_filter window.location.origin} window.location.origin}%%ingress_entry%%;
}
}

View File

@@ -1,3 +1,62 @@
## 07308545.3 (19-08-2026)
- Fix an incompletely installed Codex CLI, which silently broke every Codex tool call. Since
codex-cli 0.147.0 the CLI does not execute shell commands or file reads itself; it delegates
them to a companion `codex-code-mode-host` binary that it looks up next to its own executable.
`81-codex_cli.sh` downloaded the `codex-<target>.tar.gz` release asset, which contains only the
`codex` executable, so that companion binary was never installed. Measured on the running add-on
(codex-cli 0.147.0, `/data/codex/bin` holding only `.version`, the launcher and `codex-real`):
`codex exec` starts, authenticates and answers, but every tool call fails with
`failed to spawn code-mode host /data/codex/bin/codex-code-mode-host: No such file or directory`
and the run still exits 0 — so Codex answered from the prompt text alone and the failure looked
like success. `--disable code_mode` does not avoid it.
- The installer now downloads the `codex-package-<target>.tar.gz` release asset, which is the
complete package tree upstream's own installer uses, and installs all of it. Not a list of
known file names: whatever the archive contains is moved into place by position, so a helper
added by a future release arrives beside the entrypoint on its own instead of being extracted
and then dropped — cherry-picking today's two binaries works today, but it is the same mistake
at a smaller scale. For release 0.148.0 that means the entrypoint as
`/data/codex/bin/codex-real`, `codex-code-mode-host` beside it, and `codex-package.json`,
`codex-resources/` (bundled bubblewrap and zsh) and `codex-path/` (bundled ripgrep) in
`/data/codex`. The installed tree grows from ~246 MB to ~300 MB, and `/data/codex` is now
explicitly add-on-owned in its entirety: every path the new release ships replaces the
installed copy of that path outright rather than merging into it, so nothing should be kept
there by hand. A path upstream stops shipping altogether is not pruned — it is left behind as
dead weight that the new entrypoint no longer looks for. Codex's own state stays in
`~/.codex` and is never touched.
- An incomplete install is no longer advertised. `82-claude_tools.sh` registers the Codex MCP
server whenever the launcher at `/data/codex/bin/codex` is executable and re-checks nothing
else, and both the launcher and the package tree persist in `/data` independently of each
other. The launcher is therefore now written only for an install that has its executable, its
code-mode host, its package manifest and its version stamp, and is removed together with the
`/usr/local/bin/codex` symlink otherwise. The stamp is part of that test because it is deleted
before the first file of a replacement is moved and written after the last, so a stamp-less
prefix is exactly the tree that may mix two releases. This covers the cases that reach the
launcher without a fresh install: a boot that cannot reach the release metadata and finds a
pre-existing incomplete install, and a launcher left behind by an interrupted replacement.
Nothing under `/data/codex` is deleted beyond that launcher — the executable, the package
tree and the ChatGPT sign-in stay, so a later boot completes the install without another
download or another login.
- That layout is load-bearing, so the install prefix was chosen to satisfy it rather than
changed. Codex canonicalises its own executable path, requires the parent directory to be
named `bin`, and reads the manifest and helper directories from that directory's parent — the
existing `/data/codex/bin` prefix already matches, and the executable's file name is not part
of the contract, so `codex-real` and the subscription-only `codex` launcher wrapping it are
both unchanged, as is the `/usr/local/bin/codex` symlink and the MCP registration.
- Existing installs repair themselves. The "already installed, skip the download" test now also
requires the code-mode host and the package manifest to be present, so an add-on that already
has a working `codex-real` and no helpers reinstalls on the next start instead of staying
quietly broken.
- `claude-tools-doctor.sh` now reports whether the package layout is complete, because the
failure mode this fixes is invisible in `codex --version`, in the version stamp and in the
exit code.
- Known limitation, unchanged by this release and not caused by it: Codex's own Linux sandbox
cannot start in this container. Running both the system `bwrap` 0.8.0 and the bundled one
directly with `--dev-bind / / --unshare-net /bin/true` fails identically with
`bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`, so it is a container capability
limitation rather than a packaging one. With the shipped `codex_sandbox_mode: workspace-write`
default, tool calls therefore still fail with that bwrap error; `danger-full-access` is the only
mode that currently executes commands, and the container is already the security boundary.
## 07308545.1 (17-08-2026)
- Minor bugs fixed

View File

@@ -209,14 +209,25 @@ registers `codex mcp-server` in both Claude Code and Claude Desktop. A Claude
session can therefore delegate a task to ChatGPT Codex and read its result back
through MCP.
Codex is not baked into the image because its Linux binary is large and the
feature is off by default. At each startup, the add-on resolves the latest
stable upstream release. It downloads the architecture-specific binary into
persistent `/data/codex/bin` only when the installed release is missing or
outdated, verifies the GitHub-published SHA-256 digest before extraction or
execution, validates the staged binary with `--version`, and replaces the
existing binary atomically. If release metadata or the download is unavailable,
startup continues and a previously working installation is retained.
Codex is not baked into the image because its Linux distribution is large and
the feature is off by default. At each startup, the add-on resolves the latest
stable upstream release. It downloads the architecture-specific package into
persistent `/data/codex` only when the installed release is missing, incomplete
or outdated, verifies the GitHub-published SHA-256 digest before extraction or
execution, and validates the staged package with `--version` before it replaces
the installed one. The complete upstream package is installed, not just the
`codex` executable: Codex delegates every shell and file-read tool call to a
companion `codex-code-mode-host` binary that it looks up next to itself, so an
executable installed on its own can answer but can never run anything. If
release metadata or the download is unavailable, startup continues and a
previously working installation is retained.
`/data/codex` belongs to the add-on: everything below it — `bin/`,
`codex-package.json`, `codex-resources/` and `codex-path/` — is replaced as a
unit whenever a new release is installed, so it is not a place to keep files by
hand. Codex's own state (`auth.json`, `config.toml`) lives in `~/.codex` and is
never touched by an install. The installed package is roughly 300 MB, and an
upgrade briefly needs room for the archive and both releases at once.
### Signing in with a ChatGPT subscription

View File

@@ -136,5 +136,5 @@ schema:
slug: claude_desktop
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "07308545.1"
version: "07308545.3"
video: true

View File

@@ -10,10 +10,20 @@ set -o pipefail
# The install prefix is /data/codex, NOT $HOME/.codex/bin: /data is persistent regardless of the
# configurable data_location, and the managed MCP merge treats commands under $HOME as
# user-installed. Codex state (auth.json, config.toml) remains in the runtime user's home.
#
# Codex is distributed as a package tree, not as a lone executable: since 0.147.0 every shell and
# file-read tool call is executed by a companion binary, codex-code-mode-host, that Codex looks up
# next to itself. Upstream publishes that tree as the codex-package-<target> release asset, and the
# whole tree is installed here. Its layout is load-bearing and must not be flattened: Codex
# canonicalises its own executable path, requires the parent directory to be named `bin`, and then
# reads codex-package.json, codex-resources/ and codex-path/ from that directory's parent. The
# executable's file name is not part of that contract, which is why codex-real keeps its name.
CODEX_ROOT="/data/codex"
CODEX_PREFIX="${CODEX_ROOT}/bin"
CODEX_BIN="${CODEX_PREFIX}/codex"
CODEX_REAL="${CODEX_PREFIX}/codex-real"
CODEX_HOST="${CODEX_PREFIX}/codex-code-mode-host"
CODEX_MANIFEST="${CODEX_ROOT}/codex-package.json"
CODEX_STAMP="${CODEX_PREFIX}/.version"
CODEX_LINK="/usr/local/bin/codex"
CODEX_RELEASE_API="https://api.github.com/repos/openai/codex/releases/latest"
@@ -28,6 +38,60 @@ run_as_runtime_user() {
s6-setuidgid abc env HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" "$@"
}
# What "installed" means, in one place. A Codex that is missing its code-mode host, its package
# manifest or its version stamp still starts, authenticates and answers — it simply cannot run a
# single tool call — so presence of the executable alone is not a usable install. The stamp counts
# because it is removed before the first file of a replacement is moved and written after the last,
# so its absence next to an executable means the tree may mix two releases.
codex_install_is_complete() {
[ -x "$CODEX_REAL" ] \
&& [ -x "$CODEX_HOST" ] \
&& [ -f "$CODEX_MANIFEST" ] \
&& [ -f "$CODEX_STAMP" ]
}
# Move a verified package tree from staging into the install prefix. Called only from an `if`
# condition, where `set -e` does not apply, so every step reports failure explicitly.
#
# Whatever the package ships is installed, rather than the file names this add-on happens to know
# about today: a helper added by a future release has to arrive beside codex-real on its own, or it
# fails exactly the way the missing code-mode host did. Only paths the archive actually contains are
# touched — /data/codex also holds this install's staging directory, so the tree below it is never
# cleared wholesale.
#
# The long, failure-prone part of an install — the download and its digest check — is already done
# by the time this runs; what is left is same-filesystem renames of an already validated tree. They
# are not one atomic operation, so the version stamp is removed first: any interruption leaves a
# stamp-less prefix, which the next boot treats as "not installed" and replaces wholesale. The
# entrypoint is moved last, so a prefix whose codex-real is the new release is a prefix whose
# helper binaries are the new release too.
install_codex_package() {
local staged="$1"
local entry name
rm -f -- "$CODEX_STAMP" || return 1
# Everything beside bin/ first — the manifest and the helper directories (codex-resources/ and
# codex-path/ today, holding bubblewrap, zsh and ripgrep) — then everything the package puts in
# bin/ except the entrypoint, then the entrypoint. The existing launcher and version stamp are
# never matched: the launcher is skipped by name and the stamp is a dot file.
for entry in "${staged}"/*; do
name="${entry##*/}"
if [ ! -e "$entry" ] || [ "$name" = "bin" ]; then
continue
fi
rm -rf -- "${CODEX_ROOT:?}/${name}" || return 1
mv -f -- "$entry" "${CODEX_ROOT}/${name}" || return 1
done
for entry in "${staged}"/bin/*; do
name="${entry##*/}"
if [ ! -e "$entry" ] || [ "$name" = "codex" ]; then
continue
fi
rm -rf -- "${CODEX_PREFIX:?}/${name}" || return 1
mv -f -- "$entry" "${CODEX_PREFIX}/${name}" || return 1
done
mv -f -- "${staged}/bin/codex" "$CODEX_REAL" || return 1
}
if ! bashio::config.true 'install_codex_cli'; then
# Non-destructive: preserve the binary and completed ChatGPT sign-in for a later re-enable.
# 82-claude_tools.sh removes only the MCP registration and managed guidance.
@@ -44,7 +108,7 @@ case "$(uname -m)" in
;;
esac
CODEX_ASSET="codex-${CODEX_TARGET}.tar.gz"
CODEX_ASSET="codex-package-${CODEX_TARGET}.tar.gz"
mkdir -p "$CODEX_PREFIX"
# Migrate the PR's earlier direct-binary layout to the enforced wrapper layout without another
@@ -116,38 +180,47 @@ PY
fi
if [ -z "$release_info" ]; then
if [ -x "$CODEX_REAL" ] && run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then
if codex_install_is_complete && run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then
bashio::log.warning "Unable to resolve the latest verified Codex release; keeping the existing install"
else
bashio::log.warning "Unable to resolve the latest verified Codex release; Codex is unavailable this boot"
exit 0
bashio::log.warning "Unable to resolve the latest verified Codex release; the installed Codex is missing or incomplete and stays unavailable until a boot can reach the release metadata"
fi
else
IFS=$'\t' read -r CODEX_WANTED CODEX_SHA256 CODEX_URL <<< "$release_info"
if [ -x "$CODEX_REAL" ] \
# An install is complete only if the code-mode host and the package manifest are there too:
# every install made before this add-on switched to the package asset has a working codex-real
# and no helpers, and repairs itself here rather than needing a fresh /data. Running the binary
# also rejects one built for another architecture, which a restored backup could leave behind.
if codex_install_is_complete \
&& [ "$(cat "$CODEX_STAMP" 2> /dev/null || true)" = "$CODEX_WANTED" ] \
&& run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then
bashio::log.info "Codex CLI ${CODEX_WANTED} already installed (latest stable)"
else
bashio::log.info "Installing latest stable Codex CLI ${CODEX_WANTED} (${CODEX_TARGET}); this is a large one-time download"
archive="${codex_tmp}/${CODEX_ASSET}"
extracted="${codex_tmp}/codex-${CODEX_TARGET}"
staged="${codex_tmp}/package"
# Fail open for add-on startup but fail closed for the candidate binary: its official
# Fail open for add-on startup but fail closed for the candidate release: its official
# release digest must match before extraction or execution, and replacement happens only
# after the staged binary successfully runs.
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 600 \
-o "$archive" "$CODEX_URL" \
# after the staged tree is complete and its entrypoint successfully runs. The candidate is
# exercised in staging with its own codex-package.json and helper directories in place, so
# the layout Codex will resolve at runtime is the layout that was validated.
if mkdir -p "$staged" \
&& chmod 0755 "$staged" \
&& curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 600 \
-o "$archive" "$CODEX_URL" \
&& printf '%s %s\n' "$CODEX_SHA256" "$archive" | sha256sum -c - > /dev/null \
&& tar -xzf "$archive" -C "$codex_tmp" \
&& [ -f "$extracted" ] \
&& chmod 0755 "$extracted" \
&& run_as_runtime_user "$extracted" --version > /dev/null 2>&1 \
&& mv -f "$extracted" "$CODEX_REAL"; then
&& tar -xzf "$archive" -C "$staged" \
&& [ -f "${staged}/codex-package.json" ] \
&& [ -f "${staged}/bin/codex" ] \
&& [ -f "${staged}/bin/codex-code-mode-host" ] \
&& chmod 0755 "${staged}/bin/codex" "${staged}/bin/codex-code-mode-host" \
&& run_as_runtime_user "${staged}/bin/codex" --version > /dev/null 2>&1 \
&& install_codex_package "$staged"; then
printf '%s' "$CODEX_WANTED" > "$CODEX_STAMP"
bashio::log.info "Codex CLI installed: $("$CODEX_REAL" --version 2> /dev/null || echo unknown)"
elif [ -x "$CODEX_REAL" ]; then
elif codex_install_is_complete; then
bashio::log.warning "Verified Codex ${CODEX_WANTED} installation failed; keeping the existing install"
else
bashio::log.warning "Verified Codex ${CODEX_WANTED} installation failed; Codex is unavailable this boot"
@@ -155,7 +228,16 @@ else
fi
fi
if [ ! -x "$CODEX_REAL" ]; then
# The launcher is the add-on's single "Codex is usable" signal: 82-claude_tools.sh registers the
# Codex MCP server when it is executable and re-checks nothing else. Write it only for a complete
# install, and remove it — together with the PATH symlink — for an incomplete one. Both the launcher
# and the package tree live in /data and survive restarts independently, so a launcher left from an
# earlier boot would otherwise outlive the install it was written for and advertise a Codex whose
# every tool call fails. The executable, the package tree and the ChatGPT sign-in are all left in
# place: a later boot completes the install without another download or another login.
if ! codex_install_is_complete; then
rm -f -- "$CODEX_BIN" "$CODEX_LINK"
bashio::log.warning "Codex is not completely installed; not registering it this boot"
exit 0
fi

View File

@@ -183,6 +183,16 @@ if bashio::config.true 'install_codex_cli'; then
if [ -x "$codex_bin" ]; then
printf '%-30s %s\n' "installed" "$("$codex_bin" --version 2> /dev/null || echo 'FAILED TO RUN')"
printf '%-30s %s\n' "installed version stamp" "$(cat /data/codex/bin/.version 2> /dev/null || echo 'MISSING')"
# Codex runs every shell and file-read tool call through this companion binary. When it is
# absent the CLI still starts, authenticates and answers, but each tool call fails and the
# run still exits 0 — so report it explicitly rather than leaving it to be inferred.
if [ -x /data/codex/bin/codex-code-mode-host ] \
&& [ -f /data/codex/codex-package.json ] \
&& [ -f /data/codex/bin/.version ]; then
printf '%-30s %s\n' "package layout" "complete"
else
printf '%-30s %s\n' "package layout" "INCOMPLETE - tool calls will fail; restart the add-on to reinstall"
fi
printf '%-30s %s\n' "release policy" "latest stable, SHA-256 verified"
printf '%-30s %s\n' "authentication policy" "ChatGPT subscription only"

3
comicarr/CHANGELOG.md Normal file
View File

@@ -0,0 +1,3 @@
## 0.34.0 (20-08-2026)
- Initial release, based on upstream frankieramirez/comicarr 0.34.0 (changelog : https://github.com/frankieramirez/comicarr/releases)
- Home Assistant ingress support through a bundled nginx reverse proxy

124
comicarr/Dockerfile Normal file
View File

@@ -0,0 +1,124 @@
#============================#
# ALEXBELGIUM'S DOCKERFILE #
#============================#
# _.------.
# _.-` ('>.-`"""-.
# '.--'` _'` _ .--.)
# -' '-.-';` `
# ' - _.' ``'--.
# '---` .-'""`
# /`
#=== Home Assistant Addon ===#
#################
# 1 Build Image #
#################
ARG BUILD_FROM
ARG BUILD_VERSION
ARG BUILD_UPSTREAM="0.34.0"
FROM ghcr.io/frankieramirez/comicarr:${BUILD_UPSTREAM}
##################
# 2 Modify Image #
##################
USER root
# No S6_* tuning here : the upstream image is a plain python:3.12-slim with no
# s6-overlay, so the vars the other addons set would be read by nobody
##################
# 3 Install apps #
##################
# Add rootfs
# Absolute paths on purpose : the upstream image sets WORKDIR /opt/comicarr, so
# the relative "find ." used by the other addons would miss /etc entirely
COPY rootfs/ /
RUN find /etc/cont-init.d /etc/services.d -type f \( -name "*.sh" -o -name "run" \) -print -exec chmod +x {} \;
# Uses /bin for compatibility purposes
# hadolint ignore=DL4005
RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; fi && \
if [ ! -f /bin/bash ] && [ -f /usr/bin/bash ]; then ln -s /usr/bin/bash /bin/bash; fi
# Modules
ARG MODULES="00-banner.sh 00-global_var.sh 01-custom_script.sh 00-local_mounts.sh 00-smb_mounts.sh"
# Automatic modules download
# Runs before the apps installer on purpose (the repo-wide order) : this script
# bootstraps bash, curl and ca-certificates itself, which the slim base lacks,
# and the apps installer below decides what to install by grepping the modules
# it downloads here
COPY ha_automodules.sh /ha_automodules.sh
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
# Manual apps
ENV PACKAGES="nginx"
# Automatic apps & bashio
COPY ha_autoapps.sh /ha_autoapps.sh
RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.sh
################
# 4 Entrypoint #
################
# The upstream image ships no s6-overlay, so ha_entrypoint runs as pid 1 : it
# executes /etc/cont-init.d, then supervises /etc/services.d. This replaces the
# upstream /entrypoint.sh, which services.d/comicarr/run still calls when the
# user asks for an unprivileged uid.
COPY ha_entrypoint.sh /ha_entrypoint.sh
RUN chmod 0755 /ha_entrypoint.sh
ENTRYPOINT ["/ha_entrypoint.sh"]
# Install bashio
COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
RUN chmod 0755 /usr/local/lib/bashio-standalone.sh
############
# 5 Labels #
############
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME
ARG BUILD_REF
ARG BUILD_REPOSITORY
ARG BUILD_VERSION
ENV BUILD_VERSION="${BUILD_VERSION}"
LABEL \
io.hass.name="${BUILD_NAME}" \
io.hass.description="${BUILD_DESCRIPTION}" \
io.hass.arch="${BUILD_ARCH}" \
io.hass.type="addon" \
io.hass.version=${BUILD_VERSION} \
maintainer="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.title="${BUILD_NAME}" \
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
org.opencontainers.image.vendor="Home Assistant Add-ons" \
org.opencontainers.image.authors="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.url="https://github.com/alexbelgium" \
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/main/README.md" \
org.opencontainers.image.created=${BUILD_DATE} \
org.opencontainers.image.revision=${BUILD_REF} \
org.opencontainers.image.version=${BUILD_VERSION}
#################
# 6 Healthcheck #
#################
# First boot runs the alembic migrations against a cold sqlite database, which
# is slow on a low-end arm board : leave it time to settle before failing
ENV HEALTH_PORT="8090" \
HEALTH_URL="/api/health"
HEALTHCHECK \
--interval=30s \
--retries=5 \
--start-period=180s \
--timeout=25s \
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" >/dev/null 2>&1 || exit 1

95
comicarr/README.md Normal file
View File

@@ -0,0 +1,95 @@
# Home Assistant Add-on: Comicarr
Automated comic book and manga downloader and library manager with a modern React UI.
[Comicarr](https://comicarr.com) is a fork of Mylar3 rebuilt around a React frontend and a
FastAPI backend. You add series, and it watches for new issues, sends them to your download
client, tags them and files them into your library.
## About
- Track comic series and manga, and grab new issues as they are released
- Works with SABnzbd, NZBGet, blackhole and torrent clients
- Metadata from ComicVine and Metron, with automatic tagging
- One-command migration from an existing Mylar3 installation
- OPDS feed for third-party readers
## Installation
1. Add this repository to Home Assistant.
2. Install the **Comicarr** add-on.
3. Start the add-on and open it from the sidebar (ingress), or on port `8090` at
`http://homeassistant:8090`.
4. Complete the first-run setup when the web interface asks for it.
5. Point Comicarr's library and download folders at a persistent location such as
`/media/comics` and `/share/downloads`.
The first start takes longer than usual: the database migrations run against a cold SQLite
database.
## Configuration
| Option | Description |
|--------|-------------|
| `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`. |
| `localdisks` | Local disks to mount, e.g. `sda1` or a disk label. |
| `networkdisks` | SMB shares to mount, e.g. `//192.168.1.2/comics`. Mounted under `/mnt`. |
| `cifsusername` / `cifspassword` / `cifsdomain` | Credentials for the SMB shares. |
| `smbv1` | Allow the legacy SMBv1 protocol. |
| `env_vars` | Extra environment variables passed to Comicarr. See the [wiki](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2). |
`COMICARR_LOG_LEVEL` (`0`, `1` or `2`) is a useful `env_vars` entry: it overrides the log
verbosity chosen in Settings on every restart.
With the default `PUID`/`PGID` of `0`, Comicarr runs as root, which is what lets it write to
Home Assistant's root-owned `/media` and `/share`. Setting `PUID` to any other value hands
startup to the upstream entrypoint, which creates a matching user and drops privileges — the
library and download folders then have to be writable by that user. Switching an existing
installation from `0` to an unprivileged uid also leaves the files already written under
`/config/comicarr` owned by root; chown them yourself, or Comicarr will fail the first time it
writes its configuration or database.
The web interface port is fixed at `8090`. Changing **Settings → Interface → port** has no
effect: the add-on forces `8090` on startup, because ingress and the health check are built
around it.
## Ingress and URLs
Comicarr has no url-base setting, so the add-on bundles an nginx reverse proxy that rewrites the
absolute `/assets`, `/api` and `/cache` urls in the served HTML, JavaScript and CSS onto the
ingress path, and replaces the upstream `X-Frame-Options: DENY` and `frame-ancestors 'none'`
headers, which would otherwise leave the panel blank.
Two consequences worth knowing:
- The app's client-side router does not know about the ingress prefix. It rewrites the panel's
address to `/` shortly after loading. Everything keeps working, because every request url is
rewritten to an absolute ingress path — but reloading the panel frame itself (rather than
reopening it from the sidebar) shows Home Assistant instead of Comicarr.
- Two places in the app navigate with `window.location` rather than the router: finishing the
first-run setup, and a session expiring while the dashboard is open. Both leave the panel;
reopening Comicarr from the sidebar recovers.
External clients — OPDS readers in particular — must use the direct `http://homeassistant:8090`
url. Ingress is browser-session based, so those clients cannot authenticate through it.
Do not enable HTTPS inside Comicarr's own settings: the add-on's proxy talks plain HTTP to it on
`127.0.0.1`, and ingress would stop working.
## Data
Comicarr's `config.ini`, database, logs and cover cache live in `/config/comicarr` inside the
add-on, which Home Assistant maps to this add-on's own configuration directory —
`/addon_configs/<repository_id>_comicarr`, browsable with the Filebrowser add-on. They survive
add-on updates. That is the same layout as the upstream `./config:/config` compose volume, so an
existing installation can be copied in as is.
Comic and download folders are **not** stored there. Point them at `/media`, `/share` or a
mounted disk. The `/comics`, `/manga` and `/downloads` paths used by the upstream docker image
are not persistent in Home Assistant — do not use them.
## Support
- [Comicarr upstream project](https://github.com/frankieramirez/comicarr)
- [Add-on repository issues](https://github.com/alexbelgium/hassio-addons/issues)

68
comicarr/apparmor.txt Normal file
View File

@@ -0,0 +1,68 @@
#include <tunables/global>
profile comicarr_addon flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
capability chown,
capability dac_override,
capability dac_read_search,
capability fowner,
capability setgid,
capability setuid,
capability sys_chroot,
capability sys_admin,
file,
signal,
mount,
umount,
remount,
network udp,
network tcp,
network dgram,
network stream,
network inet,
network inet6,
network netlink raw,
network unix dgram,
# Entrypoint stack
/init ix,
/run/{s6,s6-rc*,service}/** ix,
/package/** ix,
/command/** ix,
/run/{,**} rwk,
/dev/tty rw,
/bin/** ix,
/usr/bin/** ix,
/usr/lib/bashio/** ix,
/etc/s6/** rix,
/run/s6/** rix,
/etc/services.d/** rwix,
/etc/cont-init.d/** rwix,
/etc/cont-finish.d/** rwix,
/init rix,
/var/run/** mrwkl,
/var/run/ mrwkl,
/dev/i2c-1 mrwkl,
# Files required
/dev/fuse mrwkl,
/dev/sda1 mrwkl,
/dev/sdb1 mrwkl,
/dev/nvme0 mrwkl,
/dev/nvme1 mrwkl,
/dev/mmcblk0p1 mrwkl,
/dev/* mrwkl,
/tmp/** mrkwl,
# Data access
/data/** rw,
# suppress ptrace denials when using 'docker ps' or using 'ps' inside a container
ptrace (trace,read) peer=docker-default,
# docker daemon confinement requires explicit allow rule for signal
signal (receive) set=(kill,term) peer=/usr/bin/docker,
}

6
comicarr/build.json Normal file
View File

@@ -0,0 +1,6 @@
{
"build_from": {
"aarch64": "ghcr.io/frankieramirez/comicarr:latest",
"amd64": "ghcr.io/frankieramirez/comicarr:latest"
}
}

105
comicarr/config.yaml Normal file
View File

@@ -0,0 +1,105 @@
arch:
- aarch64
- amd64
description:
Automated comic book and manga downloader and library manager with a modern
React UI
devices:
- /dev/dri
- /dev/dri/card0
- /dev/dri/card1
- /dev/dri/renderD128
- /dev/vchiq
- /dev/video10
- /dev/video11
- /dev/video12
- /dev/video13
- /dev/video14
- /dev/video15
- /dev/video16
- /dev/ttyUSB0
- /dev/sda
- /dev/sdb
- /dev/sdc
- /dev/sdd
- /dev/sde
- /dev/sdf
- /dev/sdg
- /dev/nvme
- /dev/nvme0
- /dev/nvme0n1
- /dev/nvme0n1p1
- /dev/nvme0n1p2
- /dev/nvme0n1p3
- /dev/nvme1n1
- /dev/nvme1n1p1
- /dev/nvme1n1p2
- /dev/nvme1n1p3
- /dev/nvme2n1
- /dev/nvme2n1p1
- /dev/nvme2n1p2
- /dev/nvme2n3p3
- /dev/mmcblk
- /dev/fuse
- /dev/sda1
- /dev/sdb1
- /dev/sdc1
- /dev/sdd1
- /dev/sde1
- /dev/sdf1
- /dev/sdg1
- /dev/sda2
- /dev/sdb2
- /dev/sdc2
- /dev/sdd2
- /dev/sde2
- /dev/sdf2
- /dev/sdg2
- /dev/sda3
- /dev/sdb3
- /dev/sda4
- /dev/sdb4
- /dev/sda5
- /dev/sda6
- /dev/sda7
- /dev/sda8
- /dev/nvme0
- /dev/nvme1
- /dev/nvme2
image: ghcr.io/alexbelgium/comicarr-{arch}
ingress: true
init: false
map:
- addon_config:rw
- media:rw
- share:rw
name: Comicarr
options:
env_vars: []
PGID: 0
PUID: 0
panel_icon: mdi:book-open-page-variant
ports:
8090/tcp: 8090
ports_description:
8090/tcp: Web interface and OPDS feed
privileged:
- SYS_ADMIN
- DAC_READ_SEARCH
schema:
env_vars:
- name: match(^[A-Za-z0-9_]+$)
value: str?
PGID: int
PUID: int
TZ: str?
cifsdomain: str?
cifspassword: str?
cifsusername: str?
localdisks: str?
networkdisks: str?
smbv1: bool?
slug: comicarr
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/comicarr
version: "0.34.0"

BIN
comicarr/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
comicarr/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,28 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Comicarr keeps its config.ini, sqlite database, logs and cover cache in the
# datadir it is started with. /config is the addon_config mount, so using
# /config/comicarr reproduces the layout of the upstream compose file's
# "./config:/config" volume : an existing installation can be copied in as is.
CONFIG_LOCATION="/config/comicarr"
bashio::log.info "Config stored in $CONFIG_LOCATION"
mkdir -p "$CONFIG_LOCATION"
# Numbered 20- on purpose : it must sort after 00-global_var.sh, which is what
# exports PUID/PGID from the addon options. The upstream image sets neither, so
# the fallbacks only apply when the module is absent.
# Not recursive : the cover cache under $CONFIG_LOCATION grows to thousands of
# files, and walking it on every boot would delay startup for no gain. This is
# what the upstream entrypoint does too.
chown "${PUID:-0}:${PGID:-0}" /config "$CONFIG_LOCATION"
# The upstream entrypoint installs the timezone when it runs, and the default
# path in services.d/comicarr/run bypasses it, so do it here for both paths.
if [ -n "${TZ:-}" ] && [ -f "/usr/share/zoneinfo/${TZ}" ]; then
ln -sf "/usr/share/zoneinfo/${TZ}" /etc/localtime
echo "${TZ}" > /etc/timezone
fi

View File

@@ -0,0 +1,17 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
#################
# NGINX SETTING #
#################
declare ingress_interface
declare ingress_port
declare ingress_entry
ingress_port=$(bashio::addon.ingress_port)
ingress_interface=$(bashio::addon.ip_address)
ingress_entry=$(bashio::addon.ingress_entry)
sed -i "s/%%port%%/${ingress_port}/g" /etc/nginx/servers/ingress.conf
sed -i "s/%%interface%%/${ingress_interface}/g" /etc/nginx/servers/ingress.conf
sed -i "s|%%ingress_entry%%|${ingress_entry}|g" /etc/nginx/servers/ingress.conf

View File

@@ -0,0 +1,96 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@@ -0,0 +1 @@
resolver 127.0.0.11 ipv6=off;

View File

@@ -0,0 +1,56 @@
# Run nginx in foreground.
daemon off;
# This is run inside Docker.
user root;
# Pid storage location.
pid /var/run/nginx.pid;
# Set number of worker processes.
worker_processes 1;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
# Write error log to Hass.io add-on log.
error_log /proc/1/fd/1 error;
# Load allowed environment vars
env HASSIO_TOKEN;
# Load dynamic modules.
include /etc/nginx/modules-enabled/*.conf;
# Max num of simultaneous connections by a worker process.
events {
worker_connections 512;
}
http {
include /etc/nginx/includes/mime.types;
log_format hassio '[$time_local] $status '
'$http_x_forwarded_for($remote_addr) '
'$request ($http_user_agent)';
access_log /proc/1/fd/1 hassio;
client_max_body_size 4G;
default_type application/octet-stream;
gzip on;
keepalive_timeout 65;
sendfile on;
server_tokens off;
tcp_nodelay on;
tcp_nopush on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
include /etc/nginx/includes/resolver.conf;
include /etc/nginx/servers/*.conf;
}

View File

@@ -0,0 +1,82 @@
server {
listen %%interface%%:%%port%% default_server;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# The dashboard subscribes to /api/events/stream over SSE ; buffering
# would hold every event back until the buffer fills.
proxy_buffering off;
proxy_connect_timeout 30m;
proxy_send_timeout 30m;
proxy_read_timeout 30m;
# Comicarr refuses to be framed : SecurityHeadersMiddleware sends
# X-Frame-Options: DENY and a CSP carrying frame-ancestors 'none', which
# on their own leave the ingress panel blank. Replace both with the same
# policy narrowed to the Home Assistant origin that serves the panel.
# The CSP below is upstream's list verbatim except for two directives :
# frame-ancestors becomes 'self', and img-src takes any https origin
# instead of the metadata-provider allowlist upstream compiles into the
# header -- that allowlist grows with upstream releases, and a stale copy
# kept here would silently stop covers from loading.
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'" always;
# FastAPI's redirect-slash Location headers are built against the address
# nginx talks to and carry no ingress prefix ; the second rule covers an
# already relative Location.
absolute_redirect off;
proxy_redirect http://127.0.0.1:8090/ %%ingress_entry%%/;
proxy_redirect / %%ingress_entry%%/;
# Keep the session cookie on the ingress path rather than the Home
# Assistant root, so it is not sent to Home Assistant itself nor to any
# other add-on's ingress panel. Cookies are matched against the request
# path, and every request the app makes is rewritten below to sit under
# the ingress entry, so this does not cost the session.
proxy_cookie_path / %%ingress_entry%%/;
# Comicarr has no url-base setting of any kind : vite emits /assets/...
# with no base, and the api client, the SSE hook and the cover <img>
# tags all build absolute /api/... and /cache/... urls. Ingress strips
# its own prefix before forwarding, so the prefix has to be put back
# into what the browser sees. Only html (implicit), javascript and css
# are scanned -- json responses, cover images and archive bodies stream
# through untouched.
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types application/javascript text/javascript text/css;
sub_filter '"/assets/' '"%%ingress_entry%%/assets/';
sub_filter "'/assets/" "'%%ingress_entry%%/assets/";
sub_filter 'url(/assets/' 'url(%%ingress_entry%%/assets/';
sub_filter '"/api/' '"%%ingress_entry%%/api/';
sub_filter "'/api/" "'%%ingress_entry%%/api/";
sub_filter '`/api/' '`%%ingress_entry%%/api/';
sub_filter '"/cache/' '"%%ingress_entry%%/cache/';
sub_filter "'/cache/" "'%%ingress_entry%%/cache/";
sub_filter '`/cache/' '`%%ingress_entry%%/cache/';
sub_filter '"/favicon.ico"' '"%%ingress_entry%%/favicon.ico"';
# Rewritten javascript and css must not be kept under upstream's one
# year immutable policy for /assets : those file names are content
# hashed upstream, so a change to the rules above would otherwise never
# reach a browser that already holds the old transformed bundle. Every
# other response is already sent as no-cache by the app, so this
# overrides nothing else.
proxy_hide_header Cache-Control;
add_header Cache-Control "no-cache" always;
}
}

View File

@@ -0,0 +1,31 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# ==============================================================================
# The upstream /entrypoint.sh creates a "comicarr" user out of PUID/PGID and
# gosu's to it, but it runs "useradd -u $PUID" under set -e : with PUID=0 -- the
# default in this repo, and the only value that can write Home Assistant's
# root-owned /media and /share -- useradd refuses the duplicate uid and takes
# the whole container down. So the default path starts the app directly as root,
# the same choice the komga add-on makes, and the upstream entrypoint is used
# only when the user asks for an unprivileged uid.
#
# --port is forced on both paths. HTTP_PORT is writable from the Settings page,
# and changing it there would silently break nginx's proxy_pass and the
# healthcheck, leaving an add-on that looks healthy and serves nothing.
umask "${UMASK:-002}"
if [ "${PUID:-0}" != "0" ]; then
bashio::log.info "Starting Comicarr as ${PUID}:${PGID:-0} ..."
exec /entrypoint.sh --port 8090
fi
bashio::log.info "Starting Comicarr..."
cd /opt/comicarr
exec python /opt/comicarr/Comicarr.py \
--nolaunch \
--datadir /config/comicarr \
--port 8090

View File

@@ -0,0 +1,35 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# ==============================================================================
# Wait for Comicarr to answer before nginx starts serving ingress. First boot
# runs the alembic migrations against a cold database, so leave a wide margin,
# but poll rather than call bashio::net.wait_for : bashio takes (port host
# timeout) while the bundled bashio-standalone.sh takes (host port timeout), and
# picking the wrong one would either fail instantly or block for the whole
# timeout.
# The per probe timeouts keep the 15 minute ceiling real : without them a half
# open connection would hang a single probe, and the loop, forever.
# A wall clock deadline, not an attempt count : a failed probe costs up to
# max-time on top of the sleep, so counting attempts would stretch the wait to
# roughly twice the advertised ceiling.
comicarr_ready=false
deadline=$((SECONDS + 900))
while [ "$SECONDS" -lt "$deadline" ]; do
if curl -sf --connect-timeout 2 --max-time 5 -o /dev/null "http://127.0.0.1:8090/api/health"; then
comicarr_ready=true
break
fi
sleep 5
done
# Deliberately not fatal : nginx serving a 502 tells the user something is wrong
# and starts working by itself once Comicarr finally answers, while refusing to
# start would take ingress down for good after ha_entrypoint gives up retrying.
if [ "$comicarr_ready" != true ]; then
bashio::log.warning "Comicarr did not answer within 15 minutes. Starting NGinx anyway : ingress will return 502 until it does."
fi
bashio::log.info "Starting NGinx..."
exec nginx

10
comicarr/updater.json Normal file
View File

@@ -0,0 +1,10 @@
{
"github_beta": "false",
"github_fulltag": false,
"last_update": "2026-08-20",
"repository": "alexbelgium/hassio-addons",
"slug": "comicarr",
"source": "github",
"upstream_repo": "frankieramirez/comicarr",
"upstream_version": "0.34.0"
}

View File

@@ -1,4 +1,12 @@
## 1.5.1.2 (2026-08-19)
- Fix direct access on port 8071, which was broken in 1.5.1.1: the root
redirect pointed at the container-internal port 8072 instead of the
published one, and the page it led to referenced assets under a path the
add-on did not serve, so every asset returned 404. Requests are now passed
through unchanged, with the bare root and the two previously documented
`/filebrowser_quantum` URLs redirected to the app's configured base path.
## 1.5.1.1 (2026-08-16)
- Expose the web UI on host port 8071, reachable at `<your-ip>:8071`
(redirects to `/filebrowser_quantum/`). Direct access is served by a new,

View File

@@ -42,11 +42,11 @@ comparison to installing any other Home Assistant add-on.
1. Click the `Save` button to store your configuration.
1. Start the add-on.
1. Check the logs of the add-on to see if everything went well.
1. Access the web UI through the sidebar or at `<your-ip>:8071/filebrowser_quantum/`.
1. Access the web UI through the sidebar or at `<your-ip>:8071`.
## Configuration
The web UI can be found at `<your-ip>:8071` (redirects to `/filebrowser_quantum/`) or through the Home Assistant sidebar when using Ingress.
The web UI can be found at `<your-ip>:8071` or through the Home Assistant sidebar when using Ingress. Direct access redirects to the add-on's configured base path, so the address bar will show a longer URL than the one you typed.
**Default credentials:**
- Username: `admin`

View File

@@ -118,4 +118,4 @@ schema:
slug: filebrowser_quantum
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.5.1.1"
version: "1.5.1.2"

View File

@@ -43,13 +43,9 @@ declare ingress_interface
declare ingress_port
#declare keyfile
# The app's own baseURL is always the Supervisor ingress-entry path — this is
# unchanged from before. FileBrowser Quantum has no known "ignore baseURL for
# routing" leniency the way classic filebrowser's app does, so ingress access
# is left completely untouched here. Direct ip:port access is handled below by
# a second, separate nginx vhost (direct.conf) that rewrites a fixed public
# path onto this same ingress-entry baseURL, instead of changing the baseURL
# itself.
# The app's own baseURL is the Supervisor ingress-entry path, unchanged from
# before: FileBrowser emits that prefix as absolute links in its HTML and JS,
# so it is also the path direct ip:port access has to use (see direct.conf).
FB_BASEURL=$(bashio::addon.ingress_entry)
export FB_BASEURL
@@ -67,11 +63,11 @@ sed -i "s|%%port%%|${ingress_port}|g" /etc/nginx/servers/ingress.conf
sed -i "s|%%interface%%|${ingress_interface}|g" /etc/nginx/servers/ingress.conf
sed -i "s|%%subpath%%|${FB_BASEURL}/|g" /etc/nginx/servers/ingress.conf
# --- Direct ip:port access (separate from ingress, see comment above) ---
# Publishes a second nginx vhost on a fixed internal port (published to the
# host as 8071 via config.yaml's `ports:`), at a fixed public path
# (/filebrowser_quantum/), that proxies to the same backend the ingress vhost
# uses. This keeps the app's own baseURL, and therefore ingress, unchanged.
# --- Direct ip:port access (separate vhost, ingress untouched) ---
# Listens on 8072, published to the host as 8071 by config.yaml's `ports:`.
# Requests are passed through unchanged; the bare root and the two legacy
# /filebrowser_quantum paths are redirected to the app's baseURL, which is what
# its own links already point at.
sed -i "s|%%protocol%%|${ADDON_PROTOCOL}|g" /etc/nginx/servers/direct.conf
sed -i "s|%%subpath%%|${FB_BASEURL}/|g" /etc/nginx/servers/direct.conf

View File

@@ -6,19 +6,35 @@ server {
client_max_body_size 0;
# nginx listens on 8072 inside the container but is published to the host
# as 8071. An absolute redirect would be built from $server_port and send
# the browser to :8072, which is not published and therefore unreachable.
absolute_redirect off;
# FileBrowser serves under its baseURL (the Supervisor ingress entry) and
# emits that prefix as absolute links in its HTML/JS, so the browser must
# use that same path here. The bare root and the two legacy paths below
# redirect to it; every other request is proxied through untouched, which
# keeps asset, API and websocket URLs working without response rewriting.
location = / {
return 302 /filebrowser_quantum/;
return 302 %%subpath%%;
}
# 1.5.1.1 briefly documented /filebrowser_quantum/ as the direct URL. The
# app never served that path itself, so send those bookmarks on instead of
# letting them fall through to a 404.
location = /filebrowser_quantum {
return 301 /filebrowser_quantum/;
return 302 %%subpath%%;
}
location /filebrowser_quantum/ {
add_header Access-Control-Allow-Origin *;
location = /filebrowser_quantum/ {
return 302 %%subpath%%;
}
location / {
proxy_connect_timeout 30m;
proxy_send_timeout 30m;
proxy_read_timeout 30m;
proxy_pass %%protocol%%://backend%%subpath%%;
proxy_pass %%protocol%%://backend;
}
}

View File

@@ -1,4 +1,7 @@
## 3.1.0.1 (2026-08-17)
- Fix `password authentication failed for user` when `DB_PASSWORD` contains special characters. Passwords are now URI-encoded before being used in the psql connection string, and SQL-escaped before being used in `CREATE`/`ALTER USER` statements
## 3.1.0 (2026-08-01)
- Update to latest version from immich-app/immich (changelog : https://github.com/immich-app/immich/releases)

View File

@@ -141,6 +141,6 @@ slug: immich
udev: true
url: https://github.com/alexbelgium/hassio-addons
usb: true
version: "3.1.0"
version: "3.1.0.1"
video: true
webui: http://[HOST]:[PORT:8080]

View File

@@ -98,10 +98,11 @@ setup_root_user() {
fi
# Check if the root user exists.
if ! psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}" -tAc "SELECT 1 FROM pg_roles WHERE rolname='root'" | grep -q 1; then
if ! psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}" -tAc "SELECT 1 FROM pg_roles WHERE rolname='root'" | grep -q 1; then
bashio::log.info "Root user does not exist. Creating root user with DB_ROOT_PASSWORD..."
psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}" << EOF
CREATE ROLE root WITH LOGIN SUPERUSER CREATEDB CREATEROLE PASSWORD '${DB_ROOT_PASSWORD}';
local root_password_sql="${DB_ROOT_PASSWORD//\'/\'\'}"
psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}" << EOF
CREATE ROLE root WITH LOGIN SUPERUSER CREATEDB CREATEROLE PASSWORD '${root_password_sql}';
EOF
else
bashio::log.info "Root user exists with a non-default password. No migration needed."
@@ -113,10 +114,10 @@ setup_database() {
bashio::log.info "Setting up external PostgreSQL database..."
# Create the database if it does not exist
if ! psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}/postgres" -tAc \
if ! psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}/postgres" -tAc \
"SELECT 1 FROM pg_database WHERE datname='${DB_DATABASE_NAME}';" | grep -q 1; then
bashio::log.info "Database does not exist. Creating it now..."
psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}" << EOF
psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}" << EOF
CREATE DATABASE ${DB_DATABASE_NAME};
EOF
else
@@ -124,20 +125,21 @@ EOF
fi
# Ensure the user exists and update its password
psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}" << EOF
local db_password_sql="${DB_PASSWORD//\'/\'\'}"
psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}" << EOF
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${DB_USERNAME}') THEN
CREATE USER ${DB_USERNAME} WITH ENCRYPTED PASSWORD '${DB_PASSWORD}';
CREATE USER ${DB_USERNAME} WITH ENCRYPTED PASSWORD '${db_password_sql}';
ELSE
ALTER USER ${DB_USERNAME} WITH ENCRYPTED PASSWORD '${DB_PASSWORD}';
ALTER USER ${DB_USERNAME} WITH ENCRYPTED PASSWORD '${db_password_sql}';
END IF;
END
\$\$;
EOF
# Ensure the user has full privileges on the database
psql "postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}" << EOF
psql "postgres://${DB_USERNAME_URI}:${DB_PASSWORD_URI}@${DB_HOSTNAME}:${DB_PORT}" << EOF
GRANT ALL PRIVILEGES ON DATABASE ${DB_DATABASE_NAME} TO ${DB_USERNAME};
EOF
@@ -147,7 +149,7 @@ EOF
# Function to check if the vectors (pgvecto.rs) extension is available on the server
check_vector_extension() {
echo "Checking if 'vectors' extension is available for database '${DB_DATABASE_NAME}'..."
RESULT=$(psql "postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOSTNAME:$DB_PORT/${DB_DATABASE_NAME}" -tAc "SELECT 1 FROM pg_available_extensions WHERE name = 'vectors';")
RESULT=$(psql "postgres://$DB_USERNAME_URI:$DB_PASSWORD_URI@$DB_HOSTNAME:$DB_PORT/${DB_DATABASE_NAME}" -tAc "SELECT 1 FROM pg_available_extensions WHERE name = 'vectors';")
if [[ "$RESULT" == "1" ]]; then
echo "✅ 'vectors' extension is available."
return 0
@@ -163,7 +165,7 @@ check_vector_extension() {
# itself on first startup; checking pg_extension would false-warn on every fresh install.
check_vchord_extension() {
echo "Checking if 'vchord' extension is available for database '${DB_DATABASE_NAME}'..."
RESULT=$(psql "postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOSTNAME:$DB_PORT/${DB_DATABASE_NAME}" -tAc "SELECT 1 FROM pg_available_extensions WHERE name = 'vchord';")
RESULT=$(psql "postgres://$DB_USERNAME_URI:$DB_PASSWORD_URI@$DB_HOSTNAME:$DB_PORT/${DB_DATABASE_NAME}" -tAc "SELECT 1 FROM pg_available_extensions WHERE name = 'vchord';")
if [[ "$RESULT" == "1" ]]; then
echo "✅ 'vchord' extension is available."
return 0
@@ -187,6 +189,14 @@ export DB_PORT="$(bashio::config 'DB_PORT')"
export JWT_SECRET="$(bashio::config 'JWT_SECRET')"
export DB_HOSTNAME="$(bashio::config 'DB_HOSTNAME')"
# libpq percent-decodes the userinfo part of a postgres:// URI, so credentials
# containing reserved characters (% @ / : ? #) are misread and every psql call
# below fails with "password authentication failed". Encode them once here and
# use the encoded copies in URIs only - the app itself still gets the raw value
# through export_db_env. Same approach as the postgres_15/postgres_17 addons.
export DB_USERNAME_URI="$(jq -rn --arg x "$DB_USERNAME" '$x|@uri')"
export DB_PASSWORD_URI="$(jq -rn --arg x "$DB_PASSWORD" '$x|@uri')"
if bashio::config.true 'VIPS_NOVECTOR'; then
export VIPS_NOVECTOR="1"
fi

View File

@@ -1,4 +1,7 @@
## 1.26.3.1 (19-08-2026)
- Fix : tapping `Read` in the Home Assistant companion app opened the reader in an external browser, which carries no ingress session cookie, so Home Assistant answered `401 Unauthorized` before Komga was reached ([#2994](https://github.com/alexbelgium/hassio-addons/issues/2994)). Komga opens the reader with `window.open(url, '_blank')` ; nginx now injects a script that turns that popup into a navigation of the ingress panel itself. Only http(s) urls below Komga's own base path are affected, so the OAuth2 login popup and links out of Komga keep their own window
## 1.26.3 (2026-08-13)
- Update to latest version from gotson/komga (changelog : https://github.com/gotson/komga/releases)
## 1.26.1.4 (12-08-2026)

View File

@@ -101,4 +101,4 @@ schema:
slug: komga
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/komga
version: "1.26.3"
version: "1.26.3.1"

View File

@@ -60,6 +60,25 @@ server {
# Only the json/xml document types are added here, so book pages are
# never scanned.
sub_filter "http://127.0.0.1:25600/komga" "%%ingress_entry%%/komga";
# Komga opens the reader with window.open(url, '_blank'). In the Home
# Assistant companion apps the ingress panel is a webview, which hands
# such a popup to an external browser : that browser carries no ingress
# session cookie, so Home Assistant answers 401 before Komga is even
# reached. Turn that popup into a navigation of the panel itself, but
# only for the call shape Komga uses (name _blank, no feature string)
# and only for http(s) urls below window.resourceBaseUrl. That leaves
# the OAuth2 login popup (window.open(url, 'oauth2Login', '<features>'),
# which needs its own window), blob urls and links out of Komga alone,
# and if Komga ever stopped setting resourceBaseUrl the popup is left
# untouched rather than widened to the whole Home Assistant origin,
# which ingress shares. Anchored on the single page app mount point :
# both Komga ui shells carry it once, and only a book served as
# text/html rather than the xhtml the epub spec mandates could collide
# with it -- the same exposure the /komga filter above already has, and
# Komga sends script-src 'none' on that endpoint.
sub_filter "<div id=\"app\">" "<script>(function(){var o=window.open;window.open=function(u,n,f){try{var b=window.resourceBaseUrl;if(u&&n==='_blank'&&!f&&b){if(b.slice(-1)!=='/')b+='/';var t=new URL(u,location.href);if((t.protocol==='http:'||t.protocol==='https:')&&t.origin===location.origin&&t.pathname.indexOf(b)===0){location.assign(t.href);return window}}}catch(e){}return o.apply(window,arguments)}})();</script><div id=\"app\">";
sub_filter_types application/json application/webpub+json
application/divina+json application/opds+json
application/atom+xml;

View File

@@ -1,4 +1,7 @@
## v0.9.3.1 (2026-08-18)
- Fix startup crash: `collector-once` failed with `s6-svwait: fatal: unable to subscribe to events for /run/service/scrutiny` because the add-on entrypoint never started real s6 supervision. Keep the upstream image's own `/init` as PID 1, same as `scrutiny_original` (#2991) and `scrutiny`/`scrutiny_fa` (#2878).
## v0.9.3 (2026-08-13)
- Update to latest version from analogj/scrutiny (changelog : https://github.com/analogj/scrutiny/releases)

View File

@@ -33,7 +33,8 @@ ENV S6_CMD_WAIT_FOR_SERVICES=1 \
# Add rootfs
COPY rootfs/ /
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \;
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \
if [ -d /command ]; then ln -sf /command/* /usr/bin/; fi
# Uses /bin for compatibility purposes
# hadolint ignore=DL4005
@@ -61,19 +62,35 @@ RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.
# 4 Entrypoint #
################
# Add entrypoint
# Keep the repository initialization hook, but return after it has prepared the
# cont-init scripts. Upstream s6 remains responsible for supervising services.
ENV S6_STAGE2_HOOK=/ha_entrypoint.sh
COPY ha_entrypoint.sh /ha_entrypoint.sh
RUN chmod 777 /ha_entrypoint.sh
RUN chmod 0755 /ha_entrypoint.sh && \
awk '!inserted && $0 == "if $PID1; then" { \
print "if ! $PID1; then"; \
print " echo \"Initialization hook complete\""; \
print " exit 0"; \
print "fi"; \
inserted=1 \
} { print }' /ha_entrypoint.sh > /ha_entrypoint.sh.tmp && \
mv /ha_entrypoint.sh.tmp /ha_entrypoint.sh && \
chmod 0755 /ha_entrypoint.sh
# Install bashio
COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
RUN chmod 0755 /usr/local/lib/bashio-standalone.sh
RUN test -x /init && \
test -x /ha_entrypoint.sh && \
bash -n /ha_entrypoint.sh && \
grep -q 'Initialization hook complete' /ha_entrypoint.sh
RUN sed -i "1a if ! bashio::require.unprotected; then bashio::addon.stop; fi" /etc/cont-init.d/90-run.sh
ENTRYPOINT [ "/usr/bin/env" ]
CMD [ "/ha_entrypoint.sh" ]
# Scrutiny's image already includes s6-overlay and defines all services under
# /etc/services.d. Keep /init as PID 1 so service readiness and s6-svc calls work.
ENTRYPOINT [ "/init" ]
############
# 5 Labels #

View File

@@ -45,4 +45,4 @@ schema:
slug: scrutiny_fa_original
udev: true
url: https://github.com/analogj/scrutiny
version: "v0.9.3"
version: "v0.9.3.1"

View File

@@ -1,4 +1,7 @@
## v0.9.3.1 (2026-08-18)
- Fix startup crash: `collector-once` failed with `s6-svwait: fatal: unable to subscribe to events for /run/service/scrutiny` because the add-on entrypoint never started real s6 supervision. Keep the upstream image's own `/init` as PID 1 (same fix already shipped for `scrutiny`/`scrutiny_fa` in #2878). Closes #2989.
## v0.9.3 (2026-08-13)
- Update to latest version from analogj/scrutiny (changelog : https://github.com/analogj/scrutiny/releases)

View File

@@ -33,7 +33,8 @@ ENV S6_CMD_WAIT_FOR_SERVICES=1 \
# Add rootfs
COPY rootfs/ /
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \;
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \
if [ -d /command ]; then ln -sf /command/* /usr/bin/; fi
# Uses /bin for compatibility purposes
# hadolint ignore=DL4005
@@ -61,17 +62,33 @@ RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.
# 4 Entrypoint #
################
# Add entrypoint
# Keep the repository initialization hook, but return after it has prepared the
# cont-init scripts. Upstream s6 remains responsible for supervising services.
ENV S6_STAGE2_HOOK=/ha_entrypoint.sh
COPY ha_entrypoint.sh /ha_entrypoint.sh
RUN chmod 777 /ha_entrypoint.sh
RUN chmod 0755 /ha_entrypoint.sh && \
awk '!inserted && $0 == "if $PID1; then" { \
print "if ! $PID1; then"; \
print " echo \"Initialization hook complete\""; \
print " exit 0"; \
print "fi"; \
inserted=1 \
} { print }' /ha_entrypoint.sh > /ha_entrypoint.sh.tmp && \
mv /ha_entrypoint.sh.tmp /ha_entrypoint.sh && \
chmod 0755 /ha_entrypoint.sh
# Install bashio
COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
RUN chmod 0755 /usr/local/lib/bashio-standalone.sh
ENTRYPOINT [ "/usr/bin/env" ]
CMD [ "/ha_entrypoint.sh" ]
RUN test -x /init && \
test -x /ha_entrypoint.sh && \
bash -n /ha_entrypoint.sh && \
grep -q 'Initialization hook complete' /ha_entrypoint.sh
# Scrutiny's image already includes s6-overlay and defines all services under
# /etc/services.d. Keep /init as PID 1 so service readiness and s6-svc calls work.
ENTRYPOINT [ "/init" ]
############
# 5 Labels #

View File

@@ -113,4 +113,4 @@ schema:
slug: scrutiny_original
udev: true
url: https://github.com/analogj/scrutiny
version: "v0.9.3"
version: "v0.9.3.1"

View File

@@ -1,4 +1,11 @@
## 3.4.1.3 (2026-08-18)
- Fixed the `404: Not Found` on **Discover** persisting for browsers that had already opened Seerr through ingress, even after 3.4.1.1 and 3.4.1.2 were installed (#2975). Seerr serves its JavaScript bundle with `Cache-Control: public, max-age=31536000, immutable`, and the add-on's nginx rewrites that bundle to carry the ingress prefix - which strips the `ETag` and `Last-Modified` a browser would revalidate with. Since every add-on version served the same upstream build, the chunk URLs never changed either, so a browser kept replaying the broken 3.4.1/3.4.1.1 JavaScript from its own cache for up to a year and no fix could reach it. That is why the report persisted on the origin the reporter uses daily (`https://<domain>/`) while a browser that had never cached it (`http://<ip>:8123/`) already showed the fixed behaviour. The asset paths now carry the add-on version, so each release has its own URLs and the first page load after an update fetches the current bundle. Only ingress was affected; the directly published port 5055 always worked.
## 3.4.1.2 (2026-08-18)
- Fixed **Discover** in the sidebar still failing through ingress after 3.4.1.1 (#2975). The trailing slash added in 3.4.1.1 was also applied to the copy of the link inside Seerr's JavaScript bundle, and Next.js' client-side router strips a trailing slash before navigating: it then sent the click to a URL Home Assistant does not route, so it either landed on the same `404: Not Found` or threw `Invariant: attempted to hard navigate to the same URL` and did nothing at all. The bundle is no longer rewritten, so **Discover** routes inside the app exactly like **Requests**, **Issues** and **Settings** already did. The server-rendered link keeps its trailing slash. Only ingress was affected; the directly published port 5055 always worked.
## 3.4.1.1 (2026-08-16)
- Fixed `404: Not Found` when clicking **Discover** in the sidebar through ingress (#2975). Seerr's Discover link points at `/`, which nginx rewrote to the ingress entry without a trailing slash; Home Assistant only routes ingress on `/api/hassio_ingress/<token>/…`, so the request was rejected by Home Assistant before reaching the add-on. Only ingress was affected; the directly published port 5055 always worked.

View File

@@ -96,4 +96,4 @@ schema:
slug: seerr
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/seerr
version: "3.4.1.1"
version: "3.4.1.3"

View File

@@ -12,10 +12,27 @@ ingress_port=$(bashio::addon.ingress_port)
ingress_interface=$(bashio::addon.ip_address)
ingress_entry=$(bashio::addon.ingress_entry)
# Cache-busting marker for the rewritten JavaScript bundle.
#
# Seerr serves /_next/static/ as "public, max-age=31536000, immutable", and
# nginx's sub_filter strips ETag and Last-Modified off every response it
# rewrites, while the HTML naming those chunks is served "no-store" and keeps
# naming the same URLs. A browser therefore pins the bundle this add-on rewrote
# on its first visit for a year, with no request left that could deliver a
# later change to the sub_filter rules below - which is how #2975 outlived two
# fixes. Folding the version into the asset path gives every release its own
# URLs. njs/ingress.js strips the marker again before proxying.
#
# BUILD_VERSION is the add-on version baked in at build time (it is also what
# bashio::addon.version returns). Only [A-Za-z0-9-] survives: the marker ends up
# inside a regex literal in Seerr's own bundle, where a dot would be a wildcard.
asset_tag="ha-$(printf '%s' "${BUILD_VERSION:-0}" | tr -c 'A-Za-z0-9' '-')"
# Update ingress.conf with actual values
sed -i "s|%%port%%|${ingress_port}|g" /etc/nginx/servers/ingress.conf
sed -i "s|%%interface%%|${ingress_interface}|g" /etc/nginx/servers/ingress.conf
sed -i "s|%%ingress_entry%%|${ingress_entry}|g" /etc/nginx/servers/ingress.conf
sed -i "s|%%ingress_entry_escaped%%|${ingress_entry//\//\\\\\/}|g" /etc/nginx/servers/ingress.conf
sed -i "s|%%asset_tag%%|${asset_tag}|g" /etc/nginx/servers/ingress.conf
bashio::log.info "Nginx ingress configured on ${ingress_interface}:${ingress_port}"
bashio::log.info "Nginx ingress configured on ${ingress_interface}:${ingress_port} (asset tag ${asset_tag})"

View File

@@ -47,11 +47,27 @@ function encodePart(part) {
}
/*
* Returns the request URI with the path untouched byte-for-byte and only the
* query string repaired. Used as the proxy_pass target.
* The cache-busting marker servers/ingress.conf inserts in front of every
* rewritten "/_next" path, e.g. "/ha-3-4-1-3/_next/static/chunks/x.js". It
* gives each add-on release its own asset URLs - Seerr serves /_next/static/ as
* immutable for a year and sub_filter strips the validators, so identical URLs
* would pin the rewritten bundle in the browser forever. Seerr knows nothing
* about the marker, so it is removed again here, on the way in.
*
* Any marker is accepted, not just the one this container serves: a tab opened
* before an add-on update keeps requesting its dynamic chunks under the marker
* it was handed, and those have to keep working until it is reloaded. The
* lookahead keeps a real Seerr path that merely starts with "ha-" untouched.
*/
var ASSET_TAG = /^\/ha-[0-9A-Za-z-]+(?=\/_next(\/|$))/;
/*
* Returns the request URI with the path untouched byte-for-byte apart from the
* cache-busting marker, and only the query string repaired. Used as the
* proxy_pass target.
*/
function uri(r) {
var raw = r.variables.request_uri;
var raw = r.variables.request_uri.replace(ASSET_TAG, "");
var split = raw.indexOf("?");
if (split < 0) {

View File

@@ -43,19 +43,48 @@ server {
sub_filter_once off;
# Do not rewrite every response type blindly.
sub_filter_types text/html application/javascript text/javascript application/json;
# Do not rewrite every response type blindly. text/html is implicit and
# must not be listed - nginx pre-seeds it and warns "duplicate MIME type"
# on every config load if it appears here as well.
sub_filter_types application/javascript text/javascript application/json;
# The trailing slash is required: Home Assistant routes ingress on
# "/api/hassio_ingress/{token}/{path:.*}", so the bare entry without it
# matches no route and Home Assistant answers its own plain-text
# "404: Not Found" before the request ever reaches this add-on. Seerr's
# Discover link is href="/", so without the slash every click on it 404s.
# Seerr's "Discover" sidebar entry, the header logo and the 404 and
# error pages are all <Link href="/">. The server-rendered anchor has to
# carry the ingress prefix *with* a trailing slash: Home Assistant routes
# ingress on "/api/hassio_ingress/{token}/{path:.*}", so a slash-less
# entry matches no route and Home Assistant answers its own plain-text
# "404: Not Found" before the request ever reaches this add-on (#2975).
sub_filter 'href="/"' 'href="$app/"';
sub_filter 'href="/login"' 'href="$app/login"';
sub_filter 'href:"/"' 'href:"$app/"';
sub_filter '\/_next' '%%ingress_entry_escaped%%\/_next';
sub_filter '/_next' '$app/_next';
# A matching rule for 'href:"/"' - the form those same links take once
# compiled into the JS bundle - used to sit here. It is gone on purpose
# and must not come back: it fed the ingress prefix into Next.js' own
# route table, and next/link resolves a pushed href through
# normalizePathTrailingSlash(), which drops a trailing slash while
# `trailingSlash` is false (Seerr sets no override). Next therefore hard
# navigated to the slash-less URL and recreated the same 404; on the root
# page it instead threw "Invariant: attempted to hard navigate to the
# same URL" and the click did nothing. That is the state PR #2976 left
# #2975 in. Left alone the href stays "/", which removeTrailingSlash()
# preserves, so the router matches its own "/" route and transitions
# in-app - the path every other sidebar entry ("/requests", "/issues",
# "/users", "/settings") already takes. Prefixing belongs in the rendered
# anchor, never in the router's route table.
#
# Note that none of these rules are response-type scoped - sub_filter_types
# includes JavaScript - so the anchor rule above avoids the bundle only
# because the compiled output spells the prop 'href:"/"' and not
# 'href="/"'. These are textual substitutions over someone else's minified
# output: recheck them whenever Seerr or Next.js is upgraded.
# "%%asset_tag%%" is a cache-busting marker carrying the add-on version,
# substituted by 32-nginx_ingress.sh - which explains why it is needed.
# In short: without it a browser replays the bundle this file produced at
# the version it first loaded, for a year, and no later change to any
# rule here can reach it. njs/ingress.js strips the marker back off
# before proxying; the two belong together, do not change one alone.
sub_filter '\/_next' '%%ingress_entry_escaped%%\/%%asset_tag%%\/_next';
sub_filter '/_next' '$app/%%asset_tag%%/_next';
sub_filter '/api/v1' '$app/api/v1';
sub_filter '/login/plex/loading' '$app/login/plex/loading';
sub_filter '/images/' '$app/images/';

View File

@@ -0,0 +1,10 @@
## 1.3.1 (19-08-2026)
- Initial release, based on upstream Kapowarr 1.3.1
- Home Assistant ingress support: Kapowarr is started with `--UrlBase /kapowarr` and nginx rewrites
that prefix onto the ingress path, so the sidebar panel works without any user configuration
- The host, port and URL base are re-applied on every start, so a hosting setting changed by hand
in the web interface is repaired by restarting the add-on rather than breaking it permanently
- Database and logs stored in the add-on configuration directory, so they survive updates
- Temporary downloads redirected to persistent storage (`/config/temp_downloads`)
- `PUID`/`PGID`, `TZ`, `env_vars`, local disk and SMB share mounting supported

View File

@@ -0,0 +1,112 @@
#============================#
# ALEXBELGIUM'S DOCKERFILE #
#============================#
# _.------.
# _.-` ('>.-`"""-.
# '.--'` _'` _ .--.)
# -' '-.-';` `
# ' - _.' ``'--.
# '---` .-'""`
# /`
#=== Home Assistant Addon ===#
#################
# 1 Build Image #
#################
ARG BUILD_FROM
ARG BUILD_VERSION
ARG BUILD_UPSTREAM="1.3.1"
FROM ${BUILD_FROM}
##################
# 2 Modify Image #
##################
# No S6_* tuning here : the upstream image is a plain python:slim image with no
# s6-overlay, so the vars the other addons set would be read by nobody
##################
# 3 Install apps #
##################
# Add rootfs
# Absolute paths on purpose : the upstream image sets WORKDIR /app, so the
# relative "find ." used by the other addons would miss /etc entirely
COPY rootfs/ /
RUN find /etc/cont-init.d /etc/services.d -type f \( -name "*.sh" -o -name "run" \) -print -exec chmod +x {} \;
# Modules
ARG MODULES="00-banner.sh 00-global_var.sh 01-custom_script.sh 00-local_mounts.sh 00-smb_mounts.sh"
# Automatic modules download
COPY ha_automodules.sh /ha_automodules.sh
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
# Manual apps
ENV PACKAGES="nginx"
# Automatic apps & bashio
COPY ha_autoapps.sh /ha_autoapps.sh
RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.sh
################
# 4 Entrypoint #
################
# The upstream image has no s6-overlay, so ha_entrypoint runs as pid 1 : it
# executes /etc/cont-init.d, then supervises /etc/services.d. This replaces the
# upstream ENTRYPOINT (/app/entrypoint.sh), which is called again from
# rootfs/etc/services.d/kapowarr/run so its PUID/PGID handling is kept
COPY ha_entrypoint.sh /ha_entrypoint.sh
RUN chmod 777 /ha_entrypoint.sh
ENTRYPOINT ["/ha_entrypoint.sh"]
# Install bashio
COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
RUN chmod 0755 /usr/local/lib/bashio-standalone.sh
############
# 5 Labels #
############
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME
ARG BUILD_REF
ARG BUILD_REPOSITORY
ARG BUILD_VERSION
ENV BUILD_VERSION="${BUILD_VERSION}"
LABEL \
io.hass.name="${BUILD_NAME}" \
io.hass.description="${BUILD_DESCRIPTION}" \
io.hass.arch="${BUILD_ARCH}" \
io.hass.type="addon" \
io.hass.version=${BUILD_VERSION} \
maintainer="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.title="${BUILD_NAME}" \
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
org.opencontainers.image.vendor="Home Assistant Add-ons" \
org.opencontainers.image.authors="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.url="https://github.com/alexbelgium" \
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/main/README.md" \
org.opencontainers.image.created=${BUILD_DATE} \
org.opencontainers.image.revision=${BUILD_REF} \
org.opencontainers.image.version=${BUILD_VERSION}
#################
# 6 Healthcheck #
#################
# Kapowarr is hosted under the /kapowarr url base, see the addon documentation
ENV HEALTH_PORT="5656" \
HEALTH_URL="/kapowarr/"
HEALTHCHECK \
--interval=30s \
--retries=5 \
--start-period=120s \
--timeout=25s \
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" >/dev/null 2>&1 || exit 1

View File

@@ -0,0 +1,81 @@
# Home Assistant Add-on: Kapowarr
Build and manage a comic book library, fitting in the \*arr suite of software.
[Kapowarr](https://casvt.github.io/Kapowarr/) tracks the volumes you own, finds the issues you are
missing, downloads them through GetComics and your download clients, and keeps the files renamed
and converted the way you want them.
## About
- Import an existing comic collection and match it against ComicVine metadata
- Monitor volumes and automatically search for missing issues
- Direct downloads and Mega links, plus torrent and Usenet clients
- Automatic renaming, converting and file management
## Installation
1. Add this repository to Home Assistant.
2. Install the **Kapowarr** add-on.
3. Start the add-on and open it from the sidebar (ingress), or on port `5656` at
`http://homeassistant:5656/kapowarr` — note the `/kapowarr` suffix, see *Ingress and URLs* below.
4. Enter a ComicVine API key under *Settings > Metadata*; Kapowarr cannot search without one.
5. Add a root folder under *Settings > Media Management*, for example `/media/comics` or
`/share/comics`.
## Configuration
| Option | Description |
|--------|-------------|
| `PUID` / `PGID` | Ownership applied to the add-on configuration directory, and the user Kapowarr runs as. Defaults to `0` (root). |
| `TZ` | Timezone, e.g. `Europe/Paris`. |
| `localdisks` | Local disks to mount, e.g. `sda1` or a disk label. |
| `networkdisks` | SMB shares to mount, e.g. `//192.168.1.2/comics`. Mounted under `/mnt`. |
| `cifsusername` / `cifspassword` / `cifsdomain` | Credentials for the SMB shares. |
| `smbv1` | Allow the legacy SMBv1 protocol. |
| `env_vars` | Extra environment variables passed to Kapowarr. See the [wiki](https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2). |
Everything else — root folders, download clients, naming, the ComicVine key — is configured in
Kapowarr's own web interface, not in the add-on options.
The *host*, *port* and *URL base* fields under *Settings > General* are reserved by the add-on and
should not be changed. The add-on is built around Kapowarr listening on `0.0.0.0:5656` under the
`/kapowarr` URL base, and it sets all three back to those values every time it starts. Changing any
of them breaks the sidebar panel and the direct port until the next add-on restart, which repairs
them.
When `PUID`/`PGID` are not `0`, Kapowarr runs as that user and can only read and write the root
folders and download folders that user already has access to. The add-on only fixes ownership of
its own configuration directory.
## Ingress and URLs
Kapowarr is served from the `/kapowarr` subpath so that it works behind Home Assistant ingress:
- from the Home Assistant sidebar: ingress, no extra setup
- directly: `http://homeassistant:5656/kapowarr` — `http://homeassistant:5656/` on its own returns
a 404, the subpath is not optional
External clients that talk to Kapowarr's API must use the direct
`http://homeassistant:5656/kapowarr` url. Ingress is browser-session based, so they cannot
authenticate through it.
## Data
Kapowarr's database (`Kapowarr.db`) and logs live in `/config` inside the add-on, which Home
Assistant maps to this add-on's own configuration directory —
`/addon_configs/<repository_id>_kapowarr`, browsable with the Filebrowser add-on. They survive
add-on updates.
Temporary downloads go to `/config/temp_downloads` by default, so an interrupted download is not
lost when the add-on restarts. That directory is on the Home Assistant data disk: if space there is
tight, point *Settings > Download > Direct download temporary folder* at somewhere roomier such as
`/share/kapowarr_downloads` or a disk mounted through `localdisks`.
Your comics themselves stay where you put them, under `/media`, `/share` or a mounted disk.
## Support
- [Kapowarr upstream project](https://github.com/Casvt/Kapowarr)
- [Kapowarr documentation](https://casvt.github.io/Kapowarr/)
- [Add-on repository issues](https://github.com/alexbelgium/hassio-addons/issues)

View File

@@ -0,0 +1,68 @@
#include <tunables/global>
profile kapowarr_addon flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
capability chown,
capability dac_override,
capability dac_read_search,
capability fowner,
capability setgid,
capability setuid,
capability sys_chroot,
capability sys_admin,
file,
signal,
mount,
umount,
remount,
network udp,
network tcp,
network dgram,
network stream,
network inet,
network inet6,
network netlink raw,
network unix dgram,
# Entrypoint stack
/init ix,
/run/{s6,s6-rc*,service}/** ix,
/package/** ix,
/command/** ix,
/run/{,**} rwk,
/dev/tty rw,
/bin/** ix,
/usr/bin/** ix,
/usr/lib/bashio/** ix,
/etc/s6/** rix,
/run/s6/** rix,
/etc/services.d/** rwix,
/etc/cont-init.d/** rwix,
/etc/cont-finish.d/** rwix,
/init rix,
/var/run/** mrwkl,
/var/run/ mrwkl,
/dev/i2c-1 mrwkl,
# Files required
/dev/fuse mrwkl,
/dev/sda1 mrwkl,
/dev/sdb1 mrwkl,
/dev/nvme0 mrwkl,
/dev/nvme1 mrwkl,
/dev/mmcblk0p1 mrwkl,
/dev/* mrwkl,
/tmp/** mrkwl,
# Data access
/data/** rw,
# suppress ptrace denials when using 'docker ps' or using 'ps' inside a container
ptrace (trace,read) peer=docker-default,
# docker daemon confinement requires explicit allow rule for signal
signal (receive) set=(kill,term) peer=/usr/bin/docker,
}

View File

@@ -0,0 +1,6 @@
{
"build_from": {
"aarch64": "mrcas/kapowarr:v1.3.1",
"amd64": "mrcas/kapowarr:v1.3.1"
}
}

View File

@@ -0,0 +1,107 @@
arch:
- aarch64
- amd64
backup_exclude:
- "**/logs/*"
- "**/temp_downloads/*"
description: Comic book library manager, fitting in the *arr suite of software
devices:
- /dev/dri
- /dev/dri/card0
- /dev/dri/card1
- /dev/dri/renderD128
- /dev/vchiq
- /dev/video10
- /dev/video11
- /dev/video12
- /dev/video13
- /dev/video14
- /dev/video15
- /dev/video16
- /dev/ttyUSB0
- /dev/sda
- /dev/sdb
- /dev/sdc
- /dev/sdd
- /dev/sde
- /dev/sdf
- /dev/sdg
- /dev/nvme
- /dev/nvme0
- /dev/nvme0n1
- /dev/nvme0n1p1
- /dev/nvme0n1p2
- /dev/nvme0n1p3
- /dev/nvme1n1
- /dev/nvme1n1p1
- /dev/nvme1n1p2
- /dev/nvme1n1p3
- /dev/nvme2n1
- /dev/nvme2n1p1
- /dev/nvme2n1p2
- /dev/nvme2n1p3
- /dev/mmcblk
- /dev/fuse
- /dev/sda1
- /dev/sdb1
- /dev/sdc1
- /dev/sdd1
- /dev/sde1
- /dev/sdf1
- /dev/sdg1
- /dev/sda2
- /dev/sdb2
- /dev/sdc2
- /dev/sdd2
- /dev/sde2
- /dev/sdf2
- /dev/sdg2
- /dev/sda3
- /dev/sdb3
- /dev/sda4
- /dev/sdb4
- /dev/sda5
- /dev/sda6
- /dev/sda7
- /dev/sda8
- /dev/nvme0
- /dev/nvme1
- /dev/nvme2
image: ghcr.io/alexbelgium/kapowarr-{arch}
ingress: true
ingress_entry: kapowarr
init: false
map:
- addon_config:rw
- media:rw
- share:rw
name: Kapowarr
options:
env_vars: []
PGID: 0
PUID: 0
panel_icon: mdi:book-multiple
ports:
5656/tcp: 5656
ports_description:
5656/tcp: Web interface (path /kapowarr)
privileged:
- SYS_ADMIN
- DAC_READ_SEARCH
schema:
env_vars:
- name: match(^[A-Za-z0-9_]+$)
value: str?
PGID: int
PUID: int
TZ: str?
cifsdomain: str?
cifspassword: str?
cifsusername: str?
localdisks: str?
networkdisks: str?
smbv1: bool?
slug: kapowarr
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/kapowarr
version: "1.3.1"

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,35 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Kapowarr keeps its database, its logs and its temporary downloads outside the
# image so that they survive the container being recreated.
#
# The database and log folders are passed on the command line (see
# /etc/services.d/kapowarr/run). The temporary download folder is not: upstream
# re-applies --TempDownloadFolder on every start Kapowarr makes, including the
# self-restarts it performs after a hosting change, so passing it would keep
# undoing a folder the user picked in Settings > Download. Symlinking upstream's
# default onto persistent storage gives the same persistence and leaves the
# setting itself entirely to the user.
CONFIG_LOCATION="/config"
bashio::log.info "Config stored in $CONFIG_LOCATION"
mkdir -p "$CONFIG_LOCATION/logs" "$CONFIG_LOCATION/temp_downloads"
# Compared against the target rather than just testing for a symlink, so that a
# link left pointing somewhere else -- by a future upstream image, or by hand --
# is repaired instead of silently kept.
if [ "$(readlink /app/temp_downloads)" != "$CONFIG_LOCATION/temp_downloads" ]; then
rm -rf /app/temp_downloads
ln -s "$CONFIG_LOCATION/temp_downloads" /app/temp_downloads
fi
# Numbered 20- on purpose : it must sort after 00-global_var.sh, which is what
# exports PUID/PGID from the addon options. The upstream image sets both to 0,
# so the fallbacks only apply when the module is absent.
# Recursive because a user raising PUID after the first run would otherwise
# leave Kapowarr.db, its -wal/-shm sidecars and the logs owned by the previous
# uid, which sqlite then cannot write.
chown -R "${PUID:-0}:${PGID:-0}" "$CONFIG_LOCATION"

View File

@@ -0,0 +1,17 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
#################
# NGINX SETTING #
#################
declare ingress_interface
declare ingress_port
declare ingress_entry
ingress_port=$(bashio::addon.ingress_port)
ingress_interface=$(bashio::addon.ip_address)
ingress_entry=$(bashio::addon.ingress_entry)
sed -i "s/%%port%%/${ingress_port}/g" /etc/nginx/servers/ingress.conf
sed -i "s/%%interface%%/${ingress_interface}/g" /etc/nginx/servers/ingress.conf
sed -i "s|%%ingress_entry%%|${ingress_entry}|g" /etc/nginx/servers/ingress.conf

View File

@@ -0,0 +1,96 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@@ -0,0 +1 @@
resolver 127.0.0.11 ipv6=off;

View File

@@ -0,0 +1,56 @@
# Run nginx in foreground.
daemon off;
# This is run inside Docker.
user root;
# Pid storage location.
pid /var/run/nginx.pid;
# Set number of worker processes.
worker_processes 1;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
# Write error log to Hass.io add-on log.
error_log /proc/1/fd/1 error;
# Load allowed environment vars
env HASSIO_TOKEN;
# Load dynamic modules.
include /etc/nginx/modules-enabled/*.conf;
# Max num of simultaneous connections by a worker process.
events {
worker_connections 512;
}
http {
include /etc/nginx/includes/mime.types;
log_format hassio '[$time_local] $status '
'$http_x_forwarded_for($remote_addr) '
'$request ($http_user_agent)';
access_log /proc/1/fd/1 hassio;
client_max_body_size 4G;
default_type application/octet-stream;
gzip on;
keepalive_timeout 65;
sendfile on;
server_tokens off;
tcp_nodelay on;
tcp_nopush on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
include /etc/nginx/includes/resolver.conf;
include /etc/nginx/servers/*.conf;
}

View File

@@ -0,0 +1,56 @@
server {
listen %%interface%%:%%port%% default_server;
client_max_body_size 0;
# Kapowarr is mounted under the /kapowarr url base, so Werkzeug's
# DispatcherMiddleware answers 404 at / . Home Assistant opens the ingress
# panel at <ingress_entry>/ unless config.yaml's ingress_entry moves it, so
# bounce / to the url base whichever way the panel was opened.
# absolute_redirect off keeps the Location relative to the HA host instead
# of nginx's own listen address.
location = / {
absolute_redirect off;
return 302 %%ingress_entry%%/kapowarr/;
}
location / {
add_header Access-Control-Allow-Origin *;
proxy_connect_timeout 30m;
proxy_send_timeout 30m;
proxy_read_timeout 30m;
proxy_pass http://127.0.0.1:5656;
# Kapowarr streams queue, task and download progress over socket.io at
# <url_base>/api/socket.io, which must not be buffered or the UI stops
# updating until the buffer fills
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Werkzeug redirects /kapowarr to /kapowarr/ , and the Location it
# produces is absolute against the upstream address nginx talks to.
# proxy_redirect puts it back on the ingress path (the second rule
# covers an already relative Location).
absolute_redirect off;
proxy_redirect http://127.0.0.1:5656/ %%ingress_entry%%/;
proxy_redirect / %%ingress_entry%%/;
# Kapowarr renders every link and asset url as {{url_base}}/... and
# general.js reads the same value back out of
# <meta id="url_base" data-value="/kapowarr">, so rewriting the html
# moves the whole SPA -- including its fetch() and socket.io urls --
# onto the ingress prefix that Home Assistant strips before forwarding.
# sub_filter cannot rewrite a compressed body, hence Accept-Encoding "".
proxy_set_header Accept-Encoding "";
sub_filter_once off;
# text/html is always filtered ; the pwa manifest is added because
# /manifest.json embeds the url base in start_url, scope, id and icons,
# and it is served as application/manifest+json. Json is deliberately
# not filtered : api payloads carry user file paths that must not be
# rewritten.
sub_filter_types application/manifest+json;
sub_filter "/kapowarr" "%%ingress_entry%%/kapowarr";
}
}

View File

@@ -0,0 +1,36 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# ==============================================================================
# Kapowarr is started through the upstream entrypoint, which is what implements
# PUID/PGID (groupmod -o / usermod -o, then gosu). ha_entrypoint.sh replaced it
# as the container entrypoint so that cont-init.d and nginx could run too, so it
# is called again here rather than reimplemented. Both usermod and groupmod are
# given -o upstream, so a PUID that collides with an existing account is not an
# error.
#
# --UrlBase is what makes ingress work: Kapowarr renders absolute urls, Home
# Assistant strips its own ingress prefix before forwarding, and nginx adds it
# back by rewriting this fixed prefix. See rootfs/etc/nginx/servers/ingress.conf.
#
# --Host and --Port are passed for the same reason, even though they are already
# the upstream defaults: all three are stored in the database, and Kapowarr reads
# the stored value when the flag is absent. Without them, a host or port changed
# in Settings > General would survive every restart and upgrade while nginx and
# the healthcheck stayed pointed at 127.0.0.1:5656 -- a permanent 502 with no way
# back except editing the database. Passing all three makes the whole hosting
# section self repairing.
#
# Upstream applies these three only on a startup, never on the restarts Kapowarr
# performs itself, so they are re-applied once per container start and do not
# fight the user in between.
bashio::log.info "Starting Kapowarr (served on the /kapowarr path, see the addon documentation)"
exec /app/entrypoint.sh python3 /app/Kapowarr.py \
--DatabaseFolder /config \
--LogFolder /config/logs \
--Host 0.0.0.0 \
--Port 5656 \
--UrlBase /kapowarr

View File

@@ -0,0 +1,36 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# ==============================================================================
# Wait for Kapowarr to answer before nginx starts serving ingress. The first
# boot creates the database and runs its migrations, so leave a wide margin, but
# poll rather than call bashio::net.wait_for : bashio takes (port host timeout)
# while the bundled bashio-standalone.sh takes (host port timeout), and picking
# the wrong one would either fail instantly or block for the whole timeout.
# The per probe timeouts keep the ceiling real : without them a half open
# connection would hang a single probe, and the loop, forever.
# A wall clock deadline, not an attempt count : a failed probe costs up to
# max-time on top of the sleep, so counting attempts would stretch the wait to
# roughly twice the advertised ceiling.
# The probe asks for /kapowarr/ rather than / , because / is served by the empty
# app that DispatcherMiddleware mounts beside the url base and always answers.
kapowarr_ready=false
deadline=$((SECONDS + 300))
while [ "$SECONDS" -lt "$deadline" ]; do
if curl -sf --connect-timeout 2 --max-time 5 -o /dev/null "http://127.0.0.1:5656/kapowarr/"; then
kapowarr_ready=true
break
fi
sleep 5
done
# Deliberately not fatal : nginx serving a 502 tells the user something is wrong
# and starts working by itself once Kapowarr finally answers, while refusing to
# start would take ingress down for good after ha_entrypoint gives up retrying.
if [ "$kapowarr_ready" != true ]; then
bashio::log.warning "Kapowarr did not answer within 5 minutes. Starting NGinx anyway : ingress will return 502 until it does."
fi
bashio::log.info "Starting NGinx..."
exec nginx

View File

@@ -0,0 +1,10 @@
{
"github_beta": "false",
"last_update": "2026-08-19",
"paused": true,
"repository": "alexbelgium/hassio-addons",
"slug": "kapowarr",
"source": "github",
"upstream_repo": "Casvt/Kapowarr",
"upstream_version": "1.3.1"
}