Rewrite README in plainer language

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-10-13 19:45:58 -04:00
1 parent 54f5acc9a3
commit 268af7b9ed
1 file changed
+134 -78
+134 -78
View File
@@ -2,27 +2,39 @@
# Canon
A self-hosted page for **your** ranked favorites: movies, TV shows, albums, songs, video games, theater, and anything else you care to rank in a custom list. One owner, as many lists as you like, each with its own size (Top 5, Top 100, … up to 65,535), with proper posters and cover art pulled from the best free metadata sources.
Canon is a self-hosted page for your ranked favorites: movies, TV shows, albums, songs, video games, theater, and anything else you
want to rank in a custom list. There's one owner and as many lists as you want. Each list has its own size (Top 5, Top 100, anything up to
65,535), and posters and cover art come from the best free metadata sources around.
Your page is titled **"*Your name*'s Canon"** by default. Set your name under Settings → Profile, or give the site any title you like there.
By default the page is called "*Your name*'s Canon". Set your name under Settings → Profile, or give the site whatever title you want.
Visitors just see your lists. There's no sign-up, no feed and no login button. You manage everything from `/login`.
Visitors just see your lists. No sign-up, no feed, no login button. You manage everything from `/login`.
- **Ranked lists per category**: Movies, TV Shows, Albums, Songs, Video Games and Theater. Each list sets its own size, title, description and style.
- **Search-to-add** from TMDB, TheTVDB, IGDB, Deezer/MusicBrainz and Wikipedia, with add-manually as a fallback for anything they miss.
- **Custom lists** for anything else, like sports teams, drinks or restaurants. You name the list and add entries by hand (with your own photo or logo), or pull a name and picture from Wikipedia.
- **Theater by production**: record *which* staging you mean (Broadway, West End, North American Tour, a revival…) plus venue and cast, and use that production's own poster.
- **Four styles per list**: **Podium** (top 3 on a medal podium, the rest behind "Show more"), **Spotlight** (#1 as a big feature), **Grid** and **List**. You pick each list's style in List settings, and visitors can switch styles on a list's own page.
- **Drag to rank**, or use the arrow buttons or type a new number. Paginated editing keeps huge lists usable.
- **Refresh from source**: re-pull an entry, or a whole list, when its metadata changes upstream. Your notes, ranking and theater productions are kept, and artwork you chose yourself is only replaced if you say so. Custom lists and hand-added entries have no source, so they aren't refreshed.
- **Art is cached on your server**, so pages never hotlink and keep working if a source changes. You can also paste an image URL or upload your own.
- **Preview clips** for songs and albums: a play button plays 30 seconds from Deezer (an album plays its most popular track). Clips stream through your server, so visitors never connect to Deezer. You can turn this off in Settings → Profile.
- **Export** any list as a titles-only `.csv` or `.txt`.
- **Descriptions and your reasons**: each entry keeps the source's description (TMDB overview, IGDB summary, Wikipedia intro), and you can add a "Why it's #N" note explaining its spot. Both show in the detail view.
- **12 theme presets** plus a custom CSS box. Every color is a CSS variable.
- **Single binary + SQLite**, no external database. Around 15 MB, and it runs anywhere Docker does.
## What it does
## Quick start (Docker Compose)
- Ranked lists for Movies, TV Shows, Albums, Songs, Video Games and Theater. Each list has its own size, title, description and style.
- Search and add from TMDB, TheTVDB, IGDB, Deezer/MusicBrainz and Wikipedia. If something's missing, add it by hand.
- Custom lists for anything else: sports teams, drinks, restaurants. Name the list and add entries yourself (with your own photo or
logo), or pull a name and picture from Wikipedia.
- Theater entries are per production. You record which staging you mean (Broadway, West End, North American Tour, a revival) plus the
venue and cast, and use that production's poster.
- Four styles per list. Podium puts the top 3 on a medal podium with the rest behind "Show more". Spotlight makes #1 a big feature.
Grid and List are what they sound like. You set each list's style in List settings, and visitors can switch styles on a list's page.
- Rank by dragging, with the arrow buttons, or by typing a number. Big lists are paginated in the editor so they stay usable.
- Refresh an entry or a whole list from its source when the metadata changes upstream. Your notes, ranking and theater productions are
kept, and art you picked yourself is only replaced if you say so. Custom lists and hand-added entries have no source, so they're not
refreshed.
- Art is cached on your server, so pages never hotlink and keep working if a source changes. You can also paste an image URL or upload
your own.
- Songs and albums get a play button for a 30-second Deezer preview (albums play their most popular track). Clips stream through your
server, so visitors never talk to Deezer directly. You can turn this off in Settings → Profile.
- Each entry keeps the source's description (TMDB overview, IGDB summary, Wikipedia intro), and you can add a "Why it's #N" note
explaining its spot. Both show in the detail view.
- Export any list as a titles-only `.csv` or `.txt`.
- 13 theme presets plus a custom CSS box. Every color is a CSS variable.
- It's a single binary with SQLite, no separate database. Around 15 MB, and it runs anywhere Docker does.
## Running it (Docker Compose)
```sh
git clone https://git.iamchrisama.com/cdnutter/canon && cd canon
@@ -31,30 +43,38 @@ docker compose up -d
docker compose logs canon # shows the one-time setup code if you didn't set an admin in .env
```
Open `http://localhost:4242/setup`, enter the setup code and create your account. Then go to **Settings → Integrations** to add API keys.
Open `http://localhost:4242/setup`, enter the setup code and create your account. Then add your API keys under Settings → Integrations.
To skip the setup page, set `CANON_ADMIN_USERNAME` and `CANON_ADMIN_PASSWORD` in `.env` before the first start. They're only used when no account exists yet.
If you'd rather skip the setup page, set `CANON_ADMIN_USERNAME` and `CANON_ADMIN_PASSWORD` in `.env` before the first start. They're
only used if no account exists yet.
### Opening it from other devices
Canon listens on all network interfaces (`0.0.0.0`) by default, so phones and other computers on your network can open `http://<this computer's IP>:4242`. When run outside Docker, the startup log prints the exact addresses. If another device can't connect, the host's firewall is probably blocking the port (for example `sudo ufw allow 4242/tcp`). To keep Canon private to one machine, set `CANON_HOST=127.0.0.1`, or publish `127.0.0.1:4242:4242` in `docker-compose.yml`.
Canon listens on all interfaces (`0.0.0.0`) by default, so phones and other computers on your network can open
`http://<this computer's IP>:4242`. Outside Docker, the startup log prints the exact addresses. If another device can't connect, the
host's firewall is probably blocking the port (for example, `sudo ufw allow 4242/tcp`). To keep Canon to one machine, set
`CANON_HOST=127.0.0.1` or publish `127.0.0.1:4242:4242` in `docker-compose.yml`.
### With Arcane (Git sync)
The compose file works as an [Arcane](https://getarcane.app) project synced from this repository:
The compose file works as an [Arcane](https://getarcane.app) project synced from this repo:
1. **Projects → New → Git sync**, direction **Pull**, compose file path `docker-compose.yml`.
2. Turn on **Redeploy after sync** to rebuild whenever you push. `pull_policy: build` makes every deploy build the image from the synced source; it never looks for `canon:latest` on a registry.
1. Projects → New → Git sync, direction Pull, compose file path `docker-compose.yml`.
2. Turn on Redeploy after sync so it rebuilds whenever you push. `pull_policy: build` means every deploy builds the image from the
synced source instead of looking for `canon:latest` on a registry.
3. Put your settings (admin account, API keys) in the project's environment in Arcane. Arcane writes them to `.env`, which Canon reads.
The database lives in the `canon_canon-data` named volume, outside the synced folder, so a sync can't overwrite or delete it. To use a host folder instead, set `CANON_DATA_PATH` to an absolute path (it must be writable by UID 1000). To move data from an existing `./data` folder into the volume:
The database lives in the `canon_canon-data` named volume, outside the synced folder, so a sync can't overwrite or delete it. To use a
host folder instead, set `CANON_DATA_PATH` to an absolute path that UID 1000 can write to. To move data from an existing `./data` folder
into the volume:
```sh
docker compose stop canon
docker run --rm -v "$PWD/data:/from" -v canon_canon-data:/to alpine sh -c 'cp -a /from/. /to/ && chown -R 1000:1000 /to'
```
The compose file joins the external `chromanet` network; create it first (`docker network create chromanet`) or remove the `networks` entries.
The compose file joins the external `chromanet` network. Create it first (`docker network create chromanet`) or remove the `networks`
entries.
### Without Compose
@@ -63,45 +83,56 @@ docker build -t canon .
docker run -d --name canon -p 4242:4242 -v canon-data:/data canon
```
Or from source (Go 1.26+): `go run ./cmd/canon` stores data in `./data`.
Or from source (Go 1.26+): `go run ./cmd/canon`, which stores data in `./data`.
## Data sources and API keys
| Category | Source | Key? | Get one |
| Category | Source | Key? | Where to get one |
|---|---|---|---|
| Movies | [TMDB](https://www.themoviedb.org) | Yes (free) | [themoviedb.org/settings/api](https://www.themoviedb.org/settings/api). The v3 key or the v4 Read Access Token both work. |
| TV Shows | [TheTVDB](https://thetvdb.com) (default) or TMDB | Yes | [thetvdb.com/api-information](https://thetvdb.com/api-information). Switch TV to TMDB in Settings to reuse your TMDB key. |
| Video Games | [IGDB](https://www.igdb.com) | Yes (free, via Twitch) | Register an app at [dev.twitch.tv/console](https://dev.twitch.tv/console/apps) and use its Client ID + Secret ([guide](https://api-docs.igdb.com/#account-creation)). |
| Albums & Songs | [Deezer](https://developers.deezer.com/api) and [MusicBrainz](https://musicbrainz.org) + [Cover Art Archive](https://coverartarchive.org), searched together (or either alone) | No | Nothing to set up. |
| Movies | [TMDB](https://www.themoviedb.org) | Yes (free) | [themoviedb.org/settings/api](https://www.themoviedb.org/settings/api). Either the v3 key or the v4 Read Access Token works. |
| TV Shows | [TheTVDB](https://thetvdb.com) (default) or TMDB | Yes | [thetvdb.com/api-information](https://thetvdb.com/api-information). Or switch TV to TMDB in Settings and reuse your TMDB key. |
| Video Games | [IGDB](https://www.igdb.com) | Yes (free, via Twitch) | Register an app at [dev.twitch.tv/console](https://dev.twitch.tv/console/apps) and use its Client ID and Secret ([guide](https://api-docs.igdb.com/#account-creation)). |
| Albums & Songs | [Deezer](https://developers.deezer.com/api) and [MusicBrainz](https://musicbrainz.org) + [Cover Art Archive](https://coverartarchive.org), searched together (or either one alone) | No | Nothing to set up. |
| Theater | [Wikipedia](https://en.wikipedia.org) | No | Nothing to set up. |
| Custom | Manual entry, or [Wikipedia](https://en.wikipedia.org) | No | Nothing to set up. |
**Why these for music and theater?** Neither needs a key.
- **Music:** Canon searches Deezer and MusicBrainz together and merges the results. Deezer's public API has no key, generous limits, 1000×1000 covers and 30-second preview clips, but its search misses some albums it carries (Death Magnetic only turns up for an exact artist + album query) and has few game soundtracks. MusicBrainz is fully open, finds those, and tags soundtracks, but allows only one request per second. Entries added from MusicBrainz or by hand are matched to Deezer when saved, so they can play previews too. Spotify, Apple Music and Last.fm all need keys or OAuth and have stricter terms. For game music, [VGMdb](https://vgmdb.net) has the deepest catalogue but no public API, so add anything missing by hand.
- **Theater:** there's no open theater database with an API (IBDB and Playbill don't offer one). Wikipedia covers nearly every musical, play and opera, usually with poster or logo art. Canon filters search to pages that use the musical, play or opera infobox. Wikipedia describes the *work*, so you pick the production yourself and can swap in that production's poster.
Music and theater use these sources because neither needs a key.
Keys entered in the UI are stored in the SQLite database on your server. Keys set as environment variables take precedence and show as locked in the UI.
For music, Canon searches Deezer and MusicBrainz together and merges the results. Deezer's public API needs no key, has generous limits,
1000×1000 covers and 30-second previews, but its search misses some albums it actually has (Death Magnetic only shows up for an exact
artist + album search) and it's thin on game soundtracks. MusicBrainz is fully open, finds those, and tags soundtracks, but only allows
one request per second. Entries added from MusicBrainz or by hand get matched to Deezer when saved, so they can play previews too.
Spotify, Apple Music and Last.fm all need keys or OAuth and have stricter terms. For game music, [VGMdb](https://vgmdb.net) has the
biggest catalogue but no public API, so add anything missing by hand.
For theater, there's no open database with an API (IBDB and Playbill don't have one). Wikipedia covers almost every musical, play and
opera, usually with a poster or logo. Canon limits search to pages using the musical, play or opera infobox. Wikipedia describes the
work, not a production, so you pick the production yourself and can swap in its poster.
Keys you enter in the UI are stored in the SQLite database on your server. Keys set as environment variables win, and show as locked in
the UI.
## Configuration
| Variable | Default | |
| Variable | Default | Notes |
|---|---|---|
| `CANON_HOST` | `0.0.0.0` | Interface to listen on. `0.0.0.0` makes it reachable from other devices on your network; `127.0.0.1` keeps it on this machine |
| `CANON_PORT` | `4242` | Listen port |
| `CANON_DATA_DIR` | `/data` (Docker), `./data` | Database + cached images |
| `PUID` / `PGID` | `1000` (image), `99`/`100` (Compose) | Docker only: the user and group that own everything Canon writes. The Compose file defaults to Unraid's `nobody:users` |
| `UMASK` | `022` | Docker only: permissions for new files (`002` makes them group-writable) |
| `CANON_ADMIN_USERNAME` / `_PASSWORD` | | Create the owner account on first start |
| `CANON_HOST` | `0.0.0.0` | Interface to listen on. `0.0.0.0` lets other devices on your network reach it; `127.0.0.1` keeps it on this machine. |
| `CANON_PORT` | `4242` | Listen port. |
| `CANON_DATA_DIR` | `/data` (Docker), `./data` | Database and cached images. |
| `PUID` / `PGID` | `1000` (image), `99`/`100` (Compose) | Docker only. The user and group that own everything Canon writes. The compose file defaults to Unraid's `nobody:users`. |
| `UMASK` | `022` | Docker only. Permissions for new files (`002` makes them group-writable). |
| `CANON_ADMIN_USERNAME` / `_PASSWORD` | | Creates the owner account on first start. |
| `CANON_TMDB_API_KEY` | | |
| `CANON_TVDB_API_KEY` / `CANON_TVDB_PIN` | | The PIN is only for user-supported keys |
| `CANON_IGDB_CLIENT_ID` / `_SECRET` | | Twitch app credentials |
| `CANON_TRUST_PROXY` | `false` | Trust `X-Forwarded-*` headers (set behind a reverse proxy) |
| `CANON_SECURE_COOKIES` | `false` | Force `Secure` cookies (set if served over HTTPS) |
| `CANON_LOG_LEVEL` | `info` | `debug` logs every request |
| `CANON_TVDB_API_KEY` / `CANON_TVDB_PIN` | | The PIN is only needed for user-supported keys. |
| `CANON_IGDB_CLIENT_ID` / `_SECRET` | | Twitch app credentials. |
| `CANON_TRUST_PROXY` | `false` | Trust `X-Forwarded-*` headers. Set this behind a reverse proxy. |
| `CANON_SECURE_COOKIES` | `false` | Force `Secure` cookies. Set this if you serve over HTTPS. |
| `CANON_LOG_LEVEL` | `info` | `debug` logs every request. |
### Reverse proxy
Canon serves plain HTTP. Put it behind Caddy, Traefik or nginx for HTTPS, and set `CANON_TRUST_PROXY=true` so logins are rate-limited per real client and cookies are marked secure. Caddy example:
Canon only speaks plain HTTP. Put it behind Caddy, Traefik or nginx for HTTPS, and set `CANON_TRUST_PROXY=true` so login rate limits
apply to the real client and cookies get marked secure. With Caddy:
```
favorites.example.com {
@@ -109,9 +140,11 @@ favorites.example.com {
}
```
## Theming
## Themes
Pick a preset under **Settings → Appearance**: Midnight, Obsidian, Nord, Dracula, Mocha, Rosé Pine, Gruvbox, Forest, Synthwave, Crimson (red on OLED black), Daylight, Paper or Latte. Then add your own CSS if you like. It loads after the theme on every page, so overriding variables is enough for a whole new look:
Pick a preset under Settings → Appearance: Midnight, Obsidian, Nord, Dracula, Mocha, Rosé Pine, Gruvbox, Forest, Synthwave, Crimson
(red on OLED black), Daylight, Paper or Latte. You can add your own CSS on top. It loads after the theme on every page, so overriding a
few variables is enough for a whole new look:
```css
:root, [data-theme] {
@@ -123,53 +156,71 @@ Pick a preset under **Settings → Appearance**: Midnight, Obsidian, Nord, Dracu
}
```
See [`web/static/css/themes.css`](web/static/css/themes.css) for every variable.
Every variable is in [`web/static/css/themes.css`](web/static/css/themes.css).
## List sizes
A list can hold 1 to 65,535 entries. The editor recommends **100 or fewer** and warns past 1,000. Long lists work (public pages and the editor paginate at 100), but they're hard for visitors to take in.
A list can hold anywhere from 1 to 65,535 entries. The editor suggests 100 or fewer and warns you past 1,000. Long lists work fine
(public pages and the editor paginate at 100), they're just a lot for visitors to take in.
## Private sites
To share with friends and family without being fully public, set a password under **Settings → Privacy → Site password**. Visitors then see a password prompt over a blurred placeholder; none of your lists, images or profile are sent until they enter it. Once they do, that browser stays unlocked for 90 days. Changing the password asks everyone for the new one, and you never need it while signed in. A protected site also tells search engines not to index it.
If you want to share with friends and family without being fully public, set a password under Settings → Privacy → Site password.
Visitors get a password prompt over a blurred placeholder, and none of your lists, images or profile are sent until they enter it. After
that, their browser stays unlocked for 90 days. Changing the password makes everyone enter the new one, and you never need it while
signed in. A password-protected site also asks search engines not to index it.
## Ranking
Entries sit at places from #1 to the list's size, and a place can be left empty: a game can be #28 with nothing at #29 and the next at #30. In the list editor:
Entries sit at places from #1 to the list's size, and places can be left empty. A game can be #28 with nothing at #29 and the next one at
#30. In the list editor:
- **Type a place** in an entry's number box and press Enter (↑/↓ jump between boxes, Alt+↑/↓ moves the entry one place). Or drag it.
- Moving onto an **empty place** just puts it there. Moving onto a **taken place** shifts the entries in between one step, stopping at the first empty place, so entries you placed further down stay put.
- An entry's **⋯ menu** can insert an empty place above it, move it to #1 or the end, or remove it (you choose whether the entries below move up or the place stays empty).
- Empty places show as their own rows: **Add here** makes the next entry you add go there, and **Remove** closes them up. **Close gaps** renumbers everything 1, 2, 3….
- Type a place in an entry's number box and press Enter. ↑/↓ jump between boxes, and Alt+↑/↓ moves the entry one place. You can also
drag.
- Moving onto an empty place just puts it there. Moving onto a taken place shifts the entries in between by one, stopping at the first
empty place, so entries you put further down stay where they are.
- An entry's ⋯ menu can insert an empty place above it, move it to #1 or the end, or remove it. When removing, you choose whether the
entries below move up or the place stays empty.
- Empty places show up as their own rows. Add here makes your next added entry go there, and Remove closes the gap. Close gaps
renumbers everything 1, 2, 3 and so on.
Edits show instantly and save in the background.
Changes show up right away and save in the background.
## Importing a list
**Import** in the list editor adds entries from a file. **Line numbers are places**: line 1 becomes #1, line 38 becomes #38, and a blank line leaves its place empty (entries already at those places move down to make room).
Import in the list editor adds entries from a file. Line numbers are places: line 1 becomes #1, line 38 becomes #38, and a blank line
leaves that place empty. Entries already sitting at those places move down to make room.
- **Text** (`.txt`, or paste): one title per line. If every line is numbered (`38. Super Mario World`), the numbers are the places instead. A trailing `(1999)` is read as the year. For songs and albums, `Artist - Title` works either way round.
- **CSV / TSV**: a header row picks the columns: `Title` (or Name), `Year`, `Artist`/`Director`/`Developer`, `Note` (or Why), and `Rank` to give places directly. Without a header the first column is the title. The first row after the header is #1.
- Text (`.txt`, or paste it in): one title per line. If every line is numbered (`38. Super Mario World`), those numbers are used as the
places instead. A trailing `(1999)` is read as the year. For songs and albums, `Artist - Title` works in either order.
- CSV / TSV: a header row picks the columns. `Title` (or Name), `Year`, `Artist`/`Director`/`Developer`, `Note` (or Why), and `Rank` to
set places directly. Without a header, the first column is the title. The first row after the header is #1.
Each entry is searched for and matched. Canon only adds a match on its own when it's certain: the title matches exactly and nothing else fits as well (a year or artist in the file helps). Everything else is left for you to check, with a recommended pick, the other results, **Add as written** to make a new entry from your text, **Skip**, and a box to search again. Custom lists search all of Wikipedia, so there a match is only certain when a year or subtitle confirms it.
Canon searches for each entry and only adds a match on its own when it's sure: the title matches exactly and nothing else fits as well (a
year or artist in the file helps). Everything else waits for you, with a suggested pick, the other results, Add as written to make a new
entry from your text, Skip, and a box to search again. Custom lists search all of Wikipedia, so a match there only counts as sure when a
year or subtitle backs it up.
## Exporting a list
Every list can be downloaded as its titles only, one per line, where line N is place #N: an empty place is a blank line. There are no ranks, header row or other fields, and importing the file puts everything back where it was.
Any list can be downloaded as just its titles, one per line, where line N is place #N and an empty place is a blank line. No ranks, no
header row, no other fields. Importing the file puts everything right back.
- **Export → Titles as CSV** (`/l/<list>/export?format=csv`): a single column. It starts with a UTF-8 BOM so Excel shows accented titles correctly.
- **Export → Titles as text** (`/l/<list>/export?format=txt`): plain lines.
- Export → Titles as CSV (`/l/<list>/export?format=csv`): one column, starting with a UTF-8 BOM so Excel shows accented titles properly.
- Export → Titles as text (`/l/<list>/export?format=txt`): plain lines.
The Export menu appears in the list editor and, when you're signed in, on the list's public page. The links follow the list's visibility: anyone can download a public list, and only you can download a hidden one.
The Export menu is in the list editor, and on the list's public page when you're signed in. Downloads follow the list's visibility:
anyone can download a public list, and only you can download a hidden one.
## Upgrading from TopDeck
Canon was called TopDeck, and upgrading keeps all your data:
Canon used to be called TopDeck. Upgrading keeps all your data:
- The database is renamed from `topdeck.db` to `canon.db` automatically on first start.
- Environment variables changed prefix from `TOPDECK_` to `CANON_`. Rename them in your `.env`.
- You'll be signed out once (the session cookie was renamed).
- Docker Compose now always uses the project name `canon`. If you ran the old compose file, your data is in the `topdeck_topdeck-data` volume. Copy it into the new one before starting:
- The database gets renamed from `topdeck.db` to `canon.db` on first start.
- Environment variables changed from `TOPDECK_` to `CANON_`. Rename them in your `.env`.
- You'll be signed out once, because the session cookie was renamed.
- Docker Compose now always uses the project name `canon`. If you ran the old compose file, your data is in the `topdeck_topdeck-data`
volume. Copy it to the new one before starting:
```sh
docker volume create canon_canon-data
@@ -178,13 +229,17 @@ Canon was called TopDeck, and upgrading keeps all your data:
## Backups
Canon backs itself up every day at 3:00 and keeps the last 7. Change this under **Settings → Backups**: pick a preset (hourly, every 6 hours, daily, weekly, monthly), write your own cron expression, or turn scheduled backups off. **Back up now** makes a manual backup; manual backups are never deleted automatically. Every backup can be downloaded or deleted from the same page.
Canon backs itself up every day at 3:00 and keeps the last 7. You can change that under Settings → Backups: pick a preset (hourly,
every 6 hours, daily, weekly, monthly), write your own cron expression, or turn scheduled backups off. Back up now makes a manual backup,
and manual backups are never deleted automatically. You can download or delete any backup from the same page.
Each backup is a `.tar.gz` holding `canon.db` (a consistent snapshot, taken while Canon keeps running) and the `images/` folder.
Each backup is a `.tar.gz` with `canon.db` (a consistent snapshot taken while Canon keeps running) and the `images/` folder.
- **Where:** `CANON_BACKUP_DIR`, by default `data/backups`. Docker Compose mounts the `canon_canon-backups` volume at `/backups`; set `CANON_BACKUP_PATH` to a host folder (a NAS share, say) to keep them off the data volume.
- **Time zone:** schedules use the server's time zone, UTC in Docker unless you set `TZ` (e.g. `TZ=America/Los_Angeles`) in `.env`.
- **Cron syntax:** `minute hour day month weekday`, with `*`, lists (`1,15`), ranges (`mon-fri`), steps (`*/6`) and `@daily`-style shorthands. For example, `30 2 * * mon-fri` is 2:30 on weekdays.
- Backups go to `CANON_BACKUP_DIR`, which defaults to `data/backups`. Docker Compose mounts the `canon_canon-backups` volume at
`/backups`. Set `CANON_BACKUP_PATH` to a host folder (a NAS share, say) to keep them off the data volume.
- Schedules use the server's time zone. In Docker that's UTC unless you set `TZ` (e.g. `TZ=America/Los_Angeles`) in `.env`.
- Cron syntax is `minute hour day month weekday`, with `*`, lists (`1,15`), ranges (`mon-fri`), steps (`*/6`) and shorthands like
`@daily`. For example, `30 2 * * mon-fri` is 2:30 on weekdays.
To restore, stop Canon, extract a backup into the data folder, and start it again:
@@ -203,7 +258,8 @@ go test ./...
go test -tags live ./internal/providers # hits the real music APIs
```
Templates and static assets in `web/` are embedded into the binary, so rebuild after editing them. The front end is plain ES modules with no build step.
Templates and static files in `web/` are embedded in the binary, so rebuild after you change them. The front end is plain ES modules
with no build step.
```
cmd/canon entry point, first-run bootstrap