diff --git a/sonata/consts.py b/sonata/consts.py index 8e54a49e..74658f47 100644 --- a/sonata/consts.py +++ b/sonata/consts.py @@ -22,8 +22,9 @@ def __init__(self): self.ART_LOCAL_REMOTE = 1 self.VIEW_FILESYSTEM = 0 self.VIEW_ARTIST = 1 - self.VIEW_GENRE = 2 - self.VIEW_ALBUM = 3 + self.VIEW_COMPOSER = 2 + self.VIEW_GENRE = 3 + self.VIEW_ALBUM = 4 self.LYRIC_TIMEOUT = 10 self.NOTIFICATION_WIDTH_MAX = 500 self.NOTIFICATION_WIDTH_MIN = 350 @@ -44,9 +45,10 @@ def __init__(self): self.COVERS_TYPE_STANDARD = 0 self.COVERS_TYPE_STYLIZED = 1 self.LIB_LEVEL_GENRE = 0 - self.LIB_LEVEL_ARTIST = 1 - self.LIB_LEVEL_ALBUM = 2 - self.LIB_LEVEL_SONG = 3 + self.LIB_LEVEL_COMPOSER = 1 + self.LIB_LEVEL_ARTIST = 2 + self.LIB_LEVEL_ALBUM = 3 + self.LIB_LEVEL_SONG = 4 self.NUM_ARTISTS_FOR_VA = 2 # the names of the plug-ins that will be enabled by default diff --git a/sonata/current.py b/sonata/current.py index 6775e2c6..268b30e2 100644 --- a/sonata/current.py +++ b/sonata/current.py @@ -350,6 +350,9 @@ def on_current_column_click(self, column): self.sort('col' + str(col_num), column) return + def on_sort_by_composer(self, _action): + self.sort('composer') + def on_sort_by_artist(self, _action): self.sort('artist') @@ -398,7 +401,12 @@ def sort(self, mode, column=None): # Those items that don't have the specified tag will be put at # the end of the list (hence the 'zzzzzzz'): zzz = 'zzzzzzzz' - if mode == 'artist': + if mode == 'composer': + record["sortby"] = (misc.lower_no_the(mpdh.get(track, 'composer', zzz)), + mpdh.get(track, 'artist', zzz).lower(), + mpdh.get(track, 'disc', '0', True, 0), + mpdh.get(track, 'track', '0', True, 0)) + elif mode == 'artist': record["sortby"] = (misc.lower_no_the(mpdh.get(track, 'artist', zzz)), mpdh.get(track, 'album', zzz).lower(), mpdh.get(track, 'disc', '0', True, 0), diff --git a/sonata/info.py b/sonata/info.py index b6a36752..2496df54 100644 --- a/sonata/info.py +++ b/sonata/info.py @@ -77,6 +77,8 @@ def _widgets_song(self): self.info_labels = {} self.info_boxes_in_more = [] labels = [(_("Title"), 'title', False, "", False), + (_("Composer"), 'composer', True, + _("Launch composer in Wikipedia"), False), (_("Artist"), 'artist', True, _("Launch artist in Wikipedia"), False), (_("Album"), 'album', True, @@ -232,6 +234,7 @@ def update(self, playing_or_paused, newbitrate, songinfo, update_all): getattr(self, "_update_%s" % func)(songinfo) def _update_song(self, songinfo): + composerlabel = self.info_labels['composer'] artistlabel = self.info_labels['artist'] tracklabel = self.info_labels['track'] albumlabel = self.info_labels['album'] @@ -242,6 +245,9 @@ def _update_song(self, songinfo): label.set_text(mpdh.get(songinfo, name)) tracklabel.set_text(mpdh.get(songinfo, 'track', '', False)) + composerlabel.set_markup(misc.link_markup(misc.escape_html( + mpdh.get(songinfo, 'composer')), False, False, + self.linkcolor)) artistlabel.set_markup(misc.link_markup(misc.escape_html( mpdh.get(songinfo, 'artist')), False, False, self.linkcolor)) diff --git a/sonata/library.py b/sonata/library.py index b2923573..424c6528 100644 --- a/sonata/library.py +++ b/sonata/library.py @@ -10,16 +10,17 @@ from consts import consts import breadcrumbs -def library_set_data(album=None, artist=None, genre=None, year=None, path=None): +def library_set_data(album=None, artist=None, composer=None, genre=None, year=None, path=None): if album is not None: album = unicode(album) if artist is not None: artist = unicode(artist) + if composer is not None: composer = unicode(composer) if genre is not None: genre = unicode(genre) if year is not None: year = unicode(year) if path is not None: path = unicode(path) - return (album, artist, genre, year, path) + return (album, artist, composer, genre, year, path) def library_get_data(data, *args): - name_to_index = {'album':0, 'artist':1, 'genre':2, 'year':3, 'path':4} + name_to_index = {'album':0, 'artist':1, 'composer':2, 'genre':3, 'year':4, 'path':5} # Data retrieved from the gtktreeview model is not in # unicode anymore, so convert it. retlist = [unicode(data[name_to_index[arg]]) if data[name_to_index[arg]] else None for arg in args] @@ -45,8 +46,8 @@ def __init__(self, config, client, artwork, TAB_LIBRARY, album_filename, setting self.NOTAG = _("Untagged") self.VAstr = _("Various Artists") - self.search_terms = [_('Artist'), _('Title'), _('Album'), _('Genre'), _('Filename'), _('Everything')] - self.search_terms_mpd = ['artist', 'title', 'album', 'genre', 'file', 'any'] + self.search_terms = [_('Artist'), _('Title'), _('Album'), _('Genre'), _('Filename'), _('Composer'), _('Everything!')] + self.search_terms_mpd = ['artist', 'title', 'album', 'genre', 'file', 'composer', 'any'] self.libfilterbox_cmd_buf = None self.libfilterbox_cond = None @@ -61,8 +62,10 @@ def __init__(self, config, client, artwork, TAB_LIBRARY, album_filename, setting self.lib_view_filesystem_cache = None self.lib_view_artist_cache = None + self.lib_view_composer_cache = None self.lib_view_genre_cache = None self.lib_view_album_cache = None + self.lib_list_composers = None self.lib_list_genres = None self.lib_list_artists = None self.lib_list_albums = None @@ -105,6 +108,7 @@ def __init__(self, config, client, artwork, TAB_LIBRARY, album_filename, setting self.openpb = self.library.render_icon(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU) self.harddiskpb = self.library.render_icon(gtk.STOCK_HARDDISK, gtk.ICON_SIZE_MENU) self.albumpb = gtk.gdk.pixbuf_new_from_file_at_size(album_filename, consts.LIB_COVER_SIZE, consts.LIB_COVER_SIZE) + self.composerpb = self.library.render_icon('composer', gtk.ICON_SIZE_LARGE_TOOLBAR) self.genrepb = self.library.render_icon('gtk-orientation-portrait', gtk.ICON_SIZE_LARGE_TOOLBAR) self.artistpb = self.library.render_icon('artist', gtk.ICON_SIZE_LARGE_TOOLBAR) self.sonatapb = self.library.render_icon('sonata', gtk.ICON_SIZE_MENU) @@ -119,6 +123,8 @@ def __init__(self, config, client, artwork, TAB_LIBRARY, album_filename, setting 'artist', _("Artists")), (consts.VIEW_GENRE, 'genre', gtk.STOCK_ORIENTATION_PORTRAIT, _("Genres")), + (consts.VIEW_COMPOSER, 'composer', + 'composer', _("Composers")), ] self.library_view_assign_image() @@ -192,6 +198,8 @@ def on_libraryview_chosen(self, action): self.config.lib_view = consts.VIEW_FILESYSTEM elif action.get_name() == 'artistview': self.config.lib_view = consts.VIEW_ARTIST + elif action.get_name() == 'composerview': + self.config.lib_view = consts.VIEW_COMPOSER elif action.get_name() == 'genreview': self.config.lib_view = consts.VIEW_GENRE elif action.get_name() == 'albumview': @@ -220,8 +228,10 @@ def view_caches_reset(self): self.lib_view_filesystem_cache = None self.lib_view_artist_cache = None self.lib_view_genre_cache = None + self.lib_view_composer_cache = None self.lib_view_album_cache = None self.lib_list_genres = None + self.lib_list_composers = None self.lib_list_artists = None self.lib_list_albums = None self.lib_list_years = None @@ -255,6 +265,7 @@ def _on_library_scrolled(self): def library_browse(self, _widget=None, root=None): # Populates the library list with entries + #print "root = ", root if not self.connected(): return @@ -335,6 +346,23 @@ def library_browse(self, _widget=None, root=None): bd = self.library_populate_data(artist=artist) else: bd = self.library_populate_toplevel_data(artistview=True) + elif self.config.lib_view == consts.VIEW_COMPOSER: + composer, album, artist, year = self.library_get_data(self.config.wd, 'composer', 'album', 'artist', 'year') + # print "config.wd = ", self.config.wd + # print "composer = ", composer + # print "artist = ", artist + if composer is not None and artist is not None and year is not None: + bd = self.library_populate_data(composer=composer, artist=artist, year=year) + elif composer is not None and artist is not None: + bd = self.library_populate_data(composer=composer, artist=artist) + elif album is not None: + bd = self.library_populate_data(album=album) + elif artist is not None: + bd = self.library_populate_data(artist=artist) + elif composer is not None: + bd = self.library_populate_data(composer=composer) + else: + bd = self.library_populate_toplevel_data(composerview=True) elif self.config.lib_view == consts.VIEW_GENRE: genre, artist, album, year = self.library_get_data(self.config.wd, 'genre', 'artist', 'album', 'year') if genre is not None and artist is not None and album is not None: @@ -349,6 +377,7 @@ def library_browse(self, _widget=None, root=None): if len(bd) == 0: # Nothing found; go up a level until we reach the top level # or results are found + #print "Nothing found!" last_wd = self.config.wd self.config.wd = self.library_get_parent() if self.config.wd == last_wd: @@ -410,17 +439,18 @@ def update_breadcrumbs(self): else: if view == consts.VIEW_ALBUM: # We don't want to show an artist button in album view - keys = 'genre', 'album' - nkeys = 2 - else: - keys = 'genre', 'artist', 'album' + keys = 'genre', 'composer', 'album' nkeys = 3 + else: + keys = 'genre', 'composer', 'artist', 'album' + nkeys = 4 parts = self.library_get_data(self.config.wd, *keys) # append a crumb for each part for i, key, part in zip(range(nkeys), keys, parts): if part is None: continue partdata = dict(zip(keys, parts)[:i+1]) + #print "partdata = ", partdata target = self.library_set_data(**partdata) pb, icon = None, None if key == 'album': @@ -432,6 +462,8 @@ def update_breadcrumbs(self): icon = 'album' elif key == 'artist': icon = 'artist' + elif key == 'composer': + icon = 'composer' else: icon = gtk.STOCK_ORIENTATION_PORTRAIT crumbs.append((part, icon, pb, target)) @@ -439,6 +471,7 @@ def update_breadcrumbs(self): # add a button for each crumb for crumb in crumbs: text, icon, pb, target = crumb + #print "target = ", target text = misc.escape_html(text) if crumb is crumbs[-1]: text = "%s" % text @@ -494,8 +527,10 @@ def library_populate_filesystem_data(self, path): self.lib_view_filesystem_cache = bd return bd - def library_get_toplevel_cache(self, genreview=False, artistview=False, albumview=False): - if genreview and self.lib_view_genre_cache is not None: + def library_get_toplevel_cache(self, composerview=False, genreview=False, artistview=False, albumview=False): + if composerview and self.lib_view_composer_cache is not None: + bd = self.lib_view_composer_cache + elif genreview and self.lib_view_genre_cache is not None: bd = self.lib_view_genre_cache elif artistview and self.lib_view_artist_cache is not None: bd = self.lib_view_artist_cache @@ -514,27 +549,33 @@ def library_get_toplevel_cache(self, genreview=False, artistview=False, albumvie info[0] = pb2 return bd - def library_populate_toplevel_data(self, genreview=False, artistview=False, albumview=False): - bd = self.library_get_toplevel_cache(genreview, artistview, albumview) + def library_populate_toplevel_data(self, composerview=False, genreview=False, artistview=False, albumview=False): + bd = self.library_get_toplevel_cache(composerview, genreview, artistview, albumview) if bd is not None: # We have our cached data, woot. return bd bd = [] - if genreview or artistview: + if genreview or artistview or composerview: # Only for artist/genre views, album view is handled differently # since multiple artists can have the same album name if genreview: items = self.library_return_list_items('genre') pb = self.genrepb - else: + elif artistview: items = self.library_return_list_items('artist') pb = self.artistpb + else: + items = self.library_return_list_items('composer') + pb = self.composerpb if not (self.NOTAG in items): items.append(self.NOTAG) for item in items: if genreview: playtime, num_songs = self.library_return_count(genre=item) data = self.library_set_data(genre=item) + elif composerview: + playtime, num_songs = self.library_return_count(composer=item) + data = self.library_set_data(composer=item) else: playtime, num_songs = self.library_return_count(artist=item) data = self.library_set_data(artist=item) @@ -579,6 +620,8 @@ def library_populate_toplevel_data(self, genreview=False, artistview=False, albu self.lib_view_artist_cache = bd elif albumview: self.lib_view_album_cache = bd + elif composerview: + self.lib_view_composer_cache = bd return bd def list_identify_VA_albums(self, albums): @@ -614,10 +657,10 @@ def list_identify_VA_albums(self, albums): def get_VAstr(self): return self.VAstr - def library_populate_data(self, genre=None, artist=None, album=None, year=None): + def library_populate_data(self, composer=None, genre=None, artist=None, album=None, year=None): # Create treeview model info bd = [] - if genre is not None and artist is None and album is None: + if genre is not None and artist is None and album is None and composer is None: # Artists within a genre artists = self.library_return_list_items('artist', genre=genre) if len(artists) > 0: @@ -630,11 +673,24 @@ def library_populate_data(self, genre=None, artist=None, album=None, year=None): display += self.add_display_info(num_songs, int(playtime)/60) data = self.library_set_data(genre=genre, artist=artist) bd += [(misc.lower_no_the(artist), [self.artistpb, data, display])] + elif composer is not None and artist is None and album is None: + # Artists within a genre + artists = self.library_return_list_items('artist', composer=composer) + if len(artists) > 0: + if not self.NOTAG in artists: + artists.append(self.NOTAG) + for artist in artists: + playtime, num_songs = self.library_return_count(composer=composer, artist=artist) + if num_songs > 0: + display = misc.escape_html(artist) + display += self.add_display_info(num_songs, int(playtime)/60) + data = self.library_set_data(composer=composer, artist=artist) + bd += [(misc.lower_no_the(artist), [self.artistpb, data, display])] elif artist is not None and album is None: # Albums/songs within an artist and possibly genre # Albums first: - if genre is not None: - albums = self.library_return_list_items('album', genre=genre, artist=artist) + if composer is not None: + albums = self.library_return_list_items('album', composer=composer, artist=artist) else: albums = self.library_return_list_items('album', artist=artist) for album in albums: @@ -669,18 +725,20 @@ def library_populate_data(self, genre=None, artist=None, album=None, year=None): pb = self.artwork.get_library_artwork_cached_pb(cache_data, self.albumpb) bd += [(ordered_year + misc.lower_no_the(album), [pb, data, display])] # Now, songs not in albums: - bd += self.library_populate_data_songs(genre, artist, self.NOTAG, None) + bd += self.library_populate_data_songs(composer, genre, artist, self.NOTAG, None) else: # Songs within an album, artist, year, and possibly genre - bd += self.library_populate_data_songs(genre, artist, album, year) + bd += self.library_populate_data_songs(composer, genre, artist, album, year) if len(bd) > 0: bd = self.library_populate_add_parent_rows() + bd bd.sort(locale.strcoll, key=operator.itemgetter(0)) return bd - def library_populate_data_songs(self, genre, artist, album, year): + def library_populate_data_songs(self, composer, genre, artist, album, year): bd = [] - if genre is not None: + if composer is not None: + songs, _playtime, _num_songs = self.library_return_search_items(composer=composer, genre=genre, artist=artist, album=album, year=year) + elif genre is not None: songs, _playtime, _num_songs = self.library_return_search_items(genre=genre, artist=artist, album=album, year=year) else: songs, _playtime, _num_songs = self.library_return_search_items(artist=artist, album=album, year=year) @@ -694,21 +752,21 @@ def library_populate_data_songs(self, genre, artist, album, year): bd += [('f' + disc + track + unicode(mpdh.get(song, 'file')).lower(), [self.sonatapb, data, formatting.parse(self.config.libraryformat, song, True)])] return bd - def library_return_list_items(self, itemtype, genre=None, artist=None, album=None, year=None, ignore_case=True): + def library_return_list_items(self, itemtype, composer=None, genre=None, artist=None, album=None, year=None, ignore_case=True): # Returns all items of tag 'itemtype', in alphabetical order, # using mpd's 'list'. If searchtype is passed, use # a case insensitive search, via additional 'list' # queries, since using a single 'list' call will be # case sensitive. results = [] - searches = self.library_compose_list_count_searchlist(genre, artist, album, year) + searches = self.library_compose_list_count_searchlist(composer, genre, artist, album, year) if len(searches) > 0: for s in searches: # If we have untagged tags (''), use search instead # of list because list will not return anything. if '' in s: items = [] - songs, playtime, num_songs = self.library_return_search_items(genre, artist, album, year) + songs, playtime, num_songs = self.library_return_search_items(composer, genre, artist, album, year) for song in songs: items.append(mpdh.get(song, itemtype)) else: @@ -717,7 +775,7 @@ def library_return_list_items(self, itemtype, genre=None, artist=None, album=Non if len(item) > 0: results.append(item) else: - if genre is None and artist is None and album is None and year is None: + if genre is None and artist is None and album is None and year is None and composer is None: for item in mpdh.call(self.client, 'list', itemtype): if len(item) > 0: results.append(item) @@ -726,13 +784,13 @@ def library_return_list_items(self, itemtype, genre=None, artist=None, album=Non results.sort(locale.strcoll) return results - def library_return_count(self, genre=None, artist=None, album=None, year=None): + def library_return_count(self, composer=None, genre=None, artist=None, album=None, year=None): # Because mpd's 'count' is case sensitive, we have to # determine all equivalent items (case insensitive) and # call 'count' for each of them. Using 'list' + 'count' # involves much less data to be transferred back and # forth than to use 'search' and count manually. - searches = self.library_compose_list_count_searchlist(genre, artist, album, year) + searches = self.library_compose_list_count_searchlist(composer, genre, artist, album, year) playtime = 0 num_songs = 0 for s in searches: @@ -741,7 +799,7 @@ def library_return_count(self, genre=None, artist=None, album=None, year=None): # Can't return count for empty tags, use search instead: - _results, playtime, num_songs = self.library_return_search_items(genre=genre, artist=artist, album=album, year=year) + _results, playtime, num_songs = self.library_return_search_items(composer=composer, genre=genre, artist=artist, album=album, year=year) else: @@ -778,8 +836,11 @@ def library_compose_list_count_searchlist_single(self, search, typename, cached_ s = searchlist return s, cached_list - def library_compose_list_count_searchlist(self, genre=None, artist=None, album=None, year=None): + def library_compose_list_count_searchlist(self, composer=None, genre=None, artist=None, album=None, year=None): s = [] + s, self.lib_list_composers = self.library_compose_list_count_searchlist_single(composer, 'composer', self.lib_list_composers, s) + if s is None: + return [] s, self.lib_list_genres = self.library_compose_list_count_searchlist_single(genre, 'genre', self.lib_list_genres, s) if s is None: return [] @@ -812,18 +873,19 @@ def library_compose_search_searchlist_single(self, search, typename, searchlist) s = searchlist return s - def library_compose_search_searchlist(self, genre=None, artist=None, album=None, year=None): + def library_compose_search_searchlist(self, composer=None, genre=None, artist=None, album=None, year=None): s = [] + s = self.library_compose_search_searchlist_single(composer, 'composer', s) s = self.library_compose_search_searchlist_single(genre, 'genre', s) s = self.library_compose_search_searchlist_single(album, 'album', s) s = self.library_compose_search_searchlist_single(artist, 'artist', s) s = self.library_compose_search_searchlist_single(year, 'date', s) return s - def library_return_search_items(self, genre=None, artist=None, album=None, year=None): + def library_return_search_items(self, composer=None, genre=None, artist=None, album=None, year=None): # Returns all mpd items, using mpd's 'search', along with # playtime and num_songs. - searches = self.library_compose_search_searchlist(genre, artist, album, year) + searches = self.library_compose_search_searchlist(composer, genre, artist, album, year) for s in searches: args_tuple = tuple(map(str, s)) playtime = 0 @@ -925,14 +987,15 @@ def library_get_data_level(self, data): # Returns the number of items stored in data, excluding # the path: level = 0 - album, artist, genre, year = library_get_data(data, 'album', 'artist', 'genre', 'year') - for item in [album, artist, genre, year]: + album, artist, composer, genre, year = library_get_data(data, 'album', 'artist', 'composer', 'genre', 'year') + for item in [album, artist, composer, genre, year]: if item is not None: level += 1 return level def on_library_key_press(self, widget, event): if event.keyval == gtk.gdk.keyval_from_name('Return'): + #print "key_press: ", widget.get_cursor()[0] self.on_library_row_activated(widget, widget.get_cursor()[0]) return True @@ -983,6 +1046,12 @@ def on_library_row_activated(self, _widget, path, _column=0): else: return value = self.librarydata.get_value(self.librarydata.get_iter(path), 1) + + #print "value = ", value + # print "path = ", path + # print "self.librarydata.get_iter(path) = ", self.librarydata.get_iter(path) + # print "self.librarydata = ", self.librarydata + icon = self.librarydata.get_value(self.librarydata.get_iter(path), 0) if icon == self.sonatapb: # Song found, add item @@ -1009,6 +1078,14 @@ def library_get_parent(self): value = self.library_set_data(genre=genre) else: value = self.library_set_data(path="/") + elif self.config.lib_view == consts.VIEW_COMPOSER: + album, artist, composer = self.library_get_data(self.config.wd, 'album', 'artist', 'composer') + if album is not None: + value = self.library_set_data(composer=composer, artist=artist) + elif artist is not None: + value = self.library_set_data(composer=composer) + else: + value = self.library_set_data(path="/") else: newvalue = '/'.join(self.library_get_data(self.config.wd, 'path').split('/')[:-1]) or '/' value = self.library_set_data(path=newvalue) @@ -1018,6 +1095,7 @@ def library_browse_parent(self, _action): if not self.search_visible(): if self.library.is_focus(): value = self.library_get_parent() + #print "value = ", value self.library_browse(None, value) return True @@ -1049,8 +1127,8 @@ def get_path_child_filenames(self, return_root, selected_only=True): data = model.get_value(i, 1) value = model.get_value(i, 2) if value != ".." and value != "/": - album, artist, year, genre, path = self.library_get_data(data, 'album', 'artist', 'year', 'genre', 'path') - if path is not None and album is None and artist is None and year is None and genre is None: + album, artist, year, composer, genre, path = self.library_get_data(data, 'album', 'artist', 'year', 'composer', 'genre', 'path') + if path is not None and album is None and artist is None and year is None and genre is None and composer is None: if pb == self.sonatapb: # File items.append(path) @@ -1061,7 +1139,7 @@ def get_path_child_filenames(self, return_root, selected_only=True): else: items.append(path) else: - results, _playtime, _num_songs = self.library_return_search_items(genre=genre, artist=artist, album=album, year=year) + results, _playtime, _num_songs = self.library_return_search_items(composer=composer, genre=genre, artist=artist, album=album, year=year) for item in results: items.append(mpdh.get(item, 'file')) # Make sure we don't have any EXACT duplicates: diff --git a/sonata/main.py b/sonata/main.py index 3af99653..5122f661 100644 --- a/sonata/main.py +++ b/sonata/main.py @@ -87,6 +87,7 @@ def __init__(self, args, window=None, _sugar=False): self.remote_albumentry = None self.remote_artistentry = None + self.remote_composerentry = None self.remote_dest_filename = None self.remotefilelist = None self.seekidle = None @@ -197,6 +198,7 @@ def __init__(self, args, window=None, _sugar=False): self.iconfactory = gtk.IconFactory() ui.icon(self.iconfactory, 'sonata', self.find_path('sonata.png')) ui.icon(self.iconfactory, 'artist', self.find_path('sonata-artist.png')) + ui.icon(self.iconfactory, 'composer', self.find_path('sonata-composer.png')) ui.icon(self.iconfactory, 'album', self.find_path('sonata-album.png')) icon_theme = gtk.icon_theme_get_default() if HAVE_SUGAR: @@ -340,6 +342,7 @@ def __init__(self, args, window=None, _sugar=False): + @@ -368,6 +371,7 @@ def __init__(self, args, window=None, _sugar=False): + @@ -423,6 +427,7 @@ def __init__(self, args, window=None, _sugar=False): currentactions = [ ('centerplaylistkey', None, 'Center Playlist Key', 'i', None, self.current.center_song_in_list), + ('sortbycomposer', None, _('By Composer'), None, None, self.current.on_sort_by_composer), ('sortbyartist', None, _('By Artist'), None, None, self.current.on_sort_by_artist), ('sortbyalbum', None, _('By Album'), None, None, self.current.on_sort_by_album), ('sortbytitle', None, _('By Song Title'), None, None, self.current.on_sort_by_title), @@ -1787,9 +1792,12 @@ def update_infofile(self): info_file.write('Title: ' + mpdh.get(self.songinfo, 'artist') + ' - ' + mpdh.get(self.songinfo, 'title') + '\n') except: try: - info_file.write('Title: ' + mpdh.get(self.songinfo, 'title') + '\n') # No Arist in streams + info_file.write('Title: ' + mpdh.get(self.songinfo, 'composer') + ' - ' + mpdh.get(self.songinfo, 'title') + '\n') except: - info_file.write('Title: No - ID Tag\n') + try: + info_file.write('Title: ' + mpdh.get(self.songinfo, 'title') + '\n') # No Arist in streams + except: + info_file.write('Title: No - ID Tag\n') info_file.write('Album: ' + mpdh.get(self.songinfo, 'album', 'No Data') + '\n') info_file.write('Track: ' + mpdh.get(self.songinfo, 'track', '0') + '\n') info_file.write('File: ' + mpdh.get(self.songinfo, 'file', 'No Data') + '\n') @@ -2035,16 +2043,18 @@ def on_image_activate(self, widget, event): else: self.switch_to_tab_name(self.last_tab) elif event.button == 3: + composer = None artist = None album = None stream = None if self.status_is_play_or_pause(): self.UIManager.get_widget('/imagemenu/chooseimage_menu/').show() self.UIManager.get_widget('/imagemenu/localimage_menu/').show() + composer = mpdh.get(self.songinfo, 'composer', None) artist = mpdh.get(self.songinfo, 'artist', None) album = mpdh.get(self.songinfo, 'album', None) stream = mpdh.get(self.songinfo, 'name', None) - if not (artist or album or stream): + if not (composer or artist or album or stream): self.UIManager.get_widget('/imagemenu/localimage_menu/').hide() self.UIManager.get_widget('/imagemenu/resetimage_menu/').hide() self.UIManager.get_widget('/imagemenu/chooseimage_menu/').hide() @@ -2282,11 +2292,12 @@ def image_remote(self, _widget): hbox = gtk.HBox() vbox = gtk.VBox() vbox.pack_start(ui.label(markup=' '), False, False, 0) + self.remote_composerentry = ui.entry() self.remote_artistentry = ui.entry() self.remote_albumentry = ui.entry() - text = [("Artist"), _("Album")] + text = [("Composer"), ("Artist"), _("Album")] labels = [ui.label(text=labelname + ": ") for labelname in text] - entries = [self.remote_artistentry, self.remote_albumentry] + entries = [self.remote_composerentry, self.remote_artistentry, self.remote_albumentry] for entry, label in zip(entries, labels): tmphbox = gtk.HBox() tmphbox.pack_start(label, False, False, 5) @@ -2319,6 +2330,7 @@ def image_remote(self, _widget): artist = self.album_current_artist[1] imagewidget.connect('item-activated', self.image_remote_replace_cover, artist.replace("/", ""), album.replace("/", ""), stream) self.choose_dialog.connect('response', self.image_remote_response, imagewidget, artist, album, stream) + self.remote_composerentry.set_text(composer) self.remote_artistentry.set_text(artist) self.remote_albumentry.set_text(album) self.allow_art_search = True @@ -2888,7 +2900,9 @@ def seek(self, song, seektime): def on_link_click(self, linktype): browser_not_loaded = False - if linktype == 'artist': + if linktype == 'composer': + browser_not_loaded = not misc.browser_load("http://www.wikipedia.org/wiki/Special:Search/" + urllib.quote(mpdh.get(self.songinfo, 'composer')), self.config.url_browser, self.window) + elif linktype == 'artist': browser_not_loaded = not misc.browser_load("http://www.wikipedia.org/wiki/Special:Search/" + urllib.quote(mpdh.get(self.songinfo, 'artist')), self.config.url_browser, self.window) elif linktype == 'album': browser_not_loaded = not misc.browser_load("http://www.wikipedia.org/wiki/Special:Search/" + urllib.quote(mpdh.get(self.songinfo, 'album')), self.config.url_browser, self.window) @@ -3130,7 +3144,8 @@ def on_about(self, _action): if self.conn: # Extract some MPD stats: mpdstats = mpdh.call(self.client, 'stats') - stats = {'artists': mpdstats['artists'], + stats = {'composers': mpdstats['composers'], + 'artists': mpdstats['artists'], 'albums': mpdstats['albums'], 'songs': mpdstats['songs'], 'db_playtime': mpdstats['db_playtime'], diff --git a/sonata/pixmaps/sonata-composer.png b/sonata/pixmaps/sonata-composer.png new file mode 100644 index 00000000..d914afd9 Binary files /dev/null and b/sonata/pixmaps/sonata-composer.png differ diff --git a/sonata/tagedit.py b/sonata/tagedit.py index 05e040d9..da973ec7 100644 --- a/sonata/tagedit.py +++ b/sonata/tagedit.py @@ -16,6 +16,8 @@ tagpy = None # module loaded when needed import ui, misc +#from ctypes import * +#from ptr import * class TagEditor(): @@ -85,8 +87,9 @@ def on_tags_edit(self, files, temp_mpdpaths, music_dir): # Initialize: self.tagnum = -1 - tags = [{'title':'', 'artist':'', 'album':'', 'year':'', 'track':'', + tags = [{'title':'', 'composer':'', 'artist':'', 'album':'', 'year':'', 'track':'', 'genre':'', 'comment':'', 'title-changed':False, + 'composer-changed':False, 'artist-changed':False, 'album-changed':False, 'year-changed':False, 'track-changed':False, 'genre-changed':False, 'comment-changed':False, @@ -123,6 +126,7 @@ def on_tags_edit(self, files, temp_mpdpaths, music_dir): filehbox.pack_start(blanklabel, False, False, 2) titlelabel, titleentry, titlebutton, titlehbox = self._create_label_entry_button_hbox(_("Title:")) + composerlabel, composerentry, composerbutton, composerhbox = self._create_label_entry_button_hbox(_("Composer:")) artistlabel, artistentry, artistbutton, artisthbox = self._create_label_entry_button_hbox(_("Artist:")) albumlabel, albumentry, albumbutton, albumhbox = self._create_label_entry_button_hbox(_("Album:")) yearlabel, yearentry, yearbutton, yearhbox = self._create_label_entry_button_hbox(_("Year:")) @@ -149,9 +153,9 @@ def on_tags_edit(self, files, temp_mpdpaths, music_dir): commentlabel, commententry, commentbutton, commenthbox = self._create_label_entry_button_hbox(_("Comment:")) - ui.set_widths_equal([titlelabel, artistlabel, albumlabel, yearlabel, genrelabel, commentlabel, sonataicon]) + ui.set_widths_equal([titlelabel, composerlabel, artistlabel, albumlabel, yearlabel, genrelabel, commentlabel, sonataicon]) genrecombo.set_size_request(-1, titleentry.size_request()[1]) - tablewidgets = [ui.label(), filehbox, ui.label(), titlehbox, artisthbox, albumhbox, yearandtrackhbox, genrehbox, commenthbox, ui.label()] + tablewidgets = [ui.label(), filehbox, ui.label(), titlehbox, composerhbox, artisthbox, albumhbox, yearandtrackhbox, genrehbox, commenthbox, ui.label()] for i, widget in enumerate(tablewidgets): table.attach(widget, 1, 2, i+1, i+2, gtk.FILL|gtk.EXPAND, gtk.FILL|gtk.EXPAND, 2, 0) editwindow.vbox.pack_start(table) @@ -163,9 +167,9 @@ def on_tags_edit(self, files, temp_mpdpaths, music_dir): editwindow.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT) editwindow.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT) editwindow.connect('delete_event', self.tags_win_hide, tags) - entries = [titleentry, artistentry, albumentry, yearentry, trackentry, genreentry, commententry] - buttons = [titlebutton, artistbutton, albumbutton, yearbutton, trackbutton, genrebutton, commentbutton] - entries_names = ["title", "artist", "album", "year", "track", "genre", "comment"] + entries = [titleentry, composerentry, artistentry, albumentry, yearentry, trackentry, genreentry, commententry] + buttons = [titlebutton, composerbutton, artistbutton, albumbutton, yearbutton, trackbutton, genrebutton, commentbutton] + entries_names = ["title", "composer", "artist", "album", "year", "track", "genre", "comment"] editwindow.connect('response', self.tags_win_response, tags, entries, entries_names) if saveall_button: saveall_button.connect('clicked', self.tags_win_save_all, editwindow, tags, entries, entries_names) @@ -215,7 +219,7 @@ def tags_win_create_apply_all_button(self, button, entry, autotrack=False): def tags_win_apply_all(self, _button, item, tags, entry): for tagnum, tag in enumerate(tags): tagnum = tagnum + 1 - if item in ("title", "album", "artist", "genre", "comment"): + if item in ("title", "album", "composer", "artist", "genre", "comment"): tag[item] = entry.get_text() tag[item + '-changed'] = True elif item == "year": @@ -234,15 +238,64 @@ def tags_win_apply_all(self, _button, item, tags, entry): # Update the entry for the current song: entry.set_text(str(tags[self.tagnum]['track'])) + def getFileFormat(self, fpath): + _, fileext = os.path.splitext( fpath ) + ftype = "" + if fileext in [ ".ogg", ".oga", ".OGG", ".OGA" ]: + ftype = "oga" + elif fileext in [ ".mp3", ".MP3" ]: + ftype = "mp3" + else: + ftype = "" + return ftype + + def getTagCode(self, ext, nm ): + print "ext = ", ext + print "nm = ", nm + if ext == "mp3": + codes = { 'composer': "TCOM" } + elif ext == "oga": + codes = { 'composer': "COMPOSER" } + else: + return "" + return codes[ nm ] + + def getFileOb( self, fpath, ext): + if ext == "mp3": + return tagpy.mpeg.File( fpath ) + elif ext == "oga": + return tagpy.ogg.vorbis.File( fpath ) + else: + return tagpy.FileRef( fpath ) + + def getTagOb( self, fob, ext ): + if ext == "mp3": + return fob.ID3v2Tag() + else: + return fob.tag() + + def tags_win_update(self, window, tags, entries, entries_names): + #print "entries_names = ", entries_names current_tag = tags[self.tagnum] - tag = tagpy.FileRef(current_tag['fullpath']).tag() + #print "current_tag = ", current_tag + fpath = current_tag['fullpath'] + ext = self.getFileFormat( fpath ) + #print "fpath = ", fpath + fob = self.getFileOb( fpath, ext ) + tag = self.getTagOb( fob, ext ) + #print "tag = ", dir(tag) # Update interface: for entry, entry_name in zip(entries, entries_names): # Only retrieve info from the file if the info hasn't changed if not current_tag[entry_name + "-changed"]: - current_tag[entry_name] = getattr(tag, entry_name, '') + if not entry_name in ['composer']: + current_tag[entry_name] = getattr(tag, entry_name, '') + else: + current_tag[entry_name] = self.getOtherFromTag( tag, ext, entry_name ) tag_value = current_tag[entry_name] + #print "entry_name = ", entry_name + #print "tag_value: ", tag_value if tag_value == 0: tag_value = '' entry.set_text(str(tag_value).strip()) @@ -261,6 +314,60 @@ def tags_win_update(self, window, tags, entries, entries_names): (self.tagnum+1, len(tags))) self.tags_win_set_sensitive(window.action_area) + def getOtherFromTag( self, tag, ext, nm ): + # workaround to get other fields e.g. composer (ajd) + cd = self.getTagCode( ext, nm ) + + print "cd = ", cd + + if ext == "mp3": + frmlstmp = tag.frameListMap() + #print "frmlst = ", frmlst.keys() + try: + res = frmlstmp[cd][0].toString() + except: + res = "" + #print "attr: = ", res + return res + elif ext == "oga": + try: + xiphcomp = tag.fieldListMap()[ cd ] + res = xiphcomp[ 0 ] + except: + res = "" + return res + else: + return "" + + + def setOtherToTag( self, tag, ext, nm, val ): + # workaround to set other fields e.g. composer (ajd) + ### Must check file type!!!! + cd = self.getTagCode( ext, nm ) + if ext == "mp3": + frmlstmp = tag.frameListMap() + try: + frmlstmp[cd][0].setText( val ) + except: + newframe = tagpy.id3v2.TextIdentificationFrame( cd ) + newframe.setText( val ) + tag.addFrame( newframe ) + elif ext == "oga": + #try: + tag.addField( cd, val, True ) + #pckt = xiphprops.render().encode('utf-8') + #print "pckt = \n", pckt + #fob.setPacket( 1, pckt ) + #print "new fob: \n", fob.packet( 1 ) + #save_success = fob.save() + #print "saved fob: \n", fob.packet( 1 ) + # except: + # newframe = tagpy.id3v2.TextIdentificationFrame( cd ) + # newframe.setText( val ) + # mtag.addFrame( newframe ) + + + def tags_win_set_sensitive(self, action_area): # Hacky workaround to allow the user to click the save button again when the # mouse stays over the button (see http://bugzilla.gnome.org/show_bug.cgi?id=56070) @@ -275,14 +382,23 @@ def tags_win_save_all(self, _button, window, tags, entries, entries_names): self.tags_win_response(window, gtk.RESPONSE_ACCEPT, tags, entries, entries_names) def tags_win_response(self, window, response, tags, entries, entries_names): + #print "entries_names = ", entries_names + current_tag = tags[self.tagnum] + #print "current_tag = ", current_tag + if response == gtk.RESPONSE_REJECT: self.tags_win_hide(window, None, tags) elif response == gtk.RESPONSE_ACCEPT: window.action_area.set_sensitive(False) while window.action_area.get_property("sensitive") or gtk.events_pending(): gtk.main_iteration() - filetag = tagpy.FileRef(tags[self.tagnum]['fullpath']) - tag = filetag.tag() + fpath = current_tag['fullpath'] + ext = self.getFileFormat( fpath ) + #print "fpath = ", fpath + fob = self.getFileOb( fpath, ext ) + tag = self.getTagOb( fob, ext ) + #print "tag = ", dir(tag) + # Set tag fields according to entry text for entry, field in zip(entries, entries_names): tag_value = entry.get_text().strip() @@ -293,9 +409,12 @@ def tags_win_response(self, window, response, tags, entries, entries_names): if field is 'comment': if len(tag_value) == 0: tag_value = ' ' - setattr(tag, field, tag_value) + if not field in ( 'composer' ): + setattr(tag, field, tag_value) + else: + self.setOtherToTag( tag, ext, field, tag_value ) - save_success = filetag.save() + save_success = fob.save() if not (save_success): # FIXME: was (save_success and self.conn and self.status): ui.show_msg(self.window, _("Unable to save tag to music file."), _("Edit Tags"), 'editTagsError', gtk.BUTTONS_CLOSE, response_cb=ui.dialog_destroy) if self.tags_next_tag(tags):