← Back to team overview

nuvola-player-devel team mailing list archive

[Merge] lp:~rameshdharan/nuvola-player/songza into lp:nuvola-player

 

Ramesh Dharan has proposed merging lp:~rameshdharan/nuvola-player/songza into lp:nuvola-player.

Requested reviews:
  Jiří Janoušek (fenryxo)

For more details, see:
https://code.launchpad.net/~rameshdharan/nuvola-player/songza/+merge/230325

Add Songza service integration.
-- 
https://code.launchpad.net/~rameshdharan/nuvola-player/songza/+merge/230325
Your team Nuvola Player Development is subscribed to branch lp:nuvola-player.
=== added directory 'data/nuvolaplayer/services/songza'
=== added file 'data/nuvolaplayer/services/songza/description.html'
--- data/nuvolaplayer/services/songza/description.html	1970-01-01 00:00:00 +0000
+++ data/nuvolaplayer/services/songza/description.html	2014-08-11 14:57:25 +0000
@@ -0,0 +1,10 @@
+<p><strong>Songza</strong> is a free music streaming and recommendation
+service.</p>
+<p>
+<em>Source:
+<a href="http://songza.com";>Official website</a></em>
+<a href="http://en.wikipedia.org/wiki/Songza";>Songza on Wikipedia</a>,
+</p>
+<h2>Country Availability</h2>
+<p><strong>Songza</strong> is currently only available in the United States and
+Canada.</p>

