# arr-releases
A Dynacat ([Panonim/dynacat](https://github.com/Panonim/dynacat)) calendar widget
that aggregates upcoming releases from [Sonarr](https://sonarr.tv),
[Radarr](https://radarr.video) and [Lidarr](https://lidarr.audio).
- Always renders exactly 6 weeks (42 cells) with prev/next month spillover
- One coloured dot per service on each day that has a release
- Click any day with a dot to open a popover with full release details and cover art
- Smooth slide+fade animation on month navigation
- **Works with any subset of the three services configured** — set `SONARR_KEY`
only and the widget will silently skip Radarr and Lidarr
 *(add a screenshot if you have
one — see [How to add a screenshot](#how-to-add-a-screenshot))*
## Quick install (TL;DR)
```bash
# 1. Copy the widget definition
cp arr-calendar.yml /appdata/dynacat/config/
# 2. Copy the client JS
cp arr-calendar.js /appdata/dynacat/assets/
# 3. Add this line to /appdata/dynacat/config/dynacat.yml under document.head:
#
# 4. Restart the Dynacat container (the Go template cache is in-memory; YML
# edits do NOT recompile until the container restarts)
docker restart dynacat
```
Then `$include: arr-calendar.yml` in any page column and set at least one
of `SONARR_KEY`, `RADARR_KEY`, `LIDARR_KEY` as a container env var.
## What the widget does
Each cell of the calendar corresponds to one day. On a day that has at least
one release, the widget shows a small coloured dot:
- 🔵 Blue dot — Sonarr episode
- 🟢 Green dot — Radarr movie
- 🟣 Purple dot — Lidarr album
Multiple services on the same day produce a row of dots. Click the day to
open a popover listing every release on that date, with cover art, the
release type (Release / Digital / In Cinemas / Physical for Radarr;
`S##E## — episode title` for Sonarr; artist name for Lidarr) and a link
back to the *arr web UI.
## Full install
### 1. Environment variables
The widget reads three API keys from the container environment. **Set at
least one** — all three are optional:
| Env var | Purpose | Required? |
| ------------- | ------------------------------ | --------- |
| `SONARR_KEY` | Sonarr API key | optional |
| `RADARR_KEY` | Radarr API key | optional |
| `LIDARR_KEY` | Lidarr API key | optional |
| `BASE_HOST` | The shared part of the *arr URLs (e.g. `t3du.com`). The widget builds Sonarr/Radarr/Lidarr URLs as `https://.${BASE_HOST}`. | required |
Get an API key from each *arr under **Settings → General → API Key**.
If you set the env var on the Dynacat container itself, no per-service
configuration is needed — defaults to
`https://sonarr.${BASE_HOST}`, `https://radarr.${BASE_HOST}`,
`https://lidarr.${BASE_HOST}`. To use a different URL for a service, set
the corresponding `*-url` option in your `$include` of `arr-calendar.yml`
(see [Customising URLs](#customising-urls) below).
### 2. File layout
```
/appdata/dynacat/
config/
dynacat.yml # main config (edit to add the script tag)
arr-calendar.yml # <-- this repo, copy here
assets/
arr-calendar.js # <-- this repo, copy here
global.css # (untouched; widget CSS is inlined in the YML)
```
### 3. Edit `dynacat.yml`
Add the script tag under `document.head`:
```yaml
server:
assets-path: /app/assets
document:
head: |
```
Then `$include` the widget in a page column (any column size — it adapts):
```yaml
pages:
- name: Home
columns:
- size: small
widgets:
- $include: arr-calendar.yml
```
### 4. Restart the container
**YML edits do not recompile the Go template on the fly.** Glance/Dynacat
parses each `custom-api` template once on first render and caches the
compiled `*template.Template` in memory. File mtime updates, `touch` and
edits to the file do not invalidate the cache. After every edit to
`arr-calendar.yml`, restart the container:
```bash
docker restart dynacat
```
(or via your container manager — Dockhand, Unraid UI, etc.). The error
message you'll get if you forget is the dreaded
`Config has errors: custom-api widget: parsing template: ...` — see
[Troubleshooting](#troubleshooting) below.
### 5. Hard-refresh the browser
`Ctrl+Shift+R` (or `Cmd+Shift+R` on macOS) to bypass the browser cache.
## Customising
All options live in the top of `arr-calendar.yml` under `options:`:
```yaml
- type: custom-api
options:
sonarr-url: https://sonarr.example.com
sonarr-key: ${SONARR_KEY}
radarr-url: https://radarr.example.com
radarr-key: ${RADARR_KEY}
lidarr-url: https://lidarr.example.com
lidarr-key: ${LIDARR_KEY}
exclude-physical: true
past-days: 90
future-days: 90
```
| Option | Default | Notes |
| ---------------- | ------------------- | ----- |
| `sonarr-url` | `https://sonarr.${BASE_HOST}` | Override if your Sonarr is on a different host |
| `sonarr-key` | `${SONARR_KEY}` | Set the env var instead of hard-coding |
| `radarr-url` | `https://radarr.${BASE_HOST}` | |
| `radarr-key` | `${RADARR_KEY}` | |
| `lidarr-url` | `https://lidarr.${BASE_HOST}` | |
| `lidarr-key` | `${LIDARR_KEY}` | |
| `exclude-physical` | `true` | When `true`, Radarr movies whose display date is a physical release are hidden. Set to `false` to show them. |
| `past-days` | `90` | How many days into the past to fetch (for the "this month" view, so the first week of the month shows releases from the previous month) |
| `future-days` | `90` | How many days into the future to fetch (should be ≥ 60 to cover the full ~2-month view) |
If you want to keep the defaults but override the URLs, the cleanest
pattern is to copy the widget's options block into your main
`dynacat.yml` and `$include` the rest, or to edit `arr-calendar.yml`
directly.
## Customising URLs
The default URLs use the `BASE_HOST` env var and assume Sonarr/Radarr/Lidarr
are reachable as subdomains of that host. To use a different host for one
of the services, override the corresponding `*-url` option in your
`$include`. The YML supports Go template strings, so you can interpolate
env vars:
```yaml
- type: custom-api
options:
sonarr-url: https://sonarr.${BASE_HOST}
sonarr-key: ${SONARR_KEY}
radarr-url: http://192.168.1.50:7878 # absolute override
radarr-key: ${RADARR_KEY}
lidarr-key: ${LIDARR_KEY} # omitted URL → defaults to https://lidarr.${BASE_HOST}
```
Note: at least one of the three `(url, key)` pairs must be set per service
for the service to be queried. An empty `*-key` skips the service
entirely — see [Partial-services support](#partial-services-support).
## Partial-services support
This is the headline feature. The widget keeps working with **any subset
of the three services configured**:
- Set only `SONARR_KEY` → calendar shows only Sonarr releases (blue dots
only)
- Set all three → all three colours of dots appear
- Set none → soft empty-state message: *"Configure at least one of
SONARR_KEY, RADARR_KEY, or LIDARR_KEY in the container environment to
show releases."*
A service is "configured" if both its URL and API key are non-empty. If
either is empty, that service's API is silently skipped — no request
goes out, no error widget appears, and the widget continues to render
with the remaining services.
If a service is **configured** but **unreachable** (network down,
service dead, 401 from a wrong key), the same logic applies: that
service is skipped (the 5s HTTP timeout will trip), and the other
services continue to render.
The status of each service is exposed to the client JS in a hidden
``:
```html
```
The widget JS doesn't currently surface this in the UI, but the data is
there if you want to add a "Radarr is offline" pill in the popover.
## How it works (architecture)
```
┌────────────────────────────────────────────────────────────┐
│ Go template (server side, every update-interval = 10m) │
│ 1. Compute $xxxEnabled = (URL != "") && (key != "") │
│ 2. Declare $xxxData := newRequest "" | getResponse │
│ (outer scope; empty URL → StatusCode 0 placeholder) │
│ 3. Inside {{- if $xxxEnabled }}: │
│ $xxxData = newRequest | withHeader ... │
│ | getResponse │
│ 4. Emit hidden - per release │
└────────────────────────────────────────────────────────────┘
│
▼ HTTP response, XHR-rendered
┌────────────────────────────────────────────────────────────┐
│ Browser │
│