1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-22 17:06:17 +00:00

Media folder scanner

This commit is contained in:
Alban 2012-10-13 14:53:09 +02:00
parent 84bc079a72
commit 2cffe64946
4 changed files with 199 additions and 23 deletions

46
db.py
View File

@ -1,20 +1,23 @@
# coding: utf-8 # coding: utf-8
import config 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.ext.declarative import declarative_base
from sqlalchemy import types from sqlalchemy.types import TypeDecorator
from sqlalchemy import BINARY from sqlalchemy import BINARY
from sqlalchemy.schema import Column
import uuid import uuid
class UUID(types.TypeDecorator): class UUID(TypeDecorator):
impl = BINARY impl = BINARY
def __init__(self): def __init__(self):
self.impl.length = 16 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): def process_bind_param(self, value, dialect = None):
if value and isinstance(value, uuid.UUID): if value and isinstance(value, uuid.UUID):
@ -42,7 +45,7 @@ Base = declarative_base()
Base.query = session.query_property() Base.query = session.query_property()
class User(Base): class User(Base):
__tablename__ = 'users' __tablename__ = 'user'
id = UUID.gen_id_column() id = UUID.gen_id_column()
name = Column(String, unique = True) name = Column(String, unique = True)
@ -52,11 +55,38 @@ class User(Base):
admin = Column(Boolean) admin = Column(Boolean)
class MusicFolder(Base): class MusicFolder(Base):
__tablename__ = 'folders' __tablename__ = 'folder'
id = UUID.gen_id_column() id = UUID.gen_id_column()
name = Column(String, unique = True) name = Column(String, unique = True)
path = Column(String) 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(): def init_db():
Base.metadata.create_all(bind = engine) Base.metadata.create_all(bind = engine)

74
scanner.py Executable file
View File

@ -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)

View File

@ -11,10 +11,39 @@
<h2>Music folders</h2> <h2>Music folders</h2>
<table> <table>
<tr><th>Name</th><th>Path</th><th></th></tr> <tr><th>Name</th><th>Path</th><th></th><th></th></tr>
{% for folder in folders %} {% for folder in folders %}
<tr><td>{{ folder.name }}</td><td>{{ folder.path }}</td><td><a href="{{ url_for('del_folder', id = folder.id) }}">X</a></td></tr> <tr>
<td>{{ folder.name }}</td><td>{{ folder.path }}</td><td><a href="{{ url_for('del_folder', id = folder.id) }}">X</a></td>
<td><a href="{{ url_for('scan_folder', id = folder.id) }}">Scan</a></td>
</tr>
{% endfor %} {% endfor %}
</table> </table>
<a href="{{ url_for('add_folder') }}">Add</a> <a href="{{ url_for('add_folder') }}">Add</a> <a href="{{ url_for('scan_folder') }}">Scan all</a>
<h2>Artists</h2>
<table>
<tr><th>Id</th><th>Name</th></tr>
{% for artist in artists %}
<tr><td>{{ artist.id }}</td><td>{{ artist.name }}</td></tr>
{% endfor %}
</table>
<h2>Albums</h2>
<table>
<tr><th>Artist</th><th>Album</th></tr>
{% for album in albums %}
<tr><td>{{ album.artist.name }}</td><td>{{ album.name }}</td></tr>
{% endfor %}
</table>
<h2>Tracks</h2>
<table>
<tr><th>Artist</th><th>Album</th><th>Disc</th><th>#</th><th>Title</th><th>Len</th><th>Path</th></tr>
{% for track in tracks %}
<tr><td>{{ track.album.artist.name }}</td><td>{{ track.album.name }}</td><td>{{ track.disc }}</td><td>{{ track.number }}</td><td>{{ track.title }}</td><td>{{ track.duration }}</td><td>{{ track.path }}</td></tr>
{% endfor %}
</table>
<p>{{ tracks|length }} tracks</p>
{% endblock %} {% endblock %}

67
web.py
View File

@ -10,6 +10,7 @@ app = Flask(__name__)
app.secret_key = '?9huDM\\H' app.secret_key = '?9huDM\\H'
import db import db
from scanner import Scanner
@app.teardown_request @app.teardown_request
def teardown(exception): def teardown(exception):
@ -22,7 +23,10 @@ def index():
flash('Not configured. Please create the first admin user') flash('Not configured. Please create the first admin user')
return redirect(url_for('add_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') @app.route('/resetdb')
def reset_db(): def reset_db():
@ -64,14 +68,18 @@ def add_user():
def del_user(id): def del_user(id):
try: try:
idid = uuid.UUID(id) 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: except ValueError:
flash('Invalid user id') 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') 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')) return redirect(url_for('index'))
@ -114,15 +122,50 @@ def add_folder():
def del_folder(id): def del_folder(id):
try: try:
idid = uuid.UUID(id) 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: except ValueError:
flash('Invalid folder id') flash('Invalid folder id')
except NoResultFound: return redirect(url_for('index'))
flash('No such folder')
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/<id>')
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')) return redirect(url_for('index'))
import api.system import api.system