diff --git a/db.py b/db.py
index 72a12ea..ddcc556 100755
--- a/db.py
+++ b/db.py
@@ -1,20 +1,23 @@
# coding: utf-8
import config
-from sqlalchemy import create_engine, Column, Integer, String, Boolean
-from sqlalchemy.orm import scoped_session, sessionmaker
+
+from sqlalchemy import create_engine, Column, ForeignKey
+from sqlalchemy import Integer, String, Boolean, Date, Time
+from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy import types
+from sqlalchemy.types import TypeDecorator
from sqlalchemy import BINARY
-from sqlalchemy.schema import Column
+
import uuid
-class UUID(types.TypeDecorator):
+class UUID(TypeDecorator):
impl = BINARY
+
def __init__(self):
self.impl.length = 16
- types.TypeDecorator.__init__(self, length = self.impl.length)
+ TypeDecorator.__init__(self, length = self.impl.length)
def process_bind_param(self, value, dialect = None):
if value and isinstance(value, uuid.UUID):
@@ -42,7 +45,7 @@ Base = declarative_base()
Base.query = session.query_property()
class User(Base):
- __tablename__ = 'users'
+ __tablename__ = 'user'
id = UUID.gen_id_column()
name = Column(String, unique = True)
@@ -52,11 +55,38 @@ class User(Base):
admin = Column(Boolean)
class MusicFolder(Base):
- __tablename__ = 'folders'
+ __tablename__ = 'folder'
id = UUID.gen_id_column()
name = Column(String, unique = True)
path = Column(String)
+ last_scan = Column(Date, nullable = True)
+
+class Artist(Base):
+ __tablename__ = 'artist'
+
+ id = UUID.gen_id_column()
+ name = Column(String)
+ albums = relationship('Album', backref = 'artist', lazy = 'dynamic')
+
+class Album(Base):
+ __tablename__ = 'album'
+
+ id = UUID.gen_id_column()
+ name = Column(String)
+ artist_id = Column(UUID, ForeignKey('artist.id'))
+ tracks = relationship('Track', backref = 'album', lazy = 'dynamic')
+
+class Track(Base):
+ __tablename__ = 'track'
+
+ id = UUID.gen_id_column()
+ disc = Column(Integer)
+ number = Column(Integer)
+ title = Column(String)
+ duration = Column(Time)
+ album_id = Column(UUID, ForeignKey('album.id'))
+ path = Column(String)
def init_db():
Base.metadata.create_all(bind = engine)
diff --git a/scanner.py b/scanner.py
new file mode 100755
index 0000000..6b17d96
--- /dev/null
+++ b/scanner.py
@@ -0,0 +1,74 @@
+# coding: utf-8
+
+import os.path
+import datetime
+import eyeD3
+
+import db
+
+def seconds_to_time(secs):
+ th = secs / 3600
+ tm = (secs % 3600) / 60
+ ts = secs % 60
+ return datetime.time(int(th), int(tm), int(ts))
+
+class Scanner:
+ def __init__(self, session):
+ self.__session = session
+ self.__added_artists = 0
+ self.__added_albums = 0
+ self.__added_tracks = 0
+ self.__deleted_artists = 0
+ self.__deleted_albums = 0
+ self.__deleted_tracks = 0
+
+ def scan(self, folder):
+ for root, subfolders, files in os.walk(folder.path):
+ for f in files:
+ if f.endswith('.mp3'):
+ self.__scan_file(os.path.join(root, f))
+
+ def prune(self, folder):
+ pass
+
+ def __scan_file(self, path):
+ tag = eyeD3.Tag()
+ tag.link(path)
+
+ al = self.__find_album(tag.getArtist(), tag.getAlbum())
+ tr = al.tracks.filter(db.Track.path == path).first()
+ if tr is None:
+ tr = db.Track(path = path)
+ self.__added_tracks += 1
+
+ tr.disc = (tag.getDiscNum() or (1, 1))[0]
+ tr.number = tag.getTrackNum()[0]
+ tr.title = tag.getTitle()
+ tr.duration = seconds_to_time(eyeD3.Mp3AudioFile(path).getPlayTime())
+ tr.album = al
+
+ def __find_album(self, artist, album):
+ ar = self.__find_artist(artist)
+ al = ar.albums.filter(db.Album.name == album).first()
+ if not al is None:
+ return al
+
+ al = db.Album(name = album, artist = ar)
+ self.__added_albums += 1
+
+ return al
+
+ def __find_artist(self, artist):
+ ar = self.__session.query(db.Artist).filter(db.Artist.name == artist).first()
+ if not ar is None:
+ return ar
+
+ ar = db.Artist(name = artist)
+ self.__session.add(ar)
+ self.__added_artists += 1
+
+ return ar
+
+ def stats(self):
+ return (self.__added_artists, self.__added_albums, self.__added_tracks), (self.__deleted_artists, self.__deleted_albums, self.__deleted_tracks)
+
diff --git a/templates/home.html b/templates/home.html
index d8cb8fd..904f550 100755
--- a/templates/home.html
+++ b/templates/home.html
@@ -11,10 +11,39 @@
Music folders
- Name | Path | |
+ Name | Path | | |
{% for folder in folders %}
- {{ folder.name }} | {{ folder.path }} | X |
+
+ {{ folder.name }} | {{ folder.path }} | X |
+ Scan |
+
{% endfor %}
-Add
+Add Scan all
+
+Artists
+
+ Id | Name |
+ {% for artist in artists %}
+ {{ artist.id }} | {{ artist.name }} |
+ {% endfor %}
+
+
+Albums
+
+ Artist | Album |
+ {% for album in albums %}
+ {{ album.artist.name }} | {{ album.name }} |
+ {% endfor %}
+
+
+Tracks
+
+ Artist | Album | Disc | # | Title | Len | Path |
+ {% for track in tracks %}
+ {{ track.album.artist.name }} | {{ track.album.name }} | {{ track.disc }} | {{ track.number }} | {{ track.title }} | {{ track.duration }} | {{ track.path }} |
+ {% endfor %}
+
+{{ tracks|length }} tracks
+
{% endblock %}
diff --git a/web.py b/web.py
index 66aa9b5..f28b17c 100755
--- a/web.py
+++ b/web.py
@@ -10,6 +10,7 @@ app = Flask(__name__)
app.secret_key = '?9huDM\\H'
import db
+from scanner import Scanner
@app.teardown_request
def teardown(exception):
@@ -22,7 +23,10 @@ def index():
flash('Not configured. Please create the first admin user')
return redirect(url_for('add_user'))
"""
- return render_template('home.html', users = db.User.query.all(), folders = db.MusicFolder.query.all())
+ return render_template('home.html', users = db.User.query.all(), folders = db.MusicFolder.query.all(),
+ artists = db.Artist.query.order_by(db.Artist.name).all(),
+ albums = db.Album.query.join(db.Album.artist).order_by(db.Artist.name, db.Album.name).all(),
+ tracks = db.Track.query.join(db.Track.album, db.Album.artist).order_by(db.Artist.name, db.Album.name, db.Track.disc, db.Track.number).all())
@app.route('/resetdb')
def reset_db():
@@ -64,14 +68,18 @@ def add_user():
def del_user(id):
try:
idid = uuid.UUID(id)
- user = db.User.query.filter(db.User.id == uuid.UUID(id)).one()
- db.session.delete(user)
- db.session.commit()
- flash("Deleted user '%s'" % user.name)
except ValueError:
flash('Invalid user id')
- except NoResultFound:
+ return redirect(url_for('index'))
+
+ user = db.User.query.get(idid)
+ if user is None:
flash('No such user')
+ return redirect(url_for('index'))
+
+ db.session.delete(user)
+ db.session.commit()
+ flash("Deleted user '%s'" % user.name)
return redirect(url_for('index'))
@@ -114,15 +122,50 @@ def add_folder():
def del_folder(id):
try:
idid = uuid.UUID(id)
- folder = db.MusicFolder.query.filter(db.MusicFolder.id == uuid.UUID(id)).one()
- db.session.delete(folder)
- db.session.commit()
- flash("Deleted folder '%s'" % folder.name)
except ValueError:
flash('Invalid folder id')
- except NoResultFound:
- flash('No such folder')
+ return redirect(url_for('index'))
+ folder = db.MusicFolder.query.get(idid)
+ if folder is None:
+ flash('No such folder')
+ return redirect(url_for('index'))
+
+ db.session.delete(folder)
+ # TODO delete associated tracks
+ db.session.commit()
+ flash("Deleted folder '%s'" % folder.name)
+
+ return redirect(url_for('index'))
+
+@app.route('/scan')
+@app.route('/scan/')
+def scan_folder(id = None):
+ s = Scanner(db.session)
+ if id is None:
+ for folder in db.MusicFolder.query.all():
+ s.scan(folder)
+ s.prune(folder)
+ else:
+ try:
+ idid = uuid.UUID(id)
+ except ValueError:
+ flash('Invalid folder id')
+ return redirect(url_for('index'))
+
+ folder = db.MusicFolder.query.get(idid)
+ if folder is None:
+ flash('No such folder')
+ return redirect(url_for('index'))
+
+ s.scan(folder)
+ s.prune(folder)
+
+ added, deleted = s.stats()
+ db.session.commit()
+
+ flash('Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2]))
+ flash('Deleted: %i artists, %i albums, %i tracks' % (deleted[0], deleted[1], deleted[2]))
return redirect(url_for('index'))
import api.system