Skip to content

Commit

Permalink
WIP: Add Libre.fm scrobbler
Browse files Browse the repository at this point in the history
Issue: #17

Signed-off-by: Jiří Janoušek <janousek.jiri@gmail.com>
  • Loading branch information
jiri-janousek committed Mar 4, 2018
1 parent 3b39dcd commit b6cdc4c
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 51 deletions.
Expand Up @@ -25,14 +25,14 @@
namespace Nuvola {

public class AudioScrobblerComponent: Component {
private const int SCROBBLE_SONG_DELAY = 60;
private const int SCROBBLE_SONG_DELAY = 30;

private Bindings bindings;
private Drtgtk.Application app;
private Soup.Session connection;
private unowned Drt.KeyValueStorage config;
private unowned Drt.KeyValueStorage global_config;
private AudioScrobbler? scrobbler = null;
private AudioScrobbler[]? scrobblers = null;
private MediaPlayerModel? player = null;
private uint scrobble_timeout = 0;
private string? scrobble_title = null;
Expand All @@ -55,46 +55,51 @@ public class AudioScrobblerComponent: Component {
}

public override Gtk.Widget? get_settings() {
if (scrobbler == null) {
if (scrobblers == null || scrobblers.length == 0) {
return null;
}

var grid = new Gtk.Grid();
grid.orientation = Gtk.Orientation.VERTICAL;
var label = new Gtk.Label(Markup.printf_escaped("<b>%s</b>", scrobbler.name));
label.use_markup = true;
label.vexpand = false;
label.hexpand = true;
grid.add(label);
Gtk.Widget? widget = scrobbler.get_settings(app);
if (widget != null) {
grid.add(widget);
foreach (unowned AudioScrobbler scrobbler in scrobblers) {
var label = new Gtk.Label(Markup.printf_escaped("<b>%s</b>", scrobbler.name));
label.use_markup = true;
label.vexpand = false;
label.hexpand = true;
grid.add(label);
Gtk.Widget? widget = scrobbler.get_settings(app);
if (widget != null) {
grid.add(widget);
}
}
grid.show_all();
return grid;
}

protected override bool activate() {
var scrobbler = new LastfmScrobbler(connection);
this.scrobbler = scrobbler;
string base_key = "component.%s.%s.".printf(id, scrobbler.id);
config.bind_object_property(base_key, scrobbler, "scrobbling_enabled").set_default(true).update_property();
global_config.bind_object_property(base_key, scrobbler, "session").update_property();
global_config.bind_object_property(base_key, scrobbler, "username").update_property();

if (scrobbler.has_session) {
scrobbler.retrieve_username.begin();
}
player = bindings.get_model<MediaPlayerModel>();
player.set_track_info.connect(on_set_track_info);
scrobbler.notify.connect_after(on_scrobbler_notify);
scrobblers = {new LastfmScrobbler(connection), new LibrefmScrobbler(connection)};
foreach (unowned AudioScrobbler scrobbler in scrobblers) {
string base_key = "component.%s.%s.".printf(id, scrobbler.id);
config.bind_object_property(base_key, scrobbler, "scrobbling_enabled").set_default(true).update_property();
global_config.bind_object_property(base_key, scrobbler, "session").update_property();
global_config.bind_object_property(base_key, scrobbler, "username").update_property();
var lastfm_compatible = scrobbler as LastfmCompatibleScrobbler;
if (lastfm_compatible != null && lastfm_compatible.has_session) {
lastfm_compatible.retrieve_username.begin();
}
scrobbler.notify.connect_after(on_scrobbler_notify);
}
on_set_track_info(player.title, player.artist, player.album, player.state);
return true;
}

protected override bool deactivate() {
scrobbler.notify.disconnect(on_scrobbler_notify);
scrobbler = null;
foreach (unowned AudioScrobbler scrobbler in scrobblers) {
scrobbler.notify.disconnect(on_scrobbler_notify);
}
scrobblers = null;
player.set_track_info.disconnect(on_set_track_info);
player = null;
cancel_scrobbling();
Expand Down Expand Up @@ -158,16 +163,20 @@ public class AudioScrobblerComponent: Component {

track_info_cb_id = Timeout.add_seconds(1, () => {
track_info_cb_id = 0;
if (scrobbler.can_update_now_playing) {
if (title != null && artist != null && state == "playing" ) {
scrobbler.update_now_playing.begin(title, artist, on_update_now_playing_done);
foreach (unowned AudioScrobbler scrobbler in scrobblers) {
if (scrobbler.can_update_now_playing) {
if (title != null && artist != null && state == "playing" ) {
scrobbler.update_now_playing.begin(title, artist, on_update_now_playing_done);
}
}
}

cancel_scrobbling();

if (scrobbler.can_scrobble) {
schedule_scrobbling(title, artist, album, state);
foreach (unowned AudioScrobbler scrobbler in scrobblers) {
if (scrobbler.can_scrobble) {
schedule_scrobbling(title, artist, album, state);
}
}
return false;
});
Expand All @@ -190,11 +199,13 @@ public class AudioScrobblerComponent: Component {

private bool scrobble_cb() {
scrobble_timeout = 0;
if (scrobbler.can_scrobble) {
scrobbled = true;
var datetime = new DateTime.now_utc();
scrobbler.scrobble_track.begin(
scrobble_title, scrobble_artist, scrobble_album, datetime.to_unix(), on_scrobble_track_done);
foreach (unowned AudioScrobbler scrobbler in scrobblers) {
if (scrobbler.can_scrobble) {
scrobbled = true;
var datetime = new DateTime.now_utc();
scrobbler.scrobble_track.begin(
scrobble_title, scrobble_artist, scrobble_album, datetime.to_unix(), on_scrobble_track_done);
}
}
return false;
}
Expand Down
Expand Up @@ -72,12 +72,14 @@ public class LastfmCompatibleScrobbler: AudioScrobbler {

Json.Object response = yield send_request(HTTP_GET, params);
if (!response.has_member("token")) {
throw new AudioScrobblerError.WRONG_RESPONSE("%s: Response doesn't contain token member.", API_METHOD);
throw new AudioScrobblerError.WRONG_RESPONSE(
"%s %s: Response doesn't contain token member.", id, API_METHOD);
}

token = response.get_string_member("token");
if (token == null || token == "") {
throw new AudioScrobblerError.WRONG_RESPONSE("%s: Response contains empty token member.", API_METHOD);
throw new AudioScrobblerError.WRONG_RESPONSE(
"%s %s: Response contains empty token member.", id, API_METHOD);
}

return "%s?api_key=%s&token=%s".printf(auth_endpoint, api_key, token);
Expand All @@ -98,17 +100,20 @@ public class LastfmCompatibleScrobbler: AudioScrobbler {

Json.Object response = yield send_request(HTTP_GET, params);
if (!response.has_member("session")) {
throw new AudioScrobblerError.WRONG_RESPONSE("%s: Response doesn't contain session member.", API_METHOD);
throw new AudioScrobblerError.WRONG_RESPONSE(
"%s %s: Response doesn't contain session member.", id, API_METHOD);
}

Json.Object session_member = response.get_object_member("session");
if (!session_member.has_member("key")) {
throw new AudioScrobblerError.WRONG_RESPONSE("%s: Response doesn't contain session.key member.", API_METHOD);
throw new AudioScrobblerError.WRONG_RESPONSE(
"%s %s: Response doesn't contain session.key member.", id, API_METHOD);
}

string session_key = session_member.get_string_member("key");
if (session_key == null || session_key == "") {
throw new AudioScrobblerError.WRONG_RESPONSE("%s: Response contain empty session.key member.", API_METHOD);
throw new AudioScrobblerError.WRONG_RESPONSE(
"%s %s: Response contain empty session.key member.", id, API_METHOD);
}

if (session_member.has_member("name")) {
Expand All @@ -127,7 +132,7 @@ public class LastfmCompatibleScrobbler: AudioScrobbler {
public async void retrieve_username() throws AudioScrobblerError {
const string API_METHOD = "user.getInfo";
if (session == null) {
throw new AudioScrobblerError.NO_SESSION("%s: There is no authorized session.", API_METHOD);
throw new AudioScrobblerError.NO_SESSION("%s %s: There is no authorized session.", id, API_METHOD);
}

// http://www.last.fm/api/show/user.getInfo
Expand All @@ -137,15 +142,15 @@ public class LastfmCompatibleScrobbler: AudioScrobbler {
params.insert("sk", session);
Json.Object response = yield send_request(HTTP_GET, params);
if (!response.has_member("user")) {
throw new AudioScrobblerError.WRONG_RESPONSE("%s: Response doesn't contain user member.", API_METHOD);
throw new AudioScrobblerError.WRONG_RESPONSE("%s%s: Response doesn't contain user member.", id, API_METHOD);
}
Json.Object user = response.get_object_member("user");
if (!user.has_member("name")) {
throw new AudioScrobblerError.WRONG_RESPONSE("%s: Response doesn't contain name member.", API_METHOD);
throw new AudioScrobblerError.WRONG_RESPONSE("%s%s: Response doesn't contain name member.", id, API_METHOD);
}
username = user.get_string_member("name");
if (username == null || username == "") {
throw new AudioScrobblerError.WRONG_RESPONSE("%s: Response contains empty username.", API_METHOD);
throw new AudioScrobblerError.WRONG_RESPONSE("%s%s: Response contains empty username.", id, API_METHOD);
}
}

Expand All @@ -170,7 +175,8 @@ public class LastfmCompatibleScrobbler: AudioScrobbler {

Json.Object response = yield send_request(HTTP_POST, params, 20);
if (!response.has_member("nowplaying")) {
throw new AudioScrobblerError.WRONG_RESPONSE("%s: Response doesn't contain nowplaying member.", API_METHOD);
throw new AudioScrobblerError.WRONG_RESPONSE("%s %s: Response doesn't contain nowplaying member.",
id, API_METHOD);
}
}

Expand Down Expand Up @@ -200,7 +206,7 @@ public class LastfmCompatibleScrobbler: AudioScrobbler {

Json.Object response = yield send_request(HTTP_POST, params, 20);
if (!response.has_member("scrobbles")) {
throw new AudioScrobblerError.WRONG_RESPONSE("Response doesn't contain scrobbles member.");
throw new AudioScrobblerError.WRONG_RESPONSE("%s: Response doesn't contain scrobbles member.", id);
}
}

Expand All @@ -223,7 +229,7 @@ public class LastfmCompatibleScrobbler: AudioScrobbler {
Soup.MemoryUse.COPY, request.data);
} else {
message = null;
error("Last.fm: Unsupported request method: %s", method);
error("%s: Unsupported request method: %s", id, method);
}

while (true) {
Expand Down Expand Up @@ -251,7 +257,7 @@ public class LastfmCompatibleScrobbler: AudioScrobbler {

Json.Node root = parser.get_root();
if (root == null) {
throw new AudioScrobblerError.WRONG_RESPONSE("Empty response from the server.");
throw new AudioScrobblerError.RETRY("%s: Empty response from the server.", id);
}
Json.Object root_object = root.get_object();
if (root_object.has_member("error") && root_object.has_member("message")) {
Expand All @@ -260,13 +266,15 @@ public class LastfmCompatibleScrobbler: AudioScrobbler {
switch (error_code) {
case 9: // Invalid session key - Please re-authenticate
drop_session();
throw new AudioScrobblerError.NO_SESSION("Session expired. Please re-authenticate. %s", error_message);
throw new AudioScrobblerError.NO_SESSION(
"%s: Session expired. Please re-authenticate. %s", id, error_message);
case 11: // Service Offline - This service is temporarily offline. Try again later.
case 16: // There was a temporary error processing your request. Please try again
case 29: // Rate limit exceeded - Your IP has made too many requests in a short period
throw new AudioScrobblerError.RETRY("%s: %s", error_code.to_string(), error_message);
default:
throw new AudioScrobblerError.LASTFM_ERROR("%s: %s", error_code.to_string(), error_message);
throw new AudioScrobblerError.LASTFM_ERROR(
"%s %s: %s", id, error_code.to_string(), error_message);
}
}
return root_object;
Expand All @@ -277,7 +285,7 @@ public class LastfmCompatibleScrobbler: AudioScrobbler {
}

retry--;
warning("Retry: %s", e.message);
warning("%s: Retry: %s", id, e.message);
SourceFunc resume = send_request.callback;
Timeout.add_seconds(15, (owned) resume);
yield;
Expand Down
38 changes: 38 additions & 0 deletions src/nuvolakit-runner/components/scrobbler/LibrefmScrobbler.vala
@@ -0,0 +1,38 @@
/*
* Copyright 2018 Jiří Janoušek <janousek.jiri@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

namespace Nuvola {

public class LibrefmScrobbler : LastfmCompatibleScrobbler {
public LibrefmScrobbler(Soup.Session connection) {
base(connection, "librefm", "Libre.fm",
"http://libre.fm/api/auth/",
"nuv",
"d157730073941bdd851eac950f3154e6",
"http://libre.fm/2.0/");
}
}

} // namespace Nuvola

2 changes: 1 addition & 1 deletion web_apps/test/home.html
Expand Up @@ -183,7 +183,7 @@ <h1>Test</h1>
artist: "Billy Talent",
album: "Billy Talent II",
rating: 1,
time: 25
time: 45
},
{
name: "Holiday",
Expand Down

0 comments on commit b6cdc4c

Please sign in to comment.