source: trunk/StreamVision/StreamVision.py @ 414

Last change on this file since 414 was 414, checked in by Nicholas Riley, 14 years ago

StreamVision?.py: Support for Last.fm playback via Amua.

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