From 5cef6267e1f8099e3b0b2737c5599abc6349bf2b Mon Sep 17 00:00:00 2001 From: Bob Bell Date: Tue, 20 May 2014 15:42:17 -0400 Subject: [PATCH 1/2] Add support for playlists --- gmusicfs/gmusicfs.py | 80 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/gmusicfs/gmusicfs.py b/gmusicfs/gmusicfs.py index 7d8e7f1..9236605 100755 --- a/gmusicfs/gmusicfs.py +++ b/gmusicfs/gmusicfs.py @@ -15,6 +15,7 @@ import tempfile import threading import logging +import pprint from fuse import FUSE, FuseOSError, Operations, LoggingMixIn, fuse_get_context from gmusicapi import Mobileclient as GoogleMusicAPI @@ -25,6 +26,7 @@ logging.basicConfig(level=logging.DEBUG) log = logging.getLogger('gmusicfs') deviceId=None +pp = pprint.PrettyPrinter(indent=4) # DEBUG # Size of the ID3v1 trailer appended to the mp3 file (at read time) # Add the size to the reported size of the mp3 file so read function receive correct params. @@ -133,6 +135,8 @@ def __init__(self, username=None, password=None, self.__artists = {} # 'artist name' -> {'album name' : Album(), ...} self.__albums = [] # [Album(), ...] + self.__tracks = {} + self.__playlists = {} if scan: self.rescan() self.true_file_size = true_file_size @@ -140,6 +144,8 @@ def __init__(self, username=None, password=None, def rescan(self): self.__artists = {} # 'artist name' -> {'album name' : Album(), ...} self.__albums = [] # [Album(), ...] + self.__tracks = {} + self.__playlists = {} self.__aggregate_albums() def __login_and_setup(self, username=None, password=None): @@ -182,6 +188,7 @@ def __aggregate_albums(self): log.info('Gathering track information...') tracks = self.api.get_all_songs() for track in tracks: + log.debug('track = %s' % pp.pformat(track)) # Prefer the album artist over the track artist if there is one: artist = formatNames(track['albumArtist'].lower()) if artist.strip() == '': @@ -203,13 +210,33 @@ def __aggregate_albums(self): self.__artists[artist] = {album.normtitle: album} artist_albums = self.__artists[artist] album.add_track(track) + if 'id' in track: + self.__tracks[track['id']] = track log.debug('%d tracks loaded.' % len(tracks)) log.debug('%d artists loaded.' % len(self.__artists)) log.debug('%d albums loaded.' % len(self.__albums)) + playlists = self.api.get_all_user_playlist_contents() + for playlist in playlists: + name = formatNames(playlist['name'].lower()) + log.debug('Playlist %s' % name) + self.__playlists[name] = [] + entries = playlist['tracks'] + for entry in entries: + log.debug('Playlist entry = %s' % pp.pformat(entry)) + if 'track' in entry: + track = entry['track'] + track['id'] = entry['trackId'] + else: + track = self.__tracks[entry['trackId']] + self.__playlists[name].append(track) + log.debug('%d playlists loaded.' % len(self.__playlists)) def get_artists(self): return self.__artists + def get_playlists(self): + return self.__playlists + def get_albums(self): return self.__albums @@ -232,6 +259,9 @@ def __init__(self, path, username=None, password=None, '^/artists/(?P[^/]+)/(?P[0-9]{4}) - (?P[^/]+)/(?P[^/]+\.mp3)$') self.artist_album_image = re.compile( '^/artists/(?P[^/]+)/(?P[0-9]{4}) - (?P[^/]+)/(?P[^/]+\.jpg)$') + self.playlist_dir = re.compile('^/playlists/(?P[^/]+)$') + self.playlist_track = re.compile( + '^/playlists/(?P[^/]+)/(?P[0-9]{3}) - (?P[^/]+\.mp3)$') self.__open_files = {} # path -> urllib2_obj @@ -243,12 +273,27 @@ def __init__(self, path, username=None, password=None, def cleanup(self): self.library.cleanup() + def track_to_stat(self, track): + st = { + 'st_mode' : (S_IFREG | 0444), + 'st_size' : int(track['estimatedSize']), + 'st_ctime' : 0, + 'st_mtime' : 0, + 'st_atime' : 0 } + if 'creationTimestamp' in track: + st['st_ctime'] = st['st_mtime'] = int(track['creationTimestamp']) / 1000000 + if 'recentTimestamp' in track: + st['st_atime'] = int(track['recentTimestamp']) / 1000000 + return st + def getattr(self, path, fh=None): 'Get info about a file/dir' artist_dir_m = self.artist_dir.match(path) artist_album_dir_m = self.artist_album_dir.match(path) artist_album_track_m = self.artist_album_track.match(path) artist_album_image_m = self.artist_album_image.match(path) + playlist_dir_m = self.playlist_dir.match(path) + playlist_track_m = self.playlist_track.match(path) # Default to a directory st = { @@ -261,6 +306,8 @@ def getattr(self, path, fh=None): pass elif path == '/artists': pass + elif path == '/playlists': + pass elif artist_dir_m: pass elif artist_album_dir_m: @@ -270,12 +317,7 @@ def getattr(self, path, fh=None): album = self.library.get_artists()[ parts['artist']][parts['album']] track = album.get_track(parts['track']) - st = { - 'st_mode' : (S_IFREG | 0444), - 'st_size' : int(track['estimatedSize']), - 'st_ctime' : int(track['creationTimestamp']) / 1000000, - 'st_mtime' : int(track['creationTimestamp']) / 1000000, - 'st_atime' : int(track['recentTimestamp']) / 1000000} + st = self.track_to_stat(track) elif artist_album_image_m: parts = artist_album_image_m.groupdict() album = self.library.get_artists()[ @@ -286,6 +328,13 @@ def getattr(self, path, fh=None): st = { 'st_mode' : (S_IFREG | 0444), 'st_size' : cover_size } + elif playlist_dir_m: + pass + elif playlist_track_m: + parts = playlist_track_m.groupdict() + playlist = self.library.get_playlists()[parts['playlist']] + track = playlist[int(parts['tracknum'], 10) - 1] + st = self.track_to_stat(track) else: raise FuseOSError(ENOENT) @@ -294,6 +343,7 @@ def getattr(self, path, fh=None): def open(self, path, fh): artist_album_track_m = self.artist_album_track.match(path) artist_album_image_m = self.artist_album_image.match(path) + playlist_track_m = self.playlist_track.match(path) if artist_album_track_m: parts = artist_album_track_m.groupdict() @@ -306,6 +356,11 @@ def open(self, path, fh): album = self.library.get_artists()[ parts['artist']][parts['album']] url = album.get_cover_url() + elif playlist_track_m: + parts = playlist_track_m.groupdict() + playlist = self.library.get_playlists()[parts['playlist']] + track = playlist[int(parts['tracknum'], 10) - 1] + url = self.library.api.get_stream_url(track['id'], deviceId) else: RuntimeError('unexpected opening of path: %r' % path) @@ -351,11 +406,14 @@ def readdir(self, path, fh): artist_album_dir_m = self.artist_album_dir.match(path) artist_album_track_m = self.artist_album_track.match(path) artist_album_image_m = self.artist_album_image.match(path) + playlist_dir_m = self.playlist_dir.match(path) if path == '/': - return ['.', '..', 'artists'] + return ['.', '..', 'artists', 'playlists'] elif path == '/artists': return ['.','..'] + self.library.get_artists().keys() + elif path == '/playlists': + return ['.','..'] + self.library.get_playlists().keys() elif artist_dir_m: # Artist directory, lists albums. albums = self.library.get_artist_albums( @@ -377,6 +435,14 @@ def readdir(self, path, fh): if cover: files.append('cover.jpg') return files + elif playlist_dir_m: + tracknum = 1 + playlist = self.library.get_playlists()[playlist_dir_m.groupdict()['playlist']] + files = ['.', '..'] + for track in playlist: + files.append('%03d - %s - %s - %s.mp3' % (tracknum, formatNames(track['artist'].lower()), formatNames(track['album'].lower()), formatNames(track['title'].lower()))) + tracknum += 1 + return files def getDeviceId(verbose=False): From 5b8953af644a17484a4699cab8dd91adf484ab5b Mon Sep 17 00:00:00 2001 From: Bob Bell Date: Tue, 20 May 2014 22:29:00 -0400 Subject: [PATCH 2/2] Make lowercase filenames optional; define some classes --- gmusicfs/gmusicfs.py | 300 ++++++++++++++++++++++++++++--------------- 1 file changed, 199 insertions(+), 101 deletions(-) diff --git a/gmusicfs/gmusicfs.py b/gmusicfs/gmusicfs.py index 9236605..81022c3 100755 --- a/gmusicfs/gmusicfs.py +++ b/gmusicfs/gmusicfs.py @@ -25,8 +25,8 @@ logging.basicConfig(level=logging.DEBUG) log = logging.getLogger('gmusicfs') -deviceId=None -pp = pprint.PrettyPrinter(indent=4) # DEBUG +deviceId = None +pp = pprint.PrettyPrinter(indent=4) # For debug logging # Size of the ID3v1 trailer appended to the mp3 file (at read time) # Add the size to the reported size of the mp3 file so read function receive correct params. @@ -34,34 +34,112 @@ ID3V1_TRAILER_SIZE = 128 def formatNames(string_from): + """Format a name to make it suitable to use as a filename""" return re.sub('/', '-', string_from) class NoCredentialException(Exception): pass +class Playlist(object): + """This class manages playlist information""" + + def __init__(self, library, pldata): + self.library = library + self.__filename_re = re.compile('^([0-9]+) - [^/]+\.mp3$') + + self.realname = pldata['name'] + self.dirname = formatNames(self.realname).strip() + log.debug('New playlist: %s' % self.realname) + + self.__tracks = [] + for entry in pldata['tracks']: + log.debug('Playlist entry: %s' % pp.pformat(entry)) + if 'track' in entry: + track = entry['track'] + track['id'] = entry['trackId'] + else: + track = self.library.get_track(entry['trackId']) + self.__tracks.append(track) + + def get_tracks(self, get_size=False): + """Return the list of tracks, in order, that comprise the playlist""" + # TODO Converge implementation by creating a Track class? + # It could get the size only on demand per-track + # Retrieve and remember the filesize of each track: + if get_size and self.library.true_file_size: + for t in self.__tracks: + if not 'bytes' in t: + r = urllib2.Request(self.get_track_stream(t)[0]) + r.get_method = lambda: 'HEAD' + u = urllib2.urlopen(r) + t['bytes'] = int(u.headers['Content-Length']) + ID3V1_TRAILER_SIZE + return self.__tracks + + def get_track(self, filename): + """Return the track that corresponds to a filename from this playlist""" + m = self.__filename_re.match(filename) + if m: + tracknum = m.groups()[0] + return self.__tracks[tracknum - 1] + return None + + def get_track_stream(self, track): + """Return the track stream URL""" + return self.library.api.get_stream_url(track['id'], deviceId) + + def __repr__(self): + return u''.format(name=self.realname) + +class Artist(object): + """This class manages artist information""" + + def __init__(self, library, name): + self.library = library + self.realname = name + self.dirname = formatNames(name) + self.__albums = {} + + def add_album(self, album): + """Add an album to the artist""" + self.__albums[album.normtitle.lower()] = album + + def get_albums(self): + """Return a list of all the albums by the artist""" + return self.__albums.values() + + def get_album(self, title): + """Return a specific album from the set that belongs to the artist""" + return self.__albums.get(title.lower(), None) + + def __repr__(self): + return u''.format(name=self.realname) + class Album(object): - 'Keep record of Album information' - def __init__(self, library, normtitle): + """This class manages album information""" + + def __init__(self, library, title): self.library = library - self.normtitle = formatNames(normtitle) + self.realtitle = title + self.normtitle = formatNames(self.realtitle) self.__tracks = [] self.__sorted = True - self.__filename_re = re.compile("^[0-9]{3} - (.*)\.mp3$") + self.__filename_re = re.compile("^[0-9]+ - (.*)\.mp3$") def add_track(self, track): - 'Add a track to the Album' + """Add a track to the album""" self.__tracks.append(track) self.__sorted = False def get_tracks(self, get_size=False): - # Re-sort by track number: + """Return a sorted list of tracks in the album""" + # Re-sort by track number if not self.__sorted: self.__tracks.sort(key=lambda t: t.get('track')) - # Retrieve and remember the filesize of each track: + # Retrieve and remember the filesize of each track if get_size and self.library.true_file_size: for t in self.__tracks: - if not t.has_key('bytes'): + if not 'bytes' in t: r = urllib2.Request(self.get_track_stream(t)[0]) r.get_method = lambda: 'HEAD' u = urllib2.urlopen(r) @@ -75,25 +153,25 @@ def get_track(self, filename): if m: title = m.groups()[0] for track in self.get_tracks(): - if formatNames(track['title'].lower()) == title: + if formatNames(track['title'].lower()) == title.lower(): return track return None def get_track_stream(self, track): - "Get the track stream URL" + """Return the track stream URL""" return self.library.api.get_stream_url(track['id'], deviceId) def get_cover_url(self): - 'Get the album cover image URL' + """Return the album cover image URL""" try: - #Assume the first track has the right cover URL: + # Assume the first track has the right cover URL url = "%s" % self.__tracks[0]['albumArtRef'][0]['url'] except: url = None return url def get_cover_size(self): - 'Get the album cover size' + """Return the album cover size""" if self.library.true_file_size: r = urllib2.Request(self.get_cover_url()) r.get_method = lambda: 'HEAD' @@ -123,7 +201,7 @@ def __repr__(self): return u''.format(title=self.normtitle) class MusicLibrary(object): - 'Read information about your Google Music library' + """This class reads information about your Google Play Music library""" def __init__(self, username=None, password=None, true_file_size=False, scan=True, verbose=0): @@ -142,6 +220,7 @@ def __init__(self, username=None, password=None, self.true_file_size = true_file_size def rescan(self): + """Scan the Google Play Music library""" self.__artists = {} # 'artist name' -> {'album name' : Album(), ...} self.__albums = [] # [Album(), ...] self.__tracks = {} @@ -176,6 +255,8 @@ def __login_and_setup(self, username=None, password=None): raise NoCredentialException( 'No deviceId could be read from config file' ': %s' % cred_path) + if deviceId.startswith("0x"): + deviceId = deviceId[2:] self.api = GoogleMusicAPI(debug_logging=self.verbose) log.info('Logging in...') @@ -183,74 +264,79 @@ def __login_and_setup(self, username=None, password=None): log.info('Login successful.') def __aggregate_albums(self): - 'Get all the tracks in the library, parse into artist and album dicts' - all_artist_albums = {} # 'Artist|||Album' -> Album() + """Get all the tracks and playlists in the library, parse into relevant dicts""" log.info('Gathering track information...') tracks = self.api.get_all_songs() for track in tracks: log.debug('track = %s' % pp.pformat(track)) - # Prefer the album artist over the track artist if there is one: - artist = formatNames(track['albumArtist'].lower()) - if artist.strip() == '': - artist = formatNames(track['artist'].lower()) - # Get the Album object if it already exists: - key = '%s|||%s' % (formatNames(artist), formatNames(track['album'].lower())) - album = all_artist_albums.get(key, None) + + # Prefer the album artist over the track artist if there is one + artist_name = formatNames(track['albumArtist']) + if artist_name.strip() == '': + artist_name = formatNames(track['artist']) + if artist_name.strip() == '': + artist_name = 'Unknown' + + # Get the Artist object, or create one if it doesn't exist + artist = self.__artists.get(artist_name.lower(), None) + if not artist: + artist = Artist(self, artist_name) + self.__artists[artist_name.lower()] = artist + + # Get the Album object, or create one if it doesn't exist + album = artist.get_album(formatNames(track['album'])) if not album: - # New Album - if artist == '': - artist = 'unknown' - album = all_artist_albums[key] = Album( - self, formatNames(track['album'].lower())) - self.__albums.append(album) - artist_albums = self.__artists.get(artist, None) - if artist_albums: - artist_albums[formatNames(album.normtitle)] = album - else: - self.__artists[artist] = {album.normtitle: album} - artist_albums = self.__artists[artist] + album = Album(self, track['album']) + self.__albums.append(album) # NOTE: Current no purpose other than to count + artist.add_album(album) + + # Add track to album album.add_track(track) + + # Add track to list of all tracks, indexable by track ID if 'id' in track: self.__tracks[track['id']] = track + log.debug('%d tracks loaded.' % len(tracks)) log.debug('%d artists loaded.' % len(self.__artists)) log.debug('%d albums loaded.' % len(self.__albums)) + + # Add all playlists playlists = self.api.get_all_user_playlist_contents() - for playlist in playlists: - name = formatNames(playlist['name'].lower()) - log.debug('Playlist %s' % name) - self.__playlists[name] = [] - entries = playlist['tracks'] - for entry in entries: - log.debug('Playlist entry = %s' % pp.pformat(entry)) - if 'track' in entry: - track = entry['track'] - track['id'] = entry['trackId'] - else: - track = self.__tracks[entry['trackId']] - self.__playlists[name].append(track) + for pldata in playlists: + playlist = Playlist(self, pldata) + self.__playlists[playlist.dirname.lower()] = playlist log.debug('%d playlists loaded.' % len(self.__playlists)) def get_artists(self): - return self.__artists + """Return list of all artists in the library""" + return self.__artists.values() + + def get_artist(self, name): + """Return the artist from the library with the specified name""" + return self.__artists.get(name.lower(), None) def get_playlists(self): - return self.__playlists + """Return list of all playlists in the library""" + return self.__playlists.values() - def get_albums(self): - return self.__albums + def get_playlist(self, name): + """Return the playlist from the library with the specified name""" + return self.__playlists.get(name.lower(), None) - def get_artist_albums(self, artist): - log.debug(artist) - return self.__artists[artist] + def get_track(self, trackid): + """Return the track from the library with the specified track ID""" + return self.__tracks.get(trackid, None) def cleanup(self): pass class GMusicFS(LoggingMixIn, Operations): - 'Google Music Filesystem' + """Google Music Filesystem""" + def __init__(self, path, username=None, password=None, - true_file_size=False, verbose=0, scan_library=True): + true_file_size=False, verbose=0, scan_library=True, + lowercase=True): Operations.__init__(self) self.artist_dir = re.compile('^/artists/(?P[^/]+)$') self.artist_album_dir = re.compile( @@ -261,11 +347,17 @@ def __init__(self, path, username=None, password=None, '^/artists/(?P[^/]+)/(?P[0-9]{4}) - (?P[^/]+)/(?P[^/]+\.jpg)$') self.playlist_dir = re.compile('^/playlists/(?P[^/]+)$') self.playlist_track = re.compile( - '^/playlists/(?P[^/]+)/(?P[0-9]{3}) - (?P[^/]+\.mp3)$') + '^/playlists/(?P[^/]+)/(?P[^/]+\.mp3)$') self.__open_files = {} # path -> urllib2_obj - # login to google music and parse the tracks: + # Define transformation based on whether lowercase filenames will be used or not + if lowercase: + self.transform = lambda x: x.lower() + else: + self.transform = lambda x: x + + # Login to Google Play Music and parse the tracks: self.library = MusicLibrary(username, password, true_file_size=true_file_size, verbose=verbose, scan=scan_library) log.info("Filesystem ready : %s" % path) @@ -273,13 +365,14 @@ def __init__(self, path, username=None, password=None, def cleanup(self): self.library.cleanup() - def track_to_stat(self, track): - st = { - 'st_mode' : (S_IFREG | 0444), - 'st_size' : int(track['estimatedSize']), - 'st_ctime' : 0, - 'st_mtime' : 0, - 'st_atime' : 0 } + def track_to_stat(self, track, st = {}): + """Construct and results stat information based on a track""" + # TODO This could be moved into a Track class in the future + st['st_mode'] = (S_IFREG | 0444) + st['st_size'] = int(track['estimatedSize']) + if 'bytes' in t: + st['st_size'] = int(track['bytes']) + st['st_ctime'] = st['st_mtime'] = st['st_atime'] = 0 if 'creationTimestamp' in track: st['st_ctime'] = st['st_mtime'] = int(track['creationTimestamp']) / 1000000 if 'recentTimestamp' in track: @@ -287,7 +380,7 @@ def track_to_stat(self, track): return st def getattr(self, path, fh=None): - 'Get info about a file/dir' + """Get information about a file or directory""" artist_dir_m = self.artist_dir.match(path) artist_album_dir_m = self.artist_album_dir.match(path) artist_album_track_m = self.artist_album_track.match(path) @@ -314,14 +407,14 @@ def getattr(self, path, fh=None): pass elif artist_album_track_m: parts = artist_album_track_m.groupdict() - album = self.library.get_artists()[ - parts['artist']][parts['album']] + artist = self.library.get_artist(parts['artist']) + album = artist.get_album(parts['album']) track = album.get_track(parts['track']) st = self.track_to_stat(track) elif artist_album_image_m: parts = artist_album_image_m.groupdict() - album = self.library.get_artists()[ - parts['artist']][parts['album']] + artist = self.library.get_artist(parts['artist']) + album = artist.get_album(parts['album']) cover_size = album.get_cover_size() if cover_size is None: cover_size = 10000000 @@ -332,8 +425,8 @@ def getattr(self, path, fh=None): pass elif playlist_track_m: parts = playlist_track_m.groupdict() - playlist = self.library.get_playlists()[parts['playlist']] - track = playlist[int(parts['tracknum'], 10) - 1] + playlist = self.library.get_playlist(parts['playlist']) + track = playlist.get_track(parts['track']) st = self.track_to_stat(track) else: raise FuseOSError(ENOENT) @@ -341,25 +434,27 @@ def getattr(self, path, fh=None): return st def open(self, path, fh): + """Open a file (track or cover image) and return a filehandle""" + artist_album_track_m = self.artist_album_track.match(path) artist_album_image_m = self.artist_album_image.match(path) playlist_track_m = self.playlist_track.match(path) if artist_album_track_m: parts = artist_album_track_m.groupdict() - album = self.library.get_artists()[ - parts['artist']][parts['album']] + artist = self.library.get_artist(parts['artist']) + album = artist.get_album(parts['album']) track = album.get_track(parts['track']) url = album.get_track_stream(track) elif artist_album_image_m: parts = artist_album_image_m.groupdict() - album = self.library.get_artists()[ - parts['artist']][parts['album']] + artist = self.library.get_artist(parts['artist']) + album = artist.get_album(parts['album']) url = album.get_cover_url() elif playlist_track_m: parts = playlist_track_m.groupdict() - playlist = self.library.get_playlists()[parts['playlist']] - track = playlist[int(parts['tracknum'], 10) - 1] + playlist = self.library.get_playlist(parts['playlist']) + track = playlist.get_track(parts['track']) url = self.library.api.get_stream_url(track['id'], deviceId) else: RuntimeError('unexpected opening of path: %r' % path) @@ -369,7 +464,6 @@ def open(self, path, fh): return fh - def release(self, path, fh): u = self.__open_files.get(fh, None) if u: @@ -383,8 +477,8 @@ def read(self, path, size, offset, fh): artist_album_track_m = self.artist_album_track.match(path) if artist_album_track_m and (int(u.headers['Content-Length']) < (offset + size)): parts = artist_album_track_m.groupdict() - album = self.library.get_artists()[ - parts['artist']][parts['album']] + artist = self.library.get_artist(parts['artist']) + album = artist.get_album(parts['album']) track = album.get_track(parts['track']) # Genre tag is always set to Other as Google MP3 genre tags are not id3v1 id. id3v1 = struct.pack("!3s30s30s30s4s30sb", 'TAG', str(track['title']), str(track['artist']), @@ -411,36 +505,41 @@ def readdir(self, path, fh): if path == '/': return ['.', '..', 'artists', 'playlists'] elif path == '/artists': - return ['.','..'] + self.library.get_artists().keys() + artist_dirs = map((lambda a: self.transform(a.dirname)), self.library.get_artists()) + return ['.','..'] + artist_dirs elif path == '/playlists': - return ['.','..'] + self.library.get_playlists().keys() + playlist_dirs = map((lambda p: self.transform(p.dirname)), self.library.get_playlists()) + return ['.','..'] + playlist_dirs elif artist_dir_m: # Artist directory, lists albums. - albums = self.library.get_artist_albums( - artist_dir_m.groupdict()['artist']) + parts = artist_dir_m.groupdict() + artist = self.library.get_artist(parts['artist']) + albums = artist.get_albums() # Sort albums by year: album_dirs = [u'{year:04d} - {name}'.format( - year=a.get_year(), name=formatNames(a.normtitle)) for a in albums.values()] + year=a.get_year(), name=self.transform(a.normtitle)) for a in albums] return ['.','..'] + album_dirs elif artist_album_dir_m: # Album directory, lists tracks. parts = artist_album_dir_m.groupdict() - album = self.library.get_artists()[ - parts['artist']][parts['album']] + artist = self.library.get_artist(parts['artist']) + album = artist.get_album(parts['album']) files = ['.','..'] for track in album.get_tracks(get_size=True): - files.append('%03d - %s.mp3' % (track['trackNumber'], formatNames(track['title'].lower()))) + files.append('%03d - %s.mp3' % + (track['trackNumber'], self.transform(formatNames(track['title'])))) # Include cover image: cover = album.get_cover_url() if cover: files.append('cover.jpg') return files elif playlist_dir_m: - tracknum = 1 - playlist = self.library.get_playlists()[playlist_dir_m.groupdict()['playlist']] + parts = playlist_dir_m.groupdict() + playlist = self.library.get_playlist(parts['playlist']) files = ['.', '..'] - for track in playlist: - files.append('%03d - %s - %s - %s.mp3' % (tracknum, formatNames(track['artist'].lower()), formatNames(track['album'].lower()), formatNames(track['title'].lower()))) + tracknum = 1 + for track in playlist.get_tracks(): + files.append(self.transform(formatNames('%03d - %s - %s - %s.mp3' % (tracknum, track['artist'], track['album'], track['title'])))) tracknum += 1 return files @@ -473,7 +572,7 @@ def getDeviceId(verbose=False): for device in api.get_registered_devices(): if not device['name']: - device['name']='NoName' + device['name'] = 'NoName' if device['id'][1]=='x': print '%s : %s' % (device['name'], device['id']) @@ -511,6 +610,8 @@ def main(): action='store_true', dest='nolibrary') parser.add_argument('--deviceid', help='Get the device ids bounded to your account', action='store_true', dest='deviceId') + parser.add_argument('-l', '--lowercase', help='Convert all path elements to lowercase', + action='store_true', dest='lowercase') args = parser.parse_args() @@ -536,10 +637,7 @@ def main(): logging.getLogger('requests.packages.urllib3').setLevel(logging.WARNING) verbosity = 0 - - - - fs = GMusicFS(mountpoint, true_file_size=args.true_file_size, verbose=verbosity, scan_library= not args.nolibrary) + fs = GMusicFS(mountpoint, true_file_size=args.true_file_size, verbose=verbosity, scan_library= not args.nolibrary, lowercase=args.lowercase) try: fuse = FUSE(fs, mountpoint, foreground=args.foreground, ro=True, nothreads=True, allow_other=args.allusers)