source: trunk/StreamVision/StreamVision.py @ 649

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

tunes.py: Static terminology for iTunes to work around 'gdte' bug in
iTunes 10.6.3, from <https://github.com/abarnert/itunesterms>.

StreamVision?.py: Use above static terminology.

More information at <http://www.leancrew.com/all-this/2012/06/the-first-nail-in-the-coffin-of-python-appscript/>.

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