source: trunk/StreamVision/StreamVision.py @ 341

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