summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rwxr-xr-xapp.py18
-rw-r--r--config.py25
-rw-r--r--cuesheet.py103
-rw-r--r--db.py166
-rw-r--r--directory.py134
-rw-r--r--static/index.html8
7 files changed, 457 insertions, 0 deletions
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 '<CDText "%s" "%s">' % (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 '<Directory("{0}")>'.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 '<Artist("{0}")>'.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 '<Album("{0}")>'.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 '<Track("{0}")>'.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 '<a href="/files/{path}">{name}</a><br />'.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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>&#x97f3;&#x697d;</title>
+</head>
+<body>
+</body>
+</html>