source: trunk/StreamVision/StreamVision.py@ 655

Last change on this file since 655 was 655, checked in by Nicholas Riley, 11 years ago

StreamVision.py: Fix a logic error.

File size: 13.6 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
10from AudioDevice import default_output_device_is_airplay, set_default_output_device_changed_callback
11import httplib2
12import os
13import struct
14import scrape
15import HotKey
16import tunes # <https://github.com/abarnert/itunesterms>
17
18GROWL_APP_NAME = 'StreamVision'
19NOTIFICATION_TRACK_INFO = 'iTunes Track Info'
20NOTIFICATIONS_ALL = [NOTIFICATION_TRACK_INFO]
21
22kEventHotKeyPressedSubtype = 6
23kEventHotKeyReleasedSubtype = 9
24
25kHIDUsage_Csmr_ScanNextTrack = 0xB5
26kHIDUsage_Csmr_ScanPreviousTrack = 0xB6
27kHIDUsage_Csmr_PlayOrPause = 0xCD
28
29def growlRegister():
30 global growl
31 growl = app(id='com.Growl.GrowlHelperApp')
32
33 growl.register(
34 as_application=GROWL_APP_NAME,
35 all_notifications=NOTIFICATIONS_ALL,
36 default_notifications=NOTIFICATIONS_ALL,
37 icon_of_application='iTunes.app')
38 # if we leave off the .app, we can get Classic iTunes's icon
39
40def growlNotify(title, description, **kw):
41 try:
42 growl.notify(
43 with_name=NOTIFICATION_TRACK_INFO,
44 title=title,
45 description=description,
46 application_name=GROWL_APP_NAME,
47 **kw)
48 except CommandError:
49 growlRegister()
50 growlNotify(title, description, **kw)
51
52def radioParadiseURL():
53 session = scrape.Session()
54 session.go('http://radioparadise.com/jq_playlist.php')
55 url = session.region.firsttag('a')['href']
56 if not url.startswith('http'):
57 url = 'http://www.radioparadise.com/rp2-' + url
58 return url
59
60def cleanStreamTitle(title):
61 if title == k.missing_value:
62 return ''
63 title = title.split(' [')[0] # XXX move to description
64 try: # incorrectly encoded?
65 title = title.encode('iso-8859-1').decode('utf-8')
66 except (UnicodeDecodeError, UnicodeEncodeError):
67 pass
68 title = title.replace('`', u'’')
69 return title
70
71def cleanStreamTrackName(name):
72 name = name.split('. ')[0]
73 name = name.split(': ')[0]
74 name = name.split(' - ')
75 if len(name) > 1:
76 name = ' - '.join(name[:-1])
77 else:
78 name = name[0]
79 return name
80
81def iTunesApp(): return app(id='com.apple.iTunes', terms=tunes)
82def XTensionApp(): return app(creator='SHEx')
83def AmuaApp(): return app('Amua.app')
84
85HAVE_XTENSION = False
86try:
87 XTensionApp()
88 HAVE_XTENSION = True
89except:
90 pass
91
92HAVE_AMUA = False
93try:
94 AmuaApp()
95 HAVE_AMUA = True
96except:
97 pass
98
99needsStereoPowerOn = HAVE_XTENSION
100
101def mayUseStereo():
102 if not HAVE_XTENSION:
103 return False
104 systemEvents = app(id='com.apple.systemEvents')
105 iTunesWindow = systemEvents.application_processes[u'iTunes'].windows[u'iTunes']
106 # Can't get AirPlay status with iTunes Mini Player or window on other Space.
107 try:
108 remote_speakers = iTunesWindow.buttons[its.attributes['AXDescription'].value.beginswith(u'AirPlay')].title()
109 except CommandError: # window on another Space?
110 return True
111 return remote_speakers and remote_speakers[0] not in (None, k.missing_value)
112
113def turnStereoOn():
114 global needsStereoPowerOn
115 if not default_output_device_is_airplay() and not mayUseStereo():
116 if HAVE_XTENSION and XTensionApp().status('Stereo'):
117 XTensionApp().turnoff('Stereo')
118 return
119 if not XTensionApp().status('Stereo'):
120 XTensionApp().turnon('Stereo')
121 needsStereoPowerOn = False
122
123def turnStereoOff():
124 global needsStereoPowerOn
125 if default_output_device_is_airplay() or not mayUseStereo():
126 return
127 if not needsStereoPowerOn and XTensionApp().status('Stereo'):
128 XTensionApp().turnoff('Stereo')
129 needsStereoPowerOn = True
130
131def amuaPlaying():
132 if not HAVE_AMUA:
133 return False
134 return AmuaApp().is_playing()
135
136class OneFileCache(object):
137 __slots__ = ('key', 'cache')
138
139 def __init__(self, cache):
140 if not os.path.exists(cache):
141 os.makedirs(cache)
142 self.cache = os.path.join(cache, 'file')
143 self.key = None
144
145 def get(self, key):
146 if key == self.key:
147 return file(self.cache, 'r').read()
148
149 def set(self, key, value):
150 self.key = key
151 file(self.cache, 'w').write(value)
152
153 def delete(self, key):
154 if key == self.key:
155 self.key = None
156 os.remove(cache)
157
158class StreamVision(NSApplication):
159
160 hotKeyActions = {}
161 hotKeys = []
162
163 def displayTrackInfo(self, playerInfo=None):
164 iTunes = iTunesApp()
165
166 try:
167 trackClass = iTunes.current_track.class_()
168 except CommandError:
169 trackClass = k.property
170
171 trackName = ''
172 if trackClass != k.property:
173 trackName = iTunes.current_track.name()
174
175 try:
176 playerState = iTunes.player_state()
177 except CommandError:
178 playerState = None # probably iTunes quit
179 if playerState != k.playing:
180 if playerState != None:
181 growlNotify('iTunes is not playing.', trackName)
182 turnStereoOff()
183 return
184 turnStereoOn()
185 if trackClass == k.URL_track:
186 if amuaPlaying():
187 if playerInfo is None: # Amua displays it itself
188 AmuaApp().display_song_information()
189 return
190 url = iTunes.current_stream_URL()
191 kw = {}
192 if url != k.missing_value and url.endswith('.jpg'):
193 try:
194 response, content = self.http.request(url)
195 except Exception, e:
196 import sys
197 print >> sys.stderr, 'Request for album art failed:', e
198 else:
199 if response['content-type'].startswith('image/'):
200 file(self.imagePath, 'w').write(content)
201 kw['image_from_location'] = self.imagePath
202 growlNotify(cleanStreamTitle(iTunes.current_stream_title()),
203 cleanStreamTrackName(trackName), **kw)
204 return
205 if trackClass == k.property:
206 growlNotify('iTunes is playing.', '')
207 return
208 kw = {}
209 # XXX iTunes doesn't let you get artwork for shared tracks
210 if trackClass != k.shared_track:
211 artwork = iTunes.current_track.artworks()
212 if artwork:
213 try:
214 kw['pictImage'] = artwork[0].data_()
215 except CommandError:
216 pass
217 growlNotify(trackName + ' ' +
218 u'★' * (iTunes.current_track.rating() / 20),
219 iTunes.current_track.album() + '\n' +
220 iTunes.current_track.artist(),
221 **kw)
222
223 def goToSite(self):
224 iTunes = iTunesApp()
225 if iTunes.player_state() == k.playing:
226 if amuaPlaying():
227 AmuaApp().display_album_details()
228 return
229 url = iTunes.current_stream_URL()
230 if url != k.missing_value:
231 if 'radioparadise.com' in url and 'review' not in url:
232 url = radioParadiseURL()
233 NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(url))
234 return
235 NSBeep()
236
237 def registerHotKey(self, func, keyCode, mods=0):
238 hotKeyRef = RegisterEventHotKey(keyCode, mods, (0, 0),
239 GetApplicationEventTarget(), 0)
240 self.hotKeys.append(hotKeyRef)
241 self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)] = func
242 return hotKeyRef
243
244 def unregisterHotKey(self, hotKeyRef):
245 self.hotKeys.remove(hotKeyRef)
246 del self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)]
247 hotKeyRef.UnregisterEventHotKey()
248
249 def incrementRatingBy(self, increment):
250 iTunes = iTunesApp()
251 if amuaPlaying():
252 if increment < 0:
253 AmuaApp().ban_song()
254 growlNotify('Banned song.', '', icon_of_application='Amua.app')
255 else:
256 AmuaApp().love_song()
257 growlNotify('Loved song.', '', icon_of_application='Amua.app')
258 return
259 rating = iTunes.current_track.rating()
260 rating += increment
261 if rating < 0:
262 rating = 0
263 NSBeep()
264 elif rating > 100:
265 rating = 100
266 NSBeep()
267 iTunes.current_track.rating.set(rating)
268
269 def playPause(self, useStereo=True):
270 global needsStereoPowerOn
271
272 iTunes = iTunesApp()
273 was_playing = (iTunes.player_state() == k.playing)
274 if not useStereo:
275 needsStereoPowerOn = False
276 if was_playing and amuaPlaying():
277 AmuaApp().stop()
278 else:
279 iTunes.playpause()
280 if not was_playing and iTunes.player_state() == k.stopped:
281 # most likely, we're focused on the iPod, so playing does nothing
282 iTunes.browser_windows[1].view.set(iTunes.user_playlists[its.name=='Stations'][1]())
283 iTunes.play()
284 if not useStereo:
285 return
286 if iTunes.player_state() == k.playing:
287 turnStereoOn()
288 else:
289 turnStereoOff()
290
291 def playPauseFront(self):
292 systemEvents = app(id='com.apple.systemEvents')
293 frontName = systemEvents.processes[its.frontmost == True][1].name()
294 if frontName == 'RealPlayer':
295 realPlayer = app(id='com.RealNetworks.RealPlayer')
296 if len(realPlayer.players()) > 0:
297 if realPlayer.players[1].state() == k.playing:
298 realPlayer.pause()
299 else:
300 realPlayer.play()
301 return
302 elif frontName == 'VLC':
303 app(id='org.videolan.vlc').play() # equivalent to playpause
304 else:
305 self.playPause(useStereo=False)
306
307 def nextTrack(self):
308 if amuaPlaying():
309 AmuaApp().skip_song()
310 return
311 iTunesApp().next_track()
312
313 def registerZoomWindowHotKey(self):
314 self.zoomWindowHotKey = self.registerHotKey(self.zoomWindow, 42, cmdKey | controlKey) # cmd-ctrl-\
315
316 def unregisterZoomWindowHotKey(self):
317 self.unregisterHotKey(self.zoomWindowHotKey)
318 self.zoomWindowHotKey = None
319
320 def zoomWindow(self):
321 # XXX detect if "enable access for assistive devices" needs to be enabled
322 systemEvents = app(id='com.apple.systemEvents')
323 frontName = systemEvents.processes[its.frontmost == True][1].name()
324 if frontName == 'iTunes':
325 systemEvents.processes['iTunes'].menu_bars[1]. \
326 menu_bar_items['Window'].menus.menu_items['Zoom'].click()
327 return
328 elif frontName in ('X11', 'XQuartz', 'Emacs'): # preserve C-M-\
329 self.unregisterZoomWindowHotKey()
330 systemEvents.key_code(42, using=[k.command_down, k.control_down])
331 self.registerZoomWindowHotKey()
332 return
333 frontPID = systemEvents.processes[its.frontmost == True][1].unix_id()
334 try:
335 zoomed = app(pid=frontPID).windows[1].zoomed
336 zoomed.set(not zoomed())
337 except (CommandError, RuntimeError):
338 systemEvents.processes[frontName].windows \
339 [its.subrole == 'AXStandardWindow'].windows[1]. \
340 buttons[its.subrole == 'AXZoomButton'].buttons[1].click()
341
342 def finishLaunching(self):
343 super(StreamVision, self).finishLaunching()
344
345 caches = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
346 NSUserDomainMask, True)[0]
347 cache = os.path.join(caches, 'StreamVision')
348 self.http = httplib2.Http(OneFileCache(cache), 5)
349 self.imagePath = os.path.join(cache, 'image')
350
351 self.registerHotKey(self.displayTrackInfo, 100) # F8
352 self.registerHotKey(self.goToSite, 100, cmdKey) # cmd-F8
353 self.registerHotKey(self.playPause, 101) # F9
354 self.registerHotKey(lambda: iTunesApp().previous_track(), 109) # F10
355 self.registerHotKey(self.nextTrack, 103) # F11
356 self.registerHotKey(lambda: self.incrementRatingBy(-20), 109, shiftKey) # shift-F10
357 self.registerHotKey(lambda: self.incrementRatingBy(20), 103, shiftKey) # shift-F11
358 self.registerZoomWindowHotKey()
359 NSDistributedNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, self.displayTrackInfo, 'com.apple.iTunes.playerInfo', None)
360 try:
361 import HIDRemote
362 HIDRemote.connect()
363 except ImportError:
364 print "failed to import HIDRemote (XXX fix - on Intel)"
365 except OSError, e:
366 print "failed to connect to remote: ", e
367
368 set_default_output_device_changed_callback(turnStereoOn)
369 turnStereoOn()
370
371 def sendEvent_(self, theEvent):
372 eventType = theEvent.type()
373 if eventType == NSSystemDefined and \
374 theEvent.subtype() == kEventHotKeyPressedSubtype:
375 self.hotKeyActions[theEvent.data1()]()
376 elif eventType == NSApplicationDefined:
377 key = theEvent.data1()
378 if key == kHIDUsage_Csmr_ScanNextTrack:
379 self.nextTrack()
380 elif key == kHIDUsage_Csmr_ScanPreviousTrack:
381 iTunesApp().previous_track()
382 elif key == kHIDUsage_Csmr_PlayOrPause:
383 self.playPauseFront()
384 super(StreamVision, self).sendEvent_(theEvent)
385
386if __name__ == "__main__":
387 growlRegister()
388 AppHelper.runEventLoop()
389 try:
390 HIDRemote.disconnect()
391 except:
392 pass
Note: See TracBrowser for help on using the repository browser.