diff options
-rw-r--r-- | nephilim/common.py | 6 | ||||
-rw-r--r-- | nephilim/mpclient.py | 14 | ||||
-rw-r--r-- | nephilim/plugins/AlbumCover.py | 8 | ||||
-rw-r--r-- | nephilim/plugins/Library.py | 18 | ||||
-rw-r--r-- | nephilim/plugins/Lyrics.py | 10 | ||||
-rw-r--r-- | nephilim/plugins/Playlist.py | 20 | ||||
-rw-r--r-- | nephilim/plugins/Songinfo.py | 2 | ||||
-rw-r--r-- | nephilim/song.py | 136 | ||||
-rw-r--r-- | nephilim/winMain.py | 4 |
9 files changed, 125 insertions, 93 deletions
diff --git a/nephilim/common.py b/nephilim/common.py index 5c12225..1249ec7 100644 --- a/nephilim/common.py +++ b/nephilim/common.py @@ -72,10 +72,10 @@ def expand_tags(str, expanders): def generate_metadata_path(song, dir_tag, file_tag): """Generate dirname and (db files only) full file path for reading/writing metadata files (cover, lyrics) from $tags in dir/filename.""" - if QtCore.QDir.isAbsolutePath(song.filepath()): - dirname = os.path.dirname(song.filepath()) + if QtCore.QDir.isAbsolutePath(song['file']): + dirname = os.path.dirname(song['file']) filepath = '' - elif '://' in song.filepath(): # we are streaming + elif '://' in song['file']: # we are streaming dirname = '' filepath = '' else: diff --git a/nephilim/mpclient.py b/nephilim/mpclient.py index 35be3f5..bd137ea 100644 --- a/nephilim/mpclient.py +++ b/nephilim/mpclient.py @@ -21,7 +21,7 @@ import mpd import socket import logging -from song import Song +from song import Song, SongRef, PlaylistEntryRef class MPClient(QtCore.QObject): """This class offers another layer above pympd, with usefull events.""" @@ -45,7 +45,7 @@ class MPClient(QtCore.QObject): # SIGNALS connect_changed = QtCore.pyqtSignal(bool) db_updated = QtCore.pyqtSignal() - song_changed = QtCore.pyqtSignal(str) + song_changed = QtCore.pyqtSignal(object) time_changed = QtCore.pyqtSignal(int) state_changed = QtCore.pyqtSignal(str) volume_changed = QtCore.pyqtSignal(int) @@ -87,7 +87,7 @@ class MPClient(QtCore.QObject): self.logger.error('Don\'t have MPD read permission, diconnecting.') return self.disconnect_mpd() - self._update_current_song() + self.__update_current_song() self._db_update = self.stats()['db_update'] self.emit(QtCore.SIGNAL('connected')) #should be removed @@ -139,7 +139,7 @@ class MPClient(QtCore.QObject): def status(self): """Get current MPD status.""" return self._status - def playlist(self): + def playlistinfo(self): """Returns a list of songs in current playlist.""" self.logger.info('Listing current playlist.') if not self.__check_command_ok('playlistinfo'): @@ -368,7 +368,7 @@ class MPClient(QtCore.QObject): return map(lambda entry: Song(entry) , filter(lambda entry: not('directory' in entry), array) ) - def _update_current_song(self): + def __update_current_song(self): """Update the current song.""" song = self._retrieve(self._client.currentsong) if not song: @@ -424,10 +424,10 @@ class MPClient(QtCore.QObject): if not self._status: return self.disconnect_mpd() - self._update_current_song() + self.__update_current_song() if self._status['songid'] != old_status['songid']: - self.song_changed.emit(self._status['songid']) + self.song_changed.emit(PlaylistEntryRef(self, self._status['songid'])) if self._status['time'] != old_status['time']: self.time_changed.emit(self._status['time']) diff --git a/nephilim/plugins/AlbumCover.py b/nephilim/plugins/AlbumCover.py index db4d6f8..01aa333 100644 --- a/nephilim/plugins/AlbumCover.py +++ b/nephilim/plugins/AlbumCover.py @@ -221,9 +221,9 @@ class AlbumCover(Plugin): url = QtCore.QUrl('http://ws.audioscrobbler.com/2.0/') url.setQueryItems([('api_key', 'c325945c67b3e8327e01e3afb7cdcf35'), ('method', 'album.getInfo'), - ('artist', song.artist()), - ('album', song.album()), - ('mbid', song.tag('MUSICBRAINZ_ALBUMID'))]) + ('artist', song['artist']), + ('album', song['album']), + ('mbid', song['MUSICBRAINZ_ALBUMID'])]) self.fetch2(song, url) self.rep.finished.connect(self.__handle_search_res) @@ -390,7 +390,7 @@ class AlbumCover(Plugin): self.__abort_fetch() file = QtGui.QFileDialog.getOpenFileName(None, - 'Select album cover for %s - %s'%(song.artist(), song.album()), + 'Select album cover for %s - %s'%(song['artist'], song['album']), self.__cover_dir, '') if not file: return diff --git a/nephilim/plugins/Library.py b/nephilim/plugins/Library.py index 74c5464..50f85a3 100644 --- a/nephilim/plugins/Library.py +++ b/nephilim/plugins/Library.py @@ -83,6 +83,11 @@ class LibraryWidget(QtGui.QWidget): plugin = None logger = None + class LibrarySongItem(QtGui.QStandardItem): + # public + "Song path" + path = None + class LibraryModel(QtGui.QStandardItemModel): def fill(self, songs, mode): self.clear() @@ -91,7 +96,7 @@ class LibraryWidget(QtGui.QWidget): for song in songs: cur_item = tree for part in mode.split('/'): - tag = song.tag(part) + tag = song[part] if isinstance(tag, list): tag = tag[0] #FIXME hack to make songs with multiple genres work. if not tag: @@ -104,11 +109,10 @@ class LibraryWidget(QtGui.QWidget): cur_item[1].appendRow(it) cur_item[0][tag] = [{}, it] cur_item = cur_item[0][tag] - it = QtGui.QStandardItem('%s%02d %s'%(song.tag('disc') + '/' if song.tag('disc') else '', - song.track() if song.track() else 0, - song.title() if song.title() else song.filepath())) + it = LibraryWidget.LibrarySongItem('%s%02d %s'%(song['disc'] + '/' if 'disc' in song else '', + song['tracknum'], song['title'])) + it.path = song['file'] it.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled) - it.setData(QVariant(song), QtCore.Qt.UserRole) cur_item[1].appendRow(it) self.sort(0, QtCore.Qt.AscendingOrder) @@ -196,8 +200,8 @@ class LibraryWidget(QtGui.QWidget): self.plugin.mpclient.add(paths) def item_to_playlist(self, item, add_queue): - if not item.hasChildren(): - add_queue.append(item.data(QtCore.Qt.UserRole).toPyObject().filepath()) + if isinstance(item, self.LibrarySongItem): + add_queue.append(item.path) else: for i in range(item.rowCount()): self.item_to_playlist(item.child(i), add_queue) diff --git a/nephilim/plugins/Lyrics.py b/nephilim/plugins/Lyrics.py index 9217df8..3b6224f 100644 --- a/nephilim/plugins/Lyrics.py +++ b/nephilim/plugins/Lyrics.py @@ -91,7 +91,7 @@ class LyricsWidget(QtGui.QWidget): self.__text_view.clear() self.__label.setText('<b>%s</b> by <u>%s</u> on <u>%s</u>'\ - %(song.title(), song.artist(), song.album())) + %(song['title'], song['artist'], song['album'])) if lyrics: self.logger.info('Setting new lyrics.') self.__text_view.insertPlainText(lyrics.decode('utf-8')) @@ -147,7 +147,7 @@ class Lyrics(Plugin): def fetch(self, song): url = QtCore.QUrl('http://lyricwiki.org/api.php') - url.setQueryItems([('func', 'getArtist'), ('artist', song.artist()), + url.setQueryItems([('func', 'getArtist'), ('artist', song['artist']), ('fmt', 'xml')]) self.fetch2(song, url) self.rep.finished.connect(self.__handle_artist_res) @@ -167,7 +167,7 @@ class Lyrics(Plugin): url = QtCore.QUrl('http://lyricwiki.org/api.php') url.setQueryItems([('func', 'getSong'), ('artist', artist), - ('song', self.song.title()), ('fmt', 'xml')]) + ('song', self.song['title']), ('fmt', 'xml')]) self.rep = self.nam.get(QtNetwork.QNetworkRequest(url)) self.rep.finished.connect(self.__handle_search_res) @@ -215,7 +215,7 @@ class Lyrics(Plugin): def fetch(self, song): url = QtCore.QUrl('http://www.animelyrics.com/search.php') - url.setQueryItems([('t', 'performer'), ('q', song.artist())]) + url.setQueryItems([('t', 'performer'), ('q', song['artist'])]) self.fetch2(song, url) self.rep.finished.connect(self.__handle_search_res) @@ -229,7 +229,7 @@ class Lyrics(Plugin): url = None for elem in tree.iterfind('.//a'): - if ('href' in elem.attrib) and elem.text and (self.song.title() in elem.text): + if ('href' in elem.attrib) and elem.text and (self.song['title'] in elem.text): url = QtCore.QUrl('http://www.animelyrics.com/%s'%elem.get('href')) if not url: diff --git a/nephilim/plugins/Playlist.py b/nephilim/plugins/Playlist.py index 44d7a40..d8147bc 100644 --- a/nephilim/plugins/Playlist.py +++ b/nephilim/plugins/Playlist.py @@ -44,6 +44,14 @@ class PlaylistWidget(QtGui.QWidget): plugin = None playlist = None + class PlaylistSongItem(QtGui.QTreeWidgetItem): + # public + id = -1 + + def __init__(self, id): + QtGui.QTreeWidgetItem.__init__(self) + self.id = id + def __init__(self, plugin): QtGui.QWidget.__init__(self) self.plugin = plugin @@ -55,7 +63,6 @@ class PlaylistWidget(QtGui.QWidget): self.layout().setMargin(0) self.layout().addWidget(self.playlist) - class Playlist(QtGui.QTreeWidget): song = None plugin = None @@ -80,23 +87,22 @@ class PlaylistWidget(QtGui.QWidget): self.plugin.settings.setValue(self.plugin.name + '/header_state', QVariant(self.header().saveState())) def _song_activated(self, item): - self.plugin.mpclient.play(item.data(0, QtCore.Qt.UserRole).toPyObject().id()) + self.plugin.mpclient.play(item.id) def fill(self): columns = self.plugin.settings.value(self.plugin.name + '/columns').toStringList() self.clear() - for song in self.plugin.mpclient.playlist(): - item = QtGui.QTreeWidgetItem() + for song in self.plugin.mpclient.playlistinfo(): + item = PlaylistWidget.PlaylistSongItem(song['id']) for i in range(len(columns)): - item.setText(i, unicode(song.tag(str(columns[i])))) - item.setData(0, QtCore.Qt.UserRole, QVariant(song)) + item.setText(i, unicode(song[str(columns[i])])) self.addTopLevelItem(item) def keyPressEvent(self, event): if event.matches(QtGui.QKeySequence.Delete): ids = [] for item in self.selectedItems(): - ids.append(item.data(0, QtCore.Qt.UserRole).toPyObject().id()) + ids.append(item.id) self.plugin.mpclient.delete(ids) else: diff --git a/nephilim/plugins/Songinfo.py b/nephilim/plugins/Songinfo.py index cad5dd0..144033b 100644 --- a/nephilim/plugins/Songinfo.py +++ b/nephilim/plugins/Songinfo.py @@ -105,7 +105,7 @@ class Songinfo(Plugin): song = self.mpclient.current_song() for tag in self.__tags: - metadata[tag] = song.tag(str(tag), '') if song else '' #XXX i don't like the explicit conversion to python string + metadata[tag] = song[str(tag)] if song else '' #XXX i don't like the explicit conversion to python string self.o.set_metadata(metadata) class SonginfoWidget(QtGui.QWidget): diff --git a/nephilim/song.py b/nephilim/song.py index 0f1a627..c9c1145 100644 --- a/nephilim/song.py +++ b/nephilim/song.py @@ -1,5 +1,4 @@ # -# Copyright (C) 2008 jerous <jerous@gmail.com> # Copyright (C) 2009 Anton Khirnov <wyskas@gmail.com> # # Nephilim is free software: you can redistribute it and/or modify @@ -21,80 +20,103 @@ import os from common import sec2min -class Song: - """The Song class offers an abstraction of a song.""" - _data = None +class Song(dict): + """Song provides a dictionary-like wrapper around song metadata. + Its instances _shouldn't_ be stored, use SongRef or PlaylistEntryRef for that.""" def __init__(self, data): - self._data = data - if 'track' in self._data: - try: - self._data['track'] = int(self._data['track']) - except ValueError: - # OGG tracks come in #track/#total format - try: - self._data['track'] = int(self._data['track'].split('/')[0]) - except ValueError: - self._data['track'] = 0 + # decode all strings to unicode + for tag in data: + if isinstance(data[tag], str): + data[tag] = data[tag].decode('utf-8') - # ensure all string-values are utf-8 encoded - for tag in self._data.keys(): - if isinstance(self._data[tag], str): - self._data[tag] = self._data[tag].decode('utf-8') - if 'time' in self._data: - self._data['time'] = int(self._data['time']) - self._data['timems'] = '%i:%i'%(self._data['time'] / 60, self._data['time'] % 60) - self._data['length'] = sec2min(self._data['time']) + dict.__init__(self, data) def __eq__(self, other): if not isinstance(other, Song): return NotImplemented - return self._data['file'] == other._data['file'] + return self['file'] == other['file'] def __ne__(self, other): if not isinstance(other, Song): return NotImplemented - return self._data['file'] != other._data['file'] + return self['file'] != other['file'] - def id(self): - """Get the song's playlist ID. (-1 if not in playlist).""" - return self.tag('id', '-1') + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + if key == 'tracknum': + try: + return int(self['track']) + except ValueError: + try: + return int(self['track'].split('/')[0]) + except ValueError: + return 0 + elif key == 'length': + return sec2min(int(self['time'])) + elif key == 'id': + return '-1' + elif key == 'title': + return self['file'] + return '' + + def __nonzero__(self): + return bool(self['file']) - def title(self): - """Get the song's title (or filename if it has no title).""" - return self.tag('title', self._data['file']) + def expand_tags(self, str): + """Expands tags in form $tag in str.""" + ret = str + ret = ret.replace('$title', self['title']) #to ensure that it is set to at least filename + for tag in self.keys() + ['tracknum', 'length', 'id']: + ret = ret.replace('$' + tag, unicode(self[tag])) + ret = ret.replace('$songdir', os.path.dirname(self['file'])) + return ret - def artist(self): - """Get the song's artist.""" - return self.tag('artist') - def track(self): - """Get the song's track number.""" - return self.tag('track') +class SongRef: + """SongRef stores only a reference to the song (uri) instead + of full metadata to conserve memory. Song's tags can be accessed + as in Song, but it requires a call to MPD for each tag. """ + path = None + mpclient = None - def album(self): - """Get the album.""" - return self.tag('album') + def __init__(self, mpclient, path): + self.mpclient = mpclient + self.path = path - def filepath(self): - """Get the filepath.""" - return self._data['file'] + def __getitem__(self, key): + return self.song()[key] - def tag(self, tag, default=''): - """Get a tag. If it doesn't exist, return default.""" - if tag in self._data: - return self._data[tag] - if tag=='song': - return self.__str__() + def __nonzero__(self): + return bool(self.path) - return default + def song(self): + try: + return Song(self.mpclient.find('file', self.path)[0]) + except IndexError: + return Song({}) - def expand_tags(self, str): - """Expands tags in form $tag in str.""" - ret = str - ret = ret.replace('$title', self.title()) #to ensure that it is set to at least filename - for tag in self._data: - ret = ret.replace('$' + tag, unicode(self._data[tag])) - ret = ret.replace('$songdir', os.path.dirname(self.filepath())) - return ret +class PlaylistEntryRef: + """This class stores a reference to a playlist entry instead of full + metadata to conserve memory. Song's tags can be accessed + as in Song, but it requires a call to MPD for each tag. """ + id = None + mpclient = None + + def __init__(self, mpclient, id): + self.mpclient = mpclient + self.id = id + + def __getitem__(self, key): + return self.song()[key] + + def __nonzero__(self): + return self.id != '-1' + def song(self): + try: + return Song(self.mpclient.playlistid(id)[0]) + except IndexError: + return Song({}) diff --git a/nephilim/winMain.py b/nephilim/winMain.py index 31ab17f..728db42 100644 --- a/nephilim/winMain.py +++ b/nephilim/winMain.py @@ -178,8 +178,8 @@ class winMain(QtGui.QMainWindow): state = self.mpclient.status()['state'] state = 'playing' if state == 'play' else 'paused' if state == 'pause' else 'stopped' if song: - self.setWindowTitle('%s by %s - %s [%s]'%(song.title(), song.artist(), APPNAME, state)) - self.__statuslabel.setText('%s by %s on %s [%s]'%(song.title(), song.artist(),song.album(), state)) + self.setWindowTitle('%s by %s - %s [%s]'%(song['title'], song['artist'], APPNAME, state)) + self.__statuslabel.setText('%s by %s on %s [%s]'%(song['title'], song['artist'],song['album'], state)) else: self.setWindowTitle(APPNAME) self.__statuslabel.setText('') |