import os, mimetypes, recode, events, cuesheet, mutagen, db from config import config class DirectoryEntry(object): '''Base class for directory entries.''' def __init__(self, path, track = None, metadata = {}): self.path = path self.track = track self.metadata = metadata if '..' in path.split(): raise Exception('Invalid path') self.rel_path = os.path.relpath(path, config.get('music_root')) def __cmp__(self, other): return cmp(self.path, other.path) def __lt__(self, other): return self.path < other.path def __str__(self): return '{name}
'.format(path = self.rel_path, name = os.path.basename(self.path)) def json(self): return {'type': self.entry_type, 'name': self.rel_path, 'track': self.track, 'metadata': self.metadata} class Directory(DirectoryEntry): '''A directory entry inside a directory.''' entry_type = 'dir' def listdir(self): directories = [] files = [] for f in os.listdir(self.path): path = os.path.join(self.path, f) if os.path.isdir(path): directories.append(Directory(path)) elif os.path.isfile(path): if os.path.splitext(f)[1] == '.cue': cue = cuesheet.Cuesheet(path) for t in cue.tracks: metadata = {} info = cue.info[0] if info.performer: metadata['artist'] = info.performer if info.title: metadata['album'] = info.title if t.title: metadata['title'] = t.title files.append(File(path, track = t.track[0], metadata = metadata)) else: metadata = {} tags = mutagen.File(path) or [] if isinstance(tags, mutagen.mp3.MP3): for id3, tn in (('TPE1', 'artist'), ('TALB', 'album'), ('TIT2', 'title')): if id3 in tags: metadata[tn] = tags[id3].text[0] else: for tn in ('artist', 'album', 'title'): if tn in tags: metadata[tn] = tags[tn][0].encode('utf-8') files.append(File(path, metadata = metadata)) return sorted(directories) + sorted(files) class File(DirectoryEntry): '''A file entry inside a directory.''' entry_type = 'file' def send(self, environ, start_response): do_range = 'HTTP_RANGE' in environ if do_range: file_range = environ['HTTP_RANGE'].split('bytes=')[1] mime = mimetypes.guess_type(self.path, strict = False)[0] or 'application/octet-stream' size = os.path.getsize(self.path) if do_range: start, end = [int(x or 0) for x in file_range.split('-')] if end == 0: end = size-1 write_out = start_response('206 Partial Content', [ ('Content-Type', mime), ('Content-Range', 'bytes {start}-{end}/{size}'.format(start = start, end = end, size = size)), ('Content-Length', str(end - start + 1))]) f = open(self.path, 'rb') f.seek(start) remaining = end-start+1 s = f.read(min(remaining, 1024)) while s: write_out(s) remaining -= len(s) s = f.read(min(remaining, 1024)) return [] start_response('200 OK', [ ('Content-Type', mime), ('Content-Length', str(size))]) return open(self.path, 'rb') def get_cache_path(self): cache_path = os.path.join(config.get('cache_dir'), self.rel_path) cache_path = os.path.splitext(cache_path)[0] if self.track: cache_path += '.' + str(self.track) cache_path += '.ogg' return cache_path def get_cache_file(self): return File(self.get_cache_path()) def recode(self, decoder, encoder, sessionid = None): if self.track: cue = cuesheet.Cuesheet(self.path) t = cue.tracks[int(self.track)-1] start_time = t.get_start_time() next = cue.get_next(t) if next: end_time = next.get_start_time() else: end_time = None path = os.path.join(os.path.dirname(self.path), cue.info[0].file[0]) else: path = self.path start_time, end_time = None, None decoder = recode.decoders[decoder]() encoder = recode.encoders[encoder]() recoder = recode.Recoder(decoder, encoder) cache_path = self.get_cache_path() cache_path_dir = os.path.dirname(cache_path) # check and create cache directory if not os.path.exists(cache_path_dir): os.makedirs(cache_path_dir) # check if file is cached if not os.path.exists(cache_path): events.event_pub.recoding(self.rel_path, self.track) recoder.recode(path, cache_path, start_time = start_time, end_time = end_time) events.event_pub.cached(self.rel_path, self.track) if sessionid: events.event_pub.play(sessionid, '/cache/{0}'.format(self.rel_path)) def start_recode(self, decoder, encoder, sessionid = None): recode.RecodeThread.add((self.recode, decoder, encoder, sessionid)) def json(self): cache_path = self.get_cache_path() d = DirectoryEntry.json(self) d.update({'cached': os.path.exists(cache_path)}) return d def rec_scan(session, root, parent_id = None): directory = db.Directory.get(session, root, parent_id) d = Directory(root) for de in d.listdir(): if isinstance(de, Directory): print 'id:', directory.id rec_scan(session, de.path, directory.id) else: print de.metadata artist = db.Artist.get(session, de.metadata['artist']) if 'artist' in de.metadata else None album = db.Album.get(session, de.metadata['album'], artist.id if artist else None) if 'album' in de.metadata else None track = db.Track.get(session, de.metadata['title'] if 'title' in de.metadata else None, None, os.path.basename(de.path), de.track, directory.id, artist.id if artist else None, album.id if album else None) def scan(root = None): if not root: root = config.get('music_root') try: session = db.Session() rec_scan(session, root) session.commit() finally: session.close() if __name__ == '__main__': scan()