source: trunk/StreamVision/StreamVision.py@ 323

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

StreamVision.py: fix encoding/decoding for iTunes 7.1+; include star as Unicode string for Python 2.3 compatibility

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