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 }