import os, mimetypes, recode, events, cuesheet, mutagen, db from config import config class DirectoryEntry(object): '''Base class for directory entries.''' def __init__(self, path, isabs = False, track = None, metadata = {}): self.path = path self.track = track self.metadata = metadata if isabs: self.abs_path = path else: path_list = os.path.split(config.get('music_root')) + os.path.split(self.path) if '..' in path_list: raise Exception('Invalid path') self.abs_path = os.path.normpath(os.path.sep.join(path_list)) 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.path, name = os.path.basename(self.path)) def json(self): return {'type': self.entry_type, 'name': self.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.abs_path): abs_path = os.path.join(self.abs_path, f) rel_path = os.path.relpath(abs_path, config.get('music_root')) if os.path.isdir(abs_path): directories.append(Directory(rel_path)) elif os.path.isfile(abs_path): if os.path.splitext(f)[1] == '.cue': cue = cuesheet.Cuesheet(abs_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(rel_path, track = t.track[0], metadata = metadata)) else: metadata = {} tags = mutagen.File(abs_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(rel_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.abs_path, strict = False)[0] or 'application/octet-stream' size = os.path.getsize(self.abs_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.abs_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.abs_path, 'rb') def get_cache_path(self): cache_path = os.path.join(config.get('cache_dir'), self.path) cache_path = os.path.splitext(cache_path)[0] if self.track: cache_path += '.' + self.track cache_path += '.ogg' return cache_path def get_cache_file(self): return File(self.get_cache_path(), True) def recode(self, decoder, encoder, sessionid = None): if self.track: cue = cuesheet.Cuesheet(self.abs_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.abs_path), cue.info[0].file[0]) else: path = self.abs_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.path, self.track) recoder.recode(path, cache_path, start_time = start_time, end_time = end_time) events.event_pub.cached(self.path, self.track) if sessionid: events.event_pub.play(sessionid, '/cache/{0}'.format(self.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): directory = db.Directory.get(session, root) d = Directory(root, isabs = True) for de in d.listdir(): if isinstance(de, Directory): rec_scan(session, de.abs_path) 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()