1 /*****************************************************************************
2 * growl.m : growl notification plugin
3 *****************************************************************************
6 * Copyright © 2008,2011,2012,2015 the VideoLAN team
9 * Authors: Rafaël Carré <funman@videolanorg>
10 * Felix Paul Kühne <fkuehne@videolan.org
11 * Marvin Scholz <epirat07@gmail.com>
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
23 * You should have received a copy of the GNU General Public License
24 * along with this program; if not, write to the Free Software
25 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
27 * Growl specific code, ripped from growlnotify:
29 * Copyright (c) The Growl Project, 2004-2005
30 * All rights reserved.
32 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
34 * 1. Redistributions of source code must retain the above copyright
35 notice, this list of conditions and the following disclaimer.
36 * 2. Redistributions in binary form must reproduce the above copyright
37 notice, this list of conditions and the following disclaimer in the
38 documentation and/or other materials provided with the distribution.
39 * 3. Neither the name of Growl nor the names of its contributors
40 may be used to endorse or promote products derived from this software
41 without specific prior written permission.
43 * 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.
45 *****************************************************************************/
47 /*****************************************************************************
49 *****************************************************************************/
55 #import <Foundation/Foundation.h>
56 #import <Cocoa/Cocoa.h>
57 #import <Growl/Growl.h>
59 #define VLC_MODULE_LICENSE VLC_LICENSE_GPL_2_PLUS
60 #include <vlc_common.h>
61 #include <vlc_plugin.h>
62 #include <vlc_playlist.h>
63 #include <vlc_input.h>
65 #include <vlc_interface.h>
68 /*****************************************************************************
69 * intf_sys_t, VLCGrowlDelegate
70 *****************************************************************************/
71 @interface VLCGrowlDelegate : NSObject <GrowlApplicationBridgeDelegate>
73 NSString *applicationName;
74 NSString *notificationType;
75 NSMutableDictionary *registrationDictionary;
78 intf_thread_t *interfaceThread;
81 - (id)initWithInterfaceThread:(intf_thread_t *)thread;
82 - (void)registerToGrowl;
83 - (void)notifyWithTitle:(const char *)title
84 artist:(const char *)artist
85 album:(const char *)album
86 andArtUrl:(const char *)url;
91 VLCGrowlDelegate *o_growl_delegate;
96 /*****************************************************************************
98 *****************************************************************************/
99 static int Open ( vlc_object_t * );
100 static void Close ( vlc_object_t * );
102 static int ItemChange( vlc_object_t *, const char *,
103 vlc_value_t, vlc_value_t, void * );
105 /*****************************************************************************
107 ****************************************************************************/
110 set_category( CAT_INTERFACE )
111 set_subcategory( SUBCAT_INTERFACE_CONTROL )
112 set_shortname( "Growl" )
113 set_description( N_("Growl Notification Plugin") )
114 set_capability( "interface", 0 )
115 set_callbacks( Open, Close )
118 /*****************************************************************************
119 * Open: initialize and create stuff
120 *****************************************************************************/
121 static int Open( vlc_object_t *p_this )
123 intf_thread_t *p_intf = (intf_thread_t *)p_this;
124 playlist_t *p_playlist;
127 p_sys = p_intf->p_sys = calloc( 1, sizeof(intf_sys_t) );
131 p_sys->o_growl_delegate = [[VLCGrowlDelegate alloc] initWithInterfaceThread:p_intf];
132 if( !p_sys->o_growl_delegate )
135 p_playlist = pl_Get( p_intf );
136 var_AddCallback( p_playlist, "item-change", ItemChange, p_intf );
137 var_AddCallback( p_playlist, "input-current", ItemChange, p_intf );
139 [p_sys->o_growl_delegate registerToGrowl];
143 /*****************************************************************************
144 * Close: destroy interface stuff
145 *****************************************************************************/
146 static void Close( vlc_object_t *p_this )
148 intf_thread_t *p_intf = (intf_thread_t *)p_this;
149 playlist_t *p_playlist = pl_Get( p_intf );
150 intf_sys_t *p_sys = p_intf->p_sys;
152 var_DelCallback( p_playlist, "item-change", ItemChange, p_intf );
153 var_DelCallback( p_playlist, "input-current", ItemChange, p_intf );
155 [GrowlApplicationBridge setGrowlDelegate:nil];
156 [p_sys->o_growl_delegate release];
160 /*****************************************************************************
161 * ItemChange: Playlist item change callback
162 *****************************************************************************/
163 static int ItemChange( vlc_object_t *p_this, const char *psz_var,
164 vlc_value_t oldval, vlc_value_t newval, void *param )
168 intf_thread_t *p_intf = (intf_thread_t *)param;
169 char *psz_tmp = NULL;
170 char *psz_title = NULL;
171 char *psz_artist = NULL;
172 char *psz_album = NULL;
173 input_item_t *p_item = newval.p_address;
175 bool b_is_item_current = !strcmp( "input-current", psz_var );
177 /* Don't update each time an item has been preparsed */
178 if( b_is_item_current )
179 { /* stores the current input item id */
180 input_thread_t *p_input = newval.p_address;
184 p_item = input_GetItem( p_input );
185 if( p_intf->p_sys->i_id != p_item->i_id )
187 p_intf->p_sys->i_id = p_item->i_id;
188 p_intf->p_sys->i_item_changes = 0;
193 /* ignore items which weren't pre-parsed yet */
194 else if( !input_item_IsPreparsed(p_item) )
197 { /* "item-change" */
199 if( p_item->i_id != p_intf->p_sys->i_id )
202 /* Some variable bitrate inputs call "item-change" callbacks each time
203 * their length is updated, that is several times per second.
204 * We'll limit the number of changes to 1 per input. */
205 if( p_intf->p_sys->i_item_changes > 0 )
208 p_intf->p_sys->i_item_changes++;
211 /* Playing something ... */
212 if( input_item_GetNowPlayingFb( p_item ) )
213 psz_title = input_item_GetNowPlayingFb( p_item );
215 psz_title = input_item_GetTitleFbName( p_item );
216 if( EMPTY_STR( psz_title ) )
222 psz_artist = input_item_GetArtist( p_item );
223 if( EMPTY_STR( psz_artist ) ) FREENULL( psz_artist );
224 psz_album = input_item_GetAlbum( p_item ) ;
225 if( EMPTY_STR( psz_album ) ) FREENULL( psz_album );
228 if( psz_artist && psz_album )
229 i_ret = asprintf( &psz_tmp, "%s\n%s [%s]",
230 psz_title, psz_artist, psz_album );
231 else if( psz_artist )
232 i_ret = asprintf( &psz_tmp, "%s\n%s", psz_title, psz_artist );
234 i_ret = asprintf(&psz_tmp, "%s", psz_title );
244 char *psz_arturl = input_item_GetArtURL( p_item );
247 char *psz = make_path( psz_arturl );
252 [p_intf->p_sys->o_growl_delegate notifyWithTitle:psz_title
255 andArtUrl:psz_arturl];
266 /*****************************************************************************
268 *****************************************************************************/
269 @implementation VLCGrowlDelegate
271 - (id)initWithInterfaceThread:(intf_thread_t *)thread {
272 if( !( self = [super init] ) )
275 applicationName = nil;
276 notificationType = nil;
277 registrationDictionary = nil;
278 interfaceThread = thread;
280 // Assume we start in foreground
281 isInForeground = YES;
283 // Subscribe to notifications to determine if VLC is in foreground or not
285 [[NSNotificationCenter defaultCenter] addObserver:self
286 selector:@selector(applicationActiveChange:)
287 name:NSApplicationDidBecomeActiveNotification
290 [[NSNotificationCenter defaultCenter] addObserver:self
291 selector:@selector(applicationActiveChange:)
292 name:NSApplicationDidResignActiveNotification
300 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
301 // Clear the remaining lastNotification in Notification Center, if any
303 if (lastNotification) {
304 [NSUserNotificationCenter.defaultUserNotificationCenter
305 removeDeliveredNotification:(NSUserNotification *)lastNotification];
306 [lastNotification release];
308 [[NSNotificationCenter defaultCenter] removeObserver:self];
312 // Release everything
313 [applicationName release];
314 [notificationType release];
315 [registrationDictionary release];
319 - (void)registerToGrowl
322 applicationName = [[NSString alloc] initWithUTF8String:_( "VLC media player" )];
323 notificationType = [[NSString alloc] initWithUTF8String:_( "New input playing" )];
325 NSArray *defaultAndAllNotifications = [NSArray arrayWithObject: notificationType];
326 registrationDictionary = [[NSMutableDictionary alloc] init];
327 [registrationDictionary setObject:defaultAndAllNotifications
328 forKey:GROWL_NOTIFICATIONS_ALL];
329 [registrationDictionary setObject:defaultAndAllNotifications
330 forKey: GROWL_NOTIFICATIONS_DEFAULT];
332 [GrowlApplicationBridge setGrowlDelegate:self];
334 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
335 [[NSUserNotificationCenter defaultUserNotificationCenter]
336 setDelegate:(id<NSUserNotificationCenterDelegate>)self];
341 - (void)notifyWithTitle:(const char *)title
342 artist:(const char *)artist
343 album:(const char *)album
344 andArtUrl:(const char *)url
348 NSData *coverImageData = nil;
349 NSImage *coverImage = nil;
352 coverImageData = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:url]];
353 coverImage = [[NSImage alloc] initWithData:coverImageData];
357 NSString *titleStr = nil;
358 NSString *artistStr = nil;
359 NSString *albumStr = nil;
362 titleStr = [NSString stringWithUTF8String:title];
364 // Without title, notification makes no sense, so return here
365 // title should never be empty, but better check than crash.
369 artistStr = [NSString stringWithUTF8String:artist];
371 albumStr = [NSString stringWithUTF8String:album];
373 // Notification stuff
374 if ([GrowlApplicationBridge isGrowlRunning]) {
375 // Make the Growl notification string
376 NSString *desc = nil;
378 if (artistStr && albumStr) {
379 desc = [NSString stringWithFormat:@"%@\n%@ [%@]", titleStr, artistStr, albumStr];
380 } else if (artistStr) {
381 desc = [NSString stringWithFormat:@"%@\n%@", titleStr, artistStr];
387 [GrowlApplicationBridge notifyWithTitle:[NSString stringWithUTF8String:_("Now playing")]
389 notificationName:notificationType
390 iconData:coverImageData
395 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
396 // Make the OS X notification and string
397 NSUserNotification *notification = [NSUserNotification new];
398 NSString *desc = nil;
400 if (artistStr && albumStr) {
401 desc = [NSString stringWithFormat:@"%@ – %@", artistStr, albumStr];
402 } else if (artistStr) {
406 notification.title = titleStr;
407 notification.subtitle = desc;
408 notification.hasActionButton = YES;
409 notification.actionButtonTitle = [NSString stringWithUTF8String:_("Skip")];
411 // Private APIs to set cover image, see rdar://23148801
412 // and show action button, see rdar://23148733
413 [notification setValue:coverImage forKey:@"_identityImage"];
414 [notification setValue:@(YES) forKey:@"_showsButtons"];
415 [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification];
416 [notification release];
421 [coverImage release];
425 /*****************************************************************************
427 *****************************************************************************/
428 - (NSDictionary *)registrationDictionaryForGrowl
430 return registrationDictionary;
433 - (NSString *)applicationNameForGrowl
435 return applicationName;
438 - (void)applicationActiveChange:(NSNotification *)n {
439 if (n.name == NSApplicationDidBecomeActiveNotification)
440 isInForeground = YES;
441 else if (n.name == NSApplicationDidResignActiveNotification)
445 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
446 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
447 didActivateNotification:(NSUserNotification *)notification
449 if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
450 playlist_Next(pl_Get(interfaceThread));
454 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
455 didDeliverNotification:(NSUserNotification *)notification
457 // Only keep the most recent notification in the Notification Center
458 if (lastNotification) {
459 [center removeDeliveredNotification: (NSUserNotification *)lastNotification];
460 [lastNotification release];
462 [notification retain];
463 lastNotification = notification;
466 - (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center
467 shouldPresentNotification:(NSUserNotification *)notification
469 // Show notifications regardless if App in foreground or background