source: trunk/StreamVision/StreamVision.py @ 643

Last change on this file since 643 was 643, checked in by Nicholas Riley, 7 years ago

StreamVision?.py: iTunes 10.6 AirPlay? button description is of the form "AirPlay?[, speakers]".

File size: 13.1 KB
Line 
1#!/usr/bin/pythonw
2# -*- coding: utf-8 -*-
3
4from appscript import app, k, its, CommandError
5from AppKit import NSApplication, NSApplicationDefined, NSBeep, NSSystemDefined, NSURL, NSWorkspace
6from Foundation import NSDistributedNotificationCenter, NSSearchPathForDirectoriesInDomains, NSCachesDirectory, NSUserDomainMask
7from PyObjCTools import AppHelper
8from Carbon.CarbonEvt import RegisterEventHotKey, GetApplicationEventTarget
9from Carbon.Events import cmdKey, shiftKey, controlKey
10import httplib2
11import os
12import struct
13import scrape
14import HotKey
15
16GROWL_APP_NAME = 'StreamVision'
17NOTIFICATION_TRACK_INFO = 'iTunes Track Info'
18NOTIFICATIONS_ALL = [NOTIFICATION_TRACK_INFO]
19
20kEventHotKeyPressedSubtype = 6
21kEventHotKeyReleasedSubtype = 9
22
23kHIDUsage_Csmr_ScanNextTrack = 0xB5
24kHIDUsage_Csmr_ScanPreviousTrack = 0xB6
25kHIDUsage_Csmr_PlayOrPause = 0xCD
26
27def growlRegister():
28    global growl
29    growl = app(id='com.Growl.GrowlHelperApp')
30
31    growl.register(
32        as_application=GROWL_APP_NAME,
33        all_notifications=NOTIFICATIONS_ALL,
34        default_notifications=NOTIFICATIONS_ALL,
35        icon_of_application='iTunes.app')
36        # if we leave off the .app, we can get Classic iTunes's icon
37
38def growlNotify(title, description, **kw):
39    try:
40        growl.notify(
41            with_name=NOTIFICATION_TRACK_INFO,
42            title=title,
43            description=description,
44            application_name=GROWL_APP_NAME,
45            **kw)
46    except CommandError:
47        growlRegister()
48        growlNotify(title, description, **kw)
49
50def radioParadiseURL():
51    # XXX better to use http://www2.radioparadise.com/playlist.xml ?
52    session = scrape.Session()
53    session.go('http://www2.radioparadise.com/nowplay_b.php')
54    return session.region.firsttag('a')['href']
55
56def cleanStreamTitle(title):
57    if title == k.missing_value:
58        return ''
59    title = title.split(' [')[0] # XXX move to description
60    try: # incorrectly encoded?
61        title = title.encode('iso-8859-1').decode('utf-8')
62    except (UnicodeDecodeError, UnicodeEncodeError):
63        pass
64    title = title.replace('`', u'’')
65    return title
66
67def cleanStreamTrackName(name):
68    name = name.split('. ')[0]
69    name = name.split(': ')[0]
70    name = name.split(' - ')
71    if len(name) > 1:
72        name = ' - '.join(name[:-1])
73    else:
74        name = name[0]
75    return name
76
77def iTunesApp(): return app(id='com.apple.iTunes')
78def XTensionApp(): return app(creator='SHEx')
79def AmuaApp(): return app('Amua.app')
80
81HAVE_XTENSION = False
82try:
83    XTensionApp()
84    HAVE_XTENSION = True
85except:
86    pass
87
88HAVE_AMUA = False
89try:
90    AmuaApp()
91    HAVE_AMUA = True
92except:
93    pass
94
95needsStereoPowerOn = HAVE_XTENSION
96
97def mayUseStereo():
98    if not HAVE_XTENSION:
99        return False
100    systemEvents = app(id='com.apple.systemEvents')
101    iTunesWindow = systemEvents.application_processes[u'iTunes'].windows[u'iTunes']
102    try:
103        remote_speakers = iTunesWindow.buttons[its.attributes['AXDescription'].value.beginswith(u'AirPlay')].title()
104    except CommandError:
105        # with iTunes Store visible, the query fails with errAENoSuchObject
106        for button in iTunesWindow.buttons():
107            try:
108                if button.attributes['AXDescription'].value().endswith('AirPlay'):
109                    remote_speakers = [button.title()]
110                    break
111            except CommandError:
112                pass
113        else:
114            return False # XXX shouldn't get here
115    return remote_speakers and remote_speakers[0] not in (None, k.missing_value)
116
117def turnStereoOn():
118    global needsStereoPowerOn
119    if not mayUseStereo():
120        if HAVE_XTENSION and XTensionApp().status('Stereo'):
121            XTensionApp().turnoff('Stereo')
122        return
123    if not XTensionApp().status('Stereo'):
124        XTensionApp().turnon('Stereo')
125    needsStereoPowerOn = False
126
127def turnStereoOff():
128    global needsStereoPowerOn
129    if not mayUseStereo():
130        return
131    if not needsStereoPowerOn and XTensionApp().status('Stereo'):
132        XTensionApp().turnoff('Stereo')
133    needsStereoPowerOn = True
134
135def amuaPlaying():
136    if not HAVE_AMUA:
137        return False
138    return AmuaApp().is_playing()
139
140class OneFileCache(object):
141    __slots__ = ('key', 'cache')
142
143    def __init__(self, cache):
144        if not os.path.exists(cache):
145            os.makedirs(cache)
146        self.cache = os.path.join(cache, 'file')
147        self.key = None
148
149    def get(self, key):
150        if key == self.key:
151            return file(self.cache, 'r').read()
152
153    def set(self, key, value):
154        self.key = key
155        file(self.cache, 'w').write(value)
156
157    def delete(self, key):
158        if key == self.key:
159            self.key = None
160            os.remove(cache)
161
162class StreamVision(NSApplication):
163
164    hotKeyActions = {}
165    hotKeys = []
166
167    def displayTrackInfo(self, playerInfo=None):
168        iTunes = iTunesApp()
169
170        try:
171            trackClass = iTunes.current_track.class_()
172        except CommandError:
173            trackClass = k.property
174
175        trackName = ''
176        if trackClass != k.property:
177            trackName = iTunes.current_track.name()
178
179        if iTunes.player_state() != k.playing:
180            growlNotify('iTunes is not playing.', trackName)
181            turnStereoOff()
182            return
183        turnStereoOn()
184        if trackClass == k.URL_track:
185            if amuaPlaying():
186                if playerInfo is None: # Amua displays it itself
187                    AmuaApp().display_song_information()
188                return
189            url = iTunes.current_stream_URL()
190            kw = {}
191            if url != k.missing_value and url.endswith('.jpg'):
192                response, content = self.http.request(url)
193                if response['content-type'].startswith('image/'):
194                    file(self.imagePath, 'w').write(content)
195                    kw['image_from_location'] = self.imagePath
196            growlNotify(cleanStreamTitle(iTunes.current_stream_title()),
197                        cleanStreamTrackName(trackName), **kw)
198            return
199        if trackClass == k.property:
200            growlNotify('iTunes is playing.', '')
201            return
202        kw = {}
203        # XXX iTunes doesn't let you get artwork for shared tracks
204        if trackClass != k.shared_track:
205            artwork = iTunes.current_track.artworks()
206            if artwork:
207                try:
208                    kw['pictImage'] = artwork[0].data_()
209                except CommandError:
210                    pass
211        growlNotify(trackName + '  ' +
212                    u'★' * (iTunes.current_track.rating() / 20),
213                    iTunes.current_track.album() + '\n' +
214                    iTunes.current_track.artist(),
215                    **kw)
216
217    def goToSite(self):
218        iTunes = iTunesApp()
219        if iTunes.player_state() == k.playing:
220            if amuaPlaying():
221                AmuaApp().display_album_details()
222                return
223            url = iTunes.current_stream_URL()
224            if url != k.missing_value:
225                if 'radioparadise.com' in url and 'review' not in url:
226                    url = radioParadiseURL()
227                NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(url))
228                return
229        NSBeep()
230
231    def registerHotKey(self, func, keyCode, mods=0):
232        hotKeyRef = RegisterEventHotKey(keyCode, mods, (0, 0),
233                                        GetApplicationEventTarget(), 0)
234        self.hotKeys.append(hotKeyRef)
235        self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)] = func
236        return hotKeyRef
237
238    def unregisterHotKey(self, hotKeyRef):
239        self.hotKeys.remove(hotKeyRef)
240        del self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)]
241        hotKeyRef.UnregisterEventHotKey()
242
243    def incrementRatingBy(self, increment):
244        iTunes = iTunesApp()
245        if amuaPlaying():
246            if increment < 0:
247                AmuaApp().ban_song()
248                growlNotify('Banned song.', '', icon_of_application='Amua.app')
249            else:
250                AmuaApp().love_song()
251                growlNotify('Loved song.', '', icon_of_application='Amua.app')
252            return
253        rating = iTunes.current_track.rating()
254        rating += increment
255        if rating < 0:
256            rating = 0
257            NSBeep()
258        elif rating > 100:
259            rating = 100
260            NSBeep()
261        iTunes.current_track.rating.set(rating)
262
263    def playPause(self, useStereo=True):
264        global needsStereoPowerOn
265
266        iTunes = iTunesApp()
267        was_playing = (iTunes.player_state() == k.playing)
268        if not useStereo:
269            needsStereoPowerOn = False
270        if was_playing and amuaPlaying():
271            AmuaApp().stop()
272        else:
273            iTunes.playpause()
274        if not was_playing and iTunes.player_state() == k.stopped:
275            # most likely, we're focused on the iPod, so playing does nothing
276            iTunes.browser_windows[1].view.set(iTunes.user_playlists[its.name=='Stations'][1]())
277            iTunes.play()
278        if not useStereo:
279            return
280        if iTunes.player_state() == k.playing:
281            turnStereoOn()
282        else:
283            turnStereoOff()
284
285    def playPauseFront(self):
286        systemEvents = app(id='com.apple.systemEvents')
287        frontName = systemEvents.processes[its.frontmost == True][1].name()
288        if frontName == 'RealPlayer':
289            realPlayer = app(id='com.RealNetworks.RealPlayer')
290            if len(realPlayer.players()) > 0:
291                if realPlayer.players[1].state() == k.playing:
292                    realPlayer.pause()
293                else:
294                    realPlayer.play()
295                return
296        elif frontName == 'VLC':
297            app(id='org.videolan.vlc').play() # equivalent to playpause
298        else:
299            self.playPause(useStereo=False)
300
301    def nextTrack(self):
302        if amuaPlaying():
303            AmuaApp().skip_song()
304            return
305        iTunesApp().next_track()
306
307    def registerZoomWindowHotKey(self):
308        self.zoomWindowHotKey = self.registerHotKey(self.zoomWindow, 42, cmdKey | controlKey) # cmd-ctrl-\
309
310    def unregisterZoomWindowHotKey(self):
311        self.unregisterHotKey(self.zoomWindowHotKey)
312        self.zoomWindowHotKey = None
313
314    def zoomWindow(self):
315        # XXX detect if "enable access for assistive devices" needs to be enabled
316        systemEvents = app(id='com.apple.systemEvents')
317        frontName = systemEvents.processes[its.frontmost == True][1].name()
318        if frontName == 'iTunes':
319            systemEvents.processes['iTunes'].menu_bars[1]. \
320                menu_bar_items['Window'].menus.menu_items['Zoom'].click()
321            return
322        elif frontName in ('X11', 'XQuartz', 'Emacs'): # preserve C-M-\
323            self.unregisterZoomWindowHotKey()
324            systemEvents.key_code(42, using=[k.command_down, k.control_down])
325            self.registerZoomWindowHotKey()
326            return
327        frontPID = systemEvents.processes[its.frontmost == True][1].unix_id()
328        try:
329            zoomed = app(pid=frontPID).windows[1].zoomed
330            zoomed.set(not zoomed())
331        except (CommandError, RuntimeError):
332            systemEvents.processes[frontName].windows \
333                [its.subrole == 'AXStandardWindow'].windows[1]. \
334                buttons[its.subrole == 'AXZoomButton'].buttons[1].click()
335
336    def finishLaunching(self):
337        super(StreamVision, self).finishLaunching()
338
339        caches = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
340                                                     NSUserDomainMask, True)[0]
341        cache = os.path.join(caches, 'StreamVision')
342        self.http = httplib2.Http(OneFileCache(cache), 5)
343        self.imagePath = os.path.join(cache, 'image')
344
345        self.registerHotKey(self.displayTrackInfo, 100) # F8
346        self.registerHotKey(self.goToSite, 100, cmdKey) # cmd-F8
347        self.registerHotKey(self.playPause, 101) # F9
348        self.registerHotKey(lambda: iTunesApp().previous_track(), 109) # F10
349        self.registerHotKey(self.nextTrack, 103) # F11
350        self.registerHotKey(lambda: self.incrementRatingBy(-20), 109, shiftKey) # shift-F10
351        self.registerHotKey(lambda: self.incrementRatingBy(20), 103, shiftKey) # shift-F11
352        self.registerZoomWindowHotKey()
353        NSDistributedNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, self.displayTrackInfo, 'com.apple.iTunes.playerInfo', None)
354        try:
355            import HIDRemote
356            HIDRemote.connect()
357        except ImportError:
358            print "failed to import HIDRemote (XXX fix - on Intel)"
359        except OSError, e:
360            print "failed to connect to remote: ", e
361
362    def sendEvent_(self, theEvent):
363        eventType = theEvent.type()
364        if eventType == NSSystemDefined and \
365               theEvent.subtype() == kEventHotKeyPressedSubtype:
366            self.hotKeyActions[theEvent.data1()]()
367        elif eventType == NSApplicationDefined:
368            key = theEvent.data1()
369            if key == kHIDUsage_Csmr_ScanNextTrack:
370                self.nextTrack()
371            elif key == kHIDUsage_Csmr_ScanPreviousTrack:
372                iTunesApp().previous_track()
373            elif key == kHIDUsage_Csmr_PlayOrPause:
374                self.playPauseFront()
375        super(StreamVision, self).sendEvent_(theEvent)
376
377if __name__ == "__main__":
378    growlRegister()
379    AppHelper.runEventLoop()
380    try:
381        HIDRemote.disconnect()
382    except:
383        pass
Note: See TracBrowser for help on using the repository browser.