source: trunk/StreamVision/StreamVision.py @ 580

Last change on this file since 580 was 580, checked in by Nicholas Riley, 10 years ago

StreamVision?.py: Only turn on/off stereo if iTunes is transmitting to
AirPort? Express.

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