mirror of
https://github.com/spl0k/supysonic.git
synced 2024-11-15 06:32:16 +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:
parent
a1e430c0da
commit
c5890ab120
@ -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:
|
||||||
|
@ -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 })
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
211
cli.py
211
cli.py
@ -1,160 +1,98 @@
|
|||||||
# 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())
|
|
||||||
except RuntimeError, e:
|
|
||||||
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
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
cmd.Cmd.__init__(self)
|
|
||||||
|
|
||||||
# Generate do_* and help_* methods
|
|
||||||
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):
|
|
||||||
setattr(self.__class__, 'do_' + command, self._make_do(command))
|
|
||||||
|
|
||||||
if hasattr(self.__class__, 'do_' + command) and not hasattr(self.__class__, 'help_' + command):
|
|
||||||
setattr(self.__class__, 'help_' + command, getattr(self.__class__, parser_name).print_help)
|
|
||||||
if hasattr(self.__class__, command + '_subparsers'):
|
|
||||||
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):
|
|
||||||
return True
|
|
||||||
|
|
||||||
do_exit = do_EOF
|
|
||||||
|
|
||||||
def default(self, line):
|
|
||||||
print 'Unknown command %s' % line.split()[0]
|
|
||||||
self.do_help(None)
|
|
||||||
|
|
||||||
def postloop(self):
|
|
||||||
print
|
|
||||||
|
|
||||||
def completedefault(self, text, line, begidx, endidx):
|
|
||||||
command = line.split()[0]
|
|
||||||
parsers = getattr(self.__class__, command + '_subparsers', None)
|
|
||||||
if not parsers:
|
|
||||||
return []
|
|
||||||
|
|
||||||
num_words = len(line[len(command):begidx].split())
|
|
||||||
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)
|
|
||||||
folder_subparsers = folder_parser.add_subparsers(dest = 'action')
|
|
||||||
folder_subparsers.add_parser('list', help = 'Lists folders', add_help = False)
|
|
||||||
folder_add_parser = folder_subparsers.add_parser('add', help = 'Adds a folder', add_help = False)
|
|
||||||
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):
|
|
||||||
print 'Name\t\tPath\n----\t\t----'
|
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))
|
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):
|
|
||||||
ret = FolderManager.add(name, path)
|
@manager.command
|
||||||
|
def folder_add(name, path):
|
||||||
|
"Add a folder to the Library"
|
||||||
|
ret = FolderManager.add(path)
|
||||||
if ret != FolderManager.SUCCESS:
|
if ret != FolderManager.SUCCESS:
|
||||||
print FolderManager.error_str(ret)
|
print FolderManager.error_str(ret)
|
||||||
else:
|
else:
|
||||||
print "Folder '{}' added".format(name)
|
print "Folder '{}' added".format(name)
|
||||||
|
|
||||||
def folder_delete(self, name):
|
@manager.command
|
||||||
ret = FolderManager.delete_by_name(name)
|
def folder_delete(path):
|
||||||
|
"Delete folder from Library"
|
||||||
|
|
||||||
|
s = Scanner(db.session)
|
||||||
|
|
||||||
|
ret = FolderManager.delete_by_name(path, s)
|
||||||
if ret != FolderManager.SUCCESS:
|
if ret != FolderManager.SUCCESS:
|
||||||
print FolderManager.error_str(ret)
|
print FolderManager.error_str(ret)
|
||||||
else:
|
else:
|
||||||
print "Deleted folder '{}'".format(name)
|
print "Deleted folder" + path
|
||||||
|
|
||||||
def folder_scan(self, folders):
|
@manager.command
|
||||||
|
def folder_scan():
|
||||||
s = Scanner(db.session)
|
s = Scanner(db.session)
|
||||||
|
|
||||||
|
folders = db.Folder.query.filter(db.Folder.root == True)
|
||||||
|
|
||||||
if folders:
|
if folders:
|
||||||
folders = map(lambda n: db.Folder.query.filter(db.Folder.name == n and db.Folder.root == True).first() or n, folders)
|
for folder in folders:
|
||||||
print folders
|
print "Scanning: " + folder.path
|
||||||
if any(map(lambda f: isinstance(f, basestring), folders)):
|
|
||||||
print "No such folder(s): " + ' '.join(f for f in folders if isinstance(f, basestring))
|
|
||||||
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)
|
FolderManager.scan(folder.id, s)
|
||||||
|
|
||||||
added, deleted = s.stats()
|
added, deleted = s.stats()
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
print "\a"
|
||||||
print "Scanning done"
|
print "Scanning done"
|
||||||
print 'Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2])
|
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])
|
print 'Deleted: %i artists, %i albums, %i tracks' % (deleted[0], deleted[1], deleted[2])
|
||||||
|
|
||||||
user_parser = CLIParser(prog = 'user', add_help = False)
|
@manager.command
|
||||||
user_subparsers = user_parser.add_subparsers(dest = 'action')
|
def folder_prune():
|
||||||
user_subparsers.add_parser('list', help = 'List users', add_help = False)
|
s = Scanner(db.session)
|
||||||
user_add_parser = user_subparsers.add_parser('add', help = 'Adds a user', add_help = False)
|
|
||||||
user_add_parser.add_argument('name', help = 'Name/login of the user to add')
|
|
||||||
user_add_parser.add_argument('-a', '--admin', action = 'store_true', help = 'Give admin rights to the new user')
|
|
||||||
user_add_parser.add_argument('-p', '--password', help = "Specifies the user's password")
|
|
||||||
user_add_parser.add_argument('-e', '--email', default = '', help = "Sets the user's email address")
|
|
||||||
user_del_parser = user_subparsers.add_parser('delete', help = 'Deletes a user', add_help = False)
|
|
||||||
user_del_parser.add_argument('name', help = 'Name/login of the user to delete')
|
|
||||||
user_admin_parser = user_subparsers.add_parser('setadmin', help = 'Enable/disable admin rights for a user', add_help = False)
|
|
||||||
user_admin_parser.add_argument('name', help = 'Name/login of the user to grant/revoke admin rights')
|
|
||||||
user_admin_parser.add_argument('--off', action = 'store_true', help = 'Revoke admin rights if present, grant them otherwise')
|
|
||||||
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):
|
folders = db.Folder.query.filter(db.Folder.root == True)
|
||||||
|
|
||||||
|
if folders:
|
||||||
|
for folder in folders:
|
||||||
|
print "Pruning: " + folder.path
|
||||||
|
FolderManager.prune(folder.id, s)
|
||||||
|
|
||||||
|
added, deleted = s.stats()
|
||||||
|
|
||||||
|
print "\a"
|
||||||
|
print "Pruning 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])
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def user_list():
|
||||||
print 'Name\t\tAdmin\tEmail\n----\t\t-----\t-----'
|
print 'Name\t\tAdmin\tEmail\n----\t\t-----\t-----'
|
||||||
print '\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in db.User.query.all())
|
print '\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in db.User.query.all())
|
||||||
|
|
||||||
def user_add(self, name, admin, password, email):
|
@manager.command
|
||||||
if not password:
|
def user_add(name, admin=False, email=None):
|
||||||
password = getpass.getpass()
|
password = prompt_pass("Please enter a password")
|
||||||
confirm = getpass.getpass('Confirm password: ')
|
if password:
|
||||||
if password != confirm:
|
|
||||||
print >>sys.stderr, "Passwords don't match"
|
|
||||||
return
|
|
||||||
status = UserManager.add(name, password, email, admin)
|
status = UserManager.add(name, password, email, admin)
|
||||||
if status != UserManager.SUCCESS:
|
if status != UserManager.SUCCESS:
|
||||||
print >>sys.stderr, UserManager.error_str(status)
|
print >>sys.stderr, UserManager.error_str(status)
|
||||||
|
|
||||||
def user_delete(self, name):
|
@manager.command
|
||||||
|
def user_delete(name):
|
||||||
user = db.User.query.filter(db.User.name == name).first()
|
user = db.User.query.filter(db.User.name == name).first()
|
||||||
if not user:
|
if not user:
|
||||||
print >>sys.stderr, 'No such user'
|
print >>sys.stderr, 'No such user'
|
||||||
@ -163,7 +101,8 @@ class CLI(cmd.Cmd):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
print "User '{}' deleted".format(name)
|
print "User '{}' deleted".format(name)
|
||||||
|
|
||||||
def user_setadmin(self, name, off):
|
@manager.command
|
||||||
|
def user_setadmin(name, off):
|
||||||
user = db.User.query.filter(db.User.name == name).first()
|
user = db.User.query.filter(db.User.name == name).first()
|
||||||
if not user:
|
if not user:
|
||||||
print >>sys.stderr, 'No such user'
|
print >>sys.stderr, 'No such user'
|
||||||
@ -172,7 +111,8 @@ class CLI(cmd.Cmd):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
print "{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name)
|
print "{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name)
|
||||||
|
|
||||||
def user_changepass(self, name, password):
|
@manager.command
|
||||||
|
def user_changepass(name, password):
|
||||||
if not password:
|
if not password:
|
||||||
password = getpass.getpass()
|
password = getpass.getpass()
|
||||||
confirm = getpass.getpass('Confirm password: ')
|
confirm = getpass.getpass('Confirm password: ')
|
||||||
@ -185,19 +125,22 @@ class CLI(cmd.Cmd):
|
|||||||
else:
|
else:
|
||||||
print "Successfully changed '{}' password".format(name)
|
print "Successfully changed '{}' password".format(name)
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def init_db():
|
||||||
|
db.init_db()
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def recreate_db():
|
||||||
|
db.recreate_db()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
import config
|
||||||
|
|
||||||
if not config.check():
|
if not config.check():
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
import db
|
if not os.path.exists(config.get('base', 'cache_dir')):
|
||||||
db.init_db()
|
os.makedirs(config.get('base', 'cache_dir'))
|
||||||
|
|
||||||
from managers.folder import FolderManager
|
|
||||||
from managers.user import UserManager
|
|
||||||
from scanner import Scanner
|
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
CLI().onecmd(' '.join(sys.argv[1:]))
|
|
||||||
else:
|
|
||||||
CLI().cmdloop()
|
|
||||||
|
|
||||||
|
manager.run()
|
||||||
|
34
db.py
34
db.py
@ -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(),
|
||||||
|
@ -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'):
|
||||||
|
3
main.py
3
main.py
@ -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')
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
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):
|
|
||||||
for f in folder.children:
|
|
||||||
cleanup_folder(f)
|
|
||||||
session.delete(folder)
|
session.delete(folder)
|
||||||
|
|
||||||
cleanup_folder(folder)
|
paths = session.query(Folder.path.like(folder.path + os.sep + '%')).delete()
|
||||||
|
#for f in paths:
|
||||||
|
#if not any (p.path in f.path for p in paths) and not f.root:
|
||||||
|
#app.logger.debug('Deleting path with no parent: ' + f.path)
|
||||||
|
#self.__session.delete(f)
|
||||||
|
|
||||||
|
scanner.prune(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:
|
||||||
|
91
scanner.py
91
scanner.py
@ -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)
|
||||||
|
11
web.py
11
web.py
@ -1,11 +1,13 @@
|
|||||||
# 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
|
import db
|
||||||
|
|
||||||
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = '?9huDM\\H'
|
app.secret_key = '?9huDM\\H'
|
||||||
|
|
||||||
|
import config
|
||||||
if(config.get('base', 'accel-redirect')):
|
if(config.get('base', 'accel-redirect')):
|
||||||
app.use_x_sendfile = True
|
app.use_x_sendfile = True
|
||||||
|
|
||||||
@ -22,9 +24,12 @@ if config.get('base', 'log_file'):
|
|||||||
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = config.get('base', 'database_uri')
|
app.config['SQLALCHEMY_DATABASE_URI'] = config.get('base', 'database_uri')
|
||||||
|
|
||||||
import db
|
|
||||||
|
|
||||||
db.database.init_app(app)
|
db.database.init_app(app)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.init_db()
|
db.init_db()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user