Files
austinried 060a5f036f ci: key the test music cache by the demo album ids
The album id is the server id, so use it for the download, the local folder and the prune fixture instead of maintaining a separate remoteId mapping. The cache key moves to v3 because v2 may already hold folders named after the old local ids.
2026-11-01 03:19:17 +00:00

244 lines
12 KiB
Nu

use ./util.nu *
use ./subsonic.nu *
export const NAV_PORT = 4533
export const GONIC_PORT = 4747
export const LMS_PORT = 5082
export const NC_PORT = 8090
export const PRUNE_PORT = 4534
export const LMS_KEY = "subtracks-lms"
export const NC_KEY = "subtracks-nextcloud"
export const LIB = ".integration/music"
export const ports = [$NAV_PORT $GONIC_PORT $LMS_PORT $NC_PORT $PRUNE_PORT]
const MUSIC = [
{ id: "7GZYpfn8wypaTR00LWeNI4", name: "Ugress - Retroconnaissance", songs: 4 }
{ id: "5gMe1SlZZzuOMkRVWJaodV", name: "Ugress - Kosmonaut", songs: 4 }
{ id: "0JnRLI5lo4eNU1pOePqfcG", name: "Brad Sucks - I Don't Know What I'm Doing", songs: 12 }
]
const MUSIC_DOWNLOAD = "https://demo.navidrome.org/rest/download.view?u=demo&p=demo&v=1.16.1&c=subtracks-test&id="
const MUSIC_ART = "https://demo.navidrome.org/rest/getCoverArt.view?u=demo&p=demo&v=1.16.1&c=subtracks-test&id="
# Keep in step with TestServers.kt.
export def test-servers [] {
[
{ name: "navidrome", base: $"http://localhost:($NAV_PORT)/", user: "admin", pass: "password" }
{ name: "gonic", base: $"http://localhost:($GONIC_PORT)/", user: "admin", pass: "admin" }
{ name: "lms", base: $"http://localhost:($LMS_PORT)/", user: "admin", pass: $LMS_KEY }
{ name: "nextcloud", base: $"http://localhost:($NC_PORT)/index.php/apps/music/subsonic/", user: "admin", pass: $NC_KEY }
]
}
def song-count [dir: string] {
if not ($dir | path exists) { return 0 }
ls $dir | where type == file | where {|f| $f.name =~ '(?i)\.mp3$' } | length
}
export def ensure-music [] {
if ($MUSIC | all {|m| (song-count $"($LIB)/($m.id)") == $m.songs and ($"($LIB)/($m.id)/folder.jpg" | path exists) }) {
print $"using test music in ($LIB)"
return
}
print "downloading test music from demo.navidrome.org (one-time)"
mkdir $LIB
let tmp = (^mktemp -d | str trim)
for m in $MUSIC {
print $"($m.name)..."
^curl -fsS -o $"($tmp)/($m.id).zip" $"($MUSIC_DOWNLOAD)($m.id)"
^unzip -j -q -o $"($tmp)/($m.id).zip" -d $"($LIB)/($m.id)"
^curl -fsS -o $"($LIB)/($m.id)/folder.jpg" $"($MUSIC_ART)($m.id)"
}
rm -rf $tmp
}
export def start-navidrome [work: string] {
let data = ($work | path join "navidrome")
mkdir $data
let daemon = (job spawn { ^navidrome --address 127.0.0.1 --port $NAV_PORT --musicfolder $LIB --datafolder $data --loglevel warn out+err> ($work | path join "navidrome.log") })
let ready = (try {
poll "navidrome" {||
try { http get -m 5sec $"http://127.0.0.1:($NAV_PORT)/rest/ping.view?u=x&p=x&v=1.13.0&c=test" | ignore; true } catch { null }
}
http post -m 10sec -t application/json $"http://127.0.0.1:($NAV_PORT)/auth/createAdmin" { username: "admin", password: "password" } | ignore
true
} catch { false })
if not $ready { do -i { job kill $daemon }; error make { msg: "navidrome did not become ready" } }
$daemon
}
export def start-gonic [work: string] {
for dir in ["gonic-cache" "gonic-playlists" "gonic-podcasts"] { mkdir ($work | path join $dir) }
let daemon = (job spawn {
^gonic -music-path $LIB -listen-addr $"127.0.0.1:($GONIC_PORT)" -db-path ($work | path join "gonic.db") -cache-path ($work | path join "gonic-cache") -playlists-path ($work | path join "gonic-playlists") -podcast-path ($work | path join "gonic-podcasts") -scan-at-start-enabled -http-log=false out+err> ($work | path join "gonic.log")
})
let ready = (try {
poll "gonic" {||
try { http get -m 5sec $"http://127.0.0.1:($GONIC_PORT)/rest/ping.view?u=x&p=x&v=1.13.0&c=test" | ignore; true } catch { null }
}
true
} catch { false })
if not $ready { do -i { job kill $daemon }; error make { msg: "gonic did not become ready" } }
$daemon
}
export def start-lms [work: string] {
let lms_bin = (which lms | get 0.path)
let lms_home = ($lms_bin | path dirname | path dirname)
let dir = ($work | path join "lms")
mkdir ($dir | path join "work")
let conf = ($dir | path join "lms.conf")
(open ($lms_home | path join "share/lms/lms.conf"))
| str replace --all 'working-dir = "/var/lms"' $"working-dir = \"($dir)/work\""
| str replace --all 'listen-addr = "0.0.0.0"' 'listen-addr = "127.0.0.1"'
| save -f $conf
# Seed after the first start has migrated the schema, then run for real.
let bootstrap = (job spawn { ^$lms_bin $conf out+err> ($dir | path join "bootstrap.log") })
let ready = (try {
poll -n 60 "lms database" {||
try { http get -m 2sec $"http://127.0.0.1:($LMS_PORT)/rest/ping.view?u=x&p=x&v=1.16.0&c=subtracks-test" | ignore; true } catch { null }
}
true
} catch { false })
job kill $bootstrap
if not $ready { error make { msg: "lms failed to initialise its database" } }
let db = ($dir | path join "work/lms.db")
let user_sql = (r#'INSERT INTO user (version,type,login_name,bcrypt_round_count,password_salt,password_hash,subsonic_enable_transcoding_by_default,subsonic_default_transcode_format,subsonic_default_transcode_bitrate,subsonic_artist_list_mode,ui_theme,ui_artist_release_sort_method,ui_enable_inline_artist_relationships,ui_inline_artist_relationships,feedback_backend,scrobbling_backend,listenbrainz_token,lastfm_api_key,lastfm_api_secret,lastfm_session_key) VALUES (1,1,'admin',7,'salt','$2y$07$R6aUyqKU1HM4uX6dtpnh0OIsgqa5JID8qaH4syPlFokL3WoATtYim',0,2,128000,0,1,7,0,0,0,0,'','','','');'#)
^sqlite3 $db $user_sql
let lib_sql = (r#'INSERT INTO media_library (version,path,name) VALUES (0,'MUSIC','Main');'# | str replace 'MUSIC' ((pwd) | path join $LIB))
^sqlite3 $db $lib_sql
let token_sql = (r#'INSERT INTO auth_token (version,domain,value,expiry,use_count,last_used,max_use_count,user_id) VALUES (1,'subsonic','KEY',NULL,0,NULL,NULL,(SELECT id FROM user WHERE login_name='admin'));'# | str replace 'KEY' $LMS_KEY)
^sqlite3 $db $token_sql
let daemon = (job spawn { ^$lms_bin $conf out+err> ($dir | path join "lms.log") })
let started = (try {
poll "lms" {||
let response = (try { sub-get $"http://127.0.0.1:($LMS_PORT)/" "admin" $LMS_KEY "ping" } catch { null })
if $response == null { null } else { true }
}
sub-get $"http://127.0.0.1:($LMS_PORT)/" "admin" $LMS_KEY "startScan" | ignore
poll "lms scan" {||
let scanning = (sub-get $"http://127.0.0.1:($LMS_PORT)/" "admin" $LMS_KEY "getScanStatus" | get scanStatus.scanning)
if (($scanning | into string | str lowercase) == "false") { true } else { null }
}
true
} catch { false })
if not $started { do -i { job kill $daemon }; error make { msg: "lms did not become ready" } }
$daemon
}
export def start-nextcloud [work: string] {
let dir = ($work | path join "nextcloud")
mkdir $dir
^cp -r $"($env.NEXTCLOUD_DIR)/." $dir
^chmod -R u+w $dir
mkdir ($dir | path join "custom_apps")
^cp -r $env.NEXTCLOUD_MUSIC_APP ($dir | path join "custom_apps/music")
^chmod -R u+w ($dir | path join "custom_apps/music")
let occ = ($dir | path join "occ")
^php -d "memory_limit=512M" $occ "maintenance:install" "--database" "sqlite" "--admin-user" "admin" "--admin-pass" "password" "--data-dir" ($dir | path join "data") out+err> ($dir | path join "install.log")
^php -d "memory_limit=512M" $occ "app:enable" "music" out+err> ($dir | path join "enable.log")
mkdir ($dir | path join "data/admin/files/Music")
^cp -r $"((pwd) | path join $LIB)/." ($dir | path join "data/admin/files/Music")
^php -d "memory_limit=512M" $occ "files:scan" "admin" out+err> ($dir | path join "files-scan.log")
^php -d "memory_limit=512M" $occ "music:scan" "admin" out+err> ($dir | path join "music-scan.log")
let hash = ($NC_KEY | hash sha256)
let key_sql = (r#'INSERT INTO oc_music_ampache_users (user_id, hash, description) VALUES ('admin','HASH','subtracks');'# | str replace 'HASH' $hash)
^sqlite3 ($dir | path join "data/owncloud.db") $key_sql
let daemon = (job spawn { ^php -d "memory_limit=512M" -S $"127.0.0.1:($NC_PORT)" -t $dir out+err> ($dir | path join "php.log") })
let started = (try {
poll "nextcloud" {||
try { http get -m 3sec $"http://127.0.0.1:($NC_PORT)/status.php" | ignore; true } catch { null }
}
true
} catch { false })
if not $started { do -i { job kill $daemon }; error make { msg: "nextcloud did not become ready" } }
$daemon
}
# The prune test deletes an album from this private copy and rescans, so it must not be shared.
export def start-prune [work: string] {
let dir = ($work | path join "prune")
let music = ($dir | path join "music")
mkdir ($dir | path join "data")
mkdir $music
for m in ($MUSIC | first 2) {
^cp -r $"((pwd) | path join $LIB)/($m.id)" $music
}
let daemon = (job spawn { ^navidrome --address 127.0.0.1 --port $PRUNE_PORT --musicfolder $music --datafolder ($dir | path join "data") --loglevel warn out+err> ($dir | path join "navidrome.log") })
let ready = (try {
poll "prune navidrome" {||
try { http get -m 2sec $"http://127.0.0.1:($PRUNE_PORT)/rest/ping.view?u=x&p=x&v=1.16.1&c=test" | ignore; true } catch { null }
}
http post -m 10sec -t application/json $"http://127.0.0.1:($PRUNE_PORT)/auth/createAdmin" { username: "admin", password: "password" } | ignore
poll "prune library" {||
let albums = (try {
http get -m 5sec $"http://127.0.0.1:($PRUNE_PORT)/rest/getAlbumList2.view?u=admin&p=password&v=1.16.1&c=test&f=json&type=newest&size=500"
| get subsonic-response.albumList2.album
} catch { [] })
if (($albums | length) == 2) { true } else { null }
}
true
} catch { false })
if not $ready { do -i { job kill $daemon }; error make { msg: "prune navidrome did not become ready" } }
$daemon
}
# Wait for the library, then leave the star/playlist/scrobble state the Kotlin tests assert on.
export def prepare-fixtures [server: record] {
let base = $server.base
let user = $server.user
let pass = $server.pass
let artists = (poll "artists" {||
let items = (sub-get $base $user $pass "getArtists" | get artists.index | each {|i| $i.artist } | flatten)
if (["Ugress" "Brad Sucks"] | all {|name| $items | any {|i| $i.name == $name } }) { $items } else { null }
})
let albums = (poll "albums" {||
let items = (sub-get $base $user $pass "getAlbumList2" [["type" "newest"]] | get albumList2.album)
if (["Kosmonaut" "Retroconnaissance" "I Don't Know What I'm Doing"] | all {|name| $items | any {|i| $i.name == $name } }) { $items } else { null }
})
let ugress = (named-id $artists "Ugress")
let kosmo = (named-id $albums "Kosmonaut")
let retro = (named-id $albums "Retroconnaissance")
let dunno = (named-id $albums "I Don't Know What I'm Doing")
poll "album songs" {||
if (has-tracks $base $user $pass $retro [1 2]) and (has-tracks $base $user $pass $kosmo [2 4]) and (has-tracks $base $user $pass $dunno [9 10 11]) {
$albums
} else {
null
}
} | ignore
sub-get $base $user $pass "star" [["albumId" $kosmo]] | ignore
sub-get $base $user $pass "star" [["artistId" $ugress]] | ignore
let song_ids = [
(song-id $base $user $pass $retro 2)
(song-id $base $user $pass $retro 1)
(song-id $base $user $pass $kosmo 2)
(song-id $base $user $pass $kosmo 4)
(song-id $base $user $pass $dunno 9)
(song-id $base $user $pass $dunno 10)
(song-id $base $user $pass $dunno 11)
]
let playlist = ([["name" "Playlist 1"]] | append ($song_ids | each {|id| ["songId" $id] }))
sub-get $base $user $pass "createPlaylist" $playlist | ignore
sub-get $base $user $pass "scrobble" [["id" (song-id $base $user $pass $retro 1)] ["submission" "true"]] | ignore
sleep 1sec
sub-get $base $user $pass "scrobble" [["id" (song-id $base $user $pass $retro 2)] ["submission" "true"]] | ignore
sleep 1sec
sub-get $base $user $pass "scrobble" [["id" (song-id $base $user $pass $kosmo 1)] ["submission" "true"]] | ignore
}