source: trunk/StreamVision/StreamVision.py@ 341

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

StreamVision.py: Handle misencoded stream titles more intelligently.

File size: 9.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
7from PyObjCTools import AppHelper
8from Carbon.CarbonEvt import RegisterEventHotKey, GetApplicationEventTarget
9from Carbon.Events import cmdKey, shiftKey, controlKey
10import struct
11import scrape
12import HotKey
13
14GROWL_APP_NAME = 'StreamVision'
15NOTIFICATION_TRACK_INFO = 'iTunes Track Info'
16NOTIFICATIONS_ALL = [NOTIFICATION_TRACK_INFO]
17
18kEventHotKeyPressedSubtype = 6
19kEventHotKeyReleasedSubtype = 9
20
21kHIDUsage_Csmr_ScanNextTrack = 0xB5
22kHIDUsage_Csmr_ScanPreviousTrack = 0xB6
23kHIDUsage_Csmr_PlayOrPause = 0xCD
24
25def growlRegister():
26 global growl
27 growl = app(id='com.Growl.GrowlHelperApp')
28
29 growl.register(
30 as_application=GROWL_APP_NAME,
31 all_notifications=NOTIFICATIONS_ALL,
32 default_notifications=NOTIFICATIONS_ALL,
33 icon_of_application='iTunes.app')
34 # if we leave off the .app, we can get Classic iTunes's icon
35
36def growlNotify(title, description, **kw):
37 try:
38 growl.notify(
39 with_name=NOTIFICATION_TRACK_INFO,
40 title=title,
41 description=description,
42 application_name=GROWL_APP_NAME,
43 **kw)
44 except CommandError:
45 growlRegister()
46 growlNotify(title, description, **kw)
47
48def radioParadiseURL():
49 # XXX better to use http://www2.radioparadise.com/playlist.xml ?
50 session = scrape.Session()
51 session.go('http://www2.radioparadise.com/nowplay_b.php')
52 return session.region.firsttag('a')['href']
53
54def cleanStreamTitle(title):
55 if title == k.missing_value:
56 return ''
57 title = title.split(' [')[0] # XXX move to description
58 try: # incorrectly encoded?
59 title = title.encode('iso-8859-1').decode('utf-8')
60 except UnicodeDecodeError:
61 pass
62 title = title.replace('`', u'’')
63 return title
64
65def cleanStreamTrackName(name):
66 name = name.split('. ')[0]
67 name = name.split(': ')[0]
68 name = name.split(' - ')
69 if len(name) > 1:
70 name = ' - '.join(name[:-1])
71 else:
72 name = name[0]
73 return name
74
75def iTunesApp(): return app(id='com.apple.iTunes')
76def XTensionApp(): return app(creator='SHEx')
77
78HAVE_XTENSION = False
79try:
80 XTensionApp()
81 HAVE_XTENSION = True
82except:
83 pass
84
85needsStereoPowerOn = HAVE_XTENSION
86
87class StreamVision(NSApplication):
88
89 hotKeyActions = {}
90 hotKeys = []
91
92 def displayTrackInfo(self):
93 global needsStereoPowerOn
94
95 iTunes = iTunesApp()
96
97 trackClass = iTunes.current_track.class_()
98 trackName = ''
99 if trackClass != k.property:
100 trackName = iTunes.current_track.name()
101
102 if iTunes.player_state() != k.playing:
103 growlNotify('iTunes is not playing.', trackName)
104 if HAVE_XTENSION:
105 if not needsStereoPowerOn and XTensionApp().status('Stereo'):
106 XTensionApp().turnoff('Stereo')
107 needsStereoPowerOn = True
108 return
109 if needsStereoPowerOn:
110 if not XTensionApp().status('Stereo'):
111 XTensionApp().turnon('Stereo')
112 needsStereoPowerOn = False
113 if trackClass == k.URL_track:
114 growlNotify(cleanStreamTitle(iTunes.current_stream_title()),
115 cleanStreamTrackName(trackName))
116 return
117 if trackClass == k.property:
118 growlNotify('iTunes is playing.', '')
119 return
120 kw = {}
121 # XXX iTunes doesn't let you get artwork for shared tracks
122 if trackClass != k.shared_track:
123 artwork = iTunes.current_track.artworks()
124 if artwork:
125 kw['pictImage'] = artwork[0].data()
126 growlNotify(trackName + ' ' +
127 u'★' * (iTunes.current_track.rating() / 20),
128 iTunes.current_track.album() + '\n' +
129 iTunes.current_track.artist(),
130 **kw)
131
132 def goToSite(self):
133 iTunes = iTunesApp()
134 if iTunes.player_state() == k.playing:
135 url = iTunes.current_stream_URL()
136 if url != k.missing_value:
137 if 'radioparadise.com' in url and 'review' not in url:
138 url = radioParadiseURL()
139 NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(url))
140 return
141 NSBeep()
142
143 def registerHotKey(self, func, keyCode, mods=0):
144 hotKeyRef = RegisterEventHotKey(keyCode, mods, (0, 0),
145 GetApplicationEventTarget(), 0)
146 self.hotKeys.append(hotKeyRef)
147 self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)] = func
148 return hotKeyRef
149
150 def unregisterHotKey(self, hotKeyRef):
151 self.hotKeys.remove(hotKeyRef)
152 del self.hotKeyActions[HotKey.HotKeyAddress(hotKeyRef)]
153 hotKeyRef.UnregisterEventHotKey()
154
155 def incrementRatingBy(self, increment):
156 iTunes = iTunesApp()
157 rating = iTunes.current_track.rating()
158 rating += increment
159 if rating < 0:
160 rating = 0
161 NSBeep()
162 elif rating > 100:
163 rating = 100
164 NSBeep()
165 iTunes.current_track.rating.set(rating)
166
167 def playPause(self, useStereo=True):
168 global needsStereoPowerOn
169
170 iTunes = iTunesApp()
171 was_playing = (iTunes.player_state() == k.playing)
172 if not useStereo:
173 needsStereoPowerOn = False
174 iTunes.playpause()
175 if not was_playing and iTunes.player_state() == k.stopped:
176 # most likely, we're focused on the iPod, so playing does nothing
177 iTunes.browser_windows[1].view.set(iTunes.user_playlists[its.name=='Stations'][1]())
178 iTunes.play()
179 if HAVE_XTENSION and useStereo:
180 if iTunes.player_state() == k.playing:
181 XTensionApp().turnon('Stereo')
182 needsStereoPowerOn = False
183 else:
184 XTensionApp().turnoff('Stereo')
185 needsStereoPowerOn = True
186
187 def playPauseFront(self):
188 systemEvents = app(id='com.apple.systemEvents')
189 frontName = systemEvents.processes[its.frontmost == True][1].name()
190 if frontName == 'RealPlayer':
191 realPlayer = app(id='com.RealNetworks.RealPlayer')
192 if len(realPlayer.players()) > 0:
193 if realPlayer.players[1].state() == k.playing:
194 realPlayer.pause()
195 else:
196 realPlayer.play()
197 return
198 elif frontName == 'VLC':
199 app(id='org.videolan.vlc').play() # equivalent to playpause
200 else:
201 self.playPause(useStereo=False)
202
203 def registerZoomWindowHotKey(self):
204 self.zoomWindowHotKey = self.registerHotKey(self.zoomWindow, 42, cmdKey | controlKey) # cmd-ctrl-\
205
206 def unregisterZoomWindowHotKey(self):
207 self.unregisterHotKey(self.zoomWindowHotKey)
208 self.zoomWindowHotKey = None
209
210 def zoomWindow(self):
211 # XXX detect if "enable access for assistive devices" needs to be enabled
212 systemEvents = app(id='com.apple.systemEvents')
213 frontName = systemEvents.processes[its.frontmost == True][1].name()
214 if frontName == 'iTunes':
215 systemEvents.processes['iTunes'].menu_bars[1]. \
216 menu_bar_items['Window'].menus.menu_items['Zoom'].click()
217 return
218 elif frontName in ('X11', 'Emacs'): # preserve C-M-\
219 self.unregisterZoomWindowHotKey()
220 systemEvents.key_code(42, using=[k.command_down, k.control_down])
221 self.registerZoomWindowHotKey()
222 return
223 frontPID = systemEvents.processes[its.frontmost == True][1].unix_id()
224 try:
225 zoomed = app(pid=frontPID).windows[1].zoomed
226 zoomed.set(not zoomed())
227 except (CommandError, RuntimeError):
228 systemEvents.processes[frontName].windows \
229 [its.subrole == 'AXStandardWindow'].windows[1]. \
230 buttons[its.subrole == 'AXZoomButton'].buttons[1].click()
231
232 def finishLaunching(self):
233 super(StreamVision, self).finishLaunching()
234 self.registerHotKey(self.displayTrackInfo, 100) # F8
235 self.registerHotKey(self.goToSite, 100, cmdKey) # cmd-F8
236 self.registerHotKey(self.playPause, 101) # F9
237 self.registerHotKey(lambda: iTunesApp().previous_track(), 109) # F10
238 self.registerHotKey(lambda: iTunesApp().next_track(), 103) # F11
239 self.registerHotKey(lambda: self.incrementRatingBy(-20), 109, shiftKey) # shift-F10
240 self.registerHotKey(lambda: self.incrementRatingBy(20), 103, shiftKey) # shift-F11
241 self.registerZoomWindowHotKey()
242 NSDistributedNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, self.displayTrackInfo, 'com.apple.iTunes.playerInfo', None)
243 try:
244 import HIDRemote
245 HIDRemote.connect()
246 except ImportError:
247 print "failed to import HIDRemote (XXX fix - on Intel)"
248 except OSError, e:
249 print "failed to connect to remote: ", e
250
251 def sendEvent_(self, theEvent):
252 eventType = theEvent.type()
253 if eventType == NSSystemDefined and \
254 theEvent.subtype() == kEventHotKeyPressedSubtype:
255 self.hotKeyActions[theEvent.data1()]()
256 elif eventType == NSApplicationDefined:
257 key = theEvent.data1()
258 if key == kHIDUsage_Csmr_ScanNextTrack:
259 iTunesApp().next_track()
260 elif key == kHIDUsage_Csmr_ScanPreviousTrack:
261 iTunesApp().previous_track()
262 elif key == kHIDUsage_Csmr_PlayOrPause:
263 self.playPauseFront()
264 super(StreamVision, self).sendEvent_(theEvent)
265
266if __name__ == "__main__":
267 growlRegister()
268 AppHelper.runEventLoop()
269 try:
270 HIDRemote.disconnect()
271 except:
272 pass
Note: See TracBrowser for help on using the repository browser.