def api_fetchalbumart(self, value): cherrypy.session.release_lock() params = json.loads(value) directory = params['directory'] #try getting a cached album art image b64imgpath = albumArtFilePath(directory) img_data = self.albumartcache_load(b64imgpath) if img_data: cherrypy.response.headers["Content-Length"] = len(img_data) return img_data #try getting album art inside local folder fetcher = albumartfetcher.AlbumArtFetcher() localpath = os.path.join(cherry.config['media.basedir'], directory) header, data, resized = fetcher.fetchLocal(localpath) if header: if resized: #cache resized image for next time self.albumartcache_save(b64imgpath, data) cherrypy.response.headers.update(header) return data elif cherry.config['media.fetch_album_art']: #fetch album art from online source album = os.path.basename(directory) artist = os.path.basename(os.path.dirname(directory)) keywords = artist + ' ' + album log.i("Fetching album art for keywords '%s'" % keywords) header, data = fetcher.fetch(keywords) if header: cherrypy.response.headers.update(header) self.albumartcache_save(b64imgpath, data) return data cherrypy.HTTPRedirect("/res/img/folder.png", 302)
def addUser(self, username, password, admin): if not (username.strip() or password.strip()): log.d(_('empty username or password!')) return False user = User.create(username, password, admin) try: exists = self.conn.execute( 'SELECT username' ' FROM users WHERE lower(username) = lower(?)', (username, )).fetchone() if (not exists): self.conn.execute( ''' INSERT INTO users (username, admin, password, salt) VALUES (?,?,?,?)''', (user.name, 1 if user.isadmin else 0, user.password, user.salt)) else: raise sqlite3.IntegrityError except sqlite3.IntegrityError: log.e('cannot create user "%s", already exists!' % user.name) return False self.conn.commit() log.i('added user: ' + user.name) return True
def createOrAlterTable(self, sqlconn): updatedTable = False #table exists? if sqlconn.execute("""SELECT name FROM sqlite_master WHERE type='table' AND name=? """, (self.tablename,)).fetchall(): dbtablelayout = sqlconn.execute("""PRAGMA table_info('%s')""" % self.tablename).fetchall() #map dict to column name dbtablelayout = dict((col[1], col) for col in dbtablelayout) #remove columns from db when not in template for columnname in dbtablelayout.keys(): if columnname not in self.columns: #can't do this in sqlite... #log.i('Dropping column %s from table %s' % (columnname, self.tablename)) #sqlconn.execute("""ALTER TABLE %s DROP COLUMN %s"""%(self.tablename, columnname)) #updatedTable = True pass else: log.d('Column %s in table %s exists and needs no change' % (columnname, self.tablename)) #add new columns to db when not in db for templatecolumnname, templatecolumn in self.columns.items(): if templatecolumnname not in dbtablelayout.keys(): log.i('Adding column %s to table %s' % (templatecolumnname, self.tablename)) sqlconn.execute("""ALTER TABLE %s ADD COLUMN %s""" % (self.tablename, templatecolumn.sql())) updatedTable = True else: log.d('Column %s in table %s exists and needs no change' % (templatecolumnname, self.tablename)) #TODO add checks for DEFAULT value and NOT NULL else: log.i('Creating table %s' % self.tablename) sqlconn.execute("""CREATE TABLE %s (%s)""" % (self.tablename, ', '.join(map(lambda x: x.sql(), self.columns.values())))) updatedTable = True return updatedTable
def ensure_current_version(dbname=None, autoconsent=False, consentcallback=None): """Make sure all defined databases exist and are up to date. Will connect to all these databases and try to update their layout, if necessary, possibly asking the user for consent. dbname : str When given, only make sure of the database with that name. autoconsent : bool When ``True``, don't ask for consent, ever. consentcallback: callable Called when an update requires user consent; if the return value does not evaluate to ``True``, abort don't run any updates and return ``False``. If no callback is given or autoconsent == True, the value of autoconsent will be used to decide if the update should run. Returns : bool ``True`` if requirements are met. """ if autoconsent or consentcallback is None: consentcallback = lambda _: autoconsent with MultiUpdater(dbname) as update: if update.needed: if update.requires_consent and not consentcallback(update.prompts): return False log.w("Database schema update; don't turn off the program!") update.run() log.i("Database schema update complete.") return True
def api_fetchalbumart(self, directory): cherrypy.session.release_lock() #try getting a cached album art image b64imgpath = albumArtFilePath(directory) img_data = self.albumartcache_load(b64imgpath) if img_data: cherrypy.response.headers["Content-Length"] = len(img_data) return img_data #try getting album art inside local folder fetcher = albumartfetcher.AlbumArtFetcher() localpath = os.path.join(cherry.config['media.basedir'], directory) header, data, resized = fetcher.fetchLocal(localpath) if header: if resized: #cache resized image for next time self.albumartcache_save(b64imgpath, data) cherrypy.response.headers.update(header) return data elif cherry.config['media.fetch_album_art']: #fetch album art from online source album = os.path.basename(directory) artist = os.path.basename(os.path.dirname(directory)) keywords = artist+' '+album log.i("Fetching album art for keywords '%s'" % keywords) header, data = fetcher.fetch(keywords) if header: cherrypy.response.headers.update(header) self.albumartcache_save(b64imgpath, data) return data cherrypy.HTTPRedirect("/res/img/folder.png", 302)
def remove_recursive(self, fileobj, progress=None): '''recursively remove fileobj and all its children from the media db.''' if progress is None: log.i( _('removing dead reference(s): %s "%s"'), 'directory' if fileobj.isdir else 'file', fileobj.relpath, ) factory = None remove = lambda item: self.remove_file(item) else: def factory(new, pnt): if pnt is None: return (new, None, progress) return (new, pnt, pnt[2].spawnchild('[-] ' + new.relpath)) remove = lambda item: (self.remove_file(item[0]), item[2].tick()) deld = 0 try: with self.conn: for item in self.db_recursive_filelister(fileobj, factory): remove(item) deld += 1 except Exception as e: log.e(_('error while removing dead reference(s): %s'), e) log.e(_('rolled back to safe state.')) return 0 else: return deld
def index(self, *args, **kwargs): self.getBaseUrl(redirect_unencrypted=True) firstrun = 0 == self.userdb.getUserCount(); if debug: #reload pages everytime in debig mode self.mainpage = readRes('res/main.html') self.loginpage = readRes('res/login.html') self.firstrunpage = readRes('res/firstrun.html') if 'login' in kwargs: username = kwargs.get('username','') password = kwargs.get('password','') login_action = kwargs.get('login','') if login_action == 'login': self.session_auth(username, password) if cherrypy.session['username']: log.i('user ' + cherrypy.session['username'] + ' just logged in.') elif login_action == 'create admin user': if firstrun: if username.strip() and password.strip(): self.userdb.addUser(username, password, True) self.session_auth(username, password) return self.mainpage else: return "No, you can't." if firstrun: return self.firstrunpage else: if self.isAuthorized(): return self.mainpage else: return self.loginpage
def index(self, *args, **kwargs): self.getBaseUrl(redirect_unencrypted=True) firstrun = 0 == self.userdb.getUserCount() show_page = self.mainpage # generated main.html from devel.html if "devel" in kwargs: # reload pages everytime in devel mode show_page = readRes("res/devel.html") self.loginpage = readRes("res/login.html") self.firstrunpage = readRes("res/firstrun.html") if "login" in kwargs: username = kwargs.get("username", "") password = kwargs.get("password", "") login_action = kwargs.get("login", "") if login_action == "login": self.session_auth(username, password) if cherrypy.session["username"]: username = cherrypy.session["username"] log.i(_("user {name} just logged in.").format(name=username)) elif login_action == "create admin user": if firstrun: if username.strip() and password.strip(): self.userdb.addUser(username, password, True) self.session_auth(username, password) return show_page else: return "No, you can't." if firstrun: return self.firstrunpage else: if self.isAuthorized(): return show_page else: return self.loginpage
def update_word_occurrences(self): log.i(_('updating word occurrences...')) with closing( self.conn.execute('''UPDATE dictionary SET occurrences = ( select count(*) from search WHERE search.drowid = dictionary.rowid )''')): pass
def update_word_occurrences(self): log.i("updating word occurrences...") self.conn.execute( """UPDATE dictionary SET occurrences = ( select count(*) from search WHERE search.drowid = dictionary.rowid )""" )
def ensure_current_version(dbname=None, autoconsent=False, consentcallback=None): '''Make sure all defined databases exist and are up to date. Will connect to all these databases and try to update their layout, if necessary, possibly asking the user for consent. dbname : str When given, only make sure of the database with that name. autoconsent : bool When ``True``, don't ask for consent, ever. consentcallback: callable Called when an update requires user consent; if the return value does not evaluate to ``True``, abort don't run any updates and return ``False``. If no callback is given or autoconsent == True, the value of autoconsent will be used to decide if the update should run. Returns : bool ``True`` if requirements are met. ''' if autoconsent or consentcallback is None: consentcallback = lambda _: autoconsent with MultiUpdater(dbname) as update: if update.needed: if update.requires_consent and not consentcallback(update.prompts): return False log.w("Database schema update; don't turn off the program!") update.run() log.i('Database schema update complete.') return True
def update_db_recursive(self, fullpath, skipfirst=False): '''recursively update the media database for a path in basedir''' from collections import namedtuple Item = namedtuple('Item', 'infs indb parent progress') def factory(fs, db, parent): fileobj = fs if fs is not None else db name = fileobj.relpath or fileobj.fullpath if fileobj else '<path not found in filesystem or database>' if parent is None: progress = ProgressTree(name=name) maxlen = lambda s: util.trim_to_maxlen(50, s) progress.reporter = ProgressReporter(lvl=1, namefmt=maxlen) else: progress = parent.progress.spawnchild(name) return Item(fs, db, parent, progress) log.d(_('recursive update for %s'), fullpath) generator = self.enumerate_fs_with_db(fullpath, itemfactory=factory) skipfirst and generator.send(None) adds_without_commit = 0 add = 0 deld = 0 try: with self.conn: for item in generator: infs, indb, progress = (item.infs, item.indb, item.progress) if infs and indb: if infs.isdir != indb.isdir: progress.name = '[±] ' + progress.name deld += self.remove_recursive(indb, progress) self.register_file_with_db(infs) adds_without_commit = 1 else: infs.uid = indb.uid progress.name = '[=] ' + progress.name elif indb: progress.name = '[-] ' + progress.name deld += self.remove_recursive(indb, progress) adds_without_commit = 0 continue # progress ticked by remove; don't tick again elif infs: self.register_file_with_db(item.infs) adds_without_commit += 1 progress.name = '[+] ' + progress.name else: progress.name = '[?] ' + progress.name if adds_without_commit == AUTOSAVEINTERVAL: self.conn.commit() add += adds_without_commit adds_without_commit = 0 progress.tick() except Exception as exc: log.e(_("error while updating media: %s %s"), exc.__class__.__name__, exc) log.e(_("rollback to previous commit.")) traceback.print_exc() raise exc finally: add += adds_without_commit log.i(_('items added %d, removed %d'), add, deld) self.load_db_to_memory()
def db_find_file_by_path(self, fullpath, create=False): '''Finds an absolute path in the file database. If found, returns a File object matching the database record; otherwise, returns None. Paths matching a media basedir are a special case: these will yield a File object with an invalid record id matching the one listed by its children. ''' assert os.path.isabs(fullpath) basedir = cherry.config['media.basedir'] if not fullpath.startswith(basedir): return None relpath = fullpath[len(basedir):].strip(os.path.sep) root = File(basedir, isdir=True, uid= -1) if not relpath: return root file = root for part in relpath.split(os.path.sep): found = False for child in self.fetch_child_files(file): # gotta be ugly: don't know if name/ext split in db if part == child.basename: found = True file = child break if not found: if create: file = File(part, parent=file) log.i('creating database entry for %r', file.relpath) self.register_file_with_db(file) else: return None return file
def index(self, *args, **kwargs): self.getBaseUrl(redirect_unencrypted=True) firstrun = 0 == self.userdb.getUserCount() show_page = self.mainpage #generated main.html from devel.html if 'devel' in kwargs: #reload pages everytime in devel mode show_page = readRes('res/devel.html') self.loginpage = readRes('res/login.html') self.firstrunpage = readRes('res/firstrun.html') if 'login' in kwargs: username = kwargs.get('username', '') password = kwargs.get('password', '') login_action = kwargs.get('login', '') if login_action == 'login': self.session_auth(username, password) if cherrypy.session['username']: username = cherrypy.session['username'] log.i(_('user {name} just logged in.').format(name=username)) elif login_action == 'create admin user': if firstrun: if username.strip() and password.strip(): self.userdb.addUser(username, password, True) self.session_auth(username, password) return show_page else: return "No, you can't." if firstrun: return self.firstrunpage else: if self.isAuthorized(): return show_page else: return self.loginpage
def db_find_file_by_path(self, fullpath, create=False): '''Finds an absolute path in the file database. If found, returns a File object matching the database record; otherwise, returns None. Paths matching a media basedir are a special case: these will yield a File object with an invalid record id matching the one listed by its children. ''' assert os.path.isabs(fullpath) basedir = cherry.config['media.basedir'] if not fullpath.startswith(basedir): return None relpath = fullpath[len(basedir):].strip(os.path.sep) root = File(basedir, isdir=True, uid=-1) if not relpath: return root file = root for part in relpath.split(os.path.sep): found = False for child in self.fetch_child_files( file): # gotta be ugly: don't know if name/ext split in db if part == child.basename: found = True file = child break if not found: if create: file = File(part, parent=file) log.i('creating database entry for %r', file.relpath) self.register_file_with_db(file) else: return None return file
def setup_config(self, createNewConfig, browsersetup, cfg_override): """start the in-browser configuration server, create a config if no configuration is found or provide migration help for old CM versions initialize the configuration if no config setup is needed/requested """ if browsersetup: port = cfg_override.pop('server.port', False) cherrymusicserver.browsersetup.configureAndStartCherryPy(port) if createNewConfig: newconfigpath = pathprovider.configurationFile() + '.new' cfg.write_to_file(cfg.from_defaults(), newconfigpath) log.i(_('New configuration file was written to:{br}{path}').format( path=newconfigpath, br=os.linesep )) sys.exit(0) if not pathprovider.configurationFileExists(): if pathprovider.fallbackPathInUse(): # temp. remove @ v0.30 or so self.printMigrationNoticeAndExit() else: cfg.write_to_file(cfg.from_defaults(), pathprovider.configurationFile()) self.printWelcomeAndExit() self._init_config(cfg_override)
def api_fetchalbumart(self, directory): _save_and_release_session() default_folder_image = "../res/img/folder.png" log.i('Fetching album art for: %s' % directory) filepath = os.path.join(cherry.config['media.basedir'], directory) if os.path.isfile(filepath): # if the given path is a file, try to get the image from ID3 tag = TinyTag.get(filepath, image=True) image_data = tag.get_image() if image_data: log.d('Image found in tag.') header = {'Content-Type': 'image/jpg', 'Content-Length': len(image_data)} cherrypy.response.headers.update(header) return image_data else: # if the file does not contain an image, display the image of the # parent directory directory = os.path.dirname(directory) #try getting a cached album art image b64imgpath = albumArtFilePath(directory) img_data = self.albumartcache_load(b64imgpath) if img_data: cherrypy.response.headers["Content-Length"] = len(img_data) return img_data #try getting album art inside local folder fetcher = albumartfetcher.AlbumArtFetcher() localpath = os.path.join(cherry.config['media.basedir'], directory) header, data, resized = fetcher.fetchLocal(localpath) if header: if resized: #cache resized image for next time self.albumartcache_save(b64imgpath, data) cherrypy.response.headers.update(header) return data elif cherry.config['media.fetch_album_art']: #fetch album art from online source try: foldername = os.path.basename(directory) keywords = foldername log.i(_("Fetching album art for keywords {keywords!r}").format(keywords=keywords)) header, data = fetcher.fetch(keywords) if header: cherrypy.response.headers.update(header) self.albumartcache_save(b64imgpath, data) return data else: # albumart fetcher failed, so we serve a standard image raise cherrypy.HTTPRedirect(default_folder_image, 302) except: # albumart fetcher threw exception, so we serve a standard image raise cherrypy.HTTPRedirect(default_folder_image, 302) else: # no local album art found, online fetching deactivated, show default raise cherrypy.HTTPRedirect(default_folder_image, 302)
def createIndex(self, sqlconn, columns): for c in columns: if not c in self.columns: raise IndexError('column %s does not exist in table %s, cannot create index!' % (c, self.tablename)) existing_indexes = map(itemgetter(0), sqlconn.execute("""SELECT name FROM sqlite_master WHERE type='index' ORDER BY name""").fetchall()) indexname = '_'.join(['idx', self.tablename, '_'.join(columns)]) if not indexname in existing_indexes: log.i('Creating index %s' % indexname) sqlconn.execute('CREATE INDEX IF NOT EXISTS %s ON %s(%s)' % (indexname, self.tablename, ', '.join(columns)))
def run(self): """Update database schema to the highest possible version.""" self._validate_locked() log.i('%r: updating database schema', self.name) log.d('from version %r to %r', self._version, self._target) if None is self._version: self._init_with_version(self._target) else: for version in self._updates_due: self._update_to_version(version)
def session_auth(self, username, password): user = self.userdb.auth(username, password) allow_remote = cherry.config["server.permit_remote_admin_login"] is_loopback = cherrypy.request.remote.ip in ("127.0.0.1", "::1") if not is_loopback and user.isadmin and not allow_remote: log.i(_("Rejected remote admin login from user: {name}").format(name=user.name)) user = userdb.User.nobody() cherrypy.session["username"] = user.name cherrypy.session["userid"] = user.uid cherrypy.session["admin"] = user.isadmin
def session_auth(self, username, password): user = self.userdb.auth(username, password) allow_remote = cherry.config['server.permit_remote_admin_login'] is_loopback = cherrypy.request.remote.ip in ('127.0.0.1', '::1') if not is_loopback and user.isadmin and not allow_remote: log.i(_('Rejected remote admin login from user: {name}').format(name=user.name)) user = userdb.User.nobody() cherrypy.session['username'] = user.name cherrypy.session['userid'] = user.uid cherrypy.session['admin'] = user.isadmin
def __init__(self, db_file, table_to_dump): log.i("Loading files database into memory...") self.db = sqlite3.connect(":memory:", check_same_thread=False) cu = self.db.cursor() cu.execute("attach database '" + db_file + "' as attached_db") cu.execute("select sql from attached_db.sqlite_master " "where type='table' and name='" + table_to_dump + "'") sql_create_table = cu.fetchone()[0] cu.execute(sql_create_table) cu.execute("insert into " + table_to_dump + " select * from attached_db." + table_to_dump) self.db.commit() cu.execute("detach database attached_db")
def full_update(self): '''verify complete media database against the filesystem and make necesary changes.''' log.i(_('running full update...')) try: self.update_db_recursive(cherry.config['media.basedir'], skipfirst=True) except: log.e(_('error during media update. database update incomplete.')) finally: self.update_word_occurrences() log.i(_('media database update complete.'))
def full_update(self): """verify complete media database against the filesystem and make necesary changes.""" log.i("running full update...") try: self.update_db_recursive(cherry.config["media.basedir"], skipfirst=True) except: log.e("error during media update. database update incomplete.") finally: self.update_word_occurrences() log.i("media database update complete.")
def __init__(self, db_file, table_to_dump): log.i(_("Loading files database into memory...")) self.db = sqlite3.connect(':memory:', check_same_thread=False) cu = self.db.cursor() cu.execute('attach database "%s" as attached_db' % db_file) cu.execute("select sql from attached_db.sqlite_master " "where type='table' and name='" + table_to_dump + "'") sql_create_table = cu.fetchone()[0] cu.execute(sql_create_table); cu.execute("insert into " + table_to_dump + " select * from attached_db." + table_to_dump) self.db.commit() cu.execute("detach database attached_db")
def full_update(self): '''verify complete media database against the filesystem and make necesary changes.''' log.i('running full update...') try: self.update_db_recursive(cherry.config.media.basedir.str, skipfirst=True) except: log.e('error during media update. database update incomplete.') finally: self.__create_index_if_non_exist() self.update_word_occurences() log.i('media database update complete.')
def migrate_databases(): """ Makes sure CherryMusic's databases are up to date, migrating them if necessary. This might prompt the user for consent if a migration requires it and terminate the program if no consent is obtained. See :mod:`~cherrymusicserver.databases`. """ db_is_ready = database.ensure_current_version( consentcallback=_get_user_consent_for_db_schema_update) if not db_is_ready: log.i(_("database schema update aborted. quitting.")) sys.exit(1)
def reset(self): """Delete all content from the database along with supporting structures.""" self._validate_locked() version = self._version log.i('%s: resetting database', self.name) log.d('version: %s', version) if None is version: log.d('nothing to reset.') return with self.db.connection() as cxn: cxn.executescript(self.desc[version]['drop.sql']) cxn.executescript(self._metatable['drop.sql']) cxn.executescript(self._metatable['create.sql']) self._setversion(None, cxn) cxn.close()
def transcode(filepath, newformat): log.i("""Transcoding file {} {} ---[{}]---> {}""".format(filepath,filetype(filepath),Encoders[newformat][0],newformat)) try: fromdecoder = decode(filepath) encoder = encode(newformat, fromdecoder) while True: data = encoder.stdout.read(TRANSCODE_BUFFER) if not data: break yield data except OSError: log.w("Transcode of file '{}' to format '{}' failed!".format(filepath,newformat)) finally: encoder.terminate() fromdecoder.terminate()
def addUser(self, username, password, admin): if not (username.strip() or password.strip()): log.d(_('empty username or password!')) return False user = User.create(username, password, admin) try: self.conn.execute(''' INSERT INTO users (username, admin, password, salt) VALUES (?,?,?,?)''', (user.name, 1 if user.isadmin else 0, user.password, user.salt)) except sqlite3.IntegrityError: log.e('cannot create user "%s", already exists!' % user.name) return False self.conn.commit() log.i('added user: ' + user.name) return True
def addUser(self, username, password, admin): if not (username.strip() or password.strip()): log.d(_('empty username or password!')) return False user = User.create(username, password, admin) try: self.conn.execute( ''' INSERT INTO users (username, admin, password, salt) VALUES (?,?,?,?)''', (user.name, 1 if user.isadmin else 0, user.password, user.salt)) except sqlite3.IntegrityError: log.e('cannot create user "%s", already exists!' % user.name) return False self.conn.commit() log.i('added user: ' + user.name) return True
def setup_config(self, createNewConfig, browsersetup, cfg_override): if browsersetup: port = cfg_override.pop('server.port', False) cherrymusicserver.browsersetup.configureAndStartCherryPy(port) if createNewConfig: newconfigpath = pathprovider.configurationFile() + '.new' cfg.write_to_file(cfg.from_defaults(), newconfigpath) log.i('New configuration file was written to:{br}{path}'.format( path=newconfigpath, br=os.linesep)) sys.exit(0) if not pathprovider.configurationFileExists(): if pathprovider.fallbackPathInUse(): # temp. remove @ v0.30 or so self.printMigrationNoticeAndExit() else: cfg.write_to_file(cfg.from_defaults(), pathprovider.configurationFile()) self.printWelcomeAndExit() self._init_config(cfg_override)
def api_fetchalbumart(self, directory): cherrypy.session.release_lock() default_folder_image = "/res/img/folder.png" #try getting a cached album art image b64imgpath = albumArtFilePath(directory) img_data = self.albumartcache_load(b64imgpath) if img_data: cherrypy.response.headers["Content-Length"] = len(img_data) return img_data #try getting album art inside local folder fetcher = albumartfetcher.AlbumArtFetcher() localpath = os.path.join(cherry.config['media.basedir'], directory) header, data, resized = fetcher.fetchLocal(localpath) if header: if resized: #cache resized image for next time self.albumartcache_save(b64imgpath, data) cherrypy.response.headers.update(header) return data elif cherry.config['media.fetch_album_art']: #fetch album art from online source try: foldername = os.path.basename(directory) keywords = foldername log.i( _("Fetching album art for keywords {keywords!r}").format( keywords=keywords)) header, data = fetcher.fetch(keywords) if header: cherrypy.response.headers.update(header) self.albumartcache_save(b64imgpath, data) return data else: # albumart fetcher failed, so we serve a standard image raise cherrypy.HTTPRedirect(default_folder_image, 302) except: # albumart fetcher threw exception, so we serve a standard image raise cherrypy.HTTPRedirect(default_folder_image, 302) else: # no local album art found, online fetching deactivated, show default raise cherrypy.HTTPRedirect(default_folder_image, 302)
def __init__(self, update=None, createNewConfig=False, dropfiledb=False): if createNewConfig: newconfigpath = util.configurationFile() + '.new' configuration.write_to_file(configuration.from_defaults(), newconfigpath) log.i('''New configuration file was written to: ''' + newconfigpath) exit(0) if not util.configurationFileExists(): configuration.write_to_file(configuration.from_defaults(), util.configurationFile()) self.printWelcomeAndExit() self._init_config() self.db = sqlitecache.SQLiteCache(util.databaseFilePath('cherry.cache.db')) if not update == None or dropfiledb: CherryMusic.UpdateThread(self.db,update,dropfiledb).start() else: self.cherrymodel = cherrymodel.CherryModel(self.db) self.httphandler = httphandler.HTTPHandler(config, self.cherrymodel) self.server()
def setup_config(self, createNewConfig, browsersetup, cfg_override): if browsersetup: port = cfg_override.pop('server.port', False) cherrymusicserver.browsersetup.configureAndStartCherryPy(port) if createNewConfig: newconfigpath = pathprovider.configurationFile() + '.new' cfg.write_to_file(cfg.from_defaults(), newconfigpath) log.i('New configuration file was written to:{br}{path}'.format( path=newconfigpath, br=os.linesep )) sys.exit(0) if not pathprovider.configurationFileExists(): if pathprovider.fallbackPathInUse(): # temp. remove @ v0.30 or so self.printMigrationNoticeAndExit() else: cfg.write_to_file(cfg.from_defaults(), pathprovider.configurationFile()) self.printWelcomeAndExit() self._init_config(cfg_override)
def setup_databases(self, update, dropfiledb, setup): if dropfiledb: update = () database.resetdb(sqlitecache.DBNAME) if setup: update = update or () db_is_ready = database.ensure_current_version( consentcallback=self._get_user_consent_for_db_schema_update) if not db_is_ready: log.i("database schema update aborted. quitting.") sys.exit(1) if update is not None: cacheupdate = threading.Thread(name="Updater", target=self._update_if_necessary, args=(update,)) cacheupdate.start() # self._update_if_necessary(update) if not setup: sys.exit(0)
def api_fetchalbumart(self, directory): cherrypy.session.release_lock() default_folder_image = "/res/img/folder.png" #try getting a cached album art image b64imgpath = albumArtFilePath(directory) img_data = self.albumartcache_load(b64imgpath) if img_data: cherrypy.response.headers["Content-Length"] = len(img_data) return img_data #try getting album art inside local folder fetcher = albumartfetcher.AlbumArtFetcher() localpath = os.path.join(cherry.config['media.basedir'], directory) header, data, resized = fetcher.fetchLocal(localpath) if header: if resized: #cache resized image for next time self.albumartcache_save(b64imgpath, data) cherrypy.response.headers.update(header) return data elif cherry.config['media.fetch_album_art']: #fetch album art from online source try: foldername = os.path.basename(directory) keywords = foldername log.i(_("Fetching album art for keywords {keywords!r}").format(keywords=keywords)) header, data = fetcher.fetch(keywords) if header: cherrypy.response.headers.update(header) self.albumartcache_save(b64imgpath, data) return data else: # albumart fetcher failed, so we serve a standard image raise cherrypy.HTTPRedirect(default_folder_image, 302) except: # albumart fetcher threw exception, so we serve a standard image raise cherrypy.HTTPRedirect(default_folder_image, 302) else: # no local album art found, online fetching deactivated, show default raise cherrypy.HTTPRedirect(default_folder_image, 302)
def _check_for_config_updates(self, default, known_config): """check if there are new or deprecated configuration keys in the config file """ new = [] deprecated = [] transform = lambda s: '[{0}]: {2}'.format(*(s.partition('.'))) for property in cfg.to_list(default): if property.key not in known_config and not property.hidden: new.append(transform(property.key)) for property in cfg.to_list(known_config): if property.key not in default: deprecated.append(transform(property.key)) if new: log.i(_('''New configuration options available: %s Using default values for now.'''), '\n\t\t\t'.join(new)) if deprecated: log.i(_('''The following configuration options are not used anymore: %s'''), '\n\t\t\t'.join(deprecated)) if new or deprecated: log.i(_('Start with --newconfig to generate a new default config' ' file next to your current one.'))
def _check_for_config_updates(self, known_config): new = [] deprecated = [] default = configuration.from_defaults() for property in configuration.to_list(default): #@ReservedAssignment if property.name not in known_config and not property.hidden: new.append(property.name) for property in configuration.to_list(known_config): #@ReservedAssignment if property.name not in default: deprecated.append(property.name) if new: log.i('''New configuration options available: %s Using default values for now.''', '\n\t\t\t'.join(new)) if deprecated: log.i('''The following configuration options are not used anymore: %s''', '\n\t\t\t'.join(deprecated)) if new or deprecated: log.i('''Start with --newconfig to generate a new default config file next to your current one. ''', )
def setup_databases(self, update, dropfiledb, setup): """ delete or update the file db if so requested. check if the db schema is up to date """ if dropfiledb: update = () database.resetdb(sqlitecache.DBNAME) if setup: update = update or () db_is_ready = database.ensure_current_version( consentcallback=self._get_user_consent_for_db_schema_update) if not db_is_ready: log.i(_("database schema update aborted. quitting.")) sys.exit(1) if update is not None: cacheupdate = threading.Thread(name="Updater", target=self._update_if_necessary, args=(update,)) cacheupdate.start() # self._update_if_necessary(update) if not setup: CherryMusic.stopAndCleanUp()
def __init__(self, lvl=-1, dly=1, timefmt=None, namefmt=None, repf=None): ''' Creates a progress reporter with the following customization options: lvl : int (default -1) The maximum level in the progress hierarchy that will trigger a report. When a report is triggered, it will contain all progress events up to this level that have occurred since the last report. A negative value will use the time trigger (see ``dly``) to report the newest progress event with the upmost available level. dly : float (default 1) The target maximum delay between reports, in seconds. Triggers a report conforming with ``lvl`` if ``dly`` seconds have passed since the last report. Set to 0 to turn off timed reporting; set to a value < 0 for a time trigger without delay. timefmt : callable(float) -> str (default ProgressReport.timefmt) A function that turns the number for the estimated completion time into a string. That number is provided by ``progress.root.eta``. Per default, it interpreted as seconds until completion, with negative values meaning overtime since estimated completion. namefmt : callable(str) -> str (default: no conversion) A function that converts the name given by ``progress.name`` into a more suitable format. repf : callable(dict) (default: log '%(eta) %(nam) (%(tix))' as info) Function callback to handle the actual reporting. The dict argument contains the following items:: 'eta': completion time as str, 'nam': progress name, 'tix': str giving total ticks registered with this reporter, 'progress': progress to report on, containing the raw data ''' self._eta_adjuster = lambda e: e + 1 self._eta_formatter = self.prettytime if timefmt is None else timefmt self._name_formatter = (lambda s: s) if namefmt is None else namefmt self._reportfunc = (lambda d: log.i('%(eta)s %(nam)s (%(tix)s)', d) ) if repf is None else repf self._replevel = lvl self._repintvl = dly self._maxlevel = 0 self._levelcache = {} self._ticks = 0 self._lastreport = 0
def partial_update(self, path, *paths): basedir = cherry.config['media.basedir'] paths = (path,) + paths log.i(_('updating paths: %s') % (paths,)) for path in paths: path = os.path.normcase(path) abspath = path if os.path.isabs(path) else os.path.join(basedir, path) normpath = os.path.normpath(abspath) if not normpath.startswith(basedir): log.e(_('path is not in basedir. skipping %r') % abspath) continue log.i(_('updating %r...') % path) try: self.update_db_recursive(normpath, skipfirst=False) except Exception as exception: log.e(_('update incomplete: %r'), exception) self.update_word_occurrences() log.i(_('done updating paths.'))
def api_fetchalbumart(self, directory): _save_and_release_session() default_folder_image = "../res/img/folder.png" log.i('Fetching album art for: %s' % directory) filepath = os.path.join(cherry.config['media.basedir'], directory) if os.path.isfile(filepath): # if the given path is a file, try to get the image from ID3 tag = TinyTag.get(filepath, image=True) image_data = tag.get_image() if image_data: log.d('Image found in tag.') header = {'Content-Type': 'image/jpg', 'Content-Length': len(image_data)} cherrypy.response.headers.update(header) return image_data else: # if the file does not contain an image, display the image of the # parent directory directory = os.path.dirname(directory) #try getting a cached album art image b64imgpath = albumArtFilePath(directory) img_data = self.albumartcache_load(b64imgpath) if img_data: cherrypy.response.headers["Content-Length"] = len(img_data) return img_data #try getting album art inside local folder fetcher = albumartfetcher.AlbumArtFetcher() localpath = os.path.join(cherry.config['media.basedir'], directory) header, data, resized = fetcher.fetchLocal(localpath) if header: if resized: #cache resized image for next time self.albumartcache_save(b64imgpath, data) cherrypy.response.headers.update(header) return data elif cherry.config['media.fetch_album_art']: # maximum of files to try to fetch metadata for albumart keywords METADATA_ALBUMART_MAX_FILES = 10 #fetch album art from online source try: foldername = os.path.basename(directory) keywords = foldername # remove any odd characters from the folder name keywords = re.sub('[^A-Za-z\s]', ' ', keywords) # try getting metadata from files in the folder for a more # accurate match files = os.listdir(localpath) for i, filename in enumerate(files): if i >= METADATA_ALBUMART_MAX_FILES: break path = os.path.join(localpath, filename) metadata = metainfo.getSongInfo(path) if metadata.artist and metadata.album: keywords = '{} - {}'.format(metadata.artist, metadata.album) break log.i(_("Fetching album art for keywords {keywords!r}").format(keywords=keywords)) header, data = fetcher.fetch(keywords) if header: cherrypy.response.headers.update(header) self.albumartcache_save(b64imgpath, data) return data else: # albumart fetcher failed, so we serve a standard image raise cherrypy.HTTPRedirect(default_folder_image, 302) except: # albumart fetcher threw exception, so we serve a standard image raise cherrypy.HTTPRedirect(default_folder_image, 302) else: # no local album art found, online fetching deactivated, show default raise cherrypy.HTTPRedirect(default_folder_image, 302)
def create_default_config_file(path): """ Creates or overwrites a default configuration file at `path` """ cfg.write_to_file(cfg.from_defaults(), path) log.i(_('Default configuration file written to %(path)r'), {'path': path})
def start(self, httphandler): socket_host = "127.0.0.1" if config['server.localhost_only'] else "0.0.0.0" resourcedir = os.path.abspath(pathprovider.getResourcePath('res')) if config['server.ssl_enabled']: cherrypy.config.update({ 'server.ssl_certificate': config['server.ssl_certificate'], 'server.ssl_private_key': config['server.ssl_private_key'], 'server.socket_port': config['server.ssl_port'], }) # Create second server for http redirect: redirecter = cherrypy._cpserver.Server() redirecter.socket_port = config['server.port'] redirecter._socket_host = socket_host redirecter.thread_pool = 10 redirecter.subscribe() else: cherrypy.config.update({ 'server.socket_port': config['server.port'], }) cherrypy.config.update({ 'log.error_file': os.path.join( pathprovider.getUserDataPath(), 'server.log'), 'environment': 'production', 'server.socket_host': socket_host, 'server.thread_pool': 30, 'tools.sessions.on': True, 'tools.sessions.timeout': 60 * 24, }) if not config['server.keep_session_in_ram']: sessiondir = os.path.join( pathprovider.getUserDataPath(), 'sessions') if not os.path.exists(sessiondir): os.mkdir(sessiondir) cherrypy.config.update({ 'tools.sessions.storage_type': "file", 'tools.sessions.storage_path': sessiondir, }) cherrypy.tree.mount( httphandler, '/', config={ '/res': { 'tools.staticdir.on': True, 'tools.staticdir.dir': resourcedir, 'tools.staticdir.index': 'index.html', 'tools.caching.on': False, }, '/serve': { 'tools.staticdir.on': True, 'tools.staticdir.dir': config['media.basedir'], 'tools.staticdir.index': 'index.html', 'tools.encode.on': True, 'tools.encode.encoding': 'utf-8', 'tools.caching.on': False, }, '/favicon.ico': { 'tools.staticfile.on': True, 'tools.staticfile.filename': resourcedir + '/favicon.ico', }}) log.i('Starting server on port %s ...' % config['server.port']) cherrypy.lib.caching.expires(0) # disable expiry caching cherrypy.engine.start() cherrypy.engine.block()
def api_fetchalbumart(self, directory): _save_and_release_session() default_folder_image = "../res/img/folder.png" log.i('Fetching album art for: %s' % directory) filepath = os.path.join(cherry.config['media.basedir'], directory) if os.path.isfile(filepath): # if the given path is a file, try to get the image from ID3 tag = TinyTag.get(filepath, image=True) image_data = tag.get_image() if image_data: log.d('Image found in tag.') header = { 'Content-Type': 'image/jpg', 'Content-Length': len(image_data) } cherrypy.response.headers.update(header) return image_data else: # if the file does not contain an image, display the image of the # parent directory directory = os.path.dirname(directory) #try getting a cached album art image b64imgpath = albumArtFilePath(directory) img_data = self.albumartcache_load(b64imgpath) if img_data: cherrypy.response.headers["Content-Length"] = len(img_data) return img_data #try getting album art inside local folder fetcher = albumartfetcher.AlbumArtFetcher() localpath = os.path.join(cherry.config['media.basedir'], directory) header, data, resized = fetcher.fetchLocal(localpath) if header: if resized: #cache resized image for next time self.albumartcache_save(b64imgpath, data) cherrypy.response.headers.update(header) return data elif cherry.config['media.fetch_album_art']: #fetch album art from online source try: foldername = os.path.basename(directory) keywords = foldername log.i( _("Fetching album art for keywords {keywords!r}").format( keywords=keywords)) header, data = fetcher.fetch(keywords) if header: cherrypy.response.headers.update(header) self.albumartcache_save(b64imgpath, data) return data else: # albumart fetcher failed, so we serve a standard image raise cherrypy.HTTPRedirect(default_folder_image, 302) except: # albumart fetcher threw exception, so we serve a standard image raise cherrypy.HTTPRedirect(default_folder_image, 302) else: # no local album art found, online fetching deactivated, show default raise cherrypy.HTTPRedirect(default_folder_image, 302)
def start_server(self, httphandler): """use the configuration to setup and start the cherrypy server """ cherrypy.config.update({'log.screen': True}) ipv6_enabled = config['server.ipv6_enabled'] if config['server.localhost_only']: socket_host = "::1" if ipv6_enabled else "127.0.0.1" else: socket_host = "::" if ipv6_enabled else "0.0.0.0" resourcedir = os.path.abspath(pathprovider.getResourcePath('res')) if config['server.ssl_enabled']: cert = pathprovider.absOrConfigPath(config['server.ssl_certificate']) pkey = pathprovider.absOrConfigPath(config['server.ssl_private_key']) cherrypy.config.update({ 'server.ssl_certificate': cert, 'server.ssl_private_key': pkey, 'server.socket_port': config['server.ssl_port'], }) # Create second server for redirecting http to https: redirecter = cherrypy._cpserver.Server() redirecter.socket_port = config['server.port'] redirecter._socket_host = socket_host redirecter.thread_pool = 10 redirecter.subscribe() else: cherrypy.config.update({ 'server.socket_port': config['server.port'], }) cherrypy.config.update({ 'log.error_file': os.path.join( pathprovider.getUserDataPath(), 'server.log'), 'environment': 'production', 'server.socket_host': socket_host, 'server.thread_pool': 30, 'tools.sessions.on': True, 'tools.sessions.timeout': 60 * 24, }) if not config['server.keep_session_in_ram']: sessiondir = os.path.join( pathprovider.getUserDataPath(), 'sessions') if not os.path.exists(sessiondir): os.mkdir(sessiondir) cherrypy.config.update({ 'tools.sessions.storage_type': "file", 'tools.sessions.storage_path': sessiondir, }) basedirpath = config['media.basedir'] if sys.version_info < (3,0): basedirpath = codecs.encode(basedirpath, 'utf-8') scriptname = codecs.encode(config['server.rootpath'], 'utf-8') else: # fix cherrypy unicode issue (only for Python3) # see patch to cherrypy.lib.static.serve_file way above and # https://bitbucket.org/cherrypy/cherrypy/issue/1148/wrong-encoding-for-urls-containing-utf-8 basedirpath = codecs.decode(codecs.encode(basedirpath, 'utf-8'), 'latin-1') scriptname = config['server.rootpath'] cherrypy.tree.mount( httphandler, scriptname, config={ '/res': { 'tools.staticdir.on': True, 'tools.staticdir.dir': resourcedir, 'tools.staticdir.index': 'index.html', 'tools.caching.on': False, 'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/javascript', 'text/css'], 'tools.gzip.on': True, }, '/serve': { 'tools.staticdir.on': True, 'tools.staticdir.dir': basedirpath, # 'tools.staticdir.index': 'index.html', if ever needed: in py2 MUST utf-8 encode 'tools.staticdir.content_types': MimeTypes, 'tools.encode.on': True, 'tools.encode.encoding': 'utf-8', 'tools.caching.on': False, 'tools.cm_auth.on': True, 'tools.cm_auth.httphandler': httphandler, }, '/favicon.ico': { 'tools.staticfile.on': True, 'tools.staticfile.filename': resourcedir + '/img/favicon.ico', }}) api.v1.mount('/api/v1') log.i(_('Starting server on port %s ...') % config['server.port']) cherrypy.lib.caching.expires(0) # disable expiry caching cherrypy.engine.start() cherrypy.engine.block()