source: trunk/StreamVision/StreamVision.py@ 494

Last change on this file since 494 was 487, checked in by Nicholas Riley, 16 years ago

StreamVision.py: Trap annoying "CommandError -108: Out of memory." messages when retrieving album artwork in iTunes 8.

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