source: trunk/StreamVision/StreamVision.py @ 497

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

StreamVision?.py: When playing nothing, iTunes 8.1 has no reference to the current track - actually an improvement.

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