1 /*****************************************************************************
2 * osx_notifications.m : OS X 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.
29 * Growl specific code, ripped from growlnotify:
31 * Copyright (c) The Growl Project, 2004-2005
32 * All rights reserved.
34 * Redistribution and use in source and binary forms, with or without modification,
35 * are permitted provided that the following conditions are met:
37 * 1. Redistributions of source code must retain the above copyright
38 * notice, this list of conditions and the following disclaimer.
39 * 2. Redistributions in binary form must reproduce the above copyright
40 * notice, this list of conditions and the following disclaimer in the
41 * documentation and/or other materials provided with the distribution.
42 * 3. Neither the name of Growl nor the names of its contributors
43 * may be used to endorse or promote products derived from this software
44 * without specific prior written permission.
46 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
47 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
48 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
49 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
50 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
51 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
52 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
53 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
54 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
55 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
57 *****************************************************************************/
59 /*****************************************************************************
61 *****************************************************************************/
67 #import <Foundation/Foundation.h>
68 #import <Cocoa/Cocoa.h>
69 #import <Growl/Growl.h>
71 #define VLC_MODULE_LICENSE VLC_LICENSE_GPL_2_PLUS
72 #include <vlc_common.h>
73 #include <vlc_plugin.h>
74 #include <vlc_playlist.h>
75 #include <vlc_input.h>
77 #include <vlc_interface.h>
80 /*****************************************************************************
81 * intf_sys_t, VLCGrowlDelegate
82 *****************************************************************************/
83 @interface VLCGrowlDelegate : NSObject <GrowlApplicationBridgeDelegate>
85 NSString *applicationName;
86 NSString *notificationType;
87 NSMutableDictionary *registrationDictionary;
89 intf_thread_t *interfaceThread;
92 - (id)initWithInterfaceThread:(intf_thread_t *)thread;
93 - (void)registerToGrowl;
94 - (void)notifyWithTitle:(const char *)title
95 artist:(const char *)artist
96 album:(const char *)album
97 andArtUrl:(const char *)url;
102 VLCGrowlDelegate *o_growl_delegate;
107 /*****************************************************************************
109 *****************************************************************************/
110 static int Open ( vlc_object_t * );
111 static void Close ( vlc_object_t * );
113 static int ItemChange( vlc_object_t *, const char *,
114 vlc_value_t, vlc_value_t, void * );
116 /*****************************************************************************
118 ****************************************************************************/
121 set_category( CAT_INTERFACE )
122 set_subcategory( SUBCAT_INTERFACE_CONTROL )
123 set_shortname( "OSX-Notifications" )
124 add_shortcut( "growl" )
125 set_description( N_("OS X Notification Plugin") )
126 set_capability( "interface", 0 )
127 set_callbacks( Open, Close )
130 /*****************************************************************************
131 * Open: initialize and create stuff
132 *****************************************************************************/
133 static int Open( vlc_object_t *p_this )
135 intf_thread_t *p_intf = (intf_thread_t *)p_this;
136 playlist_t *p_playlist;
139 p_sys = p_intf->p_sys = calloc( 1, sizeof(intf_sys_t) );
143 p_sys->o_growl_delegate = [[VLCGrowlDelegate alloc] initWithInterfaceThread:p_intf];
144 if( !p_sys->o_growl_delegate )
147 p_playlist = pl_Get( p_intf );
148 var_AddCallback( p_playlist, "item-change", ItemChange, p_intf );
149 var_AddCallback( p_playlist, "input-current", ItemChange, p_intf );
151 [p_sys->o_growl_delegate registerToGrowl];
155 /*****************************************************************************
156 * Close: destroy interface stuff
157 *****************************************************************************/
158 static void Close( vlc_object_t *p_this )
160 intf_thread_t *p_intf = (intf_thread_t *)p_this;
161 playlist_t *p_playlist = pl_Get( p_intf );
162 intf_sys_t *p_sys = p_intf->p_sys;
164 var_DelCallback( p_playlist, "item-change", ItemChange, p_intf );
165 var_DelCallback( p_playlist, "input-current", ItemChange, p_intf );
167 [GrowlApplicationBridge setGrowlDelegate:nil];
168 [p_sys->o_growl_delegate release];
172 /*****************************************************************************
173 * ItemChange: Playlist item change callback
174 *****************************************************************************/
175 static int ItemChange( vlc_object_t *p_this, const char *psz_var,
176 vlc_value_t oldval, vlc_value_t newval, void *param )
180 intf_thread_t *p_intf = (intf_thread_t *)param;
181 char *psz_tmp = NULL;
182 char *psz_title = NULL;
183 char *psz_artist = NULL;
184 char *psz_album = NULL;
185 input_item_t *p_item = newval.p_address;
187 bool b_is_item_current = !strcmp( "input-current", psz_var );
189 /* Don't update each time an item has been preparsed */
190 if( b_is_item_current )
191 { /* stores the current input item id */
192 input_thread_t *p_input = newval.p_address;
196 p_item = input_GetItem( p_input );
197 if( p_intf->p_sys->i_id != p_item->i_id )
199 p_intf->p_sys->i_id = p_item->i_id;
200 p_intf->p_sys->i_item_changes = 0;
205 /* ignore items which weren't pre-parsed yet */
206 else if( !input_item_IsPreparsed(p_item) )
209 { /* "item-change" */
210 if( p_item->i_id != p_intf->p_sys->i_id )
213 /* Some variable bitrate inputs call "item-change" callbacks each time
214 * their length is updated, that is several times per second.
215 * We'll limit the number of changes to 1 per input. */
216 if( p_intf->p_sys->i_item_changes > 0 )
219 p_intf->p_sys->i_item_changes++;
222 /* Playing something ... */
223 if( input_item_GetNowPlayingFb( p_item ) )
224 psz_title = input_item_GetNowPlayingFb( p_item );
226 psz_title = input_item_GetTitleFbName( p_item );
227 if( EMPTY_STR( psz_title ) )
233 psz_artist = input_item_GetArtist( p_item );
234 if( EMPTY_STR( psz_artist ) ) FREENULL( psz_artist );
235 psz_album = input_item_GetAlbum( p_item ) ;
236 if( EMPTY_STR( psz_album ) ) FREENULL( psz_album );
239 if( psz_artist && psz_album )
240 i_ret = asprintf( &psz_tmp, "%s\n%s [%s]",
241 psz_title, psz_artist, psz_album );
242 else if( psz_artist )
243 i_ret = asprintf( &psz_tmp, "%s\n%s", psz_title, psz_artist );
245 i_ret = asprintf(&psz_tmp, "%s", psz_title );
255 char *psz_arturl = input_item_GetArtURL( p_item );
258 char *psz = make_path( psz_arturl );
263 [p_intf->p_sys->o_growl_delegate notifyWithTitle:psz_title
266 andArtUrl:psz_arturl];
277 /*****************************************************************************
279 *****************************************************************************/
280 @implementation VLCGrowlDelegate
282 - (id)initWithInterfaceThread:(intf_thread_t *)thread {
283 if( !( self = [super init] ) )
286 applicationName = nil;
287 notificationType = nil;
288 registrationDictionary = nil;
289 interfaceThread = thread;
296 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
297 // Clear the remaining lastNotification in Notification Center, if any
299 if (lastNotification) {
300 [NSUserNotificationCenter.defaultUserNotificationCenter
301 removeDeliveredNotification:(NSUserNotification *)lastNotification];
302 [lastNotification release];
307 // Release everything
308 [applicationName release];
309 [notificationType release];
310 [registrationDictionary release];
314 - (void)registerToGrowl
317 applicationName = [[NSString alloc] initWithUTF8String:_( "VLC media player" )];
318 notificationType = [[NSString alloc] initWithUTF8String:_( "New input playing" )];
320 NSArray *defaultAndAllNotifications = [NSArray arrayWithObject: notificationType];
321 registrationDictionary = [[NSMutableDictionary alloc] init];
322 [registrationDictionary setObject:defaultAndAllNotifications
323 forKey:GROWL_NOTIFICATIONS_ALL];
324 [registrationDictionary setObject:defaultAndAllNotifications
325 forKey: GROWL_NOTIFICATIONS_DEFAULT];
327 [GrowlApplicationBridge setGrowlDelegate:self];
329 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
330 [[NSUserNotificationCenter defaultUserNotificationCenter]
331 setDelegate:(id<NSUserNotificationCenterDelegate>)self];
336 - (void)notifyWithTitle:(const char *)title
337 artist:(const char *)artist
338 album:(const char *)album
339 andArtUrl:(const char *)url
342 // Do not notify if in foreground
343 if ([NSApplication sharedApplication].active)
347 NSData *coverImageData = nil;
348 NSImage *coverImage = nil;
351 coverImageData = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:url]];
352 coverImage = [[NSImage alloc] initWithData:coverImageData];
356 NSString *titleStr = nil;
357 NSString *artistStr = nil;
358 NSString *albumStr = nil;
361 titleStr = [NSString stringWithUTF8String:title];
363 // Without title, notification makes no sense, so return here
364 // title should never be empty, but better check than crash.
365 [coverImage release];
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
394 identifier:@"VLCNowPlayingNotification"];
396 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
397 // Make the OS X notification and string
398 NSUserNotification *notification = [NSUserNotification new];
399 NSString *desc = nil;
401 if (artistStr && albumStr) {
402 desc = [NSString stringWithFormat:@"%@ – %@", artistStr, albumStr];
403 } else if (artistStr) {
407 notification.title = titleStr;
408 notification.subtitle = desc;
409 notification.hasActionButton = YES;
410 notification.actionButtonTitle = [NSString stringWithUTF8String:_("Skip")];
412 // Private APIs to set cover image, see rdar://23148801
413 // and show action button, see rdar://23148733
414 [notification setValue:coverImage forKey:@"_identityImage"];
415 [notification setValue:@(YES) forKey:@"_showsButtons"];
416 [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification];
417 [notification release];
422 [coverImage release];
426 /*****************************************************************************
428 *****************************************************************************/
429 - (NSDictionary *)registrationDictionaryForGrowl
431 return registrationDictionary;
434 - (NSString *)applicationNameForGrowl
436 return applicationName;
439 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
440 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
441 didActivateNotification:(NSUserNotification *)notification
444 if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
445 playlist_Next(pl_Get(interfaceThread));
449 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
450 didDeliverNotification:(NSUserNotification *)notification
452 // Only keep the most recent notification in the Notification Center
453 if (lastNotification) {
454 [center removeDeliveredNotification: (NSUserNotification *)lastNotification];
455 [lastNotification release];
457 [notification retain];
458 lastNotification = notification;