Skip to content
Snippets Groups Projects
Commit 1b6fe7e4 authored by ale's avatar ale
Browse files

create a working FUSE client

parent 1d8b11d4
No related branches found
No related tags found
No related merge requests found
import json import json
import os import os
import shelve
import shutil import shutil
import threading
import urllib
import urllib2 import urllib2
from errno import EINVAL, ENOENT from errno import EINVAL, ENOENT
from stat import S_IFDIR, S_IFREG from stat import S_IFDIR, S_IFREG
from fuse import FUSE, Operations, LoggingMixIn 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): class DJFS(LoggingMixIn, Operations):
"""Simple FUSE client for djrandom. """Simple FUSE client for djrandom.
...@@ -20,65 +82,47 @@ class DJFS(LoggingMixIn, Operations): ...@@ -20,65 +82,47 @@ class DJFS(LoggingMixIn, Operations):
if not os.path.isdir(cache_dir): if not os.path.isdir(cache_dir):
os.makedirs(cache_dir) os.makedirs(cache_dir)
self._cache_dir = cache_dir self._cache_dir = cache_dir
self._server_url = server_url.rstrip('/') self._api = DJAPI(server_url)
self._opener = urllib2.build_opener() self._stat_cache = StatCache(self._api)
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']
def _trigger_cache_load(self, artist, album): def destroy(self, path):
contents = self._get('/json/album/%s/%s' % (artist, album))['songs'] pass
self._load_into_cache(artist, album, contents)
def _find_sha1(self, artist, album, title): def _find_sha1(self, artist, album, title):
key = self._hash_cache_key(artist, album, title) return self._stat_cache.get_file(artist, album, title)
sha1 = self._hash_cache.get(key)
if not sha1: def _file_cache_path(self, sha1):
self._trigger_cache_load(artist, album) return os.path.join(self._cache_dir, sha1[0], sha1)
sha1 = self._hash_cache.get(key)
return sha1
def _in_file_cache(self, sha1): def _in_file_cache(self, sha1):
path = os.path.join(self._cache_dir, sha1[0], sha1) return os.path.exists(self._file_cache_path(sha1))
return os.path.exists(path)
def _download_file(self, sha1): def _download_file(self, sha1):
path = os.path.join(self._cache_dir, sha1[0], sha1) path = self._file_cache_path(sha1)
req = urllib2.Request('/dl/%s' % sha1) dl_dir = os.path.dirname(path)
resp = self._opener.open(req) if not os.path.isdir(dl_dir):
with open(path, 'wb') as fd: os.makedirs(dl_dir)
shutil.copyfileobj(req, fd) self._api.download(sha1, path)
def _parse_path(self, path): def _parse_path(self, path):
parts = path.split('/') parts = path[1:].split('/')
if len(parts) > 3: if len(parts) > 3:
raise OSError(EINVAL) raise OSError(EINVAL)
if len(parts) < 3: 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 return parts
def getattr(self, path, fh=None): def getattr(self, path, fh=None):
artist, album, title = self._parse_path(path) artist, album, title = self._parse_path(path)
if title: if title:
sha1 = self._cache_get(artist, album, title) stats = self._stat_cache.get_file(artist, album, title)
if not sha1: if not stats:
raise OSError(ENOENT) raise OSError(ENOENT)
stats = self._get('/json/song/%s' % sha1) mtime = float(stats.get('uploaded_at', '0'))
mtime = float(stats['uploaded_at'])
return dict(st_mode=(S_IFREG | 0444), st_nlink=1, 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_ctime=mtime,
st_mtime=mtime, st_mtime=mtime,
st_atime=mtime) st_atime=mtime)
...@@ -91,20 +135,27 @@ class DJFS(LoggingMixIn, Operations): ...@@ -91,20 +135,27 @@ class DJFS(LoggingMixIn, Operations):
if title: if title:
raise OSError(EINVAL) raise OSError(EINVAL)
if not artist: if not artist:
values = self._get('/json/artists')['artists'] values = self._api.get('/json/artists')['artists']
elif not album: elif not album:
values = self._get('/json/albums/%s' % artist)['albums'] values = self._api.get('/json/albums/%s' % urllib.quote(artist))['albums']
else: else:
# equal to trigger_cache_load() songs = self._stat_cache.get_dir(artist, album)
contents = self._get('/json/album/%s/%s' % (artist, album))['songs'] if not songs:
self._load_into_cache(artist, album, contents) raise OSError(ENOENT)
values = [x['sha1'] for x in contents] values = ['%s.mp3' % (x['title'] || 'UNKNOWN') for x in songs]
return ['.', '..'] + [x for x in values] return ['.', '..'] + [x.encode('utf-8') for x in values]
def read(self, path, size, offset, fh=None): def read(self, path, size, offset, fh=None):
if not self._in_file_cache(path): artist, album, title = self._parse_path(path)
self._download_file(path) if not title:
with open(path, 'rb') as fd: 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) fd.seek(offset)
return fd.read(size) return fd.read(size)
...@@ -138,7 +189,7 @@ def main(): ...@@ -138,7 +189,7 @@ def main():
parser.error('Wrong number of args') parser.error('Wrong number of args')
fuse = FUSE(DJFS(opts.server_url, opts.cache_dir), fuse = FUSE(DJFS(opts.server_url, opts.cache_dir),
args[0], foreground=True) args[0], foreground=True, debug=True)
if __name__ == '__main__': if __name__ == '__main__':
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment