source: trunk/StreamVision/StreamVision.py@ 636

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

StreamVision.py: newer appscript spells data as data_.

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