source: trunk/StreamVision/StreamVision.py @ 339

Last change on this file since 339 was 339, checked in by Nicholas Riley, 13 years ago

StreamVision?.py: fix Finder zooming issue by using PID; document
problem with a-umlaut; handle HIDRemote missing on exit.

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