source: trunk/StreamVision/StreamVision.py @ 323

Last change on this file since 323 was 323, checked in by Nicholas Riley, 13 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.