source: trunk/StreamVision/StreamVision.py @ 645

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

StreamVision?.py: Update for Radio Paradise 2 site design.

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