From 0d8e86265f74ee36503bbcec445dcefdea208df0 Mon Sep 17 00:00:00 2001 From: Jon Bergli Heier Date: Mon, 13 Feb 2012 22:09:11 +0100 Subject: Initial import. --- .gitignore | 3 + app.py | 18 ++++++ config.py | 25 ++++++++ cuesheet.py | 103 +++++++++++++++++++++++++++++++++ db.py | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ directory.py | 134 +++++++++++++++++++++++++++++++++++++++++++ static/index.html | 8 +++ 7 files changed, 457 insertions(+) create mode 100644 .gitignore create mode 100755 app.py create mode 100644 config.py create mode 100644 cuesheet.py create mode 100644 db.py create mode 100644 directory.py create mode 100644 static/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f9998c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.swp +*.pyc +/config diff --git a/app.py b/app.py new file mode 100755 index 0000000..1ed97ab --- /dev/null +++ b/app.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python2 + +class Application(object): + def __call__(self, environ, start_response): + start_response('200 OK', [('Content-Type', 'text/html')]) + return open('static/index.html', 'r') + +if __name__ == '__main__': + import sys + if len(sys.argv) == 3: + from flup.server.fcgi import WSGIServer + WSGIServer(Application(), bindAddress = (sys.argv[1], int(sys.argv[2]))).run() + else: + from wsgiref.simple_server import make_server, WSGIServer + # enable IPv6 + WSGIServer.address_family |= 10 + httpd = make_server('', 8000, Application()) + httpd.serve_forever() diff --git a/config.py b/config.py new file mode 100644 index 0000000..3f8995b --- /dev/null +++ b/config.py @@ -0,0 +1,25 @@ +try: + from configparser import ConfigParser, NoOptionError +except ImportError: + from ConfigParser import ConfigParser, NoOptionError + +class Config(object): + config_section = 'ongaku' + + def __init__(self, filename = 'config'): + self.config = ConfigParser() + self.config.read(filename) + + def get(self, key, section = config_section, default = None): + try: + return self.config.get(section, key) + except NoOptionError: + if default != None: + return default + else: + raise + + def getint(self, key, section = config_section): + return self.config.getint(section, key) + +config = Config() diff --git a/cuesheet.py b/cuesheet.py new file mode 100644 index 0000000..cfcb64c --- /dev/null +++ b/cuesheet.py @@ -0,0 +1,103 @@ +import re + +cdtext_re = { + 'REM': r'^(REM) (.+)$', + 'PERFORMER': r'^(PERFORMER) "?(.+?)"?$', + 'TITLE': r'^(TITLE) "?(.+?)"?$', + 'FILE': r'^(FILE) "?(.+?)"? (BINARY|MOTOROLA|AIFF|WAVE|MP3)$', + 'TRACK': r'^(TRACK) (\d+) (AUDIO|CDG|MODE1/2048|MODE1/2352|MODE2/2336|MODE2/2352|CDI/2336|CDI2352)$', + 'INDEX': r'^(INDEX) (\d+) (\d+):(\d+):(\d+)$', + 'FLAGS': r'^((?:DCP|4CH|PRE|SCMS) ?){1,4}$', +} + +for k, v in cdtext_re.iteritems(): + cdtext_re[k] = re.compile(v) + +class CDText(object): + def __init__(self, str): + name = str.split()[0] + self.re = cdtext_re[name] + l = self.parse(str) + self.type, self.value = l[0], l[1:] + if type(self.value) == tuple and len(self.value) == 1: + self.value = self.value[0] + + def __repr__(self): + return '' % (self.type, self.value) + + def __str__(self): + return repr(self) + + def parse(self, str): + r = self.re.match(str) + if not r: + return None, None + return r.groups() + +class FieldDescriptor(object): + def __init__(self, field): + self.field = field + + def __get__(self, instance, owner): + def find(name): + for l in instance.cdtext: + if l.type == name: + return l + cdtext = find(self.field) + return cdtext.value if cdtext else None + +class Track(object): + def __init__(self): + self.cdtext = [] + self.abs_tot, self.abs_end, self.nextstart = 0, 0, None + + def add(self, cdtext): + self.cdtext.append(cdtext) + + def set_abs_tot(self, tot): + self.abs_tot = tot + + def set_abs_end(self, end): + self.abs_end + + def get_start_time(self): + index = self.index + return int(index[1])*60 + int(index[2]) + float(index[3])/75 + + def get_length(self): + return self.nextstart - self.get_start_time() + +for f in cdtext_re.keys(): + setattr(Track, f.lower(), FieldDescriptor(f)) + +class Cuesheet(object): + def __init__(self, filename = None, fileobj = None): + if not fileobj and filename: + fileobj = open(filename, 'rb') + if fileobj: + self.parse(fileobj) + + def parse(self, f): + info = [] + tracks = [] + track = Track() + info.append(track) + if not f.read(3) == '\xef\xbb\xbf': + f.seek(0) + for line in f: + cdtext = CDText(line.strip()) + if cdtext.type == 'TRACK': + track = Track() + tracks.append(track) + track.add(cdtext) + self.info = info + self.tracks = tracks + + def get_next(self, track): + found = False + for i in self.tracks: + if found: + return i + elif i == track: + found = True + return None diff --git a/db.py b/db.py new file mode 100644 index 0000000..1295a89 --- /dev/null +++ b/db.py @@ -0,0 +1,166 @@ +import os +from config import config +from sqlalchemy import create_engine +engine = create_engine(config.get('db_path')) + +from sqlalchemy.ext.declarative import declarative_base +Base = declarative_base() + +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship, backref +from sqlalchemy import and_, or_ + +from sqlalchemy.orm.exc import NoResultFound + +class Directory(Base): + __tablename__ = 'directories' + + id = Column(Integer, primary_key = True) + path = Column(String, nullable = False, index = True) + parent_id = Column(Integer, ForeignKey('directories.id')) + + parent = relationship('Directory', backref = backref('children'), remote_side = [id]) + #children = relationship('Directory', lazy = 'joined', join_depth = 2) + + def __init__(self, path, parent_id = None): + self.path = path + self.parent_id = parent_id + + def __repr__(self): + return ''.format(self.path.encode('utf-8')) + + @staticmethod + def get(session, path, parent_id = None): + try: + directory = session.query(Directory).filter(Directory.path == path).one() + except NoResultFound: + directory = Directory(path, parent_id) + session.add(directory) + session.commit() + return directory + +class Artist(Base): + __tablename__ = 'artists' + + id = Column(Integer, primary_key = True) + name = Column(String, nullable = False, index = True) + + def __init__(self, name): + self.name = name + + def __repr__(self): + return ''.format(self.name.encode('utf-8')) + + @staticmethod + def get(session, name): + try: + artist = session.query(Artist).filter(Artist.name == name).one() + except NoResultFound: + artist = Artist(name) + session.add(artist) + session.commit() + return artist + +class Album(Base): + __tablename__ = 'albums' + + id = Column(Integer, primary_key = True) + name = Column(String, nullable = False, index = True) + artist_id = Column(Integer, ForeignKey('artists.id'), nullable = False) + + artist = relationship(Artist, backref = backref('albums', order_by = name)) + + def __init__(self, name, artist_id): + self.name = name + self.artist_id = artist_id + + def __repr__(self): + return ''.format(self.name) + + @staticmethod + def get(session, name, artist_id = None): + try: + album = session.query(Album).filter(Album.name == name).one() + except NoResultFound: + album = Album(name, artist_id) + session.add(album) + session.commit() + return album + +class Track(Base): + __tablename__ = 'tracks' + + id = Column(Integer, primary_key = True) + name = Column(String, index = True) + num = Column(Integer) + filename = Column(String, nullable = False, index = True) + file_index = Column(Integer) + directory_id = Column(Integer, ForeignKey('directories.id'), nullable = False) + artist_id = Column(Integer, ForeignKey('artists.id')) + album_id = Column(Integer, ForeignKey('albums.id')) + + directory = relationship(Directory, backref = backref('tracks', order_by = filename)) + artist = relationship(Artist, backref = backref('tracks')) + album = relationship(Album, backref = backref('tracks')) + + def __init__(self, name, num, filename, file_index, directory_id, artist_id, album_id): + self.name = name + self.num = num + self.filename = filename + self.file_index = file_index + self.directory_id = directory_id + self.artist_id = artist_id + self.album_id = album_id + + def __repr__(self): + return ''.format(self.filename.encode('utf-8')) + + @staticmethod + def get(session, name, num, filename, file_index, directory_id, artist_id, album_id): + try: + track = session.query(Track).filter(and_(Track.filename == filename, Track.file_index == file_index)).one() + except NoResultFound: + track = Track(name, num, filename, file_index, directory_id, artist_id, album_id) + session.add(track) + return track + + @staticmethod + def find(session, path, track = None): + directory, filename = os.path.split(path) + return session.query(Track).filter(and_(Track.filename == filename, Directory.path == directory, Track.file_index == track)).one() + + @staticmethod + def search(session, *args, **kwargs): + r = session.query(Track) + s_or = [] + for f, n in ((Track, 'title'), (Album, 'album'), (Artist, 'artist')): + if f != Track: + r = r.join(f) + if n in kwargs: + r = r.filter(f.name.ilike('%{0}%'.format(kwargs[n]))) + for i in args: + s_or.append(f.name.ilike('%{0}%'.format(i))) + if len(s_or): + r = r.filter(or_(*s_or)) + return r.all() + + def get_path(self): + return os.path.join(self.directory.path, self.filename) + + def get_relpath(self): + return os.path.relpath(self.get_path(), config.get('music_root')) + + def get_metadata(self): + metadata = {} + if self.name: + metadata['title'] = self.name + if self.artist: + metadata['artist'] = self.artist.name + if self.album: + metadata['album'] = self.album.name + return metadata + +Base.metadata.create_all(engine) + +from sqlalchemy.orm import sessionmaker +Session = sessionmaker(bind = engine) diff --git a/directory.py b/directory.py new file mode 100644 index 0000000..67db83f --- /dev/null +++ b/directory.py @@ -0,0 +1,134 @@ +import os, mimetypes, 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 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() diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..ebd82b9 --- /dev/null +++ b/static/index.html @@ -0,0 +1,8 @@ + + + + 音楽 + + + + -- cgit v1.2.3