From 8e16f745600a75b432f16a9335737415643af009 Mon Sep 17 00:00:00 2001 From: Bernd Schlapsi Date: Sat, 3 Jan 2009 17:38:35 +0100 Subject: [PATCH] M3U write support for Sandisk Sansa (bug 251) Improve M3U writing (metadata, Windows-style line breaks when using the Windows-style pathnames) and really generate "relative to m3u" filenames instead of "relative to root" filenames. Thanks to Ville-Pekka Vainio for reporting and testing. --- src/gpodder/config.py | 2 +- src/gpodder/gui.py | 43 +++++++++++++++++++++++++++----- src/gpodder/libtagupdate.py | 61 +++++++++++++++++++++++++++++++++++++++++++++ src/gpodder/util.py | 54 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 7 deletions(-) diff --git a/src/gpodder/config.py b/src/gpodder/config.py index d17012f2..7eeb0db7 100644 --- a/src/gpodder/config.py +++ b/src/gpodder/config.py @@ -97,7 +97,7 @@ gPodderSettings = { # Playlist Management settings 'mp3_player_playlist_file': (str, 'PLAYLISTS/gpodder.m3u'), - 'mp3_player_playlist_absolute_path': (bool, False), + 'mp3_player_playlist_absolute_path': (bool, True), 'mp3_player_playlist_win_path': (bool, True), # Special settings (not in preferences) diff --git a/src/gpodder/gui.py b/src/gpodder/gui.py index 7347f660..63c9eafd 100644 --- a/src/gpodder/gui.py +++ b/src/gpodder/gui.py @@ -39,6 +39,7 @@ from threading import Semaphore from string import strip import gpodder +from gpodder import libtagupdate from gpodder import util from gpodder import opml from gpodder import services @@ -4110,7 +4111,9 @@ class gPodderPlaylist(GladeWidget): finger_friendly_widgets = ['btnCancelPlaylist', 'btnSavePlaylist', 'treeviewPlaylist'] def new(self): - self.m3u_header = '#EXTM3U\n' + self.linebreak = '\n' + if gl.config.mp3_player_playlist_win_path: + self.linebreak = '\r\n' self.mountpoint = util.find_mount_point(gl.config.mp3_player_folder) if self.mountpoint == '/': self.mountpoint = gl.config.mp3_player_folder @@ -4168,31 +4171,54 @@ class gPodderPlaylist(GladeWidget): read all files from the existing playlist """ tracks = [] + log("Read data from the playlistfile %s" % self.playlist_file) if os.path.exists(self.playlist_file): for line in open(self.playlist_file, 'r'): - if line != self.m3u_header: + if not line.startswith('#EXT'): if line.startswith('#'): tracks.append([False, line[1:].strip()]) else: tracks.append([True, line.strip()]) return tracks + def build_extinf(self, filename): + if gl.config.mp3_player_playlist_win_path: + filename = filename.replace('\\', os.sep) + + # rebuild the whole filename including the mountpoint + if gl.config.mp3_player_playlist_absolute_path: + absfile = self.mountpoint + filename + else: + absfile = util.rel2abs(filename, os.path.dirname(self.playlist_file)) + + # read the title from the mp3/ogg tag + metadata = libtagupdate.get_tags_from_file(absfile) + if 'title' in metadata and metadata['title']: + title = metadata['title'] + else: + # fallback: use the basename of the file + (title, extension) = os.path.splitext(os.path.basename(filename)) + + return "#EXTINF:0,%s%s" % (title.strip(), self.linebreak) + def write_m3u(self): """ write the list into the playlist on the device """ + log('Writing playlist file: %s', self.playlist_file, sender=self) playlist_folder = os.path.split(self.playlist_file)[0] if not util.make_directory(playlist_folder): self.show_message(_('Folder %s could not be created.') % playlist_folder, _('Error writing playlist')) else: try: fp = open(self.playlist_file, 'w') - fp.write(self.m3u_header) + fp.write('#EXTM3U%s' % self.linebreak) for icon, checked, filename in self.playlist: + fp.write(self.build_extinf(filename)) if not checked: fp.write('#') fp.write(filename) - fp.write('\n') + fp.write(self.linebreak) fp.close() self.show_message(_('The playlist on your MP3 player has been updated.'), _('Update successful')) except IOError, ioe: @@ -4202,6 +4228,7 @@ class gPodderPlaylist(GladeWidget): """ read all files from the device """ + log('Reading files from %s', gl.config.mp3_player_folder, sender=self) tracks = [] for root, dirs, files in os.walk(gl.config.mp3_player_folder): for file in files: @@ -4212,11 +4239,15 @@ class gPodderPlaylist(GladeWidget): # an entry in our file list, so skip it! break - if not gl.config.mp3_player_playlist_absolute_path: + if gl.config.mp3_player_playlist_absolute_path: filename = filename[len(self.mountpoint):] + else: + filename = util.relpath(os.path.dirname(self.playlist_file), + os.path.dirname(filename)) + \ + os.sep + os.path.basename(filename) if gl.config.mp3_player_playlist_win_path: - filename = filename.replace( '/', '\\') + filename = filename.replace(os.sep, '\\') tracks.append(filename) return tracks diff --git a/src/gpodder/libtagupdate.py b/src/gpodder/libtagupdate.py index 1416fef7..fadabff5 100644 --- a/src/gpodder/libtagupdate.py +++ b/src/gpodder/libtagupdate.py @@ -39,6 +39,9 @@ except: log('(tagupdate) eyed3 not found -- tag update disabled') has_eyed3 = False +# Values that we want to have for our tags +tags_keys = ['artist', 'title', 'album', 'genre'] + # do we provide tagging functions to the user? def tagging_supported(): global has_eyed3 @@ -46,6 +49,7 @@ def tagging_supported(): tag_update_methods = {} +tag_get_methods = {} def update_metadata_on_file( filename, **metadata): global tag_update_methods @@ -106,3 +110,60 @@ def update_tag_mp3( filename, **metadata): tag_update_methods['mp3'] = update_tag_mp3 +def get_tags_from_file(filename): + global tag_get_methods, tags_keys + + ext = filename[-3:] + if ext in tag_get_methods: + log('Reading tags from %s', filename) + return tag_get_methods[ext](filename) + + log('Do not know how to read file extension %s :/', ext) + return dict(map(lambda key: (key, ''), tags_keys)) + + + +def get_tags_ogg(filename): + global tags_keys + + p = popen2.Popen3('vorbiscomment -l "%s"' % filename) + reader = p.fromchild + lines = reader.readlines() + reader.close() + result = (p.wait() == 0) + + tags = dict(map(lambda key: (key, ''), tags_keys)) + + if not result: + log('Error while running vorbiscomment. Is it installed?! (vorbis-tools)') + else: + for line in lines: + (key, value) = line.split('=', 1) + key = key.lower() + if key in tags: + tags[key] = value + + return tags + +tag_get_methods['ogg'] = get_tags_ogg + + +def get_tags_mp3(filename): + global tags_keys + + if not has_eyed3: + log('eyeD3 not found -> please install. Could not read tag.') + return dict(map(lambda key: (key, ''), tags_keys)) + + tag = eyeD3.Tag() + tag.link(filename) + + return { + 'artist': tag.getArtist(), + 'title': tag.getTitle(), + 'album': tag.getAlbum(), + 'genre': tag.getGenre() + } + +tag_get_methods['mp3'] = get_tags_mp3 + diff --git a/src/gpodder/util.py b/src/gpodder/util.py index 67c9eada..2c7fea4b 100644 --- a/src/gpodder/util.py +++ b/src/gpodder/util.py @@ -1037,3 +1037,57 @@ def resize_pixbuf_keep_ratio(pixbuf, max_width, max_height, key=None, cache=None return result +# matches http:// and ftp:// and mailto:// +protocolPattern = re.compile(r'^\w+://') + +def isabs(string): + """ + @return true if string is an absolute path or protocoladdress + for addresses beginning in http:// or ftp:// or ldap:// - + they are considered "absolute" paths. + Source: http://code.activestate.com/recipes/208993/ + """ + if protocolPattern.match(string): return 1 + return os.path.isabs(string) + +def rel2abs(path, base = os.curdir): + """ converts a relative path to an absolute path. + + @param path the path to convert - if already absolute, is returned + without conversion. + @param base - optional. Defaults to the current directory. + The base is intelligently concatenated to the given relative path. + @return the relative path of path from base + Source: http://code.activestate.com/recipes/208993/ + """ + if isabs(path): return path + retval = os.path.join(base,path) + return os.path.abspath(retval) + +def commonpath(l1, l2, common=[]): + """ + helper functions for relpath + Source: http://code.activestate.com/recipes/208993/ + """ + if len(l1) < 1: return (common, l1, l2) + if len(l2) < 1: return (common, l1, l2) + if l1[0] != l2[0]: return (common, l1, l2) + return commonpath(l1[1:], l2[1:], common+[l1[0]]) + +def relpath(p1, p2): + """ + Finds relative path from p1 to p2 + Source: http://code.activestate.com/recipes/208993/ + """ + pathsplit = lambda s: s.split(os.path.sep) + + (common,l1,l2) = commonpath(pathsplit(p1), pathsplit(p2)) + p = [] + if len(l1) > 0: + p = [ ('..'+os.sep) * len(l1) ] + p = p + l2 + if len(p) is 0: + return "." + + return os.path.join(*p) + -- 2.11.4.GIT