source: trunk/StreamVision/StreamVision.py @ 487

Last change on this file since 487 was 487, checked in by Nicholas Riley, 12 years ago

StreamVision?.py: Trap annoying "CommandError? -108: Out of memory." messages when retrieving album artwork in iTunes 8.

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