source: trunk/StreamVision/StreamVision.py@ 507

Last change on this file since 507 was 497, checked in by Nicholas Riley, 15 years ago

StreamVision.py: When playing nothing, iTunes 8.1 has no reference to the current track - actually an improvement.

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