class Search: Keys = ("artist", "title", "duration", "rating") _searchText = "" _searchResults = [] @UserAttrib(type=Traits.EditableText, searchLook=True) def searchText(self, updateText=None): if updateText is not None and self._searchText != updateText: self._searchText = updateText self._searchResults = songdb.search(updateText) self.searchResults_updateEvent.push() return self._searchText @UserAttrib(type=Traits.Table( keys=Keys, format_duration=lambda d: formatTime(d) if d > 0 else "", format_rating=lambda r: "★" * int(round(r * 5)))) @property def searchResults(self): return list(self._searchResults) @searchResults.setUpdateEvent @initBy def searchResults_updateEvent(self): return Event()
class Search: Keys = ("artist", "title", "duration", "rating") def __init__(self): self._searchText = "" self._searchResults = [] import threading self._lock = threading.RLock() def _startSearch(self, txt): def search(): with self._lock: if self._searchText != txt: return res = songdb.search(txt) with self._lock: if self._searchText == txt: self._searchResults = res self.__class__.searchResults.updateEvent(self).push() with self._lock: self._searchText = txt TaskSystem.daemonThreadCall(search, name="Song DB search") @UserAttrib(type=Traits.EditableText, searchLook=True) def searchText(self, updateText=None): with self._lock: if updateText is not None and self._searchText != updateText: self._startSearch(updateText) return self._searchText @UserAttrib(type=Traits.Table( keys=Keys, format_duration=lambda d: formatTime(d) if d > 0 else "", format_rating=lambda r: "★" * int(round(r * 5))), variableHeight=True, addUpdateEvent=True) @property def searchResults(self): with self._lock: return list(self._searchResults)
class State(object): def _updateCurSongHandler(self): self.__class__.curSongStr.updateEvent(self).push() self.__class__.curSongPos.updateEvent(self).push() self.__class__.curSongDisplay.updateEvent(self).push() def __init__(self): self.__class__.curSong.updateEvent(self).register(self._updateCurSongHandler) def playPauseUpdate(self, attrib): if self.player.playing: attrib.name = "❚❚" else: attrib.name = "▶" @UserAttrib(type=Traits.Action, name="▶", updateHandler=playPauseUpdate, addUpdateEvent=True) def playPause(self): self.player.playing = not self.player.playing @UserAttrib(type=Traits.Action, name="▶▶|", alignRight=True) def nextSong(self): self.player.nextSong() @UserAttrib(type=Traits.OneLineText, alignRight=True, variableWidth=True, withBorder=True, addUpdateEvent=True) @property def curSongStr(self): if not self.player.curSong: return "" try: return self.player.curSong.userString except Exception: return "???" @UserAttrib(type=Traits.OneLineText, alignRight=True, autosizeWidth=True, withBorder=True, addUpdateEvent=True) @property def curSongPos(self): if not self.player.curSong: return "" try: return formatTime(self.player.curSongPos) + " / " + formatTime(self.player.curSong.duration) except Exception: return "???" @UserAttrib(type=Traits.SongDisplay, variableWidth=True, addUpdateEvent=True) def curSongDisplay(self): pass @initBy def _volume(self): return PersistentObject(float, "volume.dat", defaultArgs=(0.9,)) @UserAttrib(type=Traits.Real(min=0, max=2), alignRight=True, height=80, width=25) @property def volume(self): return self._volume @volume.callDeco.setter def volume(self, updateValue): self._volume = updateValue self._volume.save() self.player.volume = updateValue @UserAttrib(type=Traits.List, lowlight=True, autoScrolldown=True) @initBy def recentlyPlayedList(self): return PersistentObject(RecentlyplayedList, "recentlyplayed.dat") @UserAttrib(type=Traits.Object, spaceY=0, highlight=True, addUpdateEvent=True) @initBy def curSong(self): return PersistentObject(Song, "cursong.dat") @UserAttrib(type=Traits.Object, spaceY=0, variableHeight=True) @initBy def queue(self): import Queue return Queue.queue @initBy def updates(self): import Queue return Queue.OnRequestQueue() @initBy def player(self): from Player import loadPlayer return loadPlayer(self) def quit(self): # XXX: Is this still used? # XXX: doesn't really work. OSX ignores the SIGINT if the cocoa mainloop runs def doQuit(): """ This works in all threads except the main thread. It will quit the whole app. For more information about why we do it this way, read the comment in main.py. """ import sys, os, signal os.kill(0, signal.SIGINT) sys.stdin.close() # so that the terminal closes, if it is used import thread thread.start_new_thread(doQuit, ())
class SongEdit: @initBy def _updateEvent(self): return Event() def __init__(self, ctx=None): if not ctx: import gui ctx = gui.ctx() assert ctx, "no gui context" self.ctx = ctx self._updateHandler = lambda: self._updateEvent.push() ctx.curSelectedSong_updateEvent.register(self._updateHandler) @UserAttrib(type=Traits.Object) @property def song(self): return self.ctx.curSelectedSong @UserAttrib(type=Traits.EditableText) def artist(self, updateText=None): if self.song: if updateText: self.song.artist = updateText return self.song.artist return "" @UserAttrib(type=Traits.EditableText) def title(self, updateText=None): if self.song: if updateText: self.song.title = updateText return self.song.title return "" @staticmethod def _convertTagsToText(tags): def txtForTag(tag): value = tags[tag] if value >= 1: return tag return tag + ":" + str(value) return " ".join(map(txtForTag, sorted(tags.keys()))) @staticmethod def _convertTextToTags(txt): pass # todo... #@UserAttrib(type=Traits.EditableText) def tags(self, updateText=None): if self.song: return self._convertTagsToText(self.song.tags) return "" @staticmethod def _formatGain(gain): factor = 10.0 ** (gain / 20.0) return "%f dB (factor %f)" % (gain, factor) @UserAttrib(type=Traits.Table(keys=("key", "value")), variableHeight=True) @property def metadata(self): d = dict(self.song.metadata) for (key,func) in ( ("artist",None), ("title",None), ("album",None), ("duration",utils.formatTime), ("url",None), ("rating",None), ("tags",self._convertTagsToText), ("gain",self._formatGain), ("completedCount",None), ("skipCount",None), ("lastPlayedDate",utils.formatDate), ("id",repr), ): try: value = getattr(self.song, key) except AttributeError: pass else: if func: value = func(value) d[key] = unicode(value) l = [] for key,value in sorted(d.items()): l += [{"key": key, "value": value}] return l @metadata.setUpdateEvent @property def metadata_updateEvent(self): return self.song._updateEvent def _queryAcoustId(self): fingerprint = self.song.get("fingerprint_AcoustId", timeout=None)[0] duration = self.song.get("duration", timeout=None, accuracy=0.5)[0] import base64 fingerprint = base64.urlsafe_b64encode(fingerprint) api_url = "http://api.acoustid.org/v2/lookup" # "8XaBELgH" is the one from the web example from AcoustID. # "cSpUJKpD" is from the example from pyacoustid # get an own one here: http://acoustid.org/api-key client_api_key = "cSpUJKpD" params = { 'format': 'json', 'client': client_api_key, 'duration': int(duration), 'fingerprint': fingerprint, 'meta': 'recordings recordingids releasegroups releases tracks compress', } import urllib body = urllib.urlencode(params) import urllib2 req = urllib2.Request(api_url, body) import contextlib with contextlib.closing(urllib2.urlopen(req)) as f: data = f.read() headers = f.info() import json data = json.loads(data) return data def queryAcoustIdResults_selectionChangeHandler(self, selection): self._queryAcoustId_selection = selection @UserAttrib(type=Traits.Table(keys=("artist", "title", "album", "track", "score")), selectionChangeHandler=queryAcoustIdResults_selectionChangeHandler) @property def queryAcoustIdResults(self): if getattr(self, "_queryAcoustIdResults_songId", "") != getattr(self.song, "id", ""): return [] return list(getattr(self, "_queryAcoustIdResults", [])) @queryAcoustIdResults.setUpdateEvent @initBy def queryAcoustIdResults_updateEvent(self): return Event() @UserAttrib(type=Traits.Action, variableWidth=False) def queryAcoustId(self): data = self._queryAcoustId() self._queryAcoustIdResults_songId = self.song.id self._queryAcoustIdResults = [] for result in data.get("results", []): for recording in result.get("recordings", []): for resGroup in recording.get("releasegroups", []): artist = resGroup["artists"][0] release = resGroup["releases"][0] medium = release["mediums"][0] track = medium["tracks"][0] if artist["name"] == "Various Artists": artist = track["artists"][0] entry = { "id": result["id"], "score": result["score"], "recording-id": recording["id"], "releasegroup-id": resGroup["id"], "artist-id": artist["id"], "artist": artist["name"], "title": track["title"], "album": resGroup["title"], "track": "%i/%i" % (track["position"], medium["track_count"]) } self._queryAcoustIdResults += [entry] if not self._queryAcoustIdResults: self._queryAcoustIdResults += [{"artist":"- None found -","title":"","album":"","track":""}] self.queryAcoustIdResults_updateEvent.push() @UserAttrib(type=Traits.Action, variableWidth=False, alignRight=True) def apply(self): if getattr(self, "_queryAcoustIdResults_songId", "") != getattr(self.song, "id", ""): return sel = getattr(self, "_queryAcoustId_selection", []) if not sel: return sel = sel[0] for key in ("artist","title"): if not sel[key]: return for key in ("artist","title","album","track"): setattr(self.song, key, sel[key]) self._updateEvent.push() # the song is updating itself - but the edit fields aren't atm...
class Preferences(object): def __init__(self): self._sampleRateStr = None self.lastFm_update(self.__class__.lastFm) @UserAttrib(type=Traits.OneLineText, autosizeWidth=True) @property def soundDeviceLabel(self): return "Preferred sound device:" @UserAttrib(type=Traits.EditableText, autosizeWidth=True, alignRight=True) def preferredSoundDevice(self, updateText=None): from State import state player = state.player if updateText is not None: player.preferredSoundDevice = updateText from appinfo import config config.preferredSoundDevice = updateText config.save() return player.preferredSoundDevice @UserAttrib(type=Traits.OneLineText, variableWidth=True) @property def soundDeviceDescr(self): return "(Playback needs to be stopped to set this into effect.)" @UserAttrib(type=Traits.OneLineText, autosizeWidth=True) @property def actualSoundDeviceLabel(self): return "Current sound device:" @UserAttrib(type=Traits.OneLineText, autosizeWidth=True, withBorder=True, alignRight=True) @safe_property @property def actualSoundDevice(self): from State import state return state.player.actualSoundDevice @UserAttrib(type=Traits.OneLineText, autosizeWidth=True) @property def availableSoundDevicesLabel(self): return "Available sound devices:" @UserAttrib(type=Traits.Table(keys=("Name", )), autosizeWidth=True, alignRight=True) @safe_property @property def availableSoundDevices(self): import musicplayer l = musicplayer.getSoundDevices() return [{"Name": dev} for dev in l] @UserAttrib(type=Traits.OneLineText, autosizeWidth=True) @property def sampleRateLabel(self): return "Sample rate in Hz:" @property def _sampleRate(self): from State import state return state.player.outSamplerate @UserAttrib( type=Traits.EditableText, alignRight=True, variableWidth=True, width=200, # this forces a min-width addUpdateEvent=True) def sampleRate(self, updateText=None): if updateText is not None and self._sampleRateStr != updateText: self._sampleRateStr = updateText if self._sampleRateStr is not None: return self._sampleRateStr rate = str(self._sampleRate / 1000) return rate + "k" @UserAttrib(type=Traits.Action, name="apply", alignRight=True, variableWidth=False) def applySampleRate(self): self.__class__.sampleRate.updateEvent(self).push() self._sampleRateStr, rate = None, self._sampleRateStr if rate is None: return rate = rate.strip() if rate[-1:] == "k": factor = 1000 rate = rate[:-1] else: factor = 1 try: rate = int(rate) * factor except Exception: return # no valid integer # do some very basic check on the number. # later, our musicplayer module should allow us to check this. if rate in (44100, 48000, 88200, 96000, 176400, 192000): pass else: return from State import state state.player.playing = False # can only change that when not playing state.player.outSamplerate = rate from appinfo import config config.sampleRate = rate config.save() def lastFm_update(self, attrib): from appinfo import config if config.lastFm: attrib.name = "Last.fm is enabled. Disable it." else: attrib.name = "Last.fm is disabled. Enable it." @UserAttrib(type=Traits.Action, updateHandler=lastFm_update, variableWidth=False, addUpdateEvent=True) def lastFm(self): self.__class__.lastFm.updateEvent(self).push() from appinfo import config config.lastFm = not config.lastFm config.save() from State import getModule getModule("tracker_lastfm").reload() @UserAttrib(type=Traits.Action, alignRight=True, name="reset Last.fm login", variableWidth=False) def resetLastFm(self): import lastfm, os, sys try: os.remove(lastfm.StoredSession.TOKEN_FILE) except Exception: sys.excepthook(*sys.exc_info())