=== added file 'data/nuvolaplayer/services/songza/integration.js'
--- data/nuvolaplayer/services/songza/integration.js	1970-01-01 00:00:00 +0000
+++ data/nuvolaplayer/services/songza/integration.js	2014-08-11 14:57:25 +0000
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2014 Google Inc. All Rights Reserved.
+ * Author: dharan@xxxxxxxxxx (Ramesh Dharan)
+ *
+ * Copyright 2011-2013 Jiří Janoušek <janousek.jiri@xxxxxxxxx>
+ *
+ * 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.
+ */
+
+/* Anonymous function is used not to pollute environment */
+(function(Nuvola)
+{
+  /**
+   * Creates integration binded to Nuvola JS API
+   */
+  var Integration = function()
+  {
+    /* For debug output */
+    this.name = "Songza";
+
+    /* Overwrite default command function */
+    Nuvola.onMessageReceived = Nuvola.bind(this, this.messageHandler);
+
+    require(["songza/app", ], Nuvola.bind(this, function(App) {
+      App.postInit(Nuvola.bind(this, function() {
+        this.songza = App.getInstance();
+      }));
+    }));
+
+    /* Let's run */
+    this.state = Nuvola.STATE_NONE;
+    this.can_prev = null;
+    this.can_next = null;
+    this.can_thumbs_up = null;
+    this.can_thumbs_down = null;
+
+    this.update();
+  };
+
+  /**
+   * Updates current playback state
+   */
+  Integration.prototype.update = function()
+  {
+    // Default values
+    var state = Nuvola.STATE_NONE;
+    var can_prev = false;
+    var can_next = false;
+    var can_thumbs_up = false;
+    var can_thumbs_down = false;
+
+    var song = null;
+    var artist = null;
+    var album = null;
+    var art = null;
+
+    try {
+      var player = this.songza.getPlayer();
+      var manager = player.getManager();
+      var songContext = player.songContext(player.findSongOrCurrent());
+
+      state = player.isPlaying() ? Nuvola.STATE_PLAYING :
+              player.isPaused() ? Nuvola.STATE_PAUSED : state;
+
+      can_next = manager.canSkip();
+
+      song = songContext.song.title;
+      artist = songContext.song.artist;
+      album = songContext.album;
+      art = songContext.song.image_url;
+
+      can_thumbs_up = true;
+      can_thumbs_down = true;
+
+    } catch(e) {
+      console.debug(this.name + ": could not update song state");
+    }
+
+    // Save state
+    this.state = state;
+
+    // Submit data to Nuvola backend
+    Nuvola.updateSong(song, artist, album, art, state);
+
+    if (this.can_next !== can_next) {
+      this.can_next = can_next;
+      Nuvola.updateAction(Nuvola.ACTION_NEXT_SONG, can_next);
+    }
+
+    if (this.can_thumbs_up !== can_thumbs_up) {
+      this.can_thumbs_up = can_thumbs_up;
+      Nuvola.updateAction(Nuvola.ACTION_THUMBS_UP, can_thumbs_up);
+    }
+
+    if (this.can_thumbs_down !== can_thumbs_down) {
+      this.can_thumbs_down = can_thumbs_down;
+      Nuvola.updateAction(Nuvola.ACTION_THUMBS_DOWN, can_thumbs_down);
+    }
+
+    // Schedule update
+    setTimeout(Nuvola.bind(this, this.update), 500);
+  };
+
+  /**
+   * Message handler
+   * @param cmd command to execute
+   */
+  Integration.prototype.messageHandler = function(cmd)
+  {
+    /* Respond to user actions */
+    try
+    {
+      var player = this.songza.getPlayer();
+      var song = player.findSongOrCurrent();
+      var stream = song.get("streamSong");
+
+      switch (cmd)
+      {
+        case Nuvola.ACTION_PLAY:
+          player.play();
+          break;
+        case Nuvola.ACTION_PAUSE:
+          player.pause();
+          break;
+        case Nuvola.ACTION_TOGGLE_PLAY:
+          player.play_or_pause();
+          break;
+        case Nuvola.ACTION_NEXT_SONG:
+          player.skip();
+          break;
+
+        /*
+         * XXX The thumbs up/down action implementations are potentially
+         *     fragile. I couldn't figure out how to make player.voteUp() work,
+         *     it tries to resolve the target using a jQuery selector in a way
+         *     that didn't work inside this closure.
+         *
+         *     So I just mimicked what it was doing, but of course if the
+         *     implementation changes this could break. --rameshdharan
+         */
+        case Nuvola.ACTION_THUMBS_UP:
+          song.set({vote : "up"});
+          stream.voteUp();
+          player.songTrigger("vote-up");
+          break;
+        case Nuvola.ACTION_THUMBS_DOWN:
+          song.set({vote : "down"});
+          stream.voteDown();
+          player.songTrigger("vote-down");
+          break;
+
+        default:
+          // Other commands are not supported
+          throw {"message": "Not supported."};
+      }
+      console.log(this.name + ": comand '" + cmd + "' executed.");
+    }
+    catch (e)
+    {
+      // Older API expected exception to be a string.
+      throw (this.name + ": " + e.message);
+    }
+  };
+
+  /* Store reference */
+  Nuvola.integration = new Integration(); // Singleton
+
+  // Immediately call the anonymous function with Nuvola JS API main object as an argument.
+  // Note that "this" is set to the Nuvola JS API main object.
+})(this);

=== added file 'data/nuvolaplayer/services/songza/metadata.conf'
--- data/nuvolaplayer/services/songza/metadata.conf	1970-01-01 00:00:00 +0000
+++ data/nuvolaplayer/services/songza/metadata.conf	2014-08-11 14:57:25 +0000
@@ -0,0 +1,10 @@
+name = Songza
+home_page = https://songza.com/
+sandbox_pattern = https?://songza.com/
+maintainer_name = Ramesh Dharan
+maintainer_link = https://launchpad.net/~rameshdharan
+version = 1
+version_minor = 0
+flash_plugin = no
+requirements_specified = yes
+api_major = 2

=== modified file 'src/nuvola/extensions/lastfm.vala'
--- src/nuvola/extensions/lastfm.vala	2014-07-26 12:38:38 +0000
+++ src/nuvola/extensions/lastfm.vala	2014-08-11 14:57:25 +0000
@@ -150,6 +150,7 @@
 	private weak Diorite.ActionGroups action_groups;
 	private string? current_service_id = null;
 	private uint scrobble_track_source = 0;
