1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-11-14 22:22:18 +00:00

Changed db to use materialized paths for all folder and track operations

Changed CLI to use flask-script (much cleaner and smaller)
This commit is contained in:
Emory P 2013-12-29 14:30:12 -05:00
parent a1e430c0da
commit c5890ab120
10 changed files with 253 additions and 281 deletions

View File

@ -30,7 +30,8 @@ def rand_songs():
if genre: if genre:
query = query.filter(Track.genre == genre) query = query.filter(Track.genre == genre)
if fid: if fid:
query = query.filter(Track.root_folder_id == fid) f = Folder.query.get(fid)
query = query.filter(Track.path.like(f.path + '%'))
count = query.count() count = query.count()
if not count: if not count:
@ -82,6 +83,8 @@ def album_list():
elif ltype == 'alphabeticalByName': elif ltype == 'alphabeticalByName':
query = query.order_by(Folder.name) query = query.order_by(Folder.name)
elif ltype == 'alphabeticalByArtist': elif ltype == 'alphabeticalByArtist':
# this is a mess because who knows how your file structure is set up
# with the database changes it's more difficult to get the parent of a dir
parent = aliased(Folder) parent = aliased(Folder)
query = query.join(parent, Folder.parent).order_by(parent.name).order_by(Folder.name) query = query.join(parent, Folder.parent).order_by(parent.name).order_by(Folder.name)
else: else:

View File

@ -2,7 +2,7 @@
from flask import request from flask import request
from web import app from web import app
from db import Folder, Artist, Album, Track from db import Folder, Artist, Album, Track, func
from api import get_entity from api import get_entity
import uuid, time, string import uuid, time, string
import os.path import os.path
@ -14,7 +14,7 @@ def list_folders():
'musicFolder': [ { 'musicFolder': [ {
'id': str(f.id), 'id': str(f.id),
'name': f.name 'name': f.name
} for f in Folder.query.filter(Folder.root == True).order_by(Folder.name).all() ] } for f in Folder.query.filter(Folder.root == True).order_by(Folder.path).all() ]
} }
}) })
@ -51,10 +51,10 @@ def list_indexes():
artists = [] artists = []
childs = [] childs = []
for f in folder: for f in folder:
artists += f.children artists += f.get_children()
childs += f.tracks childs += f.tracks
else: else:
artists = folder.children artists = folder.get_children()
childs = folder.tracks childs = folder.tracks
indexes = {} indexes = {}
@ -95,10 +95,14 @@ def show_directory():
directory = { directory = {
'id': str(res.id), 'id': str(res.id),
'name': res.name, 'name': res.name,
'child': [ f.as_subsonic_child(request.user) for f in sorted(res.children, key = lambda c: c.name.lower()) ] + [ t.as_subsonic_child(request.user) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] 'child': [ f.as_subsonic_child(request.user) for f in res.get_children() ] + [ t.as_subsonic_child(request.user) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ]
} }
if not res.root: if not res.root:
directory['parent'] = str(res.parent_id) parent = Folder.query.with_entities(Folder.id) \
.filter(Folder.path.like(res.path[:len(res.path)-len(res.name)-1])) \
.order_by(func.length(Folder.path).desc()).first()
if parent:
directory['parent'] = str(parent.id)
return request.formatter({ 'directory': directory }) return request.formatter({ 'directory': directory })

View File

@ -49,7 +49,7 @@ def stream_media():
if redirect and xsendfile: if redirect and xsendfile:
response.headers['X-Accel-Charset'] = 'utf-8' response.headers['X-Accel-Charset'] = 'utf-8'
response.headers['X-Accel-Redirect'] = redirect + xsendfile.encode('UTF8') response.headers['X-Accel-Redirect'] = redirect + xsendfile.encode('UTF8')
app.logger.debug('X-Accel-Redirect: ' + redirect + xsendfile.encode('UTF8')) app.logger.debug('X-Accel-Redirect: ' + redirect + xsendfile)
return response return response
status, res = get_entity(request, Track) status, res = get_entity(request, Track)
@ -127,7 +127,7 @@ def stream_media():
encoder = transcoder[pipe_index+1:] encoder = transcoder[pipe_index+1:]
transcoder = None transcoder = None
app.logger.warn('decoder' + str(decoder) + '\nencoder' + str(encoder)) app.logger.debug('decoder' + str(decoder) + '\nencoder' + str(encoder))
try: try:
if transcoder: if transcoder:

