hythmbox-2.98/plugins/notification/rb-notification-plugin.c

No issues found

  1 /*
  2  * rb-notification-plugin.c
  3  *
  4  *  Copyright (C) 2010  Jonathan Matthew  <jonathan@d14n.org>
  5  *
  6  * This program is free software; you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation; either version 2, or (at your option)
  9  * any later version.
 10  *
 11  * The Rhythmbox authors hereby grant permission for non-GPL compatible
 12  * GStreamer plugins to be used and distributed together with GStreamer
 13  * and Rhythmbox. This permission is above and beyond the permissions granted
 14  * by the GPL license by which Rhythmbox is covered. If you modify this code
 15  * you may extend this exception to your version of the code, but you are not
 16  * obligated to do so. If you do not wish to do so, delete this exception
 17  * statement from your version.
 18  *
 19  * This program is distributed in the hope that it will be useful,
 20  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 21  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 22  * GNU General Public License for more details.
 23  *
 24  * You should have received a copy of the GNU General Public License
 25  * along with this program; if not, write to the Free Software
 26  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
 27  */
 28 
 29 #include <config.h>
 30 
 31 #include <string.h>
 32 #include <glib/gi18n-lib.h>
 33 #include <gtk/gtk.h>
 34 #include <glib.h>
 35 #include <glib-object.h>
 36 #include <pango/pango-bidi-type.h>
 37 #include <libnotify/notify.h>
 38 
 39 #include "rb-util.h"
 40 #include "rb-plugin-macros.h"
 41 #include "rb-debug.h"
 42 #include "rb-shell.h"
 43 #include "rb-shell-player.h"
 44 #include "rb-stock-icons.h"
 45 #include "rb-ext-db.h"
 46 
 47 #define PLAYING_ENTRY_NOTIFY_TIME	4
 48 
 49 #define RB_TYPE_NOTIFICATION_PLUGIN		(rb_notification_plugin_get_type ())
 50 #define RB_NOTIFICATION_PLUGIN(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_NOTIFICATION_PLUGIN, RBNotificationPlugin))
 51 #define RB_NOTIFICATION_PLUGIN_CLASS(k)	(G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_NOTIFICATION_PLUGIN, RBNotificationPluginClass))
 52 #define RB_IS_NOTIFICATION_PLUGIN(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_NOTIFICATION_PLUGIN))
 53 #define RB_IS_NOTIFICATION_PLUGIN_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_NOTIFICATION_PLUGIN))
 54 #define RB_NOTIFICATION_PLUGIN_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_NOTIFICATION_PLUGIN, RBNotificationPluginClass))
 55 
 56 typedef struct
 57 {
 58 	PeasExtensionBase parent;
 59 
 60 	/* current playing data */
 61 	char *current_title;
 62 	char *current_album_and_artist;	/* from _album_ by _artist_ */
 63 
 64 	gchar *notify_art_path;
 65 	NotifyNotification *notification;
 66 	gboolean notify_supports_actions;
 67 	gboolean notify_supports_icon_buttons;
 68 	gboolean notify_supports_persistence;
 69 
 70 	RBShellPlayer *shell_player;
 71 	RhythmDB *db;
 72 	RBExtDB *art_store;
 73 } RBNotificationPlugin;
 74 
 75 typedef struct
 76 {
 77 	PeasExtensionBaseClass parent_class;
 78 } RBNotificationPluginClass;
 79 
 80 G_MODULE_EXPORT void peas_register_types (PeasObjectModule  *module);
 81 
 82 RB_DEFINE_PLUGIN(RB_TYPE_NOTIFICATION_PLUGIN, RBNotificationPlugin, rb_notification_plugin,)
 83 
 84 static gchar *
 85 markup_escape (const char *text)
 86 {
 87 	return (text == NULL) ? NULL : g_markup_escape_text (text, -1);
 88 }
 89 
 90 static void
 91 notification_closed_cb (NotifyNotification *notification,
 92 			RBNotificationPlugin *plugin)
 93 {
 94 	rb_debug ("notification closed");
 95 }
 96 
 97 static void
 98 notification_next_cb (NotifyNotification *notification,
 99 		      const char *action,
100 		      RBNotificationPlugin *plugin)
101 {
102 	rb_debug ("notification action: %s", action);
103 	rb_shell_player_do_next (plugin->shell_player, NULL);
104 }
105 
106 static void
107 notification_playpause_cb (NotifyNotification *notification,
108 			   const char *action,
109 			   RBNotificationPlugin *plugin)
110 {
111 	rb_debug ("notification action: %s", action);
112 	rb_shell_player_playpause (plugin->shell_player, FALSE, NULL);
113 }
114 
115 static void
116 notification_previous_cb (NotifyNotification *notification,
117 			  const char *action,
118 			  RBNotificationPlugin *plugin)
119 {
120 	rb_debug ("notification action: %s", action);
121 	rb_shell_player_do_previous (plugin->shell_player, NULL);
122 }
123 
124 static void
125 do_notify (RBNotificationPlugin *plugin,
126 	   guint timeout,
127 	   const char *primary,
128 	   const char *secondary,
129 	   const char *image_uri,
130 	   gboolean playback)
131 {
132 	GError *error = NULL;
133 	NotifyNotification *notification;
134 
135 	if (notify_is_initted () == FALSE) {
136 		GList *caps;
137 
138 		if (notify_init ("Rhythmbox") == FALSE) {
139 			g_warning ("libnotify initialization failed");
140 			return;
141 		}
142 
143 		/* ask the notification server if it supports actions */
144 		caps = notify_get_server_caps ();
145 		if (g_list_find_custom (caps, "actions", (GCompareFunc)g_strcmp0) != NULL) {
146 			rb_debug ("notification server supports actions");
147 			plugin->notify_supports_actions = TRUE;
148 
149 			if (g_list_find_custom (caps, "action-icons", (GCompareFunc)g_strcmp0) != NULL) {
150 				rb_debug ("notifiction server supports icon buttons");
151 				plugin->notify_supports_icon_buttons = TRUE;
152 			}
153 		} else {
154 			rb_debug ("notification server does not support actions");
155 		}
156 		if (g_list_find_custom (caps, "persistence", (GCompareFunc)g_strcmp0) != NULL) {
157 			rb_debug ("notification server supports persistence");
158 			plugin->notify_supports_persistence = TRUE;
159 		} else {
160 			rb_debug ("notification server does not support persistence");
161 		}
162 
163 		rb_list_deep_free (caps);
164 	}
165 
166 	if (primary == NULL)
167 		primary = "";
168 
169 	if (secondary == NULL)
170 		secondary = "";
171 
172 	if (playback) {
173 		notification = plugin->notification;
174 	} else {
175 		notification = NULL;
176 	}
177 
178 	if (notification == NULL) {
179 		notification = notify_notification_new (primary, secondary, RB_APP_ICON);
180 
181 		g_signal_connect_object (notification,
182 					 "closed",
183 					 G_CALLBACK (notification_closed_cb),
184 					 plugin, 0);
185 		if (playback) {
186 			plugin->notification = notification;
187 		}
188 	} else {
189 		notify_notification_clear_hints (notification);
190 		notify_notification_update (notification, primary, secondary, RB_APP_ICON);
191 	}
192 
193 	notify_notification_set_timeout (notification, timeout);
194 
195 	if (image_uri != NULL) {
196 		notify_notification_clear_hints (notification);
197 		notify_notification_set_hint (notification,
198 					      "image_path",
199 					      g_variant_new_string (image_uri));
200 	}
201 
202 	notify_notification_clear_actions (notification);
203 	if (playback && plugin->notify_supports_actions) {
204 		if (plugin->notify_supports_icon_buttons) {
205 			gboolean playing = FALSE;
206 			rb_shell_player_get_playing (plugin->shell_player, &playing, NULL);
207 
208 			notify_notification_add_action (notification,
209 							"media-skip-backward",
210 							_("Previous"),
211 							(NotifyActionCallback) notification_previous_cb,
212 							plugin,
213 							NULL);
214 			notify_notification_add_action (notification,
215 							playing ? "media-playback-pause" : "media-playback-start",
216 							playing ? _("Pause") : _("Play"),
217 							(NotifyActionCallback) notification_playpause_cb,
218 							plugin,
219 							NULL);
220 			notify_notification_set_hint (notification, "action-icons", g_variant_new_boolean (TRUE));
221 		}
222 
223 		notify_notification_add_action (notification,
224 						"media-skip-forward",
225 						_("Next"),
226 						(NotifyActionCallback) notification_next_cb,
227 						plugin,
228 						NULL);
229 	}
230 
231 	if (plugin->notify_supports_persistence) {
232 		const char *hint;
233 
234 		if (playback) {
235 			hint = "resident";
236 		} else {
237 			hint = "transient";
238 		}
239 		notify_notification_set_hint (notification, hint, g_variant_new_boolean (TRUE));
240 	}
241 
242 	if (notify_notification_show (notification, &error) == FALSE) {
243 		g_warning ("Failed to send notification (%s): %s", primary, error->message);
244 		g_error_free (error);
245 	}
246 }
247 
248 static void
249 notify_playing_entry (RBNotificationPlugin *plugin, gboolean requested)
250 {
251 	do_notify (plugin,
252 		   PLAYING_ENTRY_NOTIFY_TIME * 1000,
253 		   plugin->current_title,
254 		   plugin->current_album_and_artist,
255 		   plugin->notify_art_path,
256 		   TRUE);
257 }
258 
259 static void
260 notify_custom (RBNotificationPlugin *plugin,
261 	       guint timeout,
262 	       const char *primary,
263 	       const char *secondary,
264 	       const char *image_uri,
265 	       gboolean requested)
266 {
267 	do_notify (plugin, timeout, primary, secondary, image_uri, FALSE);
268 }
269 
270 static void
271 cleanup_notification (RBNotificationPlugin *plugin)
272 {
273 	if (plugin->notification != NULL) {
274 		g_signal_handlers_disconnect_by_func (plugin->notification,
275 						      G_CALLBACK (notification_closed_cb),
276 						      plugin);
277 		notify_notification_close (plugin->notification, NULL);
278 		plugin->notification = NULL;
279 	}
280 }
281 
282 static void
283 shell_notify_playing_cb (RBShell *shell, gboolean requested, RBNotificationPlugin *plugin)
284 {
285 	notify_playing_entry (plugin, requested);
286 }
287 
288 static void
289 shell_notify_custom_cb (RBShell *shell,
290 			guint timeout,
291 			const char *primary,
292 			const char *secondary,
293 			const char *image_uri,
294 			gboolean requested,
295 			RBNotificationPlugin *plugin)
296 {
297 	notify_custom (plugin, timeout, primary, secondary, image_uri, requested);
298 }
299 
300 static void
301 get_artist_album_templates (const char *artist,
302 			    const char *album,
303 			    const char **artist_template,
304 			    const char **album_template)
305 {
306 	PangoDirection tag_dir;
307 	PangoDirection template_dir;
308 
309 	/* Translators: by Artist */
310 	*artist_template = _("by <i>%s</i>");
311 	/* Translators: from Album */
312 	*album_template = _("from <i>%s</i>");
313 
314 	/* find the direction (left-to-right or right-to-left) of the
315 	 * track's tags and the localized templates
316 	 */
317 	if (artist != NULL && artist[0] != '\0') {
318 		tag_dir = pango_find_base_dir (artist, -1);
319 		template_dir = pango_find_base_dir (*artist_template, -1);
320 	} else if (album != NULL && album[0] != '\0') {
321 		tag_dir = pango_find_base_dir (album, -1);
322 		template_dir = pango_find_base_dir (*album_template, -1);
323 	} else {
324 		return;
325 	}
326 
327 	/* if the track's tags and the localized templates have a different
328 	 * direction, switch to direction-neutral templates in order to improve
329 	 * display.
330 	 * text can have a neutral direction, this condition only applies when
331 	 * both directions are defined and they are conflicting.
332 	 * https://bugzilla.gnome.org/show_bug.cgi?id=609767
333 	 */
334 	if (((tag_dir == PANGO_DIRECTION_LTR) && (template_dir == PANGO_DIRECTION_RTL)) ||
335 	    ((tag_dir == PANGO_DIRECTION_RTL) && (template_dir == PANGO_DIRECTION_LTR))) {
336 		/* these strings should not be localized, they must be
337 		 * locale-neutral and direction-neutral
338 		 */
339 		*artist_template = "<i>%s</i>";
340 		*album_template = "/ <i>%s</i>";
341 	}
342 }
343 
344 static void
345 art_cb (RBExtDBKey *key, const char *filename, GValue *data, RBNotificationPlugin *plugin)
346 {
347 	RhythmDBEntry *entry;
348 
349 	entry = rb_shell_player_get_playing_entry (plugin->shell_player);
350 	if (entry == NULL) {
351 		return;
352 	}
353 
354 	if (rhythmdb_entry_matches_ext_db_key (plugin->db, entry, key)) {
355 		guint elapsed = 0;
356 
357 		plugin->notify_art_path = g_strdup (filename);
358 
359 		rb_shell_player_get_playing_time (plugin->shell_player, &elapsed, NULL);
360 		if (elapsed < PLAYING_ENTRY_NOTIFY_TIME) {
361 			notify_playing_entry (plugin, FALSE);
362 		}
363 	}
364 
365 	rhythmdb_entry_unref (entry);
366 }
367 
368 static void
369 update_current_playing_data (RBNotificationPlugin *plugin, RhythmDBEntry *entry)
370 {
371 	GValue *value;
372 	const char *stream_title = NULL;
373 	char *artist = NULL;
374 	char *album = NULL;
375 	char *title = NULL;
376 	GString *secondary;
377 	RBExtDBKey *key;
378 
379 	const char *artist_template = NULL;
380 	const char *album_template = NULL;
381 
382 	g_free (plugin->current_title);
383 	g_free (plugin->current_album_and_artist);
384 	g_free (plugin->notify_art_path);
385 	plugin->current_title = NULL;
386 	plugin->current_album_and_artist = NULL;
387 	plugin->notify_art_path = NULL;
388 
389 	if (entry == NULL) {
390 		plugin->current_title = g_strdup (_("Not Playing"));
391 		plugin->current_album_and_artist = g_strdup ("");
392 		return;
393 	}
394 
395 	secondary = g_string_sized_new (100);
396 
397 	/* request album art */
398 	key = rhythmdb_entry_create_ext_db_key (entry, RHYTHMDB_PROP_ALBUM);
399 	rb_ext_db_request (plugin->art_store,
400 			   key,
401 			   (RBExtDBRequestCallback) art_cb,
402 			   g_object_ref (plugin),
403 			   g_object_unref);
404 	rb_ext_db_key_free (key);
405 
406 	/* get artist, preferring streaming song details */
407 	value = rhythmdb_entry_request_extra_metadata (plugin->db,
408 						       entry,
409 						       RHYTHMDB_PROP_STREAM_SONG_ARTIST);
410 	if (value != NULL) {
411 		artist = markup_escape (g_value_get_string (value));
412 		g_value_unset (value);
413 		g_free (value);
414 	} else {
415 		artist = markup_escape (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST));
416 	}
417 
418 	/* get album, preferring streaming song details */
419 	value = rhythmdb_entry_request_extra_metadata (plugin->db,
420 						       entry,
421 						       RHYTHMDB_PROP_STREAM_SONG_ALBUM);
422 	if (value != NULL) {
423 		album = markup_escape (g_value_get_string (value));
424 		g_value_unset (value);
425 		g_free (value);
426 	} else {
427 		album = markup_escape (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM));
428 	}
429 
430 	get_artist_album_templates (artist, album, &artist_template, &album_template);
431 
432 	if (artist != NULL && artist[0] != '\0') {
433 		g_string_append_printf (secondary, artist_template, artist);
434 	}
435 	g_free (artist);
436 
437 	if (album != NULL && album[0] != '\0') {
438 		if (secondary->len != 0)
439 			g_string_append_c (secondary, ' ');
440 
441 		g_string_append_printf (secondary, album_template, album);
442 	}
443 	g_free (album);
444 
445 	/* get title and possibly stream name.
446 	 * if we have a streaming song title, the entry's title
447 	 * property is the stream name.
448 	 */
449 	value = rhythmdb_entry_request_extra_metadata (plugin->db,
450 						       entry,
451 						       RHYTHMDB_PROP_STREAM_SONG_TITLE);
452 	if (value != NULL) {
453 		title = g_value_dup_string (value);
454 		g_value_unset (value);
455 		g_free (value);
456 
457 		stream_title = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE);
458 	} else {
459 		title = g_strdup (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE));
460 	}
461 
462 	if (stream_title != NULL && stream_title[0] != '\0') {
463 		char *escaped;
464 
465 		escaped = markup_escape (stream_title);
466 		if (secondary->len == 0)
467 			g_string_append (secondary, escaped);
468 		else
469 			g_string_append_printf (secondary, " (%s)", escaped);
470 		g_free (escaped);
471 	}
472 
473 	if (title == NULL) {
474 		/* Translators: unknown track title */
475 		title = g_strdup (_("Unknown"));
476 	}
477 
478 	plugin->current_title = title;
479 	plugin->current_album_and_artist = g_string_free (secondary, FALSE);
480 }
481 
482 static void
483 playing_entry_changed_cb (RBShellPlayer *player,
484 			  RhythmDBEntry *entry,
485 			  RBNotificationPlugin *plugin)
486 {
487 	update_current_playing_data (plugin, entry);
488 	notify_playing_entry (plugin, FALSE);
489 }
490 
491 static void
492 playing_changed_cb (RBShellPlayer *player,
493 		    gboolean       playing,
494 		    RBNotificationPlugin *plugin)
495 {
496 	notify_playing_entry (plugin, FALSE);
497 }
498 
499 static gboolean
500 is_playing_entry (RBNotificationPlugin *plugin, RhythmDBEntry *entry)
501 {
502 	RhythmDBEntry *playing;
503 
504 	playing = rb_shell_player_get_playing_entry (plugin->shell_player);
505 	if (playing == NULL) {
506 		return FALSE;
507 	}
508 
509 	rhythmdb_entry_unref (playing);
510 	return (entry == playing);
511 }
512 
513 static void
514 db_stream_metadata_cb (RhythmDB *db,
515 		       RhythmDBEntry *entry,
516 		       const char *field,
517 		       GValue *metadata,
518 		       RBNotificationPlugin *plugin)
519 {
520 	if (is_playing_entry (plugin, entry) == FALSE)
521 		return;
522 
523 	update_current_playing_data (plugin, entry);
524 }
525 
526 /* plugin infrastructure */
527 
528 static void
529 impl_activate (PeasActivatable *bplugin)
530 {
531 	RBNotificationPlugin *plugin;
532 	RBShell *shell;
533 
534 	rb_debug ("activating notification plugin");
535 
536 	plugin = RB_NOTIFICATION_PLUGIN (bplugin);
537 	g_object_get (plugin, "object", &shell, NULL);
538 	g_object_get (shell,
539 		      "shell-player", &plugin->shell_player,
540 		      "db", &plugin->db,
541 		      NULL);
542 
543 	/* connect various things */
544 	g_signal_connect_object (shell, "notify-playing-entry", G_CALLBACK (shell_notify_playing_cb), plugin, 0);
545 	g_signal_connect_object (shell, "notify-custom", G_CALLBACK (shell_notify_custom_cb), plugin, 0);
546 
547 	g_signal_connect_object (plugin->shell_player, "playing-song-changed", G_CALLBACK (playing_entry_changed_cb), plugin, 0);
548 	g_signal_connect_object (plugin->shell_player, "playing-changed", G_CALLBACK (playing_changed_cb), plugin, 0);
549 
550 	g_signal_connect_object (plugin->db, "entry_extra_metadata_notify::" RHYTHMDB_PROP_STREAM_SONG_TITLE,
551 				 G_CALLBACK (db_stream_metadata_cb), plugin, 0);
552 	g_signal_connect_object (plugin->db, "entry_extra_metadata_notify::" RHYTHMDB_PROP_STREAM_SONG_ARTIST,
553 				 G_CALLBACK (db_stream_metadata_cb), plugin, 0);
554 	g_signal_connect_object (plugin->db, "entry_extra_metadata_notify::" RHYTHMDB_PROP_STREAM_SONG_ALBUM,
555 				 G_CALLBACK (db_stream_metadata_cb), plugin, 0);
556 
557 	plugin->art_store = rb_ext_db_new ("album-art");
558 
559 	/* hook into shell preferences so we can poke stuff into the general prefs page? */
560 
561 	g_object_unref (shell);
562 }
563 
564 static void
565 impl_deactivate	(PeasActivatable *bplugin)
566 {
567 	RBNotificationPlugin *plugin;
568 	RBShell *shell;
569 
570 	plugin = RB_NOTIFICATION_PLUGIN (bplugin);
571 
572 	g_object_get (plugin, "object", &shell, NULL);
573 
574 	cleanup_notification (plugin);
575 
576 	/* disconnect signal handlers used to update the icon */
577 	if (plugin->shell_player != NULL) {
578 		g_signal_handlers_disconnect_by_func (plugin->shell_player, playing_entry_changed_cb, plugin);
579 
580 		g_object_unref (plugin->shell_player);
581 		plugin->shell_player = NULL;
582 	}
583 
584 	if (plugin->db != NULL) {
585 		g_signal_handlers_disconnect_by_func (plugin->db, db_stream_metadata_cb, plugin);
586 
587 		g_object_unref (plugin->db);
588 		plugin->db = NULL;
589 	}
590 
591 	g_signal_handlers_disconnect_by_func (shell, shell_notify_playing_cb, plugin);
592 	g_signal_handlers_disconnect_by_func (shell, shell_notify_custom_cb, plugin);
593 
594 	g_object_unref (plugin->art_store);
595 	plugin->art_store = NULL;
596 
597 	/* forget what's playing */
598 	g_free (plugin->current_title);
599 	g_free (plugin->current_album_and_artist);
600 	g_free (plugin->notify_art_path);
601 	plugin->current_title = NULL;
602 	plugin->current_album_and_artist = NULL;
603 	plugin->notify_art_path = NULL;
604 
605 	g_object_unref (shell);
606 }
607 
608 static void
609 rb_notification_plugin_init (RBNotificationPlugin *plugin)
610 {
611 }
612 
613 G_MODULE_EXPORT void
614 peas_register_types (PeasObjectModule *module)
615 {
616 	rb_notification_plugin_register_type (G_TYPE_MODULE (module));
617 	peas_object_module_register_extension_type (module,
618 						    PEAS_TYPE_ACTIVATABLE,
619 						    RB_TYPE_NOTIFICATION_PLUGIN);
620 }