source: trunk/StreamVision/StreamVision.py@ 646

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

StreamVision.py: When we can't see the AirPlay button, assume AirPlay is in use.

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