/* application.vala * * Copyright 2024 Erica Z * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * SPDX-License-Identifier: AGPL-3.0-or-later */ errordomain SubsonicError { ERROR } public delegate void Wavelet.SongCallback (Song song); public class Wavelet.Artist : Object, Json.Serializable { public string index; public string id; public string name { get; private set; } public string? cover_art; public string? artist_image_url; public int64 album_count; public DateTime? starred; public Artist (string index, Json.Reader reader) { this.index = index; reader.read_member ("id"); this.id = reader.get_string_value (); reader.end_member (); reader.read_member ("name"); this.name = reader.get_string_value (); reader.end_member (); reader.read_member ("coverArt"); this.cover_art = reader.get_string_value (); reader.end_member (); reader.read_member ("artistImageUrl"); this.artist_image_url = reader.get_string_value (); reader.end_member (); reader.read_member ("albumCount"); this.album_count = reader.get_int_value (); reader.end_member (); reader.read_member ("starred"); if (reader.is_value ()) { this.starred = new DateTime.from_iso8601 (reader.get_string_value (), null); } reader.end_member (); } } public class Wavelet.Album : Object { public string id; public string name; public Album (Json.Reader reader) { reader.read_member ("id"); this.id = reader.get_string_value (); reader.end_member (); reader.read_member ("name"); this.name = reader.get_string_value (); reader.end_member (); } } public class Wavelet.Song : Object { public string id { get; private set; } public string title { get; private set; } public string album { get; private set; } public string artist { get; private set; } public int64 track { get; private set; } public Song (Json.Reader reader) { reader.read_member ("id"); this.id = reader.get_string_value (); reader.end_member (); reader.read_member ("title"); this.title = reader.get_string_value (); reader.end_member (); reader.read_member ("album"); this.album = reader.get_string_value (); reader.end_member (); reader.read_member ("artist"); this.artist = reader.get_string_value (); reader.end_member (); reader.read_member ("track"); this.track = reader.get_int_value (); reader.end_member (); } } public struct Wavelet.API.PlayQueue { public string current; public int64 position; public DateTime changed; public string changed_by; public ListStore songs; internal PlayQueue.from_reader (Json.Reader reader) { reader.read_member ("current"); this.current = reader.get_string_value (); reader.end_member (); reader.read_member ("position"); this.position = reader.get_int_value (); reader.end_member (); reader.read_member ("changed"); this.changed = new DateTime.from_iso8601 (reader.get_string_value (), null); reader.end_member (); reader.read_member ("changed_by"); this.changed_by = reader.get_string_value (); reader.end_member (); //print("%s %lli %s %s\n",this.current, this.position, this.changed, this.changed_by); this.songs = new ListStore (typeof (Song)); reader.read_member ("song"); for (int i = 0; i < reader.count_elements (); i += 1) { reader.read_element (i); this.songs.append (new Song (reader)); reader.end_element (); } reader.end_member (); assert (reader.get_error () == null); } } public class Wavelet.Subsonic : Object { public ListStore artist_list; public ListStore album_list; public ListStore song_list; private Soup.Session session; private string url; private string parameters; public Subsonic.with_password (string url, string username, string password) { this.url = url; this.parameters = @"u=$(Uri.escape_string(username))&p=$(Uri.escape_string(password))&v=1.16.1&c=eu.callcc.Wavelet&f=json"; this.session = new Soup.Session (); this.artist_list = new ListStore (typeof (Artist)); this.album_list = new ListStore (typeof (Album)); this.song_list = new ListStore (typeof (Song)); } public Subsonic.with_token (string url, string username, string token, string salt) { this.url = url; this.parameters = @"u=$(Uri.escape_string(username))&t=$(Uri.escape_string(token))&s=$(Uri.escape_string(salt))&v=1.16.1&c=eu.callcc.Wavelet&f=json"; this.session = new Soup.Session (); this.artist_list = new ListStore (typeof (Artist)); this.album_list = new ListStore (typeof (Album)); this.song_list = new ListStore (typeof (Song)); } private void unwrap_response (Json.Reader reader) throws Error { reader.read_member ("subsonic-response"); reader.read_member ("status"); if (reader.get_string_value () != "ok") { reader.end_member (); reader.read_member ("error"); reader.read_member ("message"); throw new SubsonicError.ERROR (reader.get_string_value () ?? "???"); } reader.end_member(); } public async void ping () throws Error { var msg = new Soup.Message ("GET", @"$(this.url)/rest/ping?$(this.parameters)"); var bytes = yield this.session.send_and_read_async (msg, Priority.DEFAULT, null); assert (msg.get_status () == Soup.Status.OK); var parser = new Json.Parser (); parser.load_from_data ((string) bytes.get_data ()); var reader = new Json.Reader (parser.get_root ()); this.unwrap_response (reader); } public signal void done_reloading (); private int remaining_artists; private int remaining_albums; public async void reload () throws Error { this.artist_list.remove_all (); this.album_list.remove_all (); this.song_list.remove_all (); this.remaining_artists = 0; this.remaining_albums = 0; var msg = new Soup.Message ("GET", @"$(this.url)/rest/getArtists?$(this.parameters)"); var bytes = yield this.session.send_and_read_async (msg, Priority.DEFAULT, null); var parser = new Json.Parser (); parser.load_from_data ((string) bytes.get_data ()); var reader = new Json.Reader (parser.get_root ()); this.unwrap_response (reader); reader.read_member ("artists"); reader.read_member ("index"); for (int i = 0; i < reader.count_elements (); i += 1) { //for (int i = 0; i < 1; i += 1) { reader.read_element (i); reader.read_member ("name"); var index = reader.get_string_value (); reader.end_member (); reader.read_member ("artist"); for (int j = 0; j < reader.count_elements (); j += 1) { reader.read_element (j); var artist = new Artist (index, reader); artist_list.append (artist); this.remaining_artists += 1; reload_artist.begin (artist.id); reader.end_element (); } reader.end_member (); reader.end_element (); assert (reader.get_error () == null); } } private async void reload_artist (string artist_id) throws Error { try { var msg = new Soup.Message ("GET", @"$(this.url)/rest/getArtist?id=$(artist_id)&$(this.parameters)"); var bytes = yield this.session.send_and_read_async (msg, Priority.DEFAULT, null); assert (msg.get_status () == Soup.Status.OK); var parser = new Json.Parser (); parser.load_from_data ((string) bytes.get_data()); var reader = new Json.Reader (parser.get_root ()); this.unwrap_response (reader); reader.read_member ("artist"); reader.read_member ("album"); for (int i = 0; i < reader.count_elements (); i += 1) { reader.read_element (i); var album = new Album (reader); album_list.append (album); this.remaining_albums += 1; reload_album.begin (album.id); reader.end_element (); } assert (reader.get_error () == null); } catch (Error e) { warning ("could not reload artist %s: %s\n", artist_id, e.message); } finally { this.remaining_artists -= 1; } } private async void reload_album (string album_id) throws Error { try { var msg = new Soup.Message ("GET", @"$(this.url)/rest/getAlbum?id=$(album_id)&$(this.parameters)"); var bytes = yield this.session.send_and_read_async (msg, Priority.DEFAULT, null); assert (msg.get_status () == Soup.Status.OK); var parser = new Json.Parser (); parser.load_from_data ((string) bytes.get_data()); var reader = new Json.Reader(parser.get_root ()); this.unwrap_response (reader); reader.read_member ("album"); reader.read_member ("song"); for (int i = 0; i < reader.count_elements (); i += 1) { reader.read_element (i); var song = new Song (reader); song_list.append (song); reader.end_element (); } assert (reader.get_error () == null); } catch (Error e) { warning ("could not reload album %s: %s\n", album_id, e.message); } finally { this.remaining_albums -= 1; if (this.remaining_artists == 0 && this.remaining_albums == 0) { this.done_reloading (); } } } public async void get_random_songs (string? parameters, SongCallback callback) throws Error { string str_parameters; if (parameters == null) { str_parameters = ""; } else { str_parameters = @"$parameters&"; } var msg = new Soup.Message("GET", @"$(this.url)/rest/getRandomSongs?$(str_parameters)size=500&$(this.parameters)"); var bytes = yield this.session.send_and_read_async (msg, Priority.DEFAULT, null); assert (msg.get_status () == Soup.Status.OK); var parser = new Json.Parser (); parser.load_from_data ((string) bytes.get_data ()); var reader = new Json.Reader (parser.get_root ()); this.unwrap_response (reader); reader.read_member ("randomSongs"); reader.read_member ("song"); for (int i = 0; i < reader.count_elements (); i += 1) { reader.read_element (i); callback (new Song (reader)); reader.end_element (); } assert (reader.get_error () == null); } public string stream_uri (string id) { return @"$(this.url)/rest/stream?id=$(id)&$(this.parameters)"; } }