diff --git a/e2e/Makefile b/e2e/Makefile index 53ef6c79..9f2e021e 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -13,7 +13,7 @@ APP_SERVICES = app nginx CORE_STACK = keycloak $(WIREMOCKS) CLIENTS_STACK = $(CORE_STACK) tracker qbittorrent transmission deluge utorrent rutorrent sabnews sabnzbd -ARR_STACK = $(CORE_STACK) wiremock-indexer tracker qbittorrent sonarr radarr +ARR_STACK = $(CORE_STACK) wiremock-indexer tracker qbittorrent sonarr radarr whisparr LAZYLIBRARIAN_STACK = $(CORE_STACK) wiremock-indexer tracker qbittorrent sabnews sabnzbd lazylibrarian setup: @@ -61,7 +61,7 @@ up-app: $(COMPOSE) up -d $(WAIT) $(APP_SERVICES) # Rebuilds e2e/arr-seed from scratch. -# Needs internet: Sonarr reads Skyhook and Radarr reads TMDB. +# Needs internet: each arr reads its metadata provider. seed-arr: bash ./scripts/seed-arr.sh diff --git a/e2e/arr-seed/whisparr/config.xml b/e2e/arr-seed/whisparr/config.xml new file mode 100644 index 00000000..6eba446c --- /dev/null +++ b/e2e/arr-seed/whisparr/config.xml @@ -0,0 +1,19 @@ + + * + 6970 + False + False + 0000000000000000000000000000e2e6 + External + DisabledForLocalAddresses + eros + info + + Whisparr + Docker + False + https://api.whisparr.com/v4/{route} + 9898 + + + \ No newline at end of file diff --git a/e2e/arr-seed/whisparr/whisparr3.db b/e2e/arr-seed/whisparr/whisparr3.db new file mode 100644 index 00000000..532c6f65 Binary files /dev/null and b/e2e/arr-seed/whisparr/whisparr3.db differ diff --git a/e2e/docker-compose.e2e.yml b/e2e/docker-compose.e2e.yml index c58b3cd2..8d2aec54 100644 --- a/e2e/docker-compose.e2e.yml +++ b/e2e/docker-compose.e2e.yml @@ -213,7 +213,7 @@ services: retries: 12 start_period: 30s - # Real Sonarr and Radarr, restored from e2e/arr-seed by setup-test-data.sh. + # Real Sonarr, Radarr and Whisparr, restored from e2e/arr-seed by setup-test-data.sh. # Every run starts with the same library, API key, indexer and download client. # Regenerate the seed with `make seed-arr`. # The tags are pinned: a seed database is only valid for the version that wrote it. @@ -253,6 +253,25 @@ services: retries: 12 start_period: 30s + # Listens on 6970 from the seeded config.xml, since the tracker holds 6969. + whisparr: + image: ghcr.io/hotio/whisparr:v3-3.1.0.2116 + network_mode: host + environment: + PUID: "${E2E_PUID:-1000}" + PGID: "${E2E_PGID:-1000}" + TZ: "UTC" + volumes: + - ./test-data/whisparr-config:/config + - ./test-data/whisparr-movies:/movies + - ./test-data/downloads/qbittorrent:/downloads + healthcheck: + test: ["CMD-SHELL", "curl -fs -o /dev/null http://localhost:6970/ping || exit 1"] + interval: 5s + timeout: 3s + retries: 12 + start_period: 30s + # Real LazyLibrarian, restored from e2e/lazylibrarian-seed by setup-test-data.sh. # LazyLibrarian publishes no versions, so the tag names the upstream commit. # The seed database is only valid for the commit that wrote it. diff --git a/e2e/scripts/seed-arr.sh b/e2e/scripts/seed-arr.sh index 1859a3d8..a6454ad1 100644 --- a/e2e/scripts/seed-arr.sh +++ b/e2e/scripts/seed-arr.sh @@ -6,8 +6,8 @@ # Snapshots /config into e2e/arr-seed, which the e2e run restores. # CI therefore needs no network and every run sees the same library. # -# Needs internet: Sonarr reads Skyhook and Radarr reads TMDB. -# Stops the sonarr and radarr containers. +# Needs internet: each arr reads its metadata provider. +# Stops the sonarr, radarr and whisparr containers. # # Re-run it when the pinned image tags change, or when the library has to change. # @@ -20,8 +20,10 @@ COMPOSE="docker compose -f $HERE/docker-compose.e2e.yml" SONARR_URL="http://localhost:8989" RADARR_URL="http://localhost:7878" +WHISPARR_URL="http://localhost:6970" SONARR_API_KEY="0000000000000000000000000000e2e1" RADARR_API_KEY="0000000000000000000000000000e2e2" +WHISPARR_API_KEY="0000000000000000000000000000e2e6" # The mock Torznab indexer, and qBittorrent, both on the host network. INDEXER_URL="http://127.0.0.1:9500" @@ -39,6 +41,8 @@ RADARR_TMDB_ID=911430 # The Wild Robot, added second and left unmonitored. # The specs that need two candidates monitor it themselves and restore it after. RADARR_SECOND_TMDB_ID=1184918 +# Package Holiday (2004): a plain title, since the seed database is public. +WHISPARR_TMDB_ID=1253309 log() { echo "[seed-arr] $*" @@ -252,9 +256,25 @@ seed_radarr() { wait_for_items "$RADARR_URL" "$RADARR_API_KEY" '/api/v3/movie' 'radarr movies' } +seed_whisparr() { + log "configuring whisparr" + arr_post "$WHISPARR_URL" "$WHISPARR_API_KEY" '/api/v3/rootfolder' '{"path":"/movies"}' > /dev/null + add_indexer "$WHISPARR_URL" "$WHISPARR_API_KEY" '[6000]' + add_download_client "$WHISPARR_URL" "$WHISPARR_API_KEY" 'movieCategory' 'whisparr' + + log "adding the whisparr movie" + local movie + movie=$(arr_get "$WHISPARR_URL" "$WHISPARR_API_KEY" "/api/v3/movie/lookup?term=tmdb%3A$WHISPARR_TMDB_ID" \ + | merge_add_options '/movies' '{"searchForMovie":false}') + arr_post "$WHISPARR_URL" "$WHISPARR_API_KEY" '/api/v3/movie' "$movie" > /dev/null + + wait_for_items "$WHISPARR_URL" "$WHISPARR_API_KEY" '/api/v3/movie' 'whisparr movies' +} + # Keeps the database and config.xml, drops everything the arr rebuilds on start. +# Whisparr v3 names its database whisparr3.db. snapshot() { - local arr="$1" + local arr="$1" db="${2:-$1}" local src="$TEST_DATA/$arr-config" local dest="$ARR_SEED/$arr" @@ -265,15 +285,15 @@ snapshot() { # The arr leaves committed rows in the write-ahead log. # The log comes along and is folded into the database before it is dropped. - cp "$src/$arr.db" "$dest/$arr.db" + cp "$src/$db.db" "$dest/$db.db" for suffix in wal shm; do - if [[ -f "$src/$arr.db-$suffix" ]]; then - cp "$src/$arr.db-$suffix" "$dest/$arr.db-$suffix" + if [[ -f "$src/$db.db-$suffix" ]]; then + cp "$src/$db.db-$suffix" "$dest/$db.db-$suffix" fi done - sqlite3 "$dest/$arr.db" 'PRAGMA journal_mode=DELETE; VACUUM;' > /dev/null - rm -f "$dest/$arr.db-wal" "$dest/$arr.db-shm" + sqlite3 "$dest/$db.db" 'PRAGMA journal_mode=DELETE; VACUUM;' > /dev/null + rm -f "$dest/$db.db-wal" "$dest/$db.db-shm" } main() { @@ -281,23 +301,30 @@ main() { write_config_xml "$TEST_DATA/sonarr-config" 8989 "$SONARR_API_KEY" 'Sonarr' write_config_xml "$TEST_DATA/radarr-config" 7878 "$RADARR_API_KEY" 'Radarr' + write_config_xml "$TEST_DATA/whisparr-config" 6970 "$WHISPARR_API_KEY" 'Whisparr' # An arr tests the indexer and the download client before it saves them. # Both have to answer during seeding. log "starting the arrs" - $COMPOSE up -d --force-recreate wiremock-indexer qbittorrent sonarr radarr + $COMPOSE up -d --force-recreate wiremock-indexer qbittorrent sonarr radarr whisparr wait_for_arr "$SONARR_URL" 'sonarr' wait_for_arr "$RADARR_URL" 'radarr' + wait_for_arr "$WHISPARR_URL" 'whisparr' seed_sonarr seed_radarr + seed_whisparr log "stopping the arrs" - $COMPOSE stop wiremock-indexer qbittorrent sonarr radarr + $COMPOSE stop wiremock-indexer qbittorrent sonarr radarr whisparr snapshot sonarr snapshot radarr + snapshot whisparr whisparr3 + + # The seed is public, so it keeps the title and drops the synopsis, posters and cast. + sqlite3 "$ARR_SEED/whisparr/whisparr3.db" "UPDATE MovieMetadata SET Overview = '', Images = '[]', Credits = '[]'; VACUUM;" log "seed written to $ARR_SEED" } diff --git a/e2e/scripts/setup-test-data.sh b/e2e/scripts/setup-test-data.sh index 94de5241..afa29929 100644 --- a/e2e/scripts/setup-test-data.sh +++ b/e2e/scripts/setup-test-data.sh @@ -16,12 +16,13 @@ ARR_SEED="$HERE/arr-seed" rm -rf "$TEST_DATA/torznab-src" # Media an arr imports during a run, wiped so its library always starts empty. -rm -rf "$TEST_DATA/sonarr-tv" "$TEST_DATA/radarr-movies" "$TEST_DATA/lazylibrarian-books" +rm -rf "$TEST_DATA/sonarr-tv" "$TEST_DATA/radarr-movies" "$TEST_DATA/whisparr-movies" "$TEST_DATA/lazylibrarian-books" mkdir -p \ "$TEST_DATA/torznab-src" \ "$TEST_DATA/sonarr-tv" \ "$TEST_DATA/radarr-movies" \ + "$TEST_DATA/whisparr-movies" \ "$TEST_DATA/lazylibrarian-books" \ "$TEST_DATA/downloads/qbittorrent" \ "$TEST_DATA/downloads/transmission" \ @@ -100,9 +101,9 @@ script = Default priority = 0 EOF -# Restore Sonarr and Radarr from the committed seed. +# Restore Sonarr, Radarr and Whisparr from the committed seed. # The arrs write to /config on every start, so the run gets a throwaway copy. -for arr in sonarr radarr; do +for arr in sonarr radarr whisparr; do if [[ -d "$ARR_SEED/$arr" ]]; then rm -rf "$TEST_DATA/$arr-config" cp -R "$ARR_SEED/$arr" "$TEST_DATA/$arr-config" diff --git a/e2e/tests/helpers/live-arr.ts b/e2e/tests/helpers/live-arr.ts index 2750bc24..81763023 100644 --- a/e2e/tests/helpers/live-arr.ts +++ b/e2e/tests/helpers/live-arr.ts @@ -2,7 +2,7 @@ import { TEST_CONFIG } from './test-config'; import { WireMockClient } from './mocks/wiremock-client'; /** - * Direct access to the real Sonarr and Radarr containers. + * Direct access to the real Sonarr, Radarr and Whisparr v3 containers. * * The specs drive Cleanuparr through its own API, like every other spec. * This is the other side of the contract: what the arr itself ended up with. @@ -141,5 +141,11 @@ export const liveRadarr = new LiveArr( 'includeUnknownMovieItems=true&includeMovie=true', ); +export const liveWhisparr = new LiveArr( + TEST_CONFIG.liveArr.whisparrUrl, + TEST_CONFIG.liveArr.whisparrApiKey, + 'includeUnknownMovieItems=true&includeMovie=true', +); + /** The fake Torznab indexer, kept out of MockServers: other suites lack its container. */ export const indexerMock = new WireMockClient(TEST_CONFIG.mocks.indexerAdminUrl); diff --git a/e2e/tests/helpers/mocks/torznab-stubs.ts b/e2e/tests/helpers/mocks/torznab-stubs.ts index fcfb35f2..acbf3d78 100644 --- a/e2e/tests/helpers/mocks/torznab-stubs.ts +++ b/e2e/tests/helpers/mocks/torznab-stubs.ts @@ -16,6 +16,7 @@ export type TorznabSearchMode = 'tvsearch' | 'movie' | 'search'; /** Torznab category. The seeded arrs are configured for the parent categories. */ export const TORZNAB_TV_CATEGORY = 5040; export const TORZNAB_MOVIE_CATEGORY = 2040; +export const TORZNAB_XXX_CATEGORY = 6040; /** * The advertised release size. diff --git a/e2e/tests/helpers/seeker-live.ts b/e2e/tests/helpers/seeker-live.ts index 45d4f068..4d2204b5 100644 --- a/e2e/tests/helpers/seeker-live.ts +++ b/e2e/tests/helpers/seeker-live.ts @@ -6,9 +6,9 @@ import { buildSingleFileTorrent } from './torrent-fixtures'; import type { CleanuparrApi } from './api'; import { buildDownloadClientPayload } from './api/download-client'; import type { ArrType } from './api/arr'; -import { LiveArr, indexerMock, liveRadarr, liveSonarr } from './live-arr'; +import { LiveArr, indexerMock, liveRadarr, liveSonarr, liveWhisparr } from './live-arr'; import { QBittorrentDriver } from './torrent-clients/qbittorrent'; -import { TORZNAB_MOVIE_CATEGORY, TORZNAB_TV_CATEGORY } from './mocks/torznab-stubs'; +import { TORZNAB_MOVIE_CATEGORY, TORZNAB_TV_CATEGORY, TORZNAB_XXX_CATEGORY } from './mocks/torznab-stubs'; import { mkdirShared, writeFileShared } from './shared-volume'; /** @@ -97,6 +97,21 @@ export const RADARR: SeededArr = { downloadCategory: 'radarr', }; +export const WHISPARR: SeededArr = { + type: 'whisparr', + arr: liveWhisparr, + url: TEST_CONFIG.liveArr.whisparrUrl, + apiKey: TEST_CONFIG.liveArr.whisparrApiKey, + version: 3, + searchMode: 'movie', + category: TORZNAB_XXX_CATEGORY, + release: `Package.Holiday.2004.1080p.WEB-DL.x264-E2E${RUN_TAG}`, + itemId: 1, + itemPath: 'movie', + seededTitle: TEST_CONFIG.liveArr.seededWhisparrTitle, + downloadCategory: 'whisparr', +}; + export const qbittorrent = new QBittorrentDriver(); /** Outside the qBittorrent save path, so a decoy download never finds its data. */ @@ -289,15 +304,15 @@ export async function waitForArrQueue(arr: LiveArr, downloadId: string, timeoutM } /** - * Empties both arr queues and the download client. + * Empties every arr queue and the download client. * * The Seeker skips an item that is already downloading. * The client is purged first, or the arr re-adds the torrent on its next refresh. */ export async function resetDownloads(): Promise { await qbittorrent.clearAllTorrents(); - await Promise.all([liveSonarr.clearQueue(), liveRadarr.clearQueue()]); - await Promise.all([waitForEmptyQueue(liveSonarr), waitForEmptyQueue(liveRadarr)]); + await Promise.all([liveSonarr.clearQueue(), liveRadarr.clearQueue(), liveWhisparr.clearQueue()]); + await Promise.all([waitForEmptyQueue(liveSonarr), waitForEmptyQueue(liveRadarr), waitForEmptyQueue(liveWhisparr)]); } async function waitForEmptyQueue(arr: LiveArr, timeoutMs = 120_000): Promise { diff --git a/e2e/tests/helpers/test-config.ts b/e2e/tests/helpers/test-config.ts index fb8649a5..63c1539d 100644 --- a/e2e/tests/helpers/test-config.ts +++ b/e2e/tests/helpers/test-config.ts @@ -26,7 +26,7 @@ export const TEST_CONFIG = { indexerAdminUrl: 'http://localhost:9500/__admin', }, - // The real Sonarr and Radarr containers. + // The real Sonarr, Radarr and Whisparr v3 containers. // Their api keys come from the committed seed in e2e/arr-seed. liveArr: { sonarrUrl: 'http://localhost:8989', @@ -35,6 +35,9 @@ export const TEST_CONFIG = { radarrApiKey: '0000000000000000000000000000e2e2', seededSeriesTitle: 'Agatha All Along', seededMovieTitle: 'F1', + whisparrUrl: 'http://localhost:6970', + whisparrApiKey: '0000000000000000000000000000e2e6', + seededWhisparrTitle: 'Package Holiday', }, // Real qBittorrent container: the API key seeded into diff --git a/e2e/wiremock-indexer/__files/torznab-caps.xml b/e2e/wiremock-indexer/__files/torznab-caps.xml index 10125f9e..33ad55cb 100644 --- a/e2e/wiremock-indexer/__files/torznab-caps.xml +++ b/e2e/wiremock-indexer/__files/torznab-caps.xml @@ -17,5 +17,8 @@ + + +