diff --git a/client/djrandom_client/djfuse.py b/client/djrandom_client/djfuse.py new file mode 100644 index 0000000000000000000000000000000000000000..e77556b536fc9b1de1adbe8c007c99b9b529f532 --- /dev/null +++ b/client/djrandom_client/djfuse.py @@ -0,0 +1,141 @@ +import json +import shelve +import shutil +import urllib2 +from errno import EINVAL, ENOENT +from stat import S_IFDIR, S_IFREG +from fuse import FUSE, FuseOSError, Operations, LoggingMixIn + + +class DJFS(LoggingMixIn, Operations): + """Simple FUSE client for djrandom. + + Quite inefficient, but it shows that it can be done easily + with the existing API, accessing files using the standard + /artist/album/title hierarchy. + """ + + def __init__(self, server_url, cache_dir): + self._server_url = server_url.rstrip('/') + self._cache_dir = cache_dir + self._opener = urllib2.build_opener() + self._hash_cache = shelve.open( + os.path.joib(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'] + + 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 _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 + + def _in_file_cache(self, sha1): + path = os.path.join(self._cache_dir, sha1[0], sha1) + return os.path.exists(path) + + 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) + + def _parse_path(self, path): + parts = path.split('/') + if len(parts) > 3: + raise FuseOSError(EINVAL) + if len(parts) < 3: + parts.extend([None for x in (3 - len(parts))]) + 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: + raise FuseOSError(ENOENT) + stats = self._get('/json/song/%s' % sha1) + mtime = float(stats['uploaded_at']) + return dict(st_mode=(S_IFREG | 0444), st_nlink=1, + st_size=int(stats['size']), + st_ctime=mtime, + st_mtime=mtime, + st_atime=mtime) + else: + return dict(st_mode=(S_IFDIR | 0555), st_nlink=2, + st_size=0, st_ctime=0, st_mtime=0, st_atime=0) + + def readdir(self, path, fh=None): + artist, album, title = self._parse_path(path) + if title: + raise FuseOSError(EINVAL) + if not artist: + values = self._get('/json/artists')['artists'] + elif not album: + values = self._get('/json/albums/%s' % 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] + + 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: + fd.seek(offset) + return fd.read(size) + + # Disable unsupported calls. + chmod = None + chown = None + create = None + rename = None + rmdir = None + statfs = None + symlink = None + truncate = None + unlink = None + utimens = None + write = None + listxattr = None + removexattr = None + setxattr = None + + +def main(): + import optparse + parser = optparse.OptionParser(usage='%Prog <MOUNTPOINT>') + parser.add_option('--api_key') + parser.add_option('--cache_dir', + default='/var/cache/djrandom/fuse') + parser.add_option('--server_url', + defaults='http://djrandom.incal.net') + opts, args = parser.parse_args() + if len(args) != 1: + parser.error('Wrong number of args') + + fuse = FUSE(DJFS(), args[0], foreground=True) + + +if __name__ == '__main__': + main() diff --git a/server/djrandom/frontend/frontend.py b/server/djrandom/frontend/frontend.py index 0f8f7210950501eb3e3f254c788e0eeb3b29a16e..da85c63943c6078c7095e3a1c53ac1e046ff9e8e 100644 --- a/server/djrandom/frontend/frontend.py +++ b/server/djrandom/frontend/frontend.py @@ -82,7 +82,10 @@ def song_info_json(sha1): mp3 = MP3.query.get(sha1) if not mp3: abort(404) - return jsonify(mp3.to_dict()) + # Use the mp3.to_dict() data, but add some more things... + data = mp3.to_dict() + data['size'] = os.path.getsize(mp3.path) + return jsonify(data) @app.route('/json/playing', methods=['POST']) diff --git a/server/djrandom/model/mp3.py b/server/djrandom/model/mp3.py index 8ec8b4851220f2440ef0996ec254557781406931..9526c01ba215c51ddd1cdf16718841ca58ebf55f 100644 --- a/server/djrandom/model/mp3.py +++ b/server/djrandom/model/mp3.py @@ -31,7 +31,8 @@ class MP3(Base): 'artist': self.artist, 'album': self.album, 'genre': self.genre, - 'sha1': self.sha1} + 'sha1': self.sha1, + 'uploaded_at': self.uploaded_at} class PlayLog(Base):