mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-09-13 22:10:11 -04:00
295 lines
8.8 KiB
Bash
295 lines
8.8 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# Rebuild e2e/arr-seed from scratch.
|
|
#
|
|
# Boots the arrs empty, configures them over their own API, adds one item each.
|
|
# 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.
|
|
#
|
|
# Re-run it when the pinned image tags change, or when the library has to change.
|
|
#
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
TEST_DATA="$HERE/test-data"
|
|
ARR_SEED="$HERE/arr-seed"
|
|
COMPOSE="docker compose -f $HERE/docker-compose.e2e.yml"
|
|
|
|
SONARR_URL="http://localhost:8989"
|
|
RADARR_URL="http://localhost:7878"
|
|
SONARR_API_KEY="0000000000000000000000000000e2e1"
|
|
RADARR_API_KEY="0000000000000000000000000000e2e2"
|
|
|
|
# The mock Torznab indexer, and qBittorrent, both on the host network.
|
|
INDEXER_URL="http://127.0.0.1:9500"
|
|
INDEXER_API_KEY="e2e-indexer-key"
|
|
QBIT_HOST="127.0.0.1"
|
|
QBIT_PORT=8090
|
|
|
|
# Agatha All Along: one season, recent, unambiguous release names.
|
|
SONARR_TVDB_ID=412429
|
|
# F1: recent, and its releases parse cleanly against the movie title plus year.
|
|
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
|
|
|
|
log() {
|
|
echo "[seed-arr] $*"
|
|
}
|
|
|
|
write_config_xml() {
|
|
local dir="$1" port="$2" api_key="$3" instance="$4"
|
|
|
|
rm -rf "$dir"
|
|
mkdir -p "$dir"
|
|
cat > "$dir/config.xml" <<EOF
|
|
<Config>
|
|
<BindAddress>*</BindAddress>
|
|
<Port>$port</Port>
|
|
<EnableSsl>False</EnableSsl>
|
|
<LaunchBrowser>False</LaunchBrowser>
|
|
<ApiKey>$api_key</ApiKey>
|
|
<AuthenticationMethod>External</AuthenticationMethod>
|
|
<AuthenticationRequired>DisabledForLocalAddresses</AuthenticationRequired>
|
|
<Branch>main</Branch>
|
|
<LogLevel>info</LogLevel>
|
|
<UrlBase></UrlBase>
|
|
<InstanceName>$instance</InstanceName>
|
|
<UpdateMechanism>Docker</UpdateMechanism>
|
|
<AnalyticsEnabled>False</AnalyticsEnabled>
|
|
</Config>
|
|
EOF
|
|
chmod -R a+rwX "$dir"
|
|
}
|
|
|
|
arr_post() {
|
|
local url="$1" api_key="$2" path="$3" body="$4"
|
|
|
|
curl -fsS -X POST "$url$path" \
|
|
-H "X-Api-Key: $api_key" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "$body"
|
|
}
|
|
|
|
arr_get() {
|
|
local url="$1" api_key="$2" path="$3"
|
|
|
|
curl -fsS "$url$path" -H "X-Api-Key: $api_key"
|
|
}
|
|
|
|
# An arr validates the posted item before it enriches it from its metadata provider.
|
|
# An add therefore carries the whole lookup result, not just the id.
|
|
merge_add_options() {
|
|
local root_folder="$1" add_options="$2" monitored="${3:-true}"
|
|
|
|
python3 -c '
|
|
import json
|
|
import sys
|
|
|
|
item = json.load(sys.stdin)[0]
|
|
item.update({
|
|
"qualityProfileId": 1,
|
|
"rootFolderPath": sys.argv[1],
|
|
"monitored": sys.argv[3] == "true",
|
|
"seasonFolder": True,
|
|
"minimumAvailability": "released",
|
|
"addOptions": json.loads(sys.argv[2]),
|
|
})
|
|
print(json.dumps(item))
|
|
' "$root_folder" "$add_options" "$monitored"
|
|
}
|
|
|
|
wait_for_arr() {
|
|
local url="$1" name="$2"
|
|
|
|
log "waiting for $name"
|
|
for _ in $(seq 1 120); do
|
|
if curl -fsS -o /dev/null "$url/ping"; then
|
|
log "$name is up"
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
echo "[seed-arr] $name did not start" >&2
|
|
return 1
|
|
}
|
|
|
|
# Waits until the arr reports at least one item of the given kind.
|
|
# The add call returns before the metadata refresh finishes.
|
|
wait_for_items() {
|
|
local url="$1" api_key="$2" path="$3" name="$4"
|
|
|
|
log "waiting for $name"
|
|
for _ in $(seq 1 90); do
|
|
local count
|
|
count=$(arr_get "$url" "$api_key" "$path" | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))' 2>/dev/null || echo 0)
|
|
if [[ "$count" -gt 0 ]]; then
|
|
log "$name ready ($count)"
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
echo "[seed-arr] $name never appeared" >&2
|
|
return 1
|
|
}
|
|
|
|
# forceSave skips the field validation, but not the connection test.
|
|
# The mock indexer has to be up and answering caps.
|
|
add_indexer() {
|
|
local url="$1" api_key="$2" categories="$3"
|
|
|
|
arr_post "$url" "$api_key" '/api/v3/indexer?forceSave=true' "$(cat <<EOF
|
|
{
|
|
"name": "E2E Torznab",
|
|
"implementation": "Torznab",
|
|
"implementationName": "Torznab",
|
|
"configContract": "TorznabSettings",
|
|
"protocol": "torrent",
|
|
"enableRss": false,
|
|
"enableAutomaticSearch": true,
|
|
"enableInteractiveSearch": true,
|
|
"supportsRss": true,
|
|
"supportsSearch": true,
|
|
"priority": 25,
|
|
"tags": [],
|
|
"fields": [
|
|
{ "name": "baseUrl", "value": "$INDEXER_URL" },
|
|
{ "name": "apiPath", "value": "/api" },
|
|
{ "name": "apiKey", "value": "$INDEXER_API_KEY" },
|
|
{ "name": "categories", "value": $categories },
|
|
{ "name": "animeCategories", "value": [] },
|
|
{ "name": "minimumSeeders", "value": 1 }
|
|
]
|
|
}
|
|
EOF
|
|
)" > /dev/null
|
|
}
|
|
|
|
add_download_client() {
|
|
local url="$1" api_key="$2" category_field="$3" category="$4"
|
|
|
|
arr_post "$url" "$api_key" '/api/v3/downloadclient?forceSave=true' "$(cat <<EOF
|
|
{
|
|
"name": "qBittorrent",
|
|
"implementation": "QBittorrent",
|
|
"implementationName": "qBittorrent",
|
|
"configContract": "QBittorrentSettings",
|
|
"protocol": "torrent",
|
|
"enable": true,
|
|
"priority": 1,
|
|
"removeCompletedDownloads": false,
|
|
"removeFailedDownloads": false,
|
|
"tags": [],
|
|
"fields": [
|
|
{ "name": "host", "value": "$QBIT_HOST" },
|
|
{ "name": "port", "value": $QBIT_PORT },
|
|
{ "name": "useSsl", "value": false },
|
|
{ "name": "urlBase", "value": "" },
|
|
{ "name": "username", "value": "admin" },
|
|
{ "name": "password", "value": "adminadmin" },
|
|
{ "name": "$category_field", "value": "$category" },
|
|
{ "name": "initialState", "value": 0 },
|
|
{ "name": "sequentialOrder", "value": false },
|
|
{ "name": "firstAndLast", "value": false }
|
|
]
|
|
}
|
|
EOF
|
|
)" > /dev/null
|
|
}
|
|
|
|
seed_sonarr() {
|
|
log "configuring sonarr"
|
|
arr_post "$SONARR_URL" "$SONARR_API_KEY" '/api/v3/rootfolder' '{"path":"/tv"}' > /dev/null
|
|
add_indexer "$SONARR_URL" "$SONARR_API_KEY" '[5000]'
|
|
add_download_client "$SONARR_URL" "$SONARR_API_KEY" 'tvCategory' 'tv-sonarr'
|
|
|
|
# firstSeason leaves the specials unmonitored.
|
|
# The Seeker then has one season to pick and the spec's release title is fixed.
|
|
log "adding the series"
|
|
local series
|
|
series=$(arr_get "$SONARR_URL" "$SONARR_API_KEY" "/api/v3/series/lookup?term=tvdb%3A$SONARR_TVDB_ID" \
|
|
| merge_add_options '/tv' '{"monitor":"firstSeason","searchForMissingEpisodes":false,"searchForCutoffUnmetEpisodes":false}')
|
|
arr_post "$SONARR_URL" "$SONARR_API_KEY" '/api/v3/series' "$series" > /dev/null
|
|
|
|
wait_for_items "$SONARR_URL" "$SONARR_API_KEY" '/api/v3/episode?seriesId=1' 'sonarr episodes'
|
|
}
|
|
|
|
seed_radarr() {
|
|
log "configuring radarr"
|
|
arr_post "$RADARR_URL" "$RADARR_API_KEY" '/api/v3/rootfolder' '{"path":"/movies"}' > /dev/null
|
|
add_indexer "$RADARR_URL" "$RADARR_API_KEY" '[2000]'
|
|
add_download_client "$RADARR_URL" "$RADARR_API_KEY" 'movieCategory' 'radarr'
|
|
|
|
log "adding the movie"
|
|
local movie
|
|
movie=$(arr_get "$RADARR_URL" "$RADARR_API_KEY" "/api/v3/movie/lookup?term=tmdb%3A$RADARR_TMDB_ID" \
|
|
| merge_add_options '/movies' '{"searchForMovie":false}')
|
|
arr_post "$RADARR_URL" "$RADARR_API_KEY" '/api/v3/movie' "$movie" > /dev/null
|
|
|
|
log "adding the second movie"
|
|
local second
|
|
second=$(arr_get "$RADARR_URL" "$RADARR_API_KEY" "/api/v3/movie/lookup?term=tmdb%3A$RADARR_SECOND_TMDB_ID" \
|
|
| merge_add_options '/movies' '{"searchForMovie":false}' 'false')
|
|
arr_post "$RADARR_URL" "$RADARR_API_KEY" '/api/v3/movie' "$second" > /dev/null
|
|
|
|
wait_for_items "$RADARR_URL" "$RADARR_API_KEY" '/api/v3/movie' 'radarr movies'
|
|
}
|
|
|
|
# Keeps the database and config.xml, drops everything the arr rebuilds on start.
|
|
snapshot() {
|
|
local arr="$1"
|
|
local src="$TEST_DATA/$arr-config"
|
|
local dest="$ARR_SEED/$arr"
|
|
|
|
log "snapshotting $arr"
|
|
rm -rf "$dest"
|
|
mkdir -p "$dest"
|
|
cp "$src/config.xml" "$dest/config.xml"
|
|
|
|
# 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"
|
|
for suffix in wal shm; do
|
|
if [[ -f "$src/$arr.db-$suffix" ]]; then
|
|
cp "$src/$arr.db-$suffix" "$dest/$arr.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"
|
|
}
|
|
|
|
main() {
|
|
bash "$HERE/scripts/setup-test-data.sh" > /dev/null
|
|
|
|
write_config_xml "$TEST_DATA/sonarr-config" 8989 "$SONARR_API_KEY" 'Sonarr'
|
|
write_config_xml "$TEST_DATA/radarr-config" 7878 "$RADARR_API_KEY" 'Radarr'
|
|
|
|
# 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
|
|
|
|
wait_for_arr "$SONARR_URL" 'sonarr'
|
|
wait_for_arr "$RADARR_URL" 'radarr'
|
|
|
|
seed_sonarr
|
|
seed_radarr
|
|
|
|
log "stopping the arrs"
|
|
$COMPOSE stop wiremock-indexer qbittorrent sonarr radarr
|
|
|
|
snapshot sonarr
|
|
snapshot radarr
|
|
|
|
log "seed written to $ARR_SEED"
|
|
}
|
|
|
|
main "$@"
|