281
cli.py
View File

@ -1,203 +1,146 @@
# coding: utf-8 # coding: utf-8
import sys, cmd, argparse, getpass
import config import config
config.check()
class CLIParser(argparse.ArgumentParser): from web import app
def error(self, message): import db
self.print_usage(sys.stderr) from flask.ext.script import Manager, Command, Option, prompt_pass
raise RuntimeError(message) import os.path
from managers.folder import FolderManager
from managers.user import UserManager
from scanner import Scanner
class CLI(cmd.Cmd): manager = Manager(app)
prompt = "supysonic> "
def _make_do(self, command): @manager.command
def method(obj, line): def folder_list():
try: "Lists all Folders to Scan"
args = getattr(obj, command + '_parser').parse_args(line.split()) print 'Name\t\tPath\n----\t\t----'
except RuntimeError, e: print '\n'.join('{0: <16}{1}'.format(f.name, f.path) for f in db.Folder.query.filter(db.Folder.root == True))
print >>sys.stderr, e
return
if hasattr(obj.__class__, command + '_subparsers'):
try:
func = getattr(obj, '{}_{}'.format(command, args.action))
except AttributeError:
return obj.default(line)
return func(** { key: vars(args)[key] for key in vars(args) if key != 'action' })
else:
try:
func = getattr(obj, command)
except AttributeError:
return obj.default(line)
return func(**vars(args))
return method @manager.command
def folder_add(name, path):
"Add a folder to the Library"
ret = FolderManager.add(path)
if ret != FolderManager.SUCCESS:
print FolderManager.error_str(ret)
else:
print "Folder '{}' added".format(name)
def __init__(self): @manager.command
cmd.Cmd.__init__(self) def folder_delete(path):
"Delete folder from Library"
# Generate do_* and help_* methods s = Scanner(db.session)
for parser_name in filter(lambda attr: attr.endswith('_parser') and '_' not in attr[:-7], dir(self.__class__)):
command = parser_name[:-7]
if not hasattr(self.__class__, 'do_' + command): ret = FolderManager.delete_by_name(path, s)
setattr(self.__class__, 'do_' + command, self._make_do(command)) if ret != FolderManager.SUCCESS:
print FolderManager.error_str(ret)
else:
print "Deleted folder" + path
if hasattr(self.__class__, 'do_' + command) and not hasattr(self.__class__, 'help_' + command): @manager.command
setattr(self.__class__, 'help_' + command, getattr(self.__class__, parser_name).print_help) def folder_scan():
if hasattr(self.__class__, command + '_subparsers'): s = Scanner(db.session)
for action, subparser in getattr(self.__class__, command + '_subparsers').choices.iteritems():
setattr(self, 'help_{} {}'.format(command, action), subparser.print_help)
def do_EOF(self, line): folders = db.Folder.query.filter(db.Folder.root == True)
return True
do_exit = do_EOF if folders:
for folder in folders:
print "Scanning: " + folder.path
FolderManager.scan(folder.id, s)
def default(self, line): added, deleted = s.stats()
print 'Unknown command %s' % line.split()[0]
self.do_help(None)
def postloop(self): print "\a"
print print "Scanning done"
print 'Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2])
print 'Deleted: %i artists, %i albums, %i tracks' % (deleted[0], deleted[1], deleted[2])
def completedefault(self, text, line, begidx, endidx): @manager.command
command = line.split()[0] def folder_prune():
parsers = getattr(self.__class__, command + '_subparsers', None) s = Scanner(db.session)
if not parsers:
return []
num_words = len(line[len(command):begidx].split()) folders = db.Folder.query.filter(db.Folder.root == True)
if num_words == 0:
return [ a for a in parsers.choices.keys() if a.startswith(text) ]
return []
folder_parser = CLIParser(prog = 'folder', add_help = False) if folders:
folder_subparsers = folder_parser.add_subparsers(dest = 'action') for folder in folders:
folder_subparsers.add_parser('list', help = 'Lists folders', add_help = False) print "Pruning: " + folder.path
folder_add_parser = folder_subparsers.add_parser('add', help = 'Adds a folder', add_help = False) FolderManager.prune(folder.id, s)
folder_add_parser.add_argument('name', help = 'Name of the folder to add')
folder_add_parser.add_argument('path', help = 'Path to the directory pointed by the folder')
folder_del_parser = folder_subparsers.add_parser('delete', help = 'Deletes a folder', add_help = False)
folder_del_parser.add_argument('name', help = 'Name of the folder to delete')
folder_scan_parser = folder_subparsers.add_parser('scan', help = 'Run a scan on specified folders', add_help = False)
folder_scan_parser.add_argument('folders', metavar = 'folder', nargs = '*', help = 'Folder(s) to be scanned. If ommitted, all folders are scanned')
def folder_list(self): added, deleted = s.stats()
print 'Name\t\tPath\n----\t\t----'
print '\n'.join('{0: <16}{1}'.format(f.name, f.path) for f in db.Folder.query.filter(db.Folder.root == True))
def folder_add(self, name, path): print "\a"
ret = FolderManager.add(name, path) print "Pruning done"
if ret != FolderManager.SUCCESS: print 'Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2])
print FolderManager.error_str(ret) print 'Deleted: %i artists, %i albums, %i tracks' % (deleted[0], deleted[1], deleted[2])
else:
print "Folder '{}' added".format(name)
def folder_delete(self, name): @manager.command
ret = FolderManager.delete_by_name(name) def user_list():
if ret != FolderManager.SUCCESS: print 'Name\t\tAdmin\tEmail\n----\t\t-----\t-----'
print FolderManager.error_str(ret) print '\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in db.User.query.all())
else:
print "Deleted folder '{}'".format(name)
def folder_scan(self, folders): @manager.command
s = Scanner(db.session) def user_add(name, admin=False, email=None):
if folders: password = prompt_pass("Please enter a password")
folders = map(lambda n: db.Folder.query.filter(db.Folder.name == n and db.Folder.root == True).first() or n, folders) if password:
print folders status = UserManager.add(name, password, email, admin)
if any(map(lambda f: isinstance(f, basestring), folders)): if status != UserManager.SUCCESS:
print "No such folder(s): " + ' '.join(f for f in folders if isinstance(f, basestring)) print >>sys.stderr, UserManager.error_str(status)
for folder in filter(lambda f: isinstance(f, db.Folder), folders):
FolderManager.scan(folder.id, s)
else:
for folder in db.Folder.query.filter(db.Folder.root == True):
FolderManager.scan(folder.id, s)
added, deleted = s.stats() @manager.command
db.session.commit() def user_delete(name):
user = db.User.query.filter(db.User.name == name).first()
if not user:
print >>sys.stderr, 'No such user'
else:
db.session.delete(user)
db.session.commit()
print "User '{}' deleted".format(name)
print "Scanning done" @manager.command
print 'Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2]) def user_setadmin(name, off):
print 'Deleted: %i artists, %i albums, %i tracks' % (deleted[0], deleted[1], deleted[2]) user = db.User.query.filter(db.User.name == name).first()
if not user:
print >>sys.stderr, 'No such user'
else:
user.admin = not off
db.session.commit()
print "{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name)
user_parser = CLIParser(prog = 'user', add_help = False) @manager.command
user_subparsers = user_parser.add_subparsers(dest = 'action') def user_changepass(name, password):
user_subparsers.add_parser('list', help = 'List users', add_help = False) if not password:
user_add_parser = user_subparsers.add_parser('add', help = 'Adds a user', add_help = False) password = getpass.getpass()
user_add_parser.add_argument('name', help = 'Name/login of the user to add') confirm = getpass.getpass('Confirm password: ')
user_add_parser.add_argument('-a', '--admin', action = 'store_true', help = 'Give admin rights to the new user') if password != confirm:
user_add_parser.add_argument('-p', '--password', help = "Specifies the user's password") print >>sys.stderr, "Passwords don't match"
user_add_parser.add_argument('-e', '--email', default = '', help = "Sets the user's email address") return
user_del_parser = user_subparsers.add_parser('delete', help = 'Deletes a user', add_help = False) status = UserManager.change_password2(name, password)
user_del_parser.add_argument('name', help = 'Name/login of the user to delete') if status != UserManager.SUCCESS:
user_admin_parser = user_subparsers.add_parser('setadmin', help = 'Enable/disable admin rights for a user', add_help = False) print >>sys.stderr, UserManager.error_str(status)
user_admin_parser.add_argument('name', help = 'Name/login of the user to grant/revoke admin rights') else:
user_admin_parser.add_argument('--off', action = 'store_true', help = 'Revoke admin rights if present, grant them otherwise') print "Successfully changed '{}' password".format(name)
user_pass_parser = user_subparsers.add_parser('changepass', help = "Changes a user's password", add_help = False)
user_pass_parser.add_argument('name', help = 'Name/login of the user to which change the password')
user_pass_parser.add_argument('password', nargs = '?', help = 'New password')
def user_list(self): @manager.command
print 'Name\t\tAdmin\tEmail\n----\t\t-----\t-----' def init_db():
print '\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in db.User.query.all()) db.init_db()
def user_add(self, name, admin, password, email): @manager.command
if not password: def recreate_db():
password = getpass.getpass() db.recreate_db()
confirm = getpass.getpass('Confirm password: ')
if password != confirm:
print >>sys.stderr, "Passwords don't match"
return
status = UserManager.add(name, password, email, admin)
if status != UserManager.SUCCESS:
print >>sys.stderr, UserManager.error_str(status)
def user_delete(self, name):
user = db.User.query.filter(db.User.name == name).first()
if not user:
print >>sys.stderr, 'No such user'
else:
db.session.delete(user)
db.session.commit()
print "User '{}' deleted".format(name)
def user_setadmin(self, name, off):
user = db.User.query.filter(db.User.name == name).first()
if not user:
print >>sys.stderr, 'No such user'
else:
user.admin = not off
db.session.commit()
print "{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name)
def user_changepass(self, name, password):
if not password:
password = getpass.getpass()
confirm = getpass.getpass('Confirm password: ')
if password != confirm:
print >>sys.stderr, "Passwords don't match"
return
status = UserManager.change_password2(name, password)
if status != UserManager.SUCCESS:
print >>sys.stderr, UserManager.error_str(status)
else:
print "Successfully changed '{}' password".format(name)
if __name__ == "__main__": if __name__ == "__main__":
if not config.check(): import config
sys.exit(1)
import db if not config.check():
db.init_db() sys.exit(1)
from managers.folder import FolderManager if not os.path.exists(config.get('base', 'cache_dir')):
from managers.user import UserManager os.makedirs(config.get('base', 'cache_dir'))
from scanner import Scanner
if len(sys.argv) > 1:
CLI().onecmd(' '.join(sys.argv[1:]))
else:
CLI().cmdloop()
manager.run()

