source: trunk/StreamVision/StreamVision.py @ 633

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

StreamVision?.py: Don't need to keep iterating after we've found the remote speakers button.

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