source: trunk/StreamVision/StreamVision.py@ 645

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

StreamVision.py: Update for Radio Paradise 2 site design.

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