34
db.py
View File

@ -4,9 +4,11 @@ import config
from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.sqlalchemy import SQLAlchemy
from sqlalchemy.types import TypeDecorator, BINARY from sqlalchemy.types import TypeDecorator, BINARY
from sqlalchemy.ext.hybrid import *
from sqlalchemy.dialects.postgresql import UUID as pgUUID from sqlalchemy.dialects.postgresql import UUID as pgUUID
import uuid, datetime, time import uuid, datetime, time
import mimetypes
import os.path import os.path
database = SQLAlchemy() database = SQLAlchemy()
@ -82,7 +84,7 @@ class User(database.Model):
lastfm_session = Column(String(32), nullable = True) lastfm_session = Column(String(32), nullable = True)
lastfm_status = Column(Boolean, default = True) # True: ok/unlinked, False: invalid session lastfm_status = Column(Boolean, default = True) # True: ok/unlinked, False: invalid session
last_play_id = Column(UUID, ForeignKey('track.id'), nullable = True) last_play_id = Column(UUID, ForeignKey('track.id', ondelete = 'SET NULL'), nullable = True)
last_play = relationship('Track') last_play = relationship('Track')
last_play_date = Column(DateTime, nullable = True) last_play_date = Column(DateTime, nullable = True)
@ -115,14 +117,17 @@ class Folder(database.Model):
id = UUID.gen_id_column() id = UUID.gen_id_column()
root = Column(Boolean, default = False) root = Column(Boolean, default = False)
name = Column(String(256))
path = Column(String(4096)) # should be unique, but mysql don't like such large columns path = Column(String(4096)) # should be unique, but mysql don't like such large columns
created = Column(DateTime, default = now) created = Column(DateTime, default = now)
has_cover_art = Column(Boolean, default = False) has_cover_art = Column(Boolean, default = False)
last_scan = Column(Integer, default = 0) last_scan = Column(Integer, default = 0)
parent_id = Column(UUID, ForeignKey('folder.id'), nullable = True) @hybrid_property
children = relationship('Folder', backref = backref('parent', remote_side = [ id ])) def name(self):
return self.path[self.path.rfind(os.sep) + 1:]
def get_children(self):
return Folder.query.filter(Folder.path.like(self.path + '/%%')).filter(~Folder.path.like(self.path + '/%%/%%'))
def as_subsonic_child(self, user): def as_subsonic_child(self, user):
info = { info = {
@ -133,8 +138,12 @@ class Folder(database.Model):
'created': self.created.isoformat() 'created': self.created.isoformat()
} }
if not self.root: if not self.root:
info['parent'] = str(self.parent_id) parent = session.query(Folder) \
info['artist'] = self.parent.name .filter(Folder.path.like(self.path[:len(self.path)-len(self.name)-1])) \
.order_by(func.length(Folder.path).desc()).first()
if(parent):
info['parent'] = str(parent.id)
info['artist'] = parent.name
if self.has_cover_art: if self.has_cover_art:
info['coverArt'] = str(self.id) info['coverArt'] = str(self.id)
@ -176,7 +185,7 @@ class Album(database.Model):
id = UUID.gen_id_column() id = UUID.gen_id_column()
name = Column(String(255)) name = Column(String(255))
artist_id = Column(UUID, ForeignKey('artist.id')) artist_id = Column(UUID, ForeignKey('artist.id'))
tracks = relationship('Track', backref = 'album') tracks = relationship('Track', backref = 'album', cascade="delete")
def as_subsonic_album(self, user): def as_subsonic_album(self, user):
info = { info = {
@ -214,17 +223,14 @@ class Track(database.Model):
bitrate = Column(Integer) bitrate = Column(Integer)
path = Column(String(4096)) # should be unique, but mysql don't like such large columns path = Column(String(4096)) # should be unique, but mysql don't like such large columns
content_type = Column(String(32))
created = Column(DateTime, default = now) created = Column(DateTime, default = now)
last_modification = Column(Integer) last_modification = Column(Integer)
play_count = Column(Integer, default = 0) play_count = Column(Integer, default = 0)
last_play = Column(DateTime, nullable = True) last_play = Column(DateTime, nullable = True)
root_folder_id = Column(UUID, ForeignKey('folder.id')) folder_id = Column(UUID, ForeignKey('folder.id', ondelete="CASCADE"))
root_folder = relationship('Folder', primaryjoin = Folder.id == root_folder_id) folder = relationship('Folder', backref = 'tracks')
folder_id = Column(UUID, ForeignKey('folder.id'))
folder = relationship('Folder', primaryjoin = Folder.id == folder_id, backref = 'tracks')
def as_subsonic_child(self, user): def as_subsonic_child(self, user):
info = { info = {
@ -236,11 +242,11 @@ class Track(database.Model):
'artist': self.album.artist.name, 'artist': self.album.artist.name,
'track': self.number, 'track': self.number,
'size': os.path.getsize(self.path), 'size': os.path.getsize(self.path),
'contentType': self.content_type, 'contentType': mimetypes.guess_type(self.path),
'suffix': self.suffix(), 'suffix': self.suffix(),
'duration': self.duration, 'duration': self.duration,
'bitRate': self.bitrate, 'bitRate': self.bitrate,
'path': self.path[len(self.root_folder.path) + 1:], 'path': self.path,
'isVideo': False, 'isVideo': False,
'discNumber': self.disc, 'discNumber': self.disc,
'created': self.created.isoformat(), 'created': self.created.isoformat(),

View File

@ -9,6 +9,8 @@ from db import session, Folder
from managers.user import UserManager from managers.user import UserManager
from managers.folder import FolderManager from managers.folder import FolderManager
import scanner
@app.before_request @app.before_request
def check_admin(): def check_admin():
if not request.path.startswith('/folder'): if not request.path.startswith('/folder'):

View File

@ -10,8 +10,7 @@ if __name__ == '__main__':
if not os.path.exists(config.get('base', 'cache_dir')): if not os.path.exists(config.get('base', 'cache_dir')):
os.makedirs(config.get('base', 'cache_dir')) os.makedirs(config.get('base', 'cache_dir'))
import db
from web import app from web import app
app.run(host = '0.0.0.0', debug = True) app.run(host = '0.0.0.0')

View File

@ -30,8 +30,8 @@ class FolderManager:
return FolderManager.SUCCESS, folder return FolderManager.SUCCESS, folder
@staticmethod @staticmethod
def add(name, path): def add(path):
if Folder.query.filter(Folder.name == name and Folder.root == True).first(): if Folder.query.filter(Folder.path == path and Folder.root == True).first():
return FolderManager.NAME_EXISTS return FolderManager.NAME_EXISTS
path = os.path.abspath(path) path = os.path.abspath(path)
@ -41,14 +41,14 @@ class FolderManager:
if folder: if folder:
return FolderManager.PATH_EXISTS return FolderManager.PATH_EXISTS
folder = Folder(root = True, name = name, path = path) folder = Folder(root = True, path = path)
session.add(folder) session.add(folder)
session.commit() session.commit()
return FolderManager.SUCCESS return FolderManager.SUCCESS
@staticmethod @staticmethod
def delete(uid): def delete(uid, scanner):
status, folder = FolderManager.get(uid) status, folder = FolderManager.get(uid)
if status != FolderManager.SUCCESS: if status != FolderManager.SUCCESS:
return status return status
@ -56,34 +56,26 @@ class FolderManager:
if not folder.root: if not folder.root:
return FolderManager.NO_SUCH_FOLDER return FolderManager.NO_SUCH_FOLDER
# delete associated tracks and prune empty albums/artists session.delete(folder)
for artist in Artist.query.all():
for album in artist.albums[:]:
for track in filter(lambda t: t.root_folder.id == folder.id, album.tracks):
album.tracks.remove(track)
session.delete(track)
if len(album.tracks) == 0:
artist.albums.remove(album)
session.delete(album)
if len(artist.albums) == 0:
session.delete(artist)
def cleanup_folder(folder): paths = session.query(Folder.path.like(folder.path + os.sep + '%')).delete()
for f in folder.children: #for f in paths:
cleanup_folder(f) #if not any (p.path in f.path for p in paths) and not f.root:
session.delete(folder) #app.logger.debug('Deleting path with no parent: ' + f.path)
#self.__session.delete(f)
scanner.prune(folder)
cleanup_folder(folder)
session.commit() session.commit()
return FolderManager.SUCCESS return FolderManager.SUCCESS
@staticmethod @staticmethod
def delete_by_name(name): def delete_by_name(path, scanner):
folder = Folder.query.filter(Folder.name == name and Folder.root == True).first() folder = Folder.query.filter(Folder.path == path and Folder.root == True).first()
if not folder: if not folder:
return FolderManager.NO_SUCH_FOLDER return FolderManager.NO_SUCH_FOLDER
return FolderManager.delete(folder.id) return FolderManager.delete(folder.id, scanner)
@staticmethod @staticmethod
def scan(uid, scanner): def scan(uid, scanner):
@ -95,6 +87,15 @@ class FolderManager:
scanner.prune(folder) scanner.prune(folder)
return FolderManager.SUCCESS return FolderManager.SUCCESS
@staticmethod
def prune(uid, scanner):
status, folder = FolderManager.get(uid)
if status != FolderManager.SUCCESS:
return status
scanner.prune(folder)
return FolderManager.SUCCESS
@staticmethod @staticmethod
def error_str(err): def error_str(err):
if err == FolderManager.SUCCESS: if err == FolderManager.SUCCESS:

View File

@ -3,10 +3,11 @@
import os, os.path import os, os.path
import time import time
import mutagen import mutagen
import config, db import config
import math import math
import sys, traceback import sys, traceback
from web import app from web import app
import db
class Scanner: class Scanner:
def __init__(self, session): def __init__(self, session):
@ -14,6 +15,7 @@ class Scanner:
self.__tracks = db.Track.query.all() self.__tracks = db.Track.query.all()
self.__tracks = {x.path: x for x in self.__tracks} self.__tracks = {x.path: x for x in self.__tracks}
self.__tracktimes = {x.path: x.last_modification for x in self.__tracks.values()}
self.__artists = db.Artist.query.all() self.__artists = db.Artist.query.all()
self.__artists = {x.name.lower(): x for x in self.__artists} self.__artists = {x.name.lower(): x for x in self.__artists}
@ -33,52 +35,59 @@ class Scanner:
extensions = config.get('base', 'scanner_extensions') extensions = config.get('base', 'scanner_extensions')
self.__extensions = map(str.lower, extensions.split()) if extensions else None self.__extensions = map(str.lower, extensions.split()) if extensions else None
def scan(self, folder): def scan(self, root_folder):
print "scanning", folder.path print "scanning", root_folder.path
valid = [x.lower() for x in config.get('base','filetypes').split(',')] valid = [x.lower() for x in config.get('base','filetypes').split(',')]
valid = tuple(valid) valid = tuple(valid)
print "valid filetypes: ",valid print "valid filetypes: ",valid
for root, subfolders, files in os.walk(folder.path, topdown=False): for root, subfolders, files in os.walk(root_folder.path, topdown=False):
if(root not in self.__folders):
app.logger.debug('Adding folder (empty): ' + root)
self.__folders[root] = db.Folder(path = root)
for f in files: for f in files:
if f.lower().endswith(valid): if f.lower().endswith(valid):
try: try:
self.__scan_file(os.path.join(root, f), folder) path = os.path.join(root, f)
self.__scan_file(path, root)
except: except:
app.logger.error('Problem adding file: ' + os.path.join(root,f)) app.logger.error('Problem adding file: ' + os.path.join(root,f))
app.logger.error(traceback.print_exc()) app.logger.error(traceback.print_exc())
sys.exit(0) sys.exit(0)
self.__session.rollback() self.__session.rollback()
print "\a" self.__session.add_all(self.__folders.values())
self.__session.add_all(self.__tracks.values()) self.__session.add_all(self.__tracks.values())
root_folder.last_scan = int(time.time())
self.__session.commit() self.__session.commit()
folder.last_scan = int(time.time())
def prune(self, folder): def prune(self, folder):
for path, root_folder_id, track_id in self.__session.query(db.Track.path, db.Track.root_folder_id, db.Track.id): # check for invalid paths still in database
if root_folder_id == folder.id and not os.path.exists(path): #app.logger.debug('Checking for invalid paths...')
app.logger.debug('Removed invalid path: ' + path) #for path in self.__tracks.keys():
self.__remove_track(self.__session.merge(db.Track(id = track_id))) #if not os.path.exists(path.encode('utf-8')):
#app.logger.debug('Removed invalid path: ' + path)
#self.__remove_track(self.__tracks[path])
self.__session.commit() app.logger.debug('Checking for empty albums...')
for album in db.Album.query.filter(~db.Album.id.in_(self.__session.query(db.Track.album_id).distinct())):
for album in [ album for artist in self.__artists.values() for album in artist.albums if len(album.tracks) == 0 ]: app.logger.debug(album.name + ' Removed')
album.artist.albums.remove(album) album.artist.albums.remove(album)
self.__session.delete(album) self.__session.delete(album)
self.__deleted_albums += 1 self.__deleted_albums += 1
self.__session.commit() app.logger.debug('Checking for artists with no albums...')
for artist in [ a for a in self.__artists.values() if len(a.albums) == 0 ]: for artist in [ a for a in self.__artists.values() if len(a.albums) == 0 ]:
self.__session.delete(artist) self.__session.delete(artist)
self.__deleted_artists += 1 self.__deleted_artists += 1
self.__session.commit() self.__session.commit()
app.logger.debug('Cleaning up folder...')
self.__cleanup_folder(folder) self.__cleanup_folder(folder)
def __scan_file(self, path, folder): def __scan_file(self, path, root):
curmtime = int(math.floor(os.path.getmtime(path))) curmtime = int(math.floor(os.path.getmtime(path)))
if path in self.__tracks: if path in self.__tracks:
@ -88,7 +97,7 @@ class Scanner:
if not tr.last_modification: if not tr.last_modification:
tr.last_modification = curmtime tr.last_modification = curmtime
if curmtime <= tr.last_modification: if curmtime <= self.__tracktimes[path]:
app.logger.debug('\tFile not modified') app.logger.debug('\tFile not modified')
return False return False
@ -107,7 +116,7 @@ class Scanner:
app.logger.debug('\tProblem reading tag') app.logger.debug('\tProblem reading tag')
return False return False
tr = db.Track(path = path, root_folder = folder, folder = self.__find_folder(path, folder)) tr = db.Track(path = path, folder = self.__find_folder(root))
self.__tracks[path] = tr self.__tracks[path] = tr
self.__added_tracks += 1 self.__added_tracks += 1
@ -137,7 +146,7 @@ class Scanner:
else: else:
#Flair! #Flair!
sys.stdout.write('\033[K') sys.stdout.write('\033[K')
sys.stdout.write('%s\r' % artist) sys.stdout.write('%s\r' % artist.encode('utf-8'))
sys.stdout.flush() sys.stdout.flush()
ar = db.Artist(name = artist) ar = db.Artist(name = artist)
self.__artists[artist] = ar self.__artists[artist] = ar
@ -150,26 +159,14 @@ class Scanner:
self.__added_albums += 1 self.__added_albums += 1
return db.Album(name = album, artist = ar) return db.Album(name = album, artist = ar)
def __find_folder(self, path, folder): def __find_folder(self, path):
path = os.path.dirname(path)
if path in self.__folders: if path in self.__folders:
return self.__folders[path] return self.__folders[path]
# must find parent directory to create new one app.logger.debug('Adding folder: ' + path)
full_path = folder.path self.__folders[path] = db.Folder(path = path)
path = path[len(folder.path) + 1:] return self.__folders[path]
for name in path.split(os.sep):
full_path = os.path.join(full_path, name)
if full_path in self.__folders:
folder = self.__folders[full_path]
else:
folder = db.Folder(root = False, name = name, path = full_path, parent = folder)
self.__folders[full_path] = folder
return folder
def __try_load_tag(self, path): def __try_load_tag(self, path):
try: try:
@ -201,11 +198,23 @@ class Scanner:
self.__deleted_tracks += 1 self.__deleted_tracks += 1
def __cleanup_folder(self, folder): def __cleanup_folder(self, folder):
for f in folder.children:
self.__cleanup_folder(f)
if len(folder.children) == 0 and len(folder.tracks) == 0 and not folder.root: # Get all subfolders of folder
folder.parent = None all_descendants = self.__session.query(db.Folder).filter(db.Folder.path.like(folder.path + os.sep + '%'))
self.__session.delete(folder)
app.logger.debug('Checking for empty paths')
# Delete folder if there is no track in a subfolder
for d in all_descendants:
if any(d.path in k for k in self.__tracks.keys()):
continue;
else:
app.logger.debug('Deleting path with no tracks: ' + d.path)
self.__session.delete(d)
self.__session.commit()
return
def stats(self): def stats(self):
return (self.__added_artists, self.__added_albums, self.__added_tracks), (self.__deleted_artists, self.__deleted_albums, self.__deleted_tracks) return (self.__added_artists, self.__added_albums, self.__added_tracks), (self.__deleted_artists, self.__deleted_albums, self.__deleted_tracks)

51
web.py
View File

@ -1,32 +1,37 @@
# coding: utf-8 # coding: utf-8
from flask import Flask, request, session, flash, render_template, redirect, url_for from flask import Flask, request, session, flash, render_template, redirect, url_for
import config
app = Flask(__name__)
app.secret_key = '?9huDM\\H'
if(config.get('base', 'accel-redirect')):
app.use_x_sendfile = True
if config.get('base', 'debug'):
app.debug = True
app.config['SQLALCHEMY_ECHO'] = True
if config.get('base', 'log_file'):
import logging
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(config.get('base', 'log_file'), when = 'midnight', encoding = 'UTF-8')
handler.setLevel(logging.DEBUG)
app.logger.addHandler(handler)
app.config['SQLALCHEMY_DATABASE_URI'] = config.get('base', 'database_uri')
import db import db
db.database.init_app(app) def create_app():
app = Flask(__name__)
app.secret_key = '?9huDM\\H'
import config
if(config.get('base', 'accel-redirect')):
app.use_x_sendfile = True
if config.get('base', 'debug'):
app.debug = True
app.config['SQLALCHEMY_ECHO'] = True
if config.get('base', 'log_file'):
import logging
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(config.get('base', 'log_file'), when = 'midnight', encoding = 'UTF-8')
handler.setLevel(logging.DEBUG)
app.logger.addHandler(handler)
app.config['SQLALCHEMY_DATABASE_URI'] = config.get('base', 'database_uri')
db.database.init_app(app)
return app
app = create_app()
with app.app_context(): with app.app_context():
db.init_db() db.init_db()
from managers.user import UserManager from managers.user import UserManager