diff options
author | Daniel Silverstone <daniel.silverstone@codethink.co.uk> | 2011-10-29 22:33:44 +0100 |
---|---|---|
committer | Daniel Silverstone <daniel.silverstone@codethink.co.uk> | 2011-10-29 22:33:44 +0100 |
commit | e62815307860d7e90ba63e281c7ed8776ef43f00 (patch) | |
tree | 5035430863455de628ada791fd5bbcd3e545959d | |
download | spotify-notify-e62815307860d7e90ba63e281c7ed8776ef43f00.tar.bz2 |
Initial rev
-rw-r--r-- | Makefile | 11 | ||||
-rw-r--r-- | spotify-notify.vala | 285 |
2 files changed, 296 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d28218c --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +all: spotify-notify + +clean: + $(RM) spotify-notify + +distclean: clean + $(RM) *~ + +spotify-notify: spotify-notify.vala + valac --pkg gio-2.0 --pkg posix --pkg libsoup-2.4 $< + diff --git a/spotify-notify.vala b/spotify-notify.vala new file mode 100644 index 0000000..1c97a5c --- /dev/null +++ b/spotify-notify.vala @@ -0,0 +1,285 @@ +[DBus (name = "org.mpris.MediaPlayer2.Player")] +interface Player : Object { + public abstract void play_pause () throws IOError; + public abstract void next () throws IOError; + public abstract void previous () throws IOError; + public abstract void stop () throws IOError; + +} + +[DBus (name = "org.freedesktop.DBus.Properties")] +interface PlayerProperties : Object { + public signal void properties_changed (string iface, HashTable<string, Variant> props, string[] invalidateds); + public abstract void get (string iface, string prop, out Variant value) throws IOError; +} + +[DBus (name = "org.gnome.SettingsDaemon.MediaKeys")] +interface MediaKeys : Object { + public abstract void grab_media_player_keys(string appname, uint32 n) throws IOError; + public signal void media_player_key_pressed (string appname, string key); +} + +[DBus (name = "org.freedesktop.DBus")] +interface BusInterface : Object { + public signal void name_owner_changed (string name, string old_owner, string new_owner); + public abstract void name_has_owner (string name, out bool has_owner) throws IOError; +} + +[DBus (name = "org.freedesktop.Notifications")] +interface Notify : Object { + public abstract uint32 notify (string app_name, uint32 replaces_id, string icon, string summary, string body, + string[] actions, HashTable<string, Variant> hints, int32 timeout) throws IOError; + public abstract void close_notification (uint32 id) throws IOError; + public signal void notification_closed (uint32 id, uint32 reason); + +} + +Player player = null; +PlayerProperties playerprops = null; +MainLoop loop; +Notify notify; +uint32 notification_id = 0; +Soup.SessionAsync soupsession; + + +const string imgcache = "/home/dsilvers/.cache/spotify-albums"; + +void cancel_notification() { + try { + if (notification_id > 0) { + stdout.printf("Cancelling notification %u\n", notification_id); + notify.close_notification(notification_id); + stdout.printf("Notification cancelled\n"); + notification_id = 0; + } + } catch (IOError e) { + stderr.printf("Error closing notification: %s\n", e.message); + } +} + +class cbdobj { + public string uri; + public string targetfile; + public Variant metadata; + public bool unsolicited; + + public void fetch_image_cb(Soup.Session sess, Soup.Message mess) + { + stdout.printf("Response for %s => %s\n", uri, targetfile); + stdout.printf("Status code %u, Content length: %zd\n", mess.status_code, (ssize_t)mess.response_body.length); + if (mess.response_body.length > 0) { + stdout.printf("Attempting to write to %s\n", targetfile); + FileStream? f = FileStream.open(targetfile, "wb"); + if (f != null) { + f.write(mess.response_body.data); + f = null; /* fclose */ + stdout.printf("Re-displaying with image for %s\n", uri); + metadata_changed(metadata, unsolicited); + } + } + /* We're done, so delete us */ + delete *&this; + } +} + +void fetch_image(string targetfile, string uri, Variant metadata, bool unsolicited) { + var msg = new Soup.Message ("GET", uri); + stdout.printf("Prepping async fetch of %s\n", uri); + cbdobj *cbd = new cbdobj (); + cbd->uri = uri; + cbd->targetfile = targetfile; + cbd->metadata = metadata; + cbd->unsolicited = unsolicited; + soupsession.queue_message (msg, cbd->fetch_image_cb); +} + +void on_notification_closed (uint32 id, uint32 reason) { + stdout.printf("Notification %u closed\n", id); + if (notification_id == id) { + stdout.printf("That was us\n"); + notification_id = 0; + } +} + +void metadata_changed(Variant metadata, bool unsolicited) { + Variant? v_artist_a = metadata.lookup_value("xesam:artist", null); + Variant? v_artist = v_artist_a == null ? null : v_artist_a.get_child_value(0); + Variant? v_album = metadata.lookup_value("xesam:album", null); + Variant? v_title = metadata.lookup_value("xesam:title", null); + Variant? v_arturl = metadata.lookup_value("mpris:artUrl", null); + string? arturl = v_arturl == null ? null : v_arturl.get_string(); + + if (v_artist == null || + v_album == null || + v_title == null) { + stderr.printf("Incomplete metadata!\n"); + return; + } + + string imgpath = ""; + string? art_id; + + bool need_load_artwork = false; + if (v_arturl != null) { + /* Do we have the image cached? */ + string s_art_id = arturl.substring(arturl.last_index_of_char('/')); + art_id = s_art_id.next_char(); + imgpath = @"$imgcache/$art_id"; + Posix.Stat sbuf; + if (Posix.stat(imgpath, out sbuf) != 0) { + stderr.printf("Artwork %s not found\n", arturl); + need_load_artwork = true; + } else { + stderr.printf("Artwork %s found\n", imgpath); + } + } + + string artist = v_artist.get_string(); + string album = v_album.get_string(); + string title = v_title.get_string(); + + string msg = @"$title\n$album"; + + string[] actions = null; + HashTable<string, Variant> hints = new HashTable<string, Variant> (null, null); + + try { + stdout.printf("Attempting to notify\n"); + var nid = notify.notify("Spotify Interface", + notification_id, + imgpath, + artist, msg, + actions, + hints, + unsolicited ? 5000 : 1000); + notification_id = nid; + stdout.printf("Notification %u sent\n", notification_id); + } catch (IOError e) { + stderr.printf("Error while notifying: %s\n", e.message); + } + if (need_load_artwork) { + stdout.printf("Queueing %s => %s\n", arturl, imgpath); + fetch_image(imgpath, arturl, metadata, unsolicited); + } +} + +void on_properties_changed (string iface, HashTable<string, Variant> props, string[] invalidateds) { + if (iface == "org.mpris.MediaPlayer2.Player") { + Variant? mode = props.lookup("PlaybackStatus"); + if (mode != null) { + if (mode.get_string() == "Paused") { + /* Something here? */ + cancel_notification(); + } else { + /* Switched to playing */ + try { + Variant v; + playerprops.get ("org.mpris.MediaPlayer2.Player", "Metadata", out v); + metadata_changed(v, false); + } catch (IOError e) { + stderr.printf("Playback metadata retrieval failed: %s\n", e.message); + } + } + } + Variant? metadata = props.lookup("Metadata"); + if (metadata != null) { + metadata_changed(metadata, true); + } + } +} + +void reconnect_spotify () { + stdout.printf("Attempting to (re)connect to spotify\n"); + try { + player = Bus.get_proxy_sync(BusType.SESSION, "com.spotify.qt", "/org/mpris/MediaPlayer2"); + playerprops = Bus.get_proxy_sync(BusType.SESSION, "com.spotify.qt", "/org/mpris/MediaPlayer2"); + playerprops.properties_changed.connect (on_properties_changed); + stdout.printf("Success!\n"); + } catch (IOError e) { + stderr.printf("Error (re)connecting: %s\n", e.message); + } +} + +void on_name_owner_changed (string name, string old_owner, string new_owner) { + // stdout.printf("name owner changed. name=%s old_owner=%s new_owner=%s\n", name, old_owner, new_owner); + if (name == "com.spotify.qt") { + if (new_owner == "") { + /* Spotify went away */ + player = null; + playerprops = null; + stdout.printf("Spotify went away\n"); + } else { + /* Spotify has arrived */ + reconnect_spotify(); + } + } +} + +void on_media_player_key_pressed (string appname, string key) { + stdout.printf("App %s key pressed: %s\n", appname, key); + + if (player == null) { + stdout.printf("Key ignored, no spotify running\n"); + } + + try { + switch (key) { + case "Play": + player.play_pause(); + break; + case "PlayPause": + player.play_pause(); + break; + case "Stop": + player.stop(); + break; + case "Previous": + player.previous(); + break; + case "Next": + player.next(); + break; + default: + stderr.printf("Unknown media key!\n"); + break; + }; + } catch (IOError e) { + stderr.printf("Error: %s\n", e.message); + } +} + +int main() { + MediaKeys mkeys; + BusInterface bi; + try { + soupsession = new Soup.SessionAsync (); + stdout.printf("Getting media keys\n"); + mkeys = Bus.get_proxy_sync (BusType.SESSION, "org.gnome.SettingsDaemon", "/org/gnome/SettingsDaemon/MediaKeys"); + mkeys.media_player_key_pressed.connect (on_media_player_key_pressed); + mkeys.grab_media_player_keys("Spotify", 0); + stdout.printf("Getting notifications\n"); + notify = Bus.get_proxy_sync (BusType.SESSION, "org.freedesktop.Notifications", "/org/freedesktop/Notifications"); + notify.notification_closed.connect (on_notification_closed); + stdout.printf("Getting owner handles\n"); + bi = Bus.get_proxy_sync (BusType.SESSION, "org.freedesktop.DBus", "/org/freedesktop/DBus"); + bi.name_owner_changed.connect (on_name_owner_changed); + + bool t; + bi.name_has_owner("com.spotify.qt", out t); + + if (t) + reconnect_spotify(); + else + stdout.printf("Spotify not running, will wait for notify\n"); + + } catch (IOError e) { + stderr.printf ("%s\n", e.message); + return 1; + } + + loop = new MainLoop (); + loop.run (); + + return 0; +} +
\ No newline at end of file |