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

- + {% for folder in folders %} - + + + + {% endfor %}
NamePath
NamePath
{{ folder.name }}{{ folder.path }}X
{{ folder.name }}{{ folder.path }}XScan
-Add +Add Scan all + +

Artists

+ + + {% for artist in artists %} + + {% endfor %} +
IdName
{{ artist.id }}{{ artist.name }}
+ +

Albums

+ + + {% for album in albums %} + + {% endfor %} +
ArtistAlbum
{{ album.artist.name }}{{ album.name }}
+ +

Tracks

+ + + {% for track in tracks %} + + {% endfor %} +
ArtistAlbumDisc#TitleLenPath
{{ track.album.artist.name }}{{ track.album.name }}{{ track.disc }}{{ track.number }}{{ track.title }}{{ track.duration }}{{ track.path }}
+

{{ 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