+	private uint now_playing_source = 0;
 	private string? song = null;
 	private string? artist = null;
 	private int64 timestamp = 0;
@@ -188,8 +189,9 @@
 		var service = web_backend.service;
 		current_service_id = service == null ? null : service.id;
 		on_song_changed(player.song, player.artist, player.album, player.album_art);
-		player.song_changed.connect_after(on_song_changed);
-		player.notify["playback-state"].connect_after(on_playback_state_changed);
+		on_song_changed_after(player.song, player.artist, player.album, player.album_art);
+		player.song_changed.connect(on_song_changed);
+		player.song_changed.connect_after(on_song_changed_after);
 		web_backend.notify["service"].connect_after(on_service_changed);
 		foreach (var scrobbler in scrobblers)
 			scrobbler.notify["has-session"].connect_after(on_has_session_changed);
@@ -227,8 +229,8 @@
 		}
 		scrobblers = {};
 		web_backend.notify["service"].disconnect(on_service_changed);
-		player.notify["playback-state"].disconnect(on_playback_state_changed);
 		player.song_changed.disconnect(on_song_changed);
+		player.song_changed.disconnect(on_song_changed_after);
 		var timestamp = get_timestamp();
 		if (can_scrobble && this.song != null && this.artist != null)
 		{
@@ -242,6 +244,12 @@
 			Source.remove(scrobble_track_source);
 			scrobble_track_source = 0;
 		}
+		
+		if (now_playing_source != 0)
+		{
+			Source.remove(now_playing_source);
+			now_playing_source = 0;
+		}
 	}
 	
 	/**
@@ -400,6 +408,12 @@
 			scrobble_track_source = 0;
 		}
 		
+		if (now_playing_source != 0)
+		{
+			Source.remove(now_playing_source);
+			now_playing_source = 0;
+		}
+		
 		if (song == null || artist == null)
 			return;
 		
@@ -416,21 +430,25 @@
 		this.timestamp = timestamp;
 		can_scrobble = false;
 		
-		update_now_playing();
+		update_now_playing.begin(song, artist, (obj, res) =>
+		{
+			update_now_playing.end(res);
+		});
 		
 		scrobble_track_source = Timeout.add_seconds(SCROBBLE_SONG_DELAY, enable_scrobbling_cb);
-		
+		now_playing_source = Timeout.add_seconds(NOW_PLAYING_TIMEOUT, update_now_playing_cb);
+	}
+	
+	/**
+	 * Callback for song change. Queues "now playing" update and track scrobbling.
+	 */
+	private void on_song_changed_after(string? song, string? artist, string? album, string? album_art)
+	{
 		update_actions.begin(song, artist, (obj, res) =>
 		{
 			update_actions.end(res);
 		});
 	}
-	
-	private void on_playback_state_changed(GLib.Object o, ParamSpec p)
-	{
-		update_now_playing();
-	}
-	
 	private bool enable_scrobbling_cb()
 	{
 		if (scrobble_track_source != 0)
@@ -521,19 +539,29 @@
 		}
 	}
 	
-	private async void update_now_playing()
-	{
-		if (current_service_id == null || player.playback_state != STATE_PLAYING
-		|| player.artist == null || player.song == null)
+	private bool update_now_playing_cb()
+	{
+		if (now_playing_source == 0 || song == null || artist == null)
+			return false;
+		
+		update_now_playing.begin(song, artist, (obj, res) =>
+		{
+			update_now_playing.end(res);
+		});
+		return true;
+	}
+	
+	private async void update_now_playing(string song, string artist)
+	{
+		if (current_service_id == null || player.artist != artist || player.song != song)
 			return;
-		
 		foreach (var scrobbler in scrobblers)
 		{
 			if (scrobbler.is_service_enabled(current_service_id))
 			{
 				try
 				{
-					yield scrobbler.update_now_playing(player.song, player.artist);
+					yield scrobbler.update_now_playing(song, artist);
 				}
 				catch (Error e)
 				{