source: trunk/StreamVision/StreamVision.py @ 458

Last change on this file since 458 was 458, checked in by Nicholas Riley, 12 years ago

Add Amua icon to banned/loved.

File size: 10.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
7from PyObjCTools import AppHelper
8from Carbon.CarbonEvt import RegisterEventHotKey, GetApplicationEventTarget
9from Carbon.Events import cmdKey, shiftKey, controlKey
10import struct
11import scrape
12import HotKey
13
14GROWL_APP_NAME = 'StreamVision'
15NOTIFICATION_TRACK_INFO = 'iTunes Track Info'
16NOTIFICATIONS_ALL = [NOTIFICATION_TRACK_INFO]
17
18kEventHotKeyPressedSubtype = 6
19kEventHotKeyReleasedSubtype = 9
20
21kHIDUsage_Csmr_ScanNextTrack = 0xB5
22kHIDUsage_Csmr_ScanPreviousTrack = 0xB6
23kHIDUsage_Csmr_PlayOrPause = 0xCD
24
25def growlRegister():
26    global growl
27    growl = app(id='com.Growl.GrowlHelperApp')
28
29    growl.register(
30        as_application=GROWL_APP_NAME,
31        all_notifications=NOTIFICATIONS_ALL,
32        default_notifications=NOTIFICATIONS_ALL,
33        icon_of_application='iTunes.app')
34        # if we leave off the .app, we can get Classic iTunes's icon
35
36def growlNotify(title, description, **kw):
37    try:
38        growl.notify(
39            with_name=NOTIFICATION_TRACK_INFO,
40            title=title,
41            description=description,
42            application_name=GROWL_APP_NAME,
43            **kw)
44    except CommandError:
45        growlRegister()
46        growlNotify(title, description, **kw)
47
48def radioParadiseURL():
49    # XXX better to use http://www2.radioparadise.com/playlist.xml ?
50    session = scrape.Session()
51    session.go('http://www2.radioparadise.com/nowplay_b.php')
52    return session.region.firsttag('a')['href']
53
54def cleanStreamTitle(title):
55    if title == k.missing_value:
56        return ''
57    title = title.split(' [')[0] # XXX move to description
58    try: # incorrectly encoded?
59        title = title.encode('iso-8859-1').decode('utf-8')
60    except UnicodeDecodeError:
61        pass
62    title = title.replace('`', u'’')
63    return title
64
65def cleanStreamTrackName(name):
66    name = name.split('. ')[0]
67    name = name.split(': ')[0]
68    name = name.split(' - ')
69    if len(name) > 1:
70        name = ' - '.join(name[:-1])
71    else:
72        name = name[0]
73    return name
74
75def iTunesApp(): return app(id='com.apple.iTunes')
76def XTensionApp(): return app(creator='SHEx')
77def AmuaApp(): return app('Amua.app')
78
79HAVE_XTENSION = False
80try:
81    XTensionApp()
82    HAVE_XTENSION = True
83except:
84    pass
85
86HAVE_AMUA = False
87try:
88    AmuaApp()
89    HAVE_AMUA = True
90except:
91    pass
92
93needsStereoPowerOn = HAVE_XTENSION
94
95def amuaPlaying():
96    if not HAVE_AMUA:
97        return False
98    return AmuaApp().is_playing()
99
100class StreamVision(NSApplication):
101
102    hotKeyActions = {}
103    hotKeys = []
104
105    def displayTrackInfo(self, playerInfo=None):
106        global needsStereoPowerOn
107
108        iTunes = iTunesApp()
109
110        trackClass = iTunes.current_track.class_()
111        trackName = ''
112        if trackClass != k.property:
113            trackName = iTunes.current_track.name()
114
115        if iTunes.player_state() != k.playing:
116            growlNotify('iTunes is not playing.', trackName)
117            if HAVE_XTENSION:
118                if not needsStereoPowerOn and XTensionApp().status('Stereo'):
119                    XTensionApp().turnoff('Stereo')
120                needsStereoPowerOn = True
121            return
122        if needsStereoPowerOn:
123            if not XTensionApp().status('Stereo'):
124                XTensionApp().turnon('Stereo')
125            needsStereoPowerOn = False
126        if trackClass == k.URL_track:
127            if amuaPlaying():
128                if playerInfo is None: # Amua displays it itself
129                    AmuaApp().display_song_information()
130                return
131            growlNotify(cleanStreamTitle(iTunes.current_stream_title()),
132                        cleanStreamTrackName(trackName))
133            return
134        if trackClass == k.property:
135            growlNotify('iTunes is playing.', '')
136            return
137        kw = {}
138        # XXX iTunes doesn't let you get artwork for shared tracks
139        if trackClass != k.shared_track:
140            artwork = iTunes.current_track.artworks()
141            if artwork:
142                kw['pictImage'] = artwork[0].data()
143        growlNotify(trackName + '  ' +
144                    u'★' * (iTunes.current_track.rating() / 20),
145                    iTunes.current_track.album() + '\n' +
146                    iTunes.current_track.artist(),
147                    **kw)
148
149    def goToSite(self):
150        iTunes = iTunesApp()
151        if iTunes.player_state() == k.playing:
152            if amuaPlaying():
153                AmuaApp().display_album_details()
154                return
155            url = iTunes.current_stream_URL()
156            if url != k.missing_value:
157                if 'radioparadise.com' in url and 'review' not in url:
158                    url = radioParadiseURL()
159                NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(url))
160                return
161        NSBeep()
162
163    def registerHotKey(self, func, keyCode, mods=0):
164        hotKeyRef = RegisterEventHotKey(keyCode, mods, (0, 0),
165                                        GetApplicationEventTarget(), 0)
166        self.hotKeys.append(hotKeyRef)
167        self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)] = func
168        return hotKeyRef
169
170    def unregisterHotKey(self, hotKeyRef):
171        self.hotKeys.remove(hotKeyRef)
172        del self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)]
173        hotKeyRef.UnregisterEventHotKey()
174
175    def incrementRatingBy(self, increment):
176        iTunes = iTunesApp()
177        if amuaPlaying():
178            if increment < 0:
179                AmuaApp().ban_song()
180                growlNotify('Banned song.', '', icon_of_application='Amua.app')
181            else:
182                AmuaApp().love_song()
183                growlNotify('Loved song.', '', icon_of_application='Amua.app')
184            return
185        rating = iTunes.current_track.rating()
186        rating += increment
187        if rating < 0:
188            rating = 0
189            NSBeep()
190        elif rating > 100:
191            rating = 100
192            NSBeep()
193        iTunes.current_track.rating.set(rating)
194
195    def playPause(self, useStereo=True):
196        global needsStereoPowerOn
197
198        iTunes = iTunesApp()
199        was_playing = (iTunes.player_state() == k.playing)
200        if not useStereo:
201            needsStereoPowerOn = False
202        if was_playing and amuaPlaying():
203            AmuaApp().stop()
204        else:
205            iTunes.playpause()
206        if not was_playing and iTunes.player_state() == k.stopped:
207            # most likely, we're focused on the iPod, so playing does nothing
208            iTunes.browser_windows[1].view.set(iTunes.user_playlists[its.name=='Stations'][1]())
209            iTunes.play()
210        if HAVE_XTENSION and useStereo:
211            if iTunes.player_state() == k.playing:
212                XTensionApp().turnon('Stereo')
213                needsStereoPowerOn = False
214            else:
215                XTensionApp().turnoff('Stereo')
216                needsStereoPowerOn = True
217
218    def playPauseFront(self):
219        systemEvents = app(id='com.apple.systemEvents')
220        frontName = systemEvents.processes[its.frontmost == True][1].name()
221        if frontName == 'RealPlayer':
222            realPlayer = app(id='com.RealNetworks.RealPlayer')
223            if len(realPlayer.players()) > 0:
224                if realPlayer.players[1].state() == k.playing:
225                    realPlayer.pause()
226                else:
227                    realPlayer.play()
228                return
229        elif frontName == 'VLC':
230            app(id='org.videolan.vlc').play() # equivalent to playpause
231        else:
232            self.playPause(useStereo=False)
233
234    def nextTrack(self):
235        if amuaPlaying():
236            AmuaApp().skip_song()
237            return
238        iTunesApp().next_track()
239
240    def registerZoomWindowHotKey(self):
241        self.zoomWindowHotKey = self.registerHotKey(self.zoomWindow, 42, cmdKey | controlKey) # cmd-ctrl-\
242
243    def unregisterZoomWindowHotKey(self):
244        self.unregisterHotKey(self.zoomWindowHotKey)
245        self.zoomWindowHotKey = None
246
247    def zoomWindow(self):
248        # XXX detect if "enable access for assistive devices" needs to be enabled
249        systemEvents = app(id='com.apple.systemEvents')
250        frontName = systemEvents.processes[its.frontmost == True][1].name()
251        if frontName == 'iTunes':
252            systemEvents.processes['iTunes'].menu_bars[1]. \
253                menu_bar_items['Window'].menus.menu_items['Zoom'].click()
254            return
255        elif frontName in ('X11', 'Emacs'): # preserve C-M-\
256            self.unregisterZoomWindowHotKey()
257            systemEvents.key_code(42, using=[k.command_down, k.control_down])
258            self.registerZoomWindowHotKey()
259            return
260        frontPID = systemEvents.processes[its.frontmost == True][1].unix_id()
261        try:
262            zoomed = app(pid=frontPID).windows[1].zoomed
263            zoomed.set(not zoomed())
264        except (CommandError, RuntimeError):
265            systemEvents.processes[frontName].windows \
266                [its.subrole == 'AXStandardWindow'].windows[1]. \
267                buttons[its.subrole == 'AXZoomButton'].buttons[1].click()
268
269    def finishLaunching(self):
270        super(StreamVision, self).finishLaunching()
271        self.registerHotKey(self.displayTrackInfo, 100) # F8
272        self.registerHotKey(self.goToSite, 100, cmdKey) # cmd-F8
273        self.registerHotKey(self.playPause, 101) # F9
274        self.registerHotKey(lambda: iTunesApp().previous_track(), 109) # F10
275        self.registerHotKey(self.nextTrack, 103) # F11
276        self.registerHotKey(lambda: self.incrementRatingBy(-20), 109, shiftKey) # shift-F10
277        self.registerHotKey(lambda: self.incrementRatingBy(20), 103, shiftKey) # shift-F11
278        self.registerZoomWindowHotKey()
279        NSDistributedNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, self.displayTrackInfo, 'com.apple.iTunes.playerInfo', None)
280        try:
281            import HIDRemote
282            HIDRemote.connect()
283        except ImportError:
284            print "failed to import HIDRemote (XXX fix - on Intel)"
285        except OSError, e:
286            print "failed to connect to remote: ", e
287
288    def sendEvent_(self, theEvent):
289        eventType = theEvent.type()
290        if eventType == NSSystemDefined and \
291               theEvent.subtype() == kEventHotKeyPressedSubtype:
292            self.hotKeyActions[theEvent.data1()]()
293        elif eventType == NSApplicationDefined:
294            key = theEvent.data1()
295            if key == kHIDUsage_Csmr_ScanNextTrack:
296                self.nextTrack()
297            elif key == kHIDUsage_Csmr_ScanPreviousTrack:
298                iTunesApp().previous_track()
299            elif key == kHIDUsage_Csmr_PlayOrPause:
300                self.playPauseFront()
301        super(StreamVision, self).sendEvent_(theEvent)
302
303if __name__ == "__main__":
304    growlRegister()
305    AppHelper.runEventLoop()
306    try:
307        HIDRemote.disconnect()
308    except:
309        pass
Note: See TracBrowser for help on using the repository browser.