source: trunk/StreamVision/StreamVision.py@ 649

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

tunes.py: Static terminology for iTunes to work around 'gdte' bug in
iTunes 10.6.3, from <https://github.com/abarnert/itunesterms>.

StreamVision.py: Use above static terminology.

More information at <http://www.leancrew.com/all-this/2012/06/the-first-nail-in-the-coffin-of-python-appscript/>.

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