source: trunk/StreamVision/StreamVision.py@ 415

Last change on this file since 415 was 415, checked in by Nicholas Riley, 15 years ago

StreamVision: actually bother to test this time

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.', '')
181 else:
182 AmuaApp().love_song()
183 growlNotify('Loved song.', '')
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.