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:
parent
84bc079a72
commit
2cffe64946
46
db.py
46
db.py
@ -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
74
scanner.py
Executable 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)
|
||||||
|
|
@ -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
67
web.py
@ -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()
|
except ValueError:
|
||||||
|
flash('Invalid user id')
|
||||||
|
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.delete(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Deleted user '%s'" % user.name)
|
flash("Deleted user '%s'" % user.name)
|
||||||
except ValueError:
|
|
||||||
flash('Invalid user id')
|
|
||||||
except NoResultFound:
|
|
||||||
flash('No such user')
|
|
||||||
|
|
||||||
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
|
||||||
|
Loading…
Reference in New Issue
Block a user