source: trunk/StreamVision/StreamVision.py @ 646

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

StreamVision?.py: When we can't see the AirPlay? button, assume AirPlay? is in use.

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