258 lines
8.2 KiB
Plaintext
258 lines
8.2 KiB
Plaintext
//
|
|
// Library browsing — async. The UI calls library_refresh_artists /
|
|
// library_select_artist / library_select_album, which submit HTTP tasks.
|
|
// When each task completes the on_done callback parses JSON and writes the
|
|
// result into app.library on the main thread.
|
|
//
|
|
// Stale-result guard: each "select" stores a generation id. Late callbacks
|
|
// compare against the current id and discard if the user has navigated
|
|
// elsewhere.
|
|
//
|
|
|
|
Artist :: struct {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
Album :: struct {
|
|
id: string;
|
|
name: string;
|
|
artist: string;
|
|
artist_id: string;
|
|
}
|
|
|
|
Track :: struct {
|
|
id: string;
|
|
name: string;
|
|
album: string;
|
|
album_id: string;
|
|
artist: string;
|
|
artist_id: string;
|
|
duration_ticks: s64;
|
|
index_number: int;
|
|
is_favourite: bool;
|
|
}
|
|
|
|
Library_State :: struct {
|
|
artists: [..] Artist;
|
|
albums: [..] Album;
|
|
tracks: [..] Track;
|
|
|
|
selected_artist_id: string;
|
|
selected_album_id: string;
|
|
|
|
artists_loaded: bool;
|
|
albums_loaded: bool;
|
|
tracks_loaded: bool;
|
|
|
|
artists_loading: bool;
|
|
albums_loading: bool;
|
|
tracks_loading: bool;
|
|
|
|
// Generation counters used by the async callbacks to discard stale
|
|
// results when the user has navigated away.
|
|
albums_request_gen: int;
|
|
tracks_request_gen: int;
|
|
|
|
artists_scroll: float;
|
|
albums_scroll: float;
|
|
tracks_scroll: float;
|
|
}
|
|
|
|
#scope_module
|
|
|
|
// Jellyfin returns the artist ID inside AlbumArtists[].Id, not as a flat
|
|
// AlbumArtistId field on track items.
|
|
Artist_Ref :: struct {
|
|
Id: string;
|
|
Name: string;
|
|
}
|
|
|
|
User_Data_Dto :: struct {
|
|
IsFavorite: bool;
|
|
}
|
|
|
|
Item_Summary :: struct {
|
|
Id: string;
|
|
Name: string;
|
|
Album: string;
|
|
AlbumId: string;
|
|
AlbumArtist: string;
|
|
AlbumArtistId: string; // populated for album items
|
|
AlbumArtists: [..] Artist_Ref; // populated for track items
|
|
UserData: User_Data_Dto;
|
|
RunTimeTicks: s64;
|
|
IndexNumber: int;
|
|
}
|
|
|
|
Items_Response :: struct {
|
|
Items: [..] Item_Summary;
|
|
TotalRecordCount: int;
|
|
}
|
|
|
|
#scope_export
|
|
|
|
//
|
|
// Public — kick off async fetches.
|
|
//
|
|
|
|
library_refresh_artists :: () {
|
|
if app.library.artists_loading return;
|
|
app.library.artists_loading = true;
|
|
path := tprint("/Users/%/Items?IncludeItemTypes=MusicArtist&Recursive=true&SortBy=SortName&Limit=2000", app.jellyfin.user_id);
|
|
log_info("library: fetching artists");
|
|
http_submit("GET", path, on_done=on_artists_loaded);
|
|
}
|
|
|
|
library_select_artist :: (artist_id: string) {
|
|
if app.library.selected_artist_id == artist_id && app.library.albums_loaded return;
|
|
|
|
free(app.library.selected_artist_id);
|
|
app.library.selected_artist_id = copy_string(artist_id);
|
|
|
|
free_albums(*app.library.albums);
|
|
app.library.albums_loaded = false;
|
|
app.library.albums_loading = true;
|
|
app.library.albums_request_gen += 1;
|
|
app.library.albums_scroll = 0;
|
|
|
|
free_tracks(*app.library.tracks);
|
|
free(app.library.selected_album_id);
|
|
app.library.selected_album_id = "";
|
|
app.library.tracks_loaded = false;
|
|
app.library.tracks_loading = false;
|
|
app.library.tracks_scroll = 0;
|
|
|
|
path := tprint("/Users/%/Items?IncludeItemTypes=MusicAlbum&ArtistIds=%&Recursive=true&SortBy=ProductionYear,SortName&Limit=2000", app.jellyfin.user_id, artist_id);
|
|
log_info("library: fetching albums for %", artist_id);
|
|
http_submit("GET", path, on_done=on_albums_loaded, user_data=cast(*void) app.library.albums_request_gen);
|
|
}
|
|
|
|
library_select_album :: (album_id: string) {
|
|
if app.library.selected_album_id == album_id && app.library.tracks_loaded return;
|
|
|
|
free(app.library.selected_album_id);
|
|
app.library.selected_album_id = copy_string(album_id);
|
|
|
|
free_tracks(*app.library.tracks);
|
|
app.library.tracks_loaded = false;
|
|
app.library.tracks_loading = true;
|
|
app.library.tracks_request_gen += 1;
|
|
app.library.tracks_scroll = 0;
|
|
|
|
path := tprint("/Users/%/Items?ParentId=%&IncludeItemTypes=Audio&SortBy=ParentIndexNumber,IndexNumber,SortName&Limit=2000", app.jellyfin.user_id, album_id);
|
|
log_info("library: fetching tracks for %", album_id);
|
|
http_submit("GET", path, on_done=on_tracks_loaded, user_data=cast(*void) app.library.tracks_request_gen);
|
|
}
|
|
|
|
#scope_file
|
|
|
|
//
|
|
// Callbacks — run on the main thread once the worker thread finishes.
|
|
//
|
|
|
|
on_artists_loaded :: (task: *Http_Task) {
|
|
app.library.artists_loading = false;
|
|
if !task.response.ok {
|
|
log_error("artists: status=% body=%", task.response.status_code, slice(task.response.body, 0, min(300, task.response.body.count)));
|
|
return;
|
|
}
|
|
ok, parsed := Jaison.json_parse_string(task.response.body, Items_Response);
|
|
if !ok {
|
|
log_error("artists: json parse failed");
|
|
return;
|
|
}
|
|
free_artists(*app.library.artists);
|
|
for parsed.Items {
|
|
a: Artist;
|
|
a.id = copy_string(it.Id);
|
|
a.name = copy_string(it.Name);
|
|
array_add(*app.library.artists, a);
|
|
}
|
|
app.library.artists_loaded = true;
|
|
log_info("library: % artists loaded", app.library.artists.count);
|
|
|
|
// Prefetch every artist thumb. The image cache caps concurrency at 4
|
|
// so this just queues — it doesn't blow up the network.
|
|
for app.library.artists image_request(it.id, .THUMB);
|
|
}
|
|
|
|
on_albums_loaded :: (task: *Http_Task) {
|
|
gen := cast(int) task.user_data;
|
|
if gen != app.library.albums_request_gen return; // user moved on; discard
|
|
|
|
app.library.albums_loading = false;
|
|
if !task.response.ok {
|
|
log_error("albums: status=% body=%", task.response.status_code, slice(task.response.body, 0, min(300, task.response.body.count)));
|
|
return;
|
|
}
|
|
ok, parsed := Jaison.json_parse_string(task.response.body, Items_Response);
|
|
if !ok {
|
|
log_error("albums: json parse failed");
|
|
return;
|
|
}
|
|
free_albums(*app.library.albums);
|
|
for parsed.Items {
|
|
a: Album;
|
|
a.id = copy_string(it.Id);
|
|
a.name = copy_string(it.Name);
|
|
a.artist = copy_string(it.AlbumArtist);
|
|
a.artist_id = copy_string(it.AlbumArtistId);
|
|
array_add(*app.library.albums, a);
|
|
}
|
|
app.library.albums_loaded = true;
|
|
log_info("library: % albums loaded", app.library.albums.count);
|
|
|
|
// Prefetch every album thumb up front so scrolling is instant.
|
|
for app.library.albums image_request(it.id, .THUMB);
|
|
}
|
|
|
|
on_tracks_loaded :: (task: *Http_Task) {
|
|
gen := cast(int) task.user_data;
|
|
if gen != app.library.tracks_request_gen return;
|
|
|
|
app.library.tracks_loading = false;
|
|
if !task.response.ok {
|
|
log_error("tracks: status=% body=%", task.response.status_code, slice(task.response.body, 0, min(300, task.response.body.count)));
|
|
return;
|
|
}
|
|
ok, parsed := Jaison.json_parse_string(task.response.body, Items_Response);
|
|
if !ok {
|
|
log_error("tracks: json parse failed");
|
|
return;
|
|
}
|
|
free_tracks(*app.library.tracks);
|
|
for parsed.Items {
|
|
t: Track;
|
|
t.id = copy_string(it.Id);
|
|
t.name = copy_string(it.Name);
|
|
t.album = copy_string(it.Album);
|
|
t.album_id = copy_string(it.AlbumId);
|
|
t.artist = copy_string(it.AlbumArtist);
|
|
t.artist_id = ifx it.AlbumArtists.count > 0
|
|
then copy_string(it.AlbumArtists[0].Id)
|
|
else copy_string(it.AlbumArtistId); // fallback
|
|
t.duration_ticks = it.RunTimeTicks;
|
|
t.index_number = it.IndexNumber;
|
|
t.is_favourite = it.UserData.IsFavorite;
|
|
array_add(*app.library.tracks, t);
|
|
}
|
|
app.library.tracks_loaded = true;
|
|
log_info("library: % tracks loaded", app.library.tracks.count);
|
|
}
|
|
|
|
free_artists :: (xs: *[..] Artist) {
|
|
for xs.* { free(it.id); free(it.name); }
|
|
array_reset(xs);
|
|
}
|
|
|
|
free_albums :: (xs: *[..] Album) {
|
|
for xs.* { free(it.id); free(it.name); free(it.artist); free(it.artist_id); }
|
|
array_reset(xs);
|
|
}
|
|
|
|
free_tracks :: (xs: *[..] Track) {
|
|
for xs.* { free(it.id); free(it.name); free(it.album); free(it.album_id); free(it.artist); free(it.artist_id); }
|
|
array_reset(xs);
|
|
}
|