/* spotify-notify.vala * * Media keys and song-change notification for the * official Spotify client for Linux. * * Copyright 2011 Daniel Silverstone */ [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 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 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_PATTERN="%s/.cache/spotify-albums"; string imgcache; 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 hints = ( new HashTable (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 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) { 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; imgcache = IMGCACHE_PATTERN.printf(Environment.get_variable("HOME")); DirUtils.create_with_parents(imgcache, 0700); 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; }