From 5fd9a05997a582e55ac0e57e39889c6b4221d21e Mon Sep 17 00:00:00 2001 From: fcote Date: Sat, 5 Sep 2026 00:01:18 +0200 Subject: [PATCH] chore: streamline project documentation The README and agent guide repeated implementation detail, making setup and the repository's important operating constraints harder to find. The README also described older Python and Docker Compose usage. Reorganize both documents around concise quick-reference sections. Keep the database, crawler, private-list, configuration, merge, and release safeguards while shortening feature and implementation descriptions. --- AGENTS.md | 221 ++++++++++++++++++---------------------------- README.md | 255 ++++++++++++++++++++++++------------------------------ 2 files changed, 196 insertions(+), 280 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c37eccf..f455c1a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,177 +1,120 @@ -# Agent instructions +# Letterboxarr agent guide -This file provides guidance to coding agents working in this repository. - -## What this is - -Letterboxarr scrapes Letterboxd lists and feeds the films on them to Radarr. It -is one FastAPI process (`main.py`, port 7373) that serves both the JSON API and -the built React SPA, with a background thread doing the crawling and syncing. -Deployed as a single Docker image. +Letterboxarr is a FastAPI service that scrapes Letterboxd lists and sends their +films to Radarr. `main.py` serves the API and built React SPA on port 7373 while +a background thread crawls and syncs. The application ships as one Docker image. ## Commands ```bash -# Run the server (serves API + frontend/build at http://localhost:7373) +# Run the API and built frontend at http://localhost:7373 python main.py -# Frontend: build, then let the backend serve it +# Install and build the frontend cd frontend && npm install && npm run build -# Frontend typecheck — the only static check in the repo +# Type-check the frontend cd frontend && npx tsc --noEmit -# Docker +# Build the image docker build -t letterboxarr . ``` -**There is no test suite** — no pytest, no `*.test.tsx`, and `react-scripts -test` has nothing to run. Verify changes by exercising the real thing: run the -server, or import the module and call the function against real scraped data. -When you touch a pure function, a throwaway script comparing its output across -cases is the expected level of rigour. - -`cd frontend && npm start` will serve the UI on :3000 but **its API calls will -404** — `package.json` has no `proxy` field and axios uses a relative -`baseURL: '/api'`. Build and let the backend serve it. - -## Importing the backend has side effects +There is no test suite. Verify changes against the running application, or use a +small throwaway script for a pure function. `npm start` serves the frontend on +port 3000, but API requests will fail because axios uses `/api` and +`frontend/package.json` has no proxy. Build the frontend and serve it through +the backend instead. -`lib_api.py` constructs its `LetterboxarrAPIContext` singleton **at module -import time** (`context = LetterboxarrAPIContext()` at the bottom of the class -definitions). Importing `lib_api` for any reason — including to unit-test one -pure function — loads `config.yml`, opens/migrates `./data/letterboxarr.db`, -and starts the background sync thread against the live Letterboxd and Radarr. +### Import safety -For a quick check of a pure helper, this is usually tolerable (the thread dies -with the process), but know that it happens and never do it against data you -care about. +Importing `lib_api` constructs the global `LetterboxarrAPIContext`. When +`config.yml` exists, that import opens and migrates `data/letterboxarr.db` and +starts the live Letterboxd/Radarr sync thread. Do not import it while pointing at +data or services you are unwilling to modify. -## Architecture +## Architecture and invariants -Data flows in one direction, and every read the UI does stops at SQLite: - -``` -config.yml ──> lib_config watch items (Letterboxd paths + per-list filters/tags) - │ - v - lib_letterboxd scraper: curl_cffi impersonation + BeautifulSoup - │ - v - lib_db SQLite — the source of truth, not a cache - │ - ┌──────────┴──────────┐ - v v - lib_radarr lib_api FastAPI routes, JWT auth, serves the SPA - (adds movies) (reads stored data only) +```text +config.yml -> lib_config -> lib_sync + |-> lib_refresh -> lib_letterboxd -> SQLite + `-> lib_radarr +SQLite -> lib_api -> React SPA ``` -`lib_sync.LetterboxarrSync.sync_once()` is the round, driven on the configured -interval by `LetterboxarrThread`: refresh the listings, hand new films to -Radarr, then read release tables, then read ratings. `lib_refresh.ListRefresher` -owns all the "keep the stored data fresh" logic. - -### The database is the application's data, not a cache - -This is the single most important idea in the codebase and it is why -`lib_db.py` has no expiry anywhere. API reads answer from SQLite; the -background refresher replaces a stored listing **only once its replacement has -been read in full**. A crawl that is slow, refused or rate-limited therefore -degrades into serving yesterday's list rather than serving nothing or, worse, -serving a half-read list as if films had left it. - -Consequences worth internalising before changing scraper or refresher code: - -- A partial crawl must raise, not return what it got. Returning a short list - silently overwrites a complete one, which reads downstream as films having - been removed — and auto-add reacts to that. -- Endpoints never crawl. Opening a page must not wait on Letterboxd. If you - need data the UI doesn't have, the fix goes in `ListRefresher`, not the route. - -### Crawl budgets - -Letterboxd rate-limits and bot-blocks, so every request goes through a single -`crawl_lock` — no two crawls ever run concurrently — and the paging loops sleep -a second between pages. Listings are a page per hundred films; -release tables and ratings are a page *per film*, so they are budgeted -separately in `lib_refresh.py`: - -| | max age | reads per round | -|---|---|---| -| Release tables | 12 h | 100 | -| Ratings | 30 d | 500 | - -Anything left over is logged and picked up by later rounds. Raising these has a -direct wall-clock cost on every sync round — the constants carry the reasoning -in their comments. - -### Scraper specifics (`lib_letterboxd.py`) - -- **`curl_cffi`, not `requests`**, for browser TLS impersonation. Fingerprints - are tried in order because Letterboxd refuses some of them on member pages - (a 403 on page 2 while page 1 answers fine). -- **Categories overlap** (`film`, `short_film`, `documentary`, `tv_show`, - `unreleased`) so `CATEGORY_SKIP_FILTERS` is ordered and first match wins, - with `unreleased` first. -- **Dates are parsed against a `MONTHS` table, not `strptime`** — `%b` follows - the process locale, and a base image that set one would silently stop reading - every date on the page. -- **Watch items accept a path or a whole URL.** A privately shared list is only - reachable through its secret `boxd.it` link; its ordinary - `//list//` URL 404s for everyone but the owner. -- Posters must be read from the main column, not the whole document — a cloned - list shows its source's posters in the sidebar on every page. +- `lib_sync.LetterboxarrSync.sync_once()` runs a round: refresh listings, send + new films to Radarr, read release tables, then read ratings. +- `lib_refresh.ListRefresher` owns stored-data freshness. API routes read SQLite + and must never crawl Letterboxd. +- SQLite is the application's source of truth, not a disposable cache. Replace a + listing only after its complete replacement has been read. +- A partial or refused crawl must raise. Returning partial results can overwrite + a complete list and make downstream code treat missing films as removals. + +### Crawl limits + +All Letterboxd requests share `crawl_lock`; never make crawls concurrent. Paging +loops pause between requests. Release tables and ratings cost one page per film, +so `lib_refresh.py` budgets them separately: + +| Data | Maximum age | Reads per round | +| --- | ---: | ---: | +| Release tables | 12 hours | 100 | +| Ratings | 30 days | 500 | + +Increasing these values directly lengthens sync rounds. Unread work is logged +and carried into later rounds. + +### Scraper constraints + +- Use `curl_cffi`'s requests-compatible client, not Requests, for Letterboxd. + Browser fingerprints are tried in order because some are refused on later + member-list pages. +- `CATEGORY_SKIP_FILTERS` is ordered because categories overlap. Keep + `unreleased` first. +- Parse release dates with `MONTHS`, not `strptime`; `%b` depends on locale. +- Watch items accept paths or full URLs. Preserve full `boxd.it` URLs because a + privately shared list may 404 at its ordinary `//list//` URL. +- Read posters from the main column. Cloned lists repeat their source's posters + in the sidebar. ## Configuration -`config.yml` (gitignored; see `examples/config.example.yml`) is the only live -configuration path — edited through the UI as well as by hand. Two traps: +`config.yml` is the live application configuration and is edited both by hand +and through the UI. See `examples/config.example.yml`. -- **`.env` in the repo root is not read by the application.** Nothing imports - `python-dotenv`, and `lib_config.load_config_from_env()` — which reads - `RADARR_*`, `LETTERBOXD_USERNAME`, `SYNC_INTERVAL_MINUTES` — **has no call - sites and is dead legacy code**. Those variables are for docker-compose and - shell use only. Changing them changes nothing about a running app. -- The env vars that *are* live are read by `lib_api.py` at import: - `SECRET_KEY`, `ADMIN_USERNAME`, `ADMIN_PASSWORD`. All three have insecure - defaults. - -`letterboxd.country` matters more than it looks: it is spelled the way -Letterboxd spells it in a film's releases table (`USA`, `UK`, `France`, -`Czechia`), and it drives the whole Upcoming tab. +- A repository `.env` file is not loaded by the application. + `lib_config.load_config_from_env()` has no call sites and is legacy code. +- `SECRET_KEY`, `ADMIN_USERNAME`, and `ADMIN_PASSWORD` are read from the + environment when `lib_api` is imported. Their defaults are insecure. +- `letterboxd.country` must use Letterboxd's spelling from its release tables, + such as `USA`, `UK`, `France`, or `Czechia`; it drives the Upcoming page. ## Code style -The prose in this codebase is load-bearing and quite specific — match it rather -than defaulting to house style. - -- **Comments and docstrings say *why*, in full sentences**, and name the - concrete failure they prevent ("a 403 on page two of a 264-film list cut it - to a hundred"). They do not restate what the code does. A rule with a - non-obvious edge gets a paragraph explaining the edge, not a bullet list. -- **Docstring first line is a phrase, not a sentence** — "The release a film is - dated by, None when it has none still to come". -- Real examples over abstractions: an actual film, an actual count. -- No emoji in code or comments. Frontend copy is sentence case and explains - itself to the user (see the empty states in `UpcomingPage.tsx`, which - distinguish four reasons a page can be empty). +- Comments and docstrings explain why, in full sentences, and name the concrete + failure an unusual rule prevents. Do not restate the code. +- Start docstrings with a phrase rather than a sentence, matching the existing + code. +- Prefer real examples and counts over abstractions. +- Do not use emoji in code or comments. +- Keep frontend copy in sentence case and make empty states explain why no data + is shown. ## Merges Use the `merge` skill when asked to commit, push, create a PR and merge without cutting a release. It switches to a conventional branch, writes a Why/What PR, -requires its build to pass and squash-merges it to `main`. Claude exposes the +requires its build to pass, and squash-merges it to `main`. Claude exposes the same workflow as `/merge`; `.agents/skills/merge/SKILL.md` remains the single source of truth. ## Releases -Use the `release` skill with a `patch`, `minor` or `major` bump. It switches to -a conventional branch, commits and pushes it, opens and squash-merges a PR, -then tags the merge and watches the builds. See -`.agents/skills/release/SKILL.md` for the conventions and known credential -failures. +Use the `release` skill with a `patch`, `minor`, or `major` bump. It switches to +a conventional branch, commits the work, opens and squash-merges a PR, tags the +merge, and watches the builds. Its full procedure and credential-failure +guidance live in `.agents/skills/release/SKILL.md`. -Pushing a `v*.*.*` tag publishes to Docker Hub and cuts a GitHub Release; a -push to `main` moves `latest`. **Both** runs must pass — the Docker Hub login -is step 5 of 8, so a lapsed credential leaves a tag with no image behind it. +A `v*.*.*` tag publishes the versioned Docker image and GitHub Release; a push +to `main` publishes `latest`. Both workflows must pass. diff --git a/README.md b/README.md index c283904..367b0a9 100644 --- a/README.md +++ b/README.md @@ -1,204 +1,177 @@
-Letterboxarr Logo +Letterboxarr logo # Letterboxarr +Automatically sync Letterboxd lists to Radarr. +
-Automatically sync your Letterboxd lists to your Radarr instance. This script periodically checks your configured Letterboxd lists and adds any new movies to Radarr. +Letterboxarr periodically reads configured Letterboxd lists, adds new films to +Radarr, and serves a web interface for configuration and monitoring. -![Letterboxarr Preview](screenshots/dashboard.png) +![Letterboxarr dashboard](screenshots/dashboard.png) ## Features -- 🎬 Scrapes multiple Letterboxd lists (watchlists, collections, actors, directors, etc.) -- 🏷️ Automatic tag assignment to movies based on their source list -- 🔄 Automatic periodic synchronization -- 📝 Keeps added movies, watched films and the lists themselves in a SQLite database -- 🌙 Reads your lists from Letterboxd in the background, so the interface never waits on a crawl -- 🐳 Docker support for easy deployment -- 🔍 Smart movie matching using title and year, falling back to TMDB ID -- ⚡ Configurable sync interval and filters -- 🎭 Per-list filtering (skip documentaries, short films, etc.) -- 🗂️ Movies view split into films, short films, documentaries and TV shows -- 📅 Upcoming tab listing what your lists are still waiting on, by release date in your country -- 👁️ Flags the films you have already watched on your Letterboxd profile -- 📊 Per-category watched progress on each watch item -- ⭐ Reads how Letterboxd rates each film, so watch items sort by rating, by rating weighted against how many films they hold, and by popularity -- 🔄 Per-list refresh button to re-read a list from Letterboxd ahead of its next scheduled refresh -- ⚙️ YAML configuration file support -- 🌐 Web interface for configuration and monitoring - -## Prerequisites - -- A running Radarr instance -- Radarr API key -- Docker and Docker Compose (for containerized deployment) -- Python 3.11+ (for local deployment) -- Node.js 18+ (for frontend development) - -## Setup - -### 1. Get your Radarr API Key - -1. Open Radarr web interface -2. Go to Settings → General → Security -3. Copy your API Key - -### 2. Find your Quality Profile ID - -Run this command to list available quality profiles: -```bash -curl -H "X-Api-Key: YOUR_API_KEY" http://your-radarr-url:7878/api/v3/qualityprofile -``` +- Multiple Letterboxd watchlists, collections, custom lists, and people pages +- Global and per-list filters for documentaries, short films, TV shows, and + unreleased titles +- Automatic Radarr tags based on the source list +- Configurable quality profile, root folder, monitoring, and search behavior +- Background synchronization with per-list manual refreshes +- Film categories, watched status, and progress for each watch item +- Upcoming releases for a preferred country +- Letterboxd rating, weighted-rating, and popularity sorting +- Persistent SQLite storage +- Authenticated web interface -Note the `id` of your preferred quality profile. +## Quick start with Docker Compose -### 3. Find your Root Folder Path +You need a running Radarr instance, its API key, and Docker with Compose. -Run this command to list available root folders: -```bash -curl -H "X-Api-Key: YOUR_API_KEY" http://your-radarr-url:7878/api/v3/rootfolder -``` +1. Copy the example files: -Note the `path` of your movies folder. + ```bash + cp examples/config.example.yml config.yml + cp examples/docker-compose.yml docker-compose.yml + ``` -### 4. Create the configuration file +2. Edit `config.yml` with your Radarr connection and Letterboxd watch items. -Copy the [example configuration file](examples/config.example.yml) to `config.yml` and customize it with your Radarr URL, API key, quality profile, root folder, and Letterboxd lists. +3. Set secure web credentials in a `.env` file used by Docker Compose: -## Deployment Options + ```dotenv + SECRET_KEY=replace-with-a-long-random-value + ADMIN_USERNAME=admin + ADMIN_PASSWORD=replace-with-a-strong-password + ``` -### Option 1: Docker Compose (Recommended) +4. Start Letterboxarr: -Create a docker-compose.yml file: + ```bash + docker compose up -d + ``` -```yaml ---- -services: - letterboxarr: - image: fcote/letterboxarr:latest - container_name: letterboxarr - restart: unless-stopped - ports: - - "7373:7373" # Web interface - volumes: - - ./config.yml:/app/config.yml # Configuration file - - ./data:/app/data # SQLite database: added movies, watched films, crawled lists - environment: - - SECRET_KEY=${SECRET_KEY:-your-secret-key-change-this-in-production} - - ADMIN_USERNAME=${ADMIN_USERNAME:-admin} - - ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin} -``` +Open [http://localhost:7373](http://localhost:7373). Application data is stored +in `./data`, and the web interface can update the mounted `config.yml`. -Build and run: -```bash -docker-compose up -d -``` +> [!IMPORTANT] +> Without environment overrides, the login is `admin` / `admin` and the JWT +> secret is insecure. Change all three values before exposing the service. -Access the web interface at `http://localhost:7373` +## Configuration -### Option 2: Docker Run +The complete configuration template is in +[`examples/config.example.yml`](examples/config.example.yml). -```bash -docker build -t letterboxarr . - -docker run -d \ - --name letterboxarr \ - --restart unless-stopped \ - -p 7373:7373 \ - -v $(pwd)/config.yml:/app/config.yml \ - -v $(pwd)/data:/app/data \ - -e SECRET_KEY=your-secret-key-change-this \ - -e ADMIN_USERNAME=admin \ - -e ADMIN_PASSWORD=admin \ - letterboxarr -``` +### Radarr -Then access the web interface at `http://localhost:7373` +Letterboxarr needs the Radarr URL, API key, quality-profile ID, and root-folder +path. Find the API key under **Settings → General → Security** in Radarr. -### Option 3: Local Development +List available quality profiles: -Install Python dependencies: ```bash -cd frontend && npm install && npm run build -pip install -r requirements.txt +curl -H "X-Api-Key: YOUR_API_KEY" \ + http://your-radarr-url:7878/api/v3/qualityprofile ``` -Run the web server: +List available root folders: + ```bash -python main.py +curl -H "X-Api-Key: YOUR_API_KEY" \ + http://your-radarr-url:7878/api/v3/rootfolder ``` -### Supported Letterboxd List Types +Use the desired profile's `id` and root folder's `path` in `config.yml`. -- **User Lists**: `username/watchlist`, `username/films`, `username/diary` -- **Collections**: `films/in/collection-name` -- **Popular/Charts**: `films/popular`, `films/popular/this/year` -- **People**: `actor/name`, `director/name`, `writer/name` -- **Genres**: `films/genre/horror`, `films/genre/sci-fi` -- **Custom Lists**: Any valid Letterboxd URL path -- **Private Lists**: The `https://boxd.it/…` link from the list's share menu +### Letterboxd watch items -A watch item is normally the path that follows `letterboxd.com`, but a whole -link is taken as it stands. That is what a private list needs: one set to be -shared "with anyone" is only reachable through the secret `boxd.it` link -Letterboxd hands out for it, and its ordinary `//list//` address -answers 404 to everyone but its owner. A list shared only with friends needs a -signed-in member, so it cannot be read here. +A watch item is normally the path after `letterboxd.com`, for example: -A path Letterboxd will not give up — misspelt, private, or refused — is -reported as such rather than as a list that happens to be empty, both on the -**Test URL** button and on the list's own page. +- `username/watchlist` +- `username/films` +- `films/in/collection-name` +- `films/popular/this/year` +- `actor/name`, `director/name`, or `writer/name` +- `films/genre/horror` -### Tags and Filtering +Other valid Letterboxd paths are accepted. Full URLs are accepted too. For a +private list shared “with anyone,” use the secret `https://boxd.it/...` URL from +its share menu; the ordinary list URL returns 404 to everyone except its owner. +Lists shared only with friends require a signed-in Letterboxd session and cannot +be read by Letterboxarr. -Movies from each list can be automatically tagged in Radarr. Filters can be applied globally or per-list to skip certain types of content. +Each watch item can override the global filters and define Radarr tags. Invalid, +private, or refused paths are reported as unavailable rather than mistaken for +empty lists. -### Upcoming Releases +Set `letterboxd.username` to flag films already watched on a public Letterboxd +profile. Set `letterboxd.country` using Letterboxd's spelling, such as `USA`, +`UK`, `France`, or `Czechia`, to localize the Upcoming page. -The **Upcoming** tab lists the films your watch lists are still waiting on, soonest first, with a button to hand one to Radarr ahead of time so it is monitored and grabbed the day it lands. +## Upcoming releases -Each row opens with what kind of entry it is — the same film strip, clock, video camera and screen the movies page counts films, short films, documentaries and TV shows under — and carries a chip per watch list the film came from, marked by the kind of list it is. Letterboxd names its own listings after what gathers them, so a chip for `director/denis-villeneuve` reads "Denis Villeneuve" rather than a row of lists all reading "Films directed b…". Hovering either the icon or the chips gives the full names, the date and how far off it is, the release and its country, and the tags. Note that a category your filters skip never reaches these lists at all, so filtering out documentaries means no row can ever be marked as one. +The Upcoming page shows future watchable releases for recent films in the +configured watch items. Festival premieres and physical releases are excluded +because they do not indicate when a film becomes generally available. -Festival premieres and physical releases are left out: neither says anything about when a film can actually be watched, and dating a film by a red carpet nobody can attend or by a disc pressed months after it has been streaming would put a date on the page that is no use. A film left with nothing but one of those counts as having nothing ahead. +With a preferred country configured, Letterboxarr uses that country's next +release. If the country has no announced date, it falls back to the earliest +release elsewhere and labels the country used. A film already released in the +preferred country is no longer shown. -Set `letterboxd.country` — on the configuration page or in `config.yml` — to be told when a film comes out where you are. Each film is dated by its soonest release still to come in that country. A film with **no** date announced there at all is dated by the soonest release anywhere instead, and the row says which country that was rather than pretending the date is local; a film whose dates there have all passed is out where you are, so it drops off the page rather than being dated by a release on the other side of the world. +Release pages are comparatively expensive to crawl. Letterboxarr reads at most +100 per sync round and refreshes them no more than twice a day, so a new large +list may take several rounds to fill in. -Only dates still ahead are considered, so a film that has already opened stays on the page for its digital or later local release rather than disappearing the day it premiered somewhere. Films with nothing ahead are counted on the page rather than listed. +## Local development -Only films from the current year onwards are considered. Finding them costs one or two pages per list rather than all of them: each list is read again sorted by release date, newest first, and the crawl stops as soon as it is past the current year. Each of those films then has its release table read from its own Letterboxd page, at most 100 per round and no more often than twice a day, so a long watch list fills in over a few rounds rather than in one long crawl. A film first released in an earlier year is not looked at, so a late local or home-media date for one of those will not appear. +Local development requires Python 3.9+ and Node.js 18+. -### Authentication +```bash +cd frontend +npm install +npm run build +cd .. -The web interface is protected by authentication. Default credentials: -- Username: `admin` (configurable via `ADMIN_USERNAME` environment variable) -- Password: `admin` (configurable via `ADMIN_PASSWORD` environment variable) +python -m pip install -r requirements.txt +python main.py +``` -**Important**: Change these credentials in production by setting the environment variables. +Open [http://localhost:7373](http://localhost:7373). The standalone React +development server does not proxy `/api`; build the frontend and let FastAPI +serve it. -## Data Persistence +## Data persistence -Everything is kept in a single SQLite database, `letterboxarr.db`, in the `/app/data` directory in the container (mapped to `./data` on the host): +`data/letterboxarr.db` stores: -- **Added movies** — the movies already handed to Radarr, so they are not added twice and failed lookups are not retried on every sync. -- **Watched films** — the films marked as watched on the configured Letterboxd profile. Refreshed on the sync interval, but only topped up: `/films/by/date/` lists films newest-logged first, so the refresh reads pages until one holds nothing new and stops, which is a single page when nothing has been watched since the last check. Adding films is all a top-up can do, so the profile is also re-read in full once a day to pick up anything no longer watched. -- **Crawled listings** — each list, with the filters it was read with and the order it was read in. Resolving categories reads a list several times, and the Upcoming tab reads the head of it once more sorted by release date, so a single watch item is six listings. -- **Release dates** — every date announced for the recent films your lists hold, one row per country and release type, alongside when each film's page was last read. A film read and found to have no date announced is remembered as such, so it is not read again on the next round. +- Films already handed to Radarr, preventing duplicate additions +- Watched films from the configured Letterboxd profile +- Complete Letterboxd listings and their filters +- Release dates and ratings +- Sync history -Nothing in the database expires. Reads always answer from it, however old it is, and a listing is only ever replaced once a newer read of the same listing has come back in full — so a refused page, a rate limit or a Letterboxd outage costs you a refresh, not your lists. Keeping it current is the background round's job: every `interval_minutes` it reads the watch lists from Letterboxd and then hands what they hold to Radarr, in that order, so a film added to a list reaches Radarr in the same round. The per-list refresh button does the reading half on demand for a single list. +SQLite is the application's source of truth. Reads continue to use the last +complete listing during a refused request, rate limit, or Letterboxd outage; a +partial crawl never replaces stored data. -The previous storage is imported on first start: the entries in `processed_movies.json` are copied into the database and the file is renamed to `processed_movies.json.migrated`, and the `data/cache` directory of per-crawl JSON files is removed. Nothing needs to be done by hand; downgrading is still possible by renaming the file back. Databases written before the listings stopped being a cache keep their added movies and watched films, and read their lists again on the first refresh. +On first start after upgrading from legacy storage, Letterboxarr imports +`data/processed_movies.json`, renames it to `processed_movies.json.migrated`, +and removes superseded per-crawl cache files automatically. ## Contributing -Feel free to submit issues or pull requests for improvements! +Issues and pull requests are welcome. ## License -MIT License +MIT ## Disclaimer -This tool is not affiliated with Letterboxd or Radarr. Use responsibly and respect the terms of service of both platforms. \ No newline at end of file +Letterboxarr is not affiliated with Letterboxd or Radarr. Use it responsibly and +respect both services' terms of use.