source: trunk/StreamVision/StreamVision.py @ 632

Last change on this file since 632 was 632, checked in by Nicholas Riley, 9 years ago

StreamVision?.py: With iTunes Store visible, some buttons can't have their AXDescription retrieved. Work around this.

File size: 13.0 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.endswith(u'remote speakers')].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('remote speakers'):
109                    remote_speakers = [button.title()]
110            except CommandError:
111                pass
112    return (remote_speakers and remote_speakers[0] != k.missing_value)
113
114def turnStereoOn():
115    global needsStereoPowerOn
116    if not mayUseStereo():
117        if HAVE_XTENSION and XTensionApp().status('Stereo'):
118            XTensionApp().turnoff('Stereo')
119        return
120    if not XTensionApp().status('Stereo'):
121        XTensionApp().turnon('Stereo')
122    needsStereoPowerOn = False
123
124def turnStereoOff():
125    global needsStereoPowerOn
126    if not mayUseStereo():
127        return
128    if not needsStereoPowerOn and XTensionApp().status('Stereo'):
129        XTensionApp().turnoff('Stereo')
130    needsStereoPowerOn = True
131
132def amuaPlaying():
133    if not HAVE_AMUA:
134        return False
135    return AmuaApp().is_playing()
136
137class OneFileCache(object):
138    __slots__ = ('key', 'cache')
139
140    def __init__(self, cache):
141        if not os.path.exists(cache):
142            os.makedirs(cache)
143        self.cache = os.path.join(cache, 'file')
144        self.key = None
145
146    def get(self, key):
147        if key == self.key:
148            return file(self.cache, 'r').read()
149
150    def set(self, key, value):
151        self.key = key
152        file(self.cache, 'w').write(value)
153
154    def delete(self, key):
155        if key == self.key:
156            self.key = None
157            os.remove(cache)
158
159class StreamVision(NSApplication):
160
161    hotKeyActions = {}
162    hotKeys = []
163
164    def displayTrackInfo(self, playerInfo=None):
165        iTunes = iTunesApp()
166
167        try:
168            trackClass = iTunes.current_track.class_()
169        except CommandError:
170            trackClass = k.property
171
172        trackName = ''
173        if trackClass != k.property:
174            trackName = iTunes.current_track.name()
175
176        if iTunes.player_state() != k.playing:
177            growlNotify('iTunes is not playing.', trackName)
178            turnStereoOff()
179            return
180        turnStereoOn()
181        if trackClass == k.URL_track:
182            if amuaPlaying():
183                if playerInfo is None: # Amua displays it itself
184                    AmuaApp().display_song_information()
185                return
186            url = iTunes.current_stream_URL()
187            kw = {}
188            if url != k.missing_value and url.endswith('.jpg'):
189                response, content = self.http.request(url)
190                if response['content-type'].startswith('image/'):
191                    file(self.imagePath, 'w').write(content)
192                    kw['image_from_location'] = self.imagePath
193            growlNotify(cleanStreamTitle(iTunes.current_stream_title()),
194                        cleanStreamTrackName(trackName), **kw)
195            return
196        if trackClass == k.property:
197            growlNotify('iTunes is playing.', '')
198            return
199        kw = {}
200        # XXX iTunes doesn't let you get artwork for shared tracks
201        if trackClass != k.shared_track:
202            artwork = iTunes.current_track.artworks()
203            if artwork:
204                try:
205                    kw['pictImage'] = artwork[0].data()
206                except CommandError:
207                    pass
208        growlNotify(trackName + '  ' +
209                    u'★' * (iTunes.current_track.rating() / 20),
210                    iTunes.current_track.album() + '\n' +
211                    iTunes.current_track.artist(),
212                    **kw)
213
214    def goToSite(self):
215        iTunes = iTunesApp()
216        if iTunes.player_state() == k.playing:
217            if amuaPlaying():
218                AmuaApp().display_album_details()
219                return
220            url = iTunes.current_stream_URL()
221            if url != k.missing_value:
222                if 'radioparadise.com' in url and 'review' not in url:
223                    url = radioParadiseURL()
224                NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(url))
225                return
226        NSBeep()
227
228    def registerHotKey(self, func, keyCode, mods=0):
229        hotKeyRef = RegisterEventHotKey(keyCode, mods, (0, 0),
230                                        GetApplicationEventTarget(), 0)
231        self.hotKeys.append(hotKeyRef)
232        self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)] = func
233        return hotKeyRef
234
235    def unregisterHotKey(self, hotKeyRef):
236        self.hotKeys.remove(hotKeyRef)
237        del self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)]
238        hotKeyRef.UnregisterEventHotKey()
239
240    def incrementRatingBy(self, increment):
241        iTunes = iTunesApp()
242        if amuaPlaying():
243            if increment < 0:
244                AmuaApp().ban_song()
245                growlNotify('Banned song.', '', icon_of_application='Amua.app')
246            else:
247                AmuaApp().love_song()
248                growlNotify('Loved song.', '', icon_of_application='Amua.app')
249            return
250        rating = iTunes.current_track.rating()
251        rating += increment
252        if rating < 0:
253            rating = 0
254            NSBeep()
255        elif rating > 100:
256            rating = 100
257            NSBeep()
258        iTunes.current_track.rating.set(rating)
259
260    def playPause(self, useStereo=True):
261        global needsStereoPowerOn
262
263        iTunes = iTunesApp()
264        was_playing = (iTunes.player_state() == k.playing)
265        if not useStereo:
266            needsStereoPowerOn = False
267        if was_playing and amuaPlaying():
268            AmuaApp().stop()
269        else:
270            iTunes.playpause()
271        if not was_playing and iTunes.player_state() == k.stopped:
272            # most likely, we're focused on the iPod, so playing does nothing
273            iTunes.browser_windows[1].view.set(iTunes.user_playlists[its.name=='Stations'][1]())
274            iTunes.play()
275        if not useStereo:
276            return
277        if iTunes.player_state() == k.playing:
278            turnStereoOn()
279        else:
280            turnStereoOff()
281
282    def playPauseFront(self):
283        systemEvents = app(id='com.apple.systemEvents')
284        frontName = systemEvents.processes[its.frontmost == True][1].name()
285        if frontName == 'RealPlayer':
286            realPlayer = app(id='com.RealNetworks.RealPlayer')
287            if len(realPlayer.players()) > 0:
288                if realPlayer.players[1].state() == k.playing:
289                    realPlayer.pause()
290                else:
291                    realPlayer.play()
292                return
293        elif frontName == 'VLC':
294            app(id='org.videolan.vlc').play() # equivalent to playpause
295        else:
296            self.playPause(useStereo=False)
297
298    def nextTrack(self):
299        if amuaPlaying():
300            AmuaApp().skip_song()
301            return
302        iTunesApp().next_track()
303
304    def registerZoomWindowHotKey(self):
305        self.zoomWindowHotKey = self.registerHotKey(self.zoomWindow, 42, cmdKey | controlKey) # cmd-ctrl-\
306
307    def unregisterZoomWindowHotKey(self):
308        self.unregisterHotKey(self.zoomWindowHotKey)
309        self.zoomWindowHotKey = None
310
311    def zoomWindow(self):
312        # XXX detect if "enable access for assistive devices" needs to be enabled
313        systemEvents = app(id='com.apple.systemEvents')
314        frontName = systemEvents.processes[its.frontmost == True][1].name()
315        if frontName == 'iTunes':
316            systemEvents.processes['iTunes'].menu_bars[1]. \
317                menu_bar_items['Window'].menus.menu_items['Zoom'].click()
318            return
319        elif frontName in ('X11', 'XQuartz', 'Emacs'): # preserve C-M-\
320            self.unregisterZoomWindowHotKey()
321            systemEvents.key_code(42, using=[k.command_down, k.control_down])
322            self.registerZoomWindowHotKey()
323            return
324        frontPID = systemEvents.processes[its.frontmost == True][1].unix_id()
325        try:
326            zoomed = app(pid=frontPID).windows[1].zoomed
327            zoomed.set(not zoomed())
328        except (CommandError, RuntimeError):
329            systemEvents.processes[frontName].windows \
330                [its.subrole == 'AXStandardWindow'].windows[1]. \
331                buttons[its.subrole == 'AXZoomButton'].buttons[1].click()
332
333    def finishLaunching(self):
334        super(StreamVision, self).finishLaunching()
335
336        caches = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
337                                                     NSUserDomainMask, True)[0]
338        cache = os.path.join(caches, 'StreamVision')
339        self.http = httplib2.Http(OneFileCache(cache), 5)
340        self.imagePath = os.path.join(cache, 'image')
341
342        self.registerHotKey(self.displayTrackInfo, 100) # F8
343        self.registerHotKey(self.goToSite, 100, cmdKey) # cmd-F8
344        self.registerHotKey(self.playPause, 101) # F9
345        self.registerHotKey(lambda: iTunesApp().previous_track(), 109) # F10
346        self.registerHotKey(self.nextTrack, 103) # F11
347        self.registerHotKey(lambda: self.incrementRatingBy(-20), 109, shiftKey) # shift-F10
348        self.registerHotKey(lambda: self.incrementRatingBy(20), 103, shiftKey) # shift-F11
349        self.registerZoomWindowHotKey()
350        NSDistributedNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, self.displayTrackInfo, 'com.apple.iTunes.playerInfo', None)
351        try:
352            import HIDRemote
353            HIDRemote.connect()
354        except ImportError:
355            print "failed to import HIDRemote (XXX fix - on Intel)"
356        except OSError, e:
357            print "failed to connect to remote: ", e
358
359    def sendEvent_(self, theEvent):
360        eventType = theEvent.type()
361        if eventType == NSSystemDefined and \
362               theEvent.subtype() == kEventHotKeyPressedSubtype:
363            self.hotKeyActions[theEvent.data1()]()
364        elif eventType == NSApplicationDefined:
365            key = theEvent.data1()
366            if key == kHIDUsage_Csmr_ScanNextTrack:
367                self.nextTrack()
368            elif key == kHIDUsage_Csmr_ScanPreviousTrack:
369                iTunesApp().previous_track()
370            elif key == kHIDUsage_Csmr_PlayOrPause:
371                self.playPauseFront()
372        super(StreamVision, self).sendEvent_(theEvent)
373
374if __name__ == "__main__":
375    growlRegister()
376    AppHelper.runEventLoop()
377    try:
378        HIDRemote.disconnect()
379    except:
380        pass
Note: See TracBrowser for help on using the repository browser.