diff --git a/client/djrandom_client/djfuse.py b/client/djrandom_client/djfuse.py index 6ece0f0cef2234021c536b5a2d77cdc606cb0f6d..b83d457b7d501c61397df4ebbe3299f5f0f424b9 100644 --- a/client/djrandom_client/djfuse.py +++ b/client/djrandom_client/djfuse.py @@ -1,13 +1,75 @@ import json import os -import shelve import shutil +import threading +import urllib import urllib2 from errno import EINVAL, ENOENT from stat import S_IFDIR, S_IFREG from fuse import FUSE, Operations, LoggingMixIn + +class DJAPI(object): + + def __init__(self, server_url, api_key='fuse'): + self._server_url = server_url + self._api_key = api_key + self._opener = urllib2.build_opener() + + def get(self, url): + print 'HTTP GET:', url + req = urllib2.Request(self._server_url + url) + return json.load(self._opener.open(req)) + + def download(self, sha1, path): + print 'HTTP DOWNLOAD:', sha1 + req = urllib2.Request('%s/dl/%s' % (self._server_url, sha1)) + resp = self._opener.open(req) + with open(path, 'wb') as fd: + shutil.copyfileobj(resp, fd) + + + +class StatCache(object): + + def __init__(self, api): + self._api = api + self._cache = {} + self._lock = threading.Lock() + + def _key(self, artist, album, title): + return '%s|%s|%s' % (artist, album, title) + + def _load(self, artist, album, songs): + with self._lock: + dir_key = self._key(artist, album, 'DIR') + self._cache[dir_key] = songs + for song in songs: + key = self._key(artist, album, song['title']) + self._cache[key] = song + + def preload(self, artist, album): + contents = self._api.get('/json/album/%s/%s' % ( + urllib.quote(artist), urllib.quote(album)))['songs'] + self._load(artist, album, contents) + + def get_dir(self, artist, album): + return self.get_file(artist, album, 'DIR') + + def get_file(self, artist, album, title): + print 'CACHE: get_file', artist, album, title + key = self._key(artist, album, title) + with self._lock: + result = self._cache.get(key) + if not result: + self.preload(artist, album) + with self._lock: + result = self._cache.get(key) + return result + + + class DJFS(LoggingMixIn, Operations): """Simple FUSE client for djrandom. @@ -20,65 +82,47 @@ class DJFS(LoggingMixIn, Operations): if not os.path.isdir(cache_dir): os.makedirs(cache_dir) self._cache_dir = cache_dir - self._server_url = server_url.rstrip('/') - self._opener = urllib2.build_opener() - self._hash_cache = shelve.open( - os.path.join(cache_dir, 'artist_album')) - - def _get(self, url): - req = urllib2.Request(self._server_url + url) - return json.load(self._opener.open(req)) - - @classmethod - def _hash_cache_key(cls, artist, album, title): - return '%s|%s|%s' % (artist, album, title) - - def _load_into_cache(self, artist, album, songs): - for song in songs: - key = self._hash_cache_key(artist, album, song['title']) - self._hash_cache[key] = song['sha1'] + self._api = DJAPI(server_url) + self._stat_cache = StatCache(self._api) - def _trigger_cache_load(self, artist, album): - contents = self._get('/json/album/%s/%s' % (artist, album))['songs'] - self._load_into_cache(artist, album, contents) + def destroy(self, path): + pass def _find_sha1(self, artist, album, title): - key = self._hash_cache_key(artist, album, title) - sha1 = self._hash_cache.get(key) - if not sha1: - self._trigger_cache_load(artist, album) - sha1 = self._hash_cache.get(key) - return sha1 + return self._stat_cache.get_file(artist, album, title) + + def _file_cache_path(self, sha1): + return os.path.join(self._cache_dir, sha1[0], sha1) def _in_file_cache(self, sha1): - path = os.path.join(self._cache_dir, sha1[0], sha1) - return os.path.exists(path) + return os.path.exists(self._file_cache_path(sha1)) def _download_file(self, sha1): - path = os.path.join(self._cache_dir, sha1[0], sha1) - req = urllib2.Request('/dl/%s' % sha1) - resp = self._opener.open(req) - with open(path, 'wb') as fd: - shutil.copyfileobj(req, fd) + path = self._file_cache_path(sha1) + dl_dir = os.path.dirname(path) + if not os.path.isdir(dl_dir): + os.makedirs(dl_dir) + self._api.download(sha1, path) def _parse_path(self, path): - parts = path.split('/') + parts = path[1:].split('/') if len(parts) > 3: raise OSError(EINVAL) if len(parts) < 3: - parts.extend([None for x in (3 - len(parts))]) + parts.extend([None for x in range(3 - len(parts))]) + if parts[2] and parts[2].endswith('.mp3'): + parts[2] = parts[2][:-4] return parts def getattr(self, path, fh=None): artist, album, title = self._parse_path(path) if title: - sha1 = self._cache_get(artist, album, title) - if not sha1: + stats = self._stat_cache.get_file(artist, album, title) + if not stats: raise OSError(ENOENT) - stats = self._get('/json/song/%s' % sha1) - mtime = float(stats['uploaded_at']) + mtime = float(stats.get('uploaded_at', '0')) return dict(st_mode=(S_IFREG | 0444), st_nlink=1, - st_size=int(stats['size']), + st_size=int(stats.get('size', 0)), st_ctime=mtime, st_mtime=mtime, st_atime=mtime) @@ -91,20 +135,27 @@ class DJFS(LoggingMixIn, Operations): if title: raise OSError(EINVAL) if not artist: - values = self._get('/json/artists')['artists'] + values = self._api.get('/json/artists')['artists'] elif not album: - values = self._get('/json/albums/%s' % artist)['albums'] + values = self._api.get('/json/albums/%s' % urllib.quote(artist))['albums'] else: - # equal to trigger_cache_load() - contents = self._get('/json/album/%s/%s' % (artist, album))['songs'] - self._load_into_cache(artist, album, contents) - values = [x['sha1'] for x in contents] - return ['.', '..'] + [x for x in values] + songs = self._stat_cache.get_dir(artist, album) + if not songs: + raise OSError(ENOENT) + values = ['%s.mp3' % (x['title'] || 'UNKNOWN') for x in songs] + return ['.', '..'] + [x.encode('utf-8') for x in values] def read(self, path, size, offset, fh=None): - if not self._in_file_cache(path): - self._download_file(path) - with open(path, 'rb') as fd: + artist, album, title = self._parse_path(path) + if not title: + raise OSError(ENOENT) + sha1 = self._find_sha1(artist, album, title) + if not sha1: + raise OSError(ENOENT) + if not self._in_file_cache(sha1): + self._download_file(sha1) + real_path = self._file_cache_path(sha1) + with open(real_path, 'rb') as fd: fd.seek(offset) return fd.read(size) @@ -138,7 +189,7 @@ def main(): parser.error('Wrong number of args') fuse = FUSE(DJFS(opts.server_url, opts.cache_dir), - args[0], foreground=True) + args[0], foreground=True, debug=True) if __name__ == '__main__':