PK!musicbot/__init__.pyPK!musicbot/commands/__init__.pyPK!Fccmusicbot/commands/completion.pyimport click import click_completion from lib import helpers @click.group(cls=helpers.GroupWithHelp) def cli(): '''Completion tool''' @cli.command() @click.option('-i', '--case-insensitive/--no-case-insensitive', help="Case insensitive completion") @click.argument('shell', required=False, type=click_completion.DocumentedChoice(click_completion.shells)) def show(shell, case_insensitive): """Show the click-completion-command completion code""" extra_env = {'_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE': 'ON'} if case_insensitive else {} click.echo(click_completion.get_code(shell, extra_env=extra_env)) @cli.command() @click.option('--append/--overwrite', help="Append the completion code to the file", default=None) @click.option('-i', '--case-insensitive/--no-case-insensitive', help="Case insensitive completion") @click.argument('shell', required=False, type=click_completion.DocumentedChoice(click_completion.shells)) @click.argument('path', required=False) def install(append, case_insensitive, shell, path): """Install the click-completion-command completion""" extra_env = {'_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE': 'ON'} if case_insensitive else {} shell, path = click_completion.install(shell=shell, path=path, append=append, extra_env=extra_env) click.echo('%s completion installed in %s' % (shell, path)) PK!'6/musicbot/commands/config.pyimport click from lib import helpers, database, server, persistence from lib.config import config from logging_tree import printout @click.group(cls=helpers.GroupWithHelp) def cli(): '''Config management''' @cli.command() def show(): '''Print default config''' print(config) @cli.command() @helpers.coro @helpers.add_options(persistence.options) @helpers.add_options(database.options) @helpers.add_options(server.options) async def save(**kwargs): '''Save config''' redis = persistence.Persistence(**kwargs) await redis.connect() await redis.execute('set', 'my-key', 'value') val = await redis.execute('get', 'my-key') print(val) await redis.close() @cli.command() def logging(): '''Show loggers tree''' printout() PK!y= musicbot/commands/consistency.pyimport click from lib import database, mfilter, helpers, collection @click.group(cls=helpers.GroupWithHelp) @click.pass_context @helpers.add_options(database.options) def cli(ctx, **kwargs): '''Inconsistencies management''' ctx.obj.db = collection.Collection(**kwargs) @cli.command() @helpers.coro @click.pass_context @helpers.add_options(mfilter.options) async def errors(ctx, **kwargs): '''Detect errors''' mf = mfilter.Filter(**kwargs) errors = await ctx.obj.db.errors(mf) for e in errors: print(e) PK!3musicbot/commands/db.pyimport click import os import logging from lib import helpers, database, collection logger = logging.getLogger(__name__) @click.group(cls=helpers.GroupWithHelp) @helpers.add_options(database.options) @click.pass_context def cli(ctx, **kwargs): '''Database management''' ctx.obj.db = collection.Collection(**kwargs) logger.info(ctx.obj.db.connection_string) @cli.command() @helpers.coro @click.pass_context async def create(ctx): '''Create database and load schema''' await ctx.obj.db.create(os.path.join(ctx.obj.folder, 'schema')) @cli.command() @helpers.coro @click.confirmation_option(help='Are you sure you want to drop the db?') @click.pass_context async def drop(ctx): '''Drop database schema''' await ctx.obj.db.drop() @cli.command() @helpers.coro @click.confirmation_option(help='Are you sure you want to drop the db?') @click.pass_context async def clear(ctx): '''Drop and recreate database and schema''' await ctx.obj.db.clear(os.path.join(ctx.obj.folder, 'schema')) @cli.command() @helpers.coro @click.pass_context async def clean(ctx): '''Clean deleted musics from database''' musics = await ctx.obj.db.musics() for m in musics: if not os.path.isfile(m['path']): logger.info('%s does not exist', m['path']) await ctx.obj.db.delete(m['path']) await ctx.obj.db.refresh() @cli.command() @helpers.coro @click.pass_context async def refresh(ctx): '''Refresh database materialized views''' await ctx.obj.db.refresh() PK!!musicbot/commands/file.pyimport click import logging from lib import file, helpers, collection, database, mfilter logger = logging.getLogger(__name__) @click.group(cls=helpers.GroupWithHelp) @helpers.add_options(database.options) @click.pass_context def cli(ctx, **kwargs): '''Music tags management''' ctx.obj.db = collection.Collection(**kwargs) @cli.command() @helpers.coro @click.pass_context @helpers.add_options(mfilter.options) async def show(ctx, **kwargs): '''Show tags of musics with filters''' ctx.obj.mf = mfilter.Filter(**kwargs) ctx.obj.musics = await ctx.obj.db.musics(ctx.obj.mf) for m in ctx.obj.musics: f = file.File(m['path']) print(f.to_list()) @cli.command() @click.pass_context @helpers.coro @helpers.add_options(file.options) @helpers.add_options(mfilter.options) async def update(ctx, **kwargs): ctx.obj.mf = mfilter.Filter(**kwargs) ctx.obj.musics = await ctx.obj.db.musics(ctx.obj.mf) logger.debug(kwargs) for m in ctx.obj.musics: f = file.File(m['path']) f.keywords = kwargs['keywords'] f.artist = kwargs['artist'] f.album = kwargs['album'] f.title = kwargs['title'] f.genre = kwargs['genre'] f.number = kwargs['number'] f.rating = kwargs['rating'] f.save() PK!Z(]ttmusicbot/commands/folder.pyimport click import os import asyncio import logging import atexit import concurrent.futures as cf from pydub import AudioSegment from tqdm import tqdm from lib import helpers, lib, collection, database, mfilter from lib.config import config from lib.helpers import watcher, fullscan from lib.lib import empty_dirs logger = logging.getLogger(__name__) @click.group(cls=helpers.GroupWithHelp) @helpers.add_options(database.options) @click.pass_context def cli(ctx, **kwargs): '''Folder scanning''' lib.raise_limits() ctx.obj.db = collection.Collection(**kwargs) @cli.command() @helpers.coro @click.argument('folders', nargs=-1) @click.pass_context async def new(ctx, folders): '''Add a new folder in database''' tasks = [asyncio.ensure_future(ctx.obj.db.new_folder(f)) for f in folders] await asyncio.gather(*tasks) @cli.command('list') @helpers.coro @click.pass_context async def ls(ctx): '''List existing folders''' folders = await ctx.obj.db.folders() for f in folders: print(f['name']) @cli.command() @helpers.coro @click.option('--crawl', envvar='MB_CRAWL', help='Crawl youtube', is_flag=True) @click.argument('folders', nargs=-1) @click.pass_context async def scan(ctx, **kwargs): '''Load musics files in database''' await fullscan(ctx.obj.db, **kwargs) @cli.command() @helpers.coro @click.option('--crawl', envvar='MB_CRAWL', help='Crawl youtube', is_flag=True) @click.pass_context async def rescan(ctx, **kwargs): '''Rescan all folders registered in database''' await fullscan(ctx.obj.db, **kwargs) @cli.command() @helpers.coro @click.pass_context async def watch(ctx): '''Watch files changes in folders''' await watcher(ctx.obj.db) @cli.command() @click.argument('folders', nargs=-1) def find(folders): '''Only list files in selected folders''' files = lib.find_files(folders) for f in files: print(f[1]) @cli.command() @click.argument('folders', nargs=-1) @helpers.add_options(helpers.concurrency) def flac2mp3(folders, concurrency): '''Convert all files in folders to mp3''' files = lib.find_files(folders) flac_files = [f[1] for f in files if f[1].endswith('.flac')] with tqdm(desc='Converting musics', total=len(flac_files), disable=config.quiet) as pbar: def convert(flac_path): logger.debug('Converting %s', flac_path) flac_audio = AudioSegment.from_file(flac_path, "flac") mp3_path = flac_path.replace('.flac', '.mp3') flac_audio.export(mp3_path, format="mp3") pbar.update(1) # Permit CTRL+C to work as intended atexit.unregister(cf.thread._python_exit) # pylint: disable=protected-access with cf.ThreadPoolExecutor(max_workers=concurrency) as executor: executor.shutdown = lambda wait: None futures = [executor.submit(convert, flac_path) for flac_path in flac_files] cf.wait(futures) # from pydub import AudioSegment # from pydub.utils import mediainfo # # seg = AudioSegment.from_file('original.mp3') # seg.export('out.mp3', format='mp3', tags=mediainfo('original.mp3').get('TAG', {})) # pylint: disable-msg=too-many-locals @cli.command() @helpers.coro @helpers.add_options(mfilter.options) @click.argument('destination') @click.pass_context async def sync(ctx, destination, **kwargs): '''Copy selected musics with filters to destination folder''' logger.info('Destination: %s', destination) ctx.obj.mf = mfilter.Filter(**kwargs) musics = await ctx.obj.db.musics(ctx.obj.mf) files = lib.all_files(destination) destinations = {f[len(destination) + 1:]: f for f in files} sources = {m['path'][len(m['folder']) + 1:]: m['path'] for m in musics} to_delete = set(destinations.keys()) - set(sources.keys()) if to_delete: with tqdm(total=len(to_delete), desc="Deleting music", disable=config.quiet) as pbar: for d in to_delete: if not config.dry: try: logger.info("Deleting %s", destinations[d]) os.remove(destinations[d]) except Exception as e: logger.error(e) else: logger.info("[DRY-RUN] False Deleting %s", destinations[d]) pbar.update(1) to_copy = set(sources.keys()) - set(destinations.keys()) if to_copy: with tqdm(total=len(to_copy), desc="Copying music", disable=config.quiet) as pbar: from shutil import copyfile for c in sorted(to_copy): final_destination = os.path.join(destination, c) if not config.dry: logger.info("Copying %s to %s", sources[c], final_destination) os.makedirs(os.path.dirname(final_destination), exist_ok=True) copyfile(sources[c], final_destination) else: logger.info("[DRY-RUN] False Copying %s to %s", sources[c], final_destination) pbar.update(1) import shutil for d in empty_dirs(destination): if not config.dry: shutil.rmtree(d) logger.info("[DRY-RUN] Removing empty dir %s", d) PK!:K} } musicbot/commands/playlist.pyimport click import codecs import os import logging from textwrap import indent from tqdm import tqdm from lib import helpers, database, collection, mfilter from lib.config import config logger = logging.getLogger(__name__) @click.group(cls=helpers.GroupWithHelp) @helpers.add_options(database.options) @click.pass_context def cli(ctx, **kwargs): '''Playlist management''' ctx.obj.db = collection.Collection(**kwargs) @cli.command() @click.pass_context @helpers.coro @helpers.add_options(mfilter.options) @click.argument('path', type=click.Path(exists=True)) @click.option('--prefix', envvar='MB_PREFIX', help="Append prefix before each path (implies relative)", default='') @click.option('--suffix', envvar='MB_SUFFIX', help="Append this suffix to playlist name", default='') async def bests(ctx, path, prefix, suffix, **kwargs): '''Generate bests playlists with some rules''' ctx.obj.mf = mfilter.Filter(**kwargs) if prefix: ctx.obj.mf.relative = True playlists = await ctx.obj.db.bests(ctx.obj.mf) if not playlists: return with tqdm(total=len(playlists), desc="Bests playlists", disable=config.quiet) as pbar: for p in playlists: playlist_filepath = os.path.join(path, p['name'] + suffix + '.m3u') content = indent(p['content'], prefix, lambda line: line != '#EXTM3U') if not config.dry: try: with codecs.open(playlist_filepath, 'w', "utf-8-sig") as playlist_file: logger.debug('Writing playlist to %s with content:\n%s', playlist_filepath, content) playlist_file.write(content) except Exception as e: logger.info('Unable to write playlist to %s because of %s', playlist_filepath, e) else: logger.info('DRY RUN: Writing playlist to %s with content:\n%s', playlist_filepath, content) pbar.update(1) @cli.command() @helpers.coro @click.pass_context @helpers.add_options(mfilter.options) @click.argument('path', type=click.File('w'), default='-') async def new(ctx, path, **kwargs): '''Generate a new playlist''' ctx.obj.mf = mfilter.Filter(**kwargs) p = await ctx.obj.db.playlist(ctx.obj.mf) if not config.dry: print(p['content'], file=path) else: logger.info('DRY RUN: Writing playlist to %s with content:\n%s', path, p['content']) PK!2. . musicbot/commands/server.pyimport click import os import logging from lib import helpers, database, server from lib.config import config from lib.lib import raise_limits, restart from lib.web import config as webconfig logger = logging.getLogger(__name__) @click.group(cls=helpers.GroupWithHelp) @helpers.add_options(database.options) def cli(**kwargs): '''API Server''' server.app.config.DB.set(**kwargs) @cli.command() @click.pass_context @helpers.add_options(server.options) @helpers.add_options(webconfig.options) def start(ctx, http_host, http_server, http_port, http_workers, http_user, http_password, **kwargs): '''Start musicbot web API''' webconfig.webconfig.set(**kwargs) if webconfig.webconfig.dev: logger.debug('Watching for python and html file changes') raise_limits() from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler class PyWatcherHandler(PatternMatchingEventHandler): patterns = [] def __init__(self): self.patterns = ['*.py', '*.html'] super().__init__() @staticmethod def on_modified(event): logger.debug('Modified: %s %s', event.src_path, event.event_type) restart() @staticmethod def on_created(event): logger.debug('Created: %s %s', event.src_path, event.event_type) restart() @staticmethod def on_deleted(event): logger.debug('Deleted: %s %s', event.src_path, event.event_type) restart() @staticmethod def on_moved(event): logger.debug('Moved: %s %s', event.src_path, event.event_type) restart() event_handler = PyWatcherHandler() observer = Observer() for f in ['lib', 'commands']: fpath = os.path.join(ctx.obj.folder, f) logger.debug('Watching internal folder: %s', fpath) observer.schedule(event_handler, fpath, recursive=True) observer.start() server.app.config.HTTP_SERVER = http_server server.app.config.HTTP_USER = http_user server.app.config.HTTP_PASSWORD = http_password server.app.run(host=http_host, port=http_port, debug=config.debug, workers=http_workers) PK!musicbot/commands/stats.pyimport click from datetime import timedelta from lib import helpers, collection, database, mfilter from lib.lib import bytes_to_human @click.group(cls=helpers.GroupWithHelp) @click.pass_context @helpers.add_options(database.options) def cli(ctx, **kwargs): '''Youtube management''' ctx.obj.db = collection.Collection(**kwargs) @cli.command() @click.pass_context @helpers.add_options(mfilter.options) @helpers.coro async def show(ctx, **kwargs): '''Generate some stats for music collection with filters''' ctx.obj.mf = mfilter.Filter(**kwargs) stats = await ctx.obj.db.stats(ctx.obj.mf) print("Music :", stats['musics']) print("Artist :", stats['artists']) print("Album :", stats['albums']) print("Genre :", stats['genres']) print("Keywords :", stats['keywords']) print("Size :", bytes_to_human(stats['size'])) print("Total duration :", str(timedelta(seconds=stats['duration']))) PK!ZXY$musicbot/commands/tag.pyimport click from lib import helpers, collection, database, mfilter default_fields = ['title', 'album', 'artist', 'genre', 'path', 'keywords', 'folder', 'rating', 'number', 'folder', 'youtube', 'duration', 'size'] tag = [ click.option('--fields', envvar='MB_FIELDS', help='Show only those fields', default=default_fields, multiple=True), # click.option('--output', envvar='MB_OUTPUT', help='Tags output format'), ] @click.group(cls=helpers.GroupWithHelp) @helpers.add_options(database.options) @click.pass_context def cli(ctx, **kwargs): '''Music tags management''' ctx.obj.db = collection.Collection(**kwargs) @cli.command() @helpers.coro @helpers.add_options(tag) @helpers.add_options(mfilter.options) @click.pass_context async def show(ctx, fields, **kwargs): '''Show tags of musics with filters''' mf = mfilter.Filter(**kwargs) musics = await ctx.obj.db.musics(mf) for m in musics: print([m[f] for f in fields]) # @cli.command() # @helpers.coro # @helpers.add_options(file.options) # @helpers.add_options(mfilter.options) # @click.pass_context # async def update(ctx, **kwargs): # '''Add tags - Not Implemented''' # mf = mfilter.Filter(**kwargs) # musics = await ctx.obj.db.musics(mf) # print(musics) PK!fz,,musicbot/commands/task.pyimport click from lib import helpers, lib, collection, database @click.group(cls=helpers.GroupWithHelp) @helpers.add_options(database.options) @click.pass_context def cli(ctx, **kwargs): '''Task management''' lib.raise_limits() ctx.obj.db = collection.Collection(**kwargs) @cli.command() @helpers.coro @click.argument('name') @click.pass_context async def new(name): '''Add a new task in database''' print('task name:', name) @cli.command('list') @helpers.coro @click.pass_context async def ls(): '''List tasks in database''' PK!1musicbot/commands/youtube.pyimport click from lib import helpers, database, collection, mfilter @click.group(cls=helpers.GroupWithHelp) @click.pass_context @helpers.add_options(database.options) def cli(ctx, **kwargs): '''Youtube management''' ctx.obj.db = collection.Collection(**kwargs) @cli.command() @click.pass_context @helpers.add_options(mfilter.options) @helpers.add_options(helpers.concurrency) @helpers.coro async def musics(ctx, concurrency, **kwargs): '''Fetch youtube links for each music''' mf = mfilter.Filter(**kwargs) concurrency = concurrency await helpers.crawl_musics(ctx.obj.db, mf=mf, concurrency=concurrency) @cli.command() @click.pass_context @helpers.add_options(mfilter.options) @helpers.add_options(helpers.concurrency) @helpers.coro @click.option('--youtube-album', envvar='MB_YOUTUBE_ALBUM', help='Select albums with a youtube link', default='') async def albums(ctx, concurrency, youtube_album, **kwargs): '''Fetch youtube links for each album''' mf = mfilter.Filter(**kwargs) concurrency = concurrency await helpers.crawl_albums(ctx.obj.db, mf=mf, youtube_album=youtube_album, concurrency=concurrency) @cli.command() @click.pass_context @helpers.coro async def only(ctx, **kwargs): '''Fetch youtube links for each album''' mf = mfilter.Filter(**kwargs) results = await ctx.obj.db.fetch("""select * from do_filter($1::filters) where youtube like 'https://www.youtube.com/watch?v=%'""", mf.to_list()) for r in results: print(r) PK!musicbot/lib/__init__.pyPK!u8cmusicbot/lib/collection.pyimport os import logging from .mfilter import Filter from .database import Database from .helpers import drier, timeit logger = logging.getLogger(__name__) class Collection(Database): def __init__(self, **kwargs): super().__init__(**kwargs) async def errors(self, mf=None): return ['not implemented for {}'.format(mf)] async def create(self, schema_dir): await self.createdb() for sqlfile in ['tables.sql', 'views.sql', 'functions.sql', 'data.sql', 'triggers.sql']: await self.executefile(os.path.join(schema_dir, sqlfile)) async def clear(self, schema_dir): await self.dropdb() await self.create(schema_dir) async def drop(self): await self.dropdb() async def refresh(self): sql = 'select refresh_views()' await self.execute(sql) async def folders(self, json=False): if json: sql = '''select array_to_json(array_agg(row_to_json(f))) as result from folders f''' return (await self.fetchrow(sql))['result'] sql = '''select name from folders order by name''' return await self.fetch(sql) async def folders_name(self): return [f['name'] for f in (await self.folders())] async def new_folder(self, name): sql = '''insert into folders as f(name, created_at, updated_at) values ($1, now(), now())''' return await self.execute(sql, name) async def keywords(self, mf=None, json=False): if mf is None: mf = Filter() tl = mf.to_list() if json: sql = '''select array_to_json(array_agg(row_to_json(k))) as json from (select distinct keyword as name from (select unnest(array_cat_agg(keywords)) as keyword from do_filter($1::filters)) k order by name) k''' return (await self.fetchrow(sql, tl))['json'] sql = """select distinct keyword as name from (select unnest(array_cat_agg(keywords)) as keyword from do_filter($1::filters)) k order by name""" return await self.fetch(sql, tl) async def keywords_name(self, mf=None): return [f['name'] for f in (await self.keywords(mf))] async def artists(self, mf=None, json=False): if mf is None: mf = Filter() tl = mf.to_list() if json: sql = '''select array_to_json(array_agg(row_to_json(a))) as json from (select distinct artist as name from do_filter($1::filters) m order by name) a''' return (await self.fetchrow(sql, tl))['json'] sql = """select distinct artist as name from do_filter($1::filters) order by name""" return await self.fetch(sql, tl) async def artists_name(self, mf=None): return [f['name'] for f in (await self.artists(mf))] async def titles(self, mf=None, json=False): if mf is None: mf = Filter() if json: sql = '''select array_to_json(array_agg(row_to_json(a))) as json from (select distinct title as name from do_filter($1::filters) order by name) a''' sql = """select distinct title as name from do_filter($1::filters) order by name""" return await self.fetch(sql, mf.to_list()) async def titles_name(self, mf=None): return [f['name'] for f in (await self.titles(mf))] async def albums(self, mf=None, youtube=None, json=False): if mf is None: mf = Filter() tl = mf.to_list() if json: sql = '''select array_to_json(array_agg(row_to_json(a))) as json from (select album as name, artist, a.youtube as youtube, sum(duration) as duration from do_filter($1::filters) m inner join albums a on a.name=album where $2::text is null or $2::text = a.youtube group by album, artist, a.youtube order by album) a''' return (await self.fetchrow(sql, tl, youtube))['json'] sql = """select album as name, artist, a.youtube as youtube, sum(duration) as duration from do_filter($1::filters) m inner join albums a on a.name=album where $2::text is null or $2::text = a.youtube group by album, artist, a.youtube order by album""" return await self.fetch(sql, tl, youtube) async def albums_name(self, mf=None): return [f['name'] for f in (await self.albums(mf))] async def genres(self, mf=None, json=False): if mf is None: mf = Filter() tl = mf.to_list() if json: sql = '''select array_to_json(array_agg(row_to_json(g))) as json from (select distinct genre as name from do_filter($1::filters) m order by name) g''' return (await self.fetchrow(sql, tl))['json'] sql = """select distinct genre as name from do_filter($1::filters) order by name""" return await self.fetch(sql, tl) async def genres_name(self, mf=None): return [f['name'] for f in (await self.genres(mf))] async def form(self, mf=None): if mf is None: mf = Filter() sql = '''select * from generate_form($1::filters)''' return await self.fetchrow(sql, mf.to_list()) async def stats(self, mf=None, json=False): if mf is None: mf = Filter() tl = mf.to_list() if json: sql = '''select row_to_json(s) as json from do_stats($1::filters) s''' return (await self.fetchrow(sql, tl))['json'] sql = '''select * from do_stats($1::filters) s''' return await self.fetchrow(sql, tl) async def filters(self, json=False): if json: sql = '''select array_to_json(array_agg(row_to_json(f))) as json from filters f''' return (await self.fetchrow(sql))['json'] sql = '''select * from filters''' return await self.fetch(sql) async def get_filter(self, name): sql = '''select * from filters where name=$1''' return await self.fetchrow(sql, name) @drier async def set_music_youtube(self, path, youtube): sql = '''update musics set youtube=$2 where path=$1''' await self.execute(sql, path, youtube) @drier async def set_album_youtube(self, name, youtube): sql = '''update albums set youtube=$2 where name=$1''' await self.execute(sql, name, youtube) @drier async def upsert(self, m): sql = '''select * from upsert($1::music)''' tl = m.to_list() await self.execute(sql, tl) @timeit async def musics(self, f=None, json=False): if f is None: f = Filter() tl = f.to_list() if json: sql = '''select array_to_json(array_agg(row_to_json(m))) as playlist from do_filter($1::filters) m''' return (await self.fetchrow(sql, tl))['playlist'] sql = '''select * from do_filter($1::filters)''' return await self.fetch(sql, tl) async def music(self, music_id, json=False): if json: sql = '''select array_to_json(array_agg(row_to_json(m))) as music from mmusics m where m.id=$1 limit 1''' return (await self.fetchrow(sql, music_id))['music'] sql = '''select * from mmusics m where m.id=$1 limit 1''' return await self.fetchrow(sql, music_id) @drier # async def update_music(self, id, title, artist, album, genre, youtube, track, keywords, rating): async def update_music(self, *args): sql = '''update mmusics set title = $2, artist= $3, album = $4, genre = $5, youtube = $6, track = $7, keywords = $8, rating = $9 where m.id=$1 limit 1''' # return await self.execute(sql, id, title, artist, album, genre, youtube, track, keywords, rating) return await self.execute(sql, *args) async def playlist(self, f=None): if f is None: f = Filter() sql = '''select * from generate_playlist($1::filters)''' tl = f.to_list() return await self.fetchrow(sql, tl) async def bests(self, f=None): if f is None: f = Filter() sql = '''select * from generate_bests($1::filters)''' tl = f.to_list() return await self.fetch(sql, tl) @drier async def delete(self, path): sql = '''select delete($1)''' await self.execute(sql, path) PK!nJ ||musicbot/lib/config.pyimport click import asyncio import os import coloredlogs import structlog import logging import attr from . import lib logger = logging.getLogger(__name__) slogger = structlog.get_logger() DEFAULT_LOG = '/var/log/musicbot.log' MB_LOG = 'MB_LOG' MB_DEBUG = 'MB_DEBUG' MB_TIMINGS = 'MB_TIMINGS' MB_VERBOSITY = 'MB_VERBOSITY' MB_DRY = 'MB_DRY' MB_QUIET = 'MB_QUIET' MB_NO_COLORS = 'MB_NO_COLORS' DEFAULT_VERBOSITY = 'warning' DEFAULT_DRY = False DEFAULT_QUIET = False DEFAULT_DEBUG = False DEFAULT_NO_COLORS = False DEFAULT_TIMINGS = False verbosities = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR, 'critical': logging.CRITICAL} options = [ click.option('--log', help='Log file path', type=click.Path(), envvar=MB_LOG, default=DEFAULT_LOG, show_default=True), click.option('--debug', help='Be very verbose, same as --verbosity debug + hide progress bars', envvar=MB_DEBUG, default=DEFAULT_DEBUG, is_flag=True), click.option('--timings', help='Set verbosity to info and show execution timings', envvar=MB_TIMINGS, default=DEFAULT_TIMINGS, is_flag=True), click.option('--verbosity', help='Verbosity levels', envvar=MB_VERBOSITY, default=DEFAULT_VERBOSITY, type=click.Choice(verbosities.keys()), show_default=True), click.option('--dry', help='Take no real action', envvar=MB_DRY, default=DEFAULT_DRY, is_flag=True), click.option('--quiet', help='Disable progress bars', envvar=MB_QUIET, default=DEFAULT_QUIET, is_flag=True), click.option('--no-colors', help='Disable colorized output', envvar=MB_NO_COLORS, default=DEFAULT_NO_COLORS, is_flag=True), ] @attr.s class Config: log = attr.ib(default=DEFAULT_LOG) quiet = attr.ib(default=DEFAULT_QUIET) debug = attr.ib(default=DEFAULT_DEBUG) timings = attr.ib(default=DEFAULT_TIMINGS) dry = attr.ib(default=DEFAULT_DRY) no_colors = attr.ib(default=DEFAULT_NO_COLORS) verbosity = attr.ib(default=DEFAULT_VERBOSITY) fmt = attr.ib(default="%(asctime)s %(name)s[%(process)d] %(levelname)s %(message)s") def set(self, debug=None, timings=None, dry=None, quiet=None, verbosity=None, no_colors=None, log=None): self.log = log if log is not None else os.getenv(MB_LOG, str(DEFAULT_LOG)) self.quiet = quiet if quiet is not None else lib.str2bool(os.getenv(MB_QUIET, str(DEFAULT_QUIET))) self.debug = debug if debug is not None else lib.str2bool(os.getenv(MB_DEBUG, str(DEFAULT_DEBUG))) self.timings = timings if timings is not None else lib.str2bool(os.getenv(MB_TIMINGS, str(DEFAULT_TIMINGS))) self.dry = dry if dry is not None else lib.str2bool(os.getenv(MB_DRY, str(DEFAULT_DRY))) self.no_colors = no_colors if no_colors is not None else lib.str2bool(os.getenv(MB_NO_COLORS, str(DEFAULT_NO_COLORS))) self.verbosity = verbosity if verbosity is not None else os.getenv(MB_VERBOSITY, DEFAULT_VERBOSITY) if self.timings: self.verbosity = 'info' self.quiet = True if self.debug: self.verbosity = 'debug' self.quiet = True if self.verbosity == 'debug': loop = asyncio.get_event_loop() loop.set_debug(True) self.level = verbosities[self.verbosity] logging.basicConfig(level=self.level, format=self.fmt) if not self.no_colors: coloredlogs.install(level=self.level, fmt=self.fmt) if self.log is not None: fh = logging.FileHandler(self.log) fh.setLevel(logging.DEBUG) logging.getLogger().addHandler(fh) logger.debug(self) # slogger.msg("config", config=self) def __repr__(self): fmt = 'log:{} timings:{} debug:{} quiet:{} dry:{} verbosity:{} no_colors:{}' return fmt.format(self.log, self.timings, self.debug, self.quiet, self.dry, self.verbosity, self.no_colors) config = Config() PK!hu33musicbot/lib/database.pyimport click import os import sys import logging import asyncpg from .helpers import drier, random_password from .config import config logger = logging.getLogger(__name__) DEFAULT_HOST = 'localhost' DEFAULT_PORT = 5432 DEFAULT_DATABASE = 'musicbot_prod' DEFAULT_USER = 'postgres' DEFAULT_PASSWORD = random_password(size=10) MB_DB_HOST = 'MB_HOST' MB_DB_PORT = 'MB_PORT' MB_DATABASE = 'MB_DATABASE' MB_DB_USER = 'MB_DB_USER' MB_DB_PW = 'MB_DB_PASSWORD' options = [ click.option('--db-host', envvar=MB_DB_HOST, help='DB host', default=DEFAULT_HOST, show_default=True), click.option('--db-port', envvar=MB_DB_PORT, help='DB port', default=DEFAULT_PORT, show_default=True), click.option('--db-database', envvar=MB_DATABASE, help='DB name', default=DEFAULT_DATABASE, show_default=True), click.option('--db-user', envvar=MB_DB_USER, help='DB user', default=DEFAULT_USER, show_default=True), click.option('--db-password', envvar=MB_DB_PW, help='DB password', default=DEFAULT_PASSWORD, show_default=False) ] class Database: def __init__(self, max_conn=100, **kwargs): self.set(**kwargs) self.max_conn = max_conn def set(self, db_host=None, db_port=None, db_database=None, db_user=None, db_password=None): self.host = db_host or os.getenv(MB_DB_HOST, DEFAULT_HOST) self.port = db_port or os.getenv(MB_DB_PORT, str(DEFAULT_PORT)) self.database = db_database or os.getenv(MB_DATABASE, DEFAULT_DATABASE) self.user = db_user or os.getenv(MB_DB_USER, DEFAULT_USER) self.password = db_password or os.getenv(MB_DB_PW, DEFAULT_PASSWORD) self._pool = None logger.info('Database: %s', self.connection_string) @property def connection_string(self): return 'postgresql://{}:{}@{}:{}/{}'.format(self.user, self.password, self.host, self.port, self.database) async def close(self): await (await self.pool).close() def __str__(self): return self.connection_string async def mogrify(self, connection, sql, *args): mogrified = await asyncpg.utils._mogrify(connection, sql, args) # pylint: disable=protected-access logger.debug('mogrified: %s', mogrified) @drier async def dropdb(self): con = await asyncpg.connect(user=self.user, host=self.host, password=self.password, port=self.port) await con.execute('drop database if exists {}'.format(self.database)) await con.close() @drier async def createdb(self): con = await asyncpg.connect(user=self.user, host=self.host, password=self.password, port=self.port) # as postgresql does not support "create database if not exists", need to check in catalog result = await con.fetchrow("select count(*) = 0 as not_exists from pg_catalog.pg_database where datname = '{}'".format(self.database)) if result['not_exists']: logger.debug('Database does not exists, create it') await con.execute('create database {}'.format(self.database)) else: logger.debug('Database already exists.') await con.close() async def connect(self): return await asyncpg.connect(user=self.user, host=self.host, password=self.password, port=self.port, database=self.database) @property async def pool(self): if self._pool is None: async def add_log_listener(conn): def log_it(conn, message): logger.debug(message) if config.debug: conn.add_log_listener(log_it) self._pool: asyncpg.pool.Pool = await asyncpg.create_pool(max_size=self.max_conn, user=self.user, host=self.host, password=self.password, port=self.port, database=self.database, setup=add_log_listener) return self._pool async def fetch(self, sql, *args): if config.debug: async with (await self.pool).acquire() as connection: await self.mogrify(connection, sql, *args) return await connection.fetch(sql, *args) return await (await self.pool).fetch(sql, *args) async def fetchrow(self, sql, *args): if config.debug: async with (await self.pool).acquire() as connection: await self.mogrify(connection, sql, *args) return await connection.fetchrow(sql, *args) return await (await self.pool).fetchrow(sql, *args) @drier async def executefile(self, filepath): schema_path = os.path.join(os.path.dirname(sys.argv[0]), filepath) logger.info('loading schema: %s', schema_path) with open(schema_path, "r") as s: sql = s.read() if config.debug: async with (await self.pool).acquire() as connection: async with connection.transaction(): return await connection.execute(sql) await (await self.pool).execute(sql) @drier async def execute(self, sql, *args): if config.debug: async with (await self.pool).acquire() as connection: async with connection.transaction(): await self.mogrify(connection, sql, *args) return await connection.execute(sql, *args) return await (await self.pool).execute(sql, *args) @drier async def executemany(self, sql, *args, **kwargs): if config.debug: async with (await self.pool).acquire() as connection: async with connection.transaction(): return await connection.executemany(sql, *args, **kwargs) return await (await self.pool).executemany(sql, *args, **kwargs) PK!s>musicbot/lib/file.pyimport taglib import click import copy import os from . import youtube options = [ click.option('--keywords', envvar='MB_KEYWORDS', help='Keywords', default=None), click.option('--artist', envvar='MB_ARTIST', help='Artist', default=None), click.option('--album', envvar='MB_ALBUM', help='Album', default=None), click.option('--title', envvar='MB_TITLE', help='Title', default=None), click.option('--genre', envvar='MB_GENRE', help='Genre', default=None), click.option('--number', envvar='MB_NUMBER', help='Track number', default=None), click.option('--rating', envvar='MB_RATING', help='Rating', default=None), ] def mysplit(s, delim=','): if isinstance(s, list): return s if s is None: return [] if isinstance(s, str): return [x for x in s.split(delim) if x] raise ValueError(s) # pylint: disable-msg=unsupported-membership-test # pylint: disable-msg=unsubscriptable-object # pylint: disable-msg=unsupported-assignment-operation class File: id = 0 def __init__(self, filename, _folder=''): self._folder = _folder self.handle = taglib.File(filename) self.youtube_link = '' def close(self): self.handle.close() def to_list(self): return [self.id, self.title, self.album, self.genre, self.artist, self._folder, self.youtube, self.number, self.path, self.rating, self.duration, self.size, mysplit(self.keywords, ' ') ] def to_tuple(self): return (self.title, self.album, self.genre, self.artist, self._folder, self.youtube, self.number, self.path, self.rating, self.duration, self.size, mysplit(self.keywords, ' ')) def to_dict(self): return {'title': self.title, 'album': self.album, 'genre': self.genre, 'artist': self.artist, 'folder': self._folder, 'youtube': self.youtube, 'number': self.number, 'path': self.path, 'rating': self.rating, 'duration': self.duration, 'size': self.size, 'keywords': mysplit(self.keywords, ' ')} @property def path(self): return self.handle.path @property def folder(self): return self._folder def __get_first(self, tag, default=''): if tag not in self.handle.tags: return default for item in self.handle.tags[tag]: return str(item) return default def __set_first(self, tag, value, force=False): if value is None: return if tag not in self.handle.tags: if force: self.handle.tags[tag] = [value] return del self.handle.tags[tag][0] self.handle.tags[tag].insert(0, value) @property def title(self, default=''): return self.__get_first('TITLE', default) @title.setter def title(self, title): self.__set_first('TITLE', title) @property def album(self, default=''): return self.__get_first('ALBUM', default) @album.setter def album(self, album): self.__set_first('ALBUM', album) @property def artist(self, default=''): return self.__get_first('ARTIST', default) @artist.setter def artist(self, artist): self.__set_first('ARTIST', artist) @property def rating(self, default=0.0): s = self.__get_first('FMPS_RATING', default) try: n = float(s) if n < 0.0: return default return n * 5.0 except ValueError: return default @rating.setter def rating(self, rating): self.__set_first('FMPS_RATING', rating) @property def comment(self, defaults=''): return self.__get_first('COMMENT', defaults) @comment.setter def comment(self, comment): self.__set_first('COMMENT', comment) def fix_comment(self, comment): self.__set_first('COMMENT', comment, force=True) @property def description(self, default=''): return self.__get_first('DESCRIPTION', default) @description.setter def description(self, description): self.__set_first('DESCRIPTION', description) def fix_description(self, description): self.__set_first('DESCRIPTION', description, force=True) @property def genre(self, default=''): return self.__get_first('GENRE', default) @genre.setter def genre(self, genre): self.__set_first('GENRE', genre) @property def number(self, default=-1): s = self.__get_first('TRACKNUMBER', default) try: n = int(s) if n < 0: return default return n except ValueError: return default @number.setter def number(self, number): self.__set_first('TRACKNUMBER', number) @property def keywords(self): if self.handle.path.endswith('.mp3'): return self.comment if self.handle.path.endswith('.flac'): if self.comment and not self.description: self.fix_description(self.comment) return self.description return '' @keywords.setter def keywords(self, keywords): if self.handle.path.endswith('.mp3'): self.comment = keywords elif self.handle.path.endswith('.flac'): self.fix_description(keywords) def add_keywords(self, keywords): tags = copy.deepcopy(self.keywords) for k in keywords: if k not in tags: tags.append(k) if set(self.keywords) != set(tags): self.keywords = tags self.save() return True return False def delete_keywords(self, keywords): tags = copy.deepcopy(self.keywords) for k in keywords: if k in tags: tags.remove(k) if set(self.keywords) != set(tags): self.keywords = tags self.save() return True return False @property def duration(self): return self.handle.length @property def size(self): return os.path.getsize(self.handle.path) @property def youtube(self): return self.youtube_link async def find_youtube(self): self.youtube_link = await youtube.search(self.artist, self.title, self.duration) def save(self): self.handle.save() PK!@Wmusicbot/lib/helpers.pyimport asyncio import time import uvloop import click import asyncpg import click_spinner import logging import string import random from click_didyoumean import DYMGroup from tqdm import tqdm from functools import wraps from hachiko.hachiko import AIOEventHandler from . import youtube from .config import config from .file import File from .lib import seconds_to_human, find_files from .mfilter import Filter, supported_formats logger = logging.getLogger(__name__) asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) DEFAULT_MB_CONCURRENCY = 8 concurrency = [ click.option('--concurrency', envvar='MB_CONCURRENCY', help='Number of coroutines', default=DEFAULT_MB_CONCURRENCY, show_default=True), ] def random_password(size=8): alphabet = string.ascii_letters + string.digits return ''.join(random.choice(alphabet) for i in range(size)) class GroupWithHelp(DYMGroup): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @click.command('help') @click.argument('command', nargs=-1) @click.pass_context def _help(ctx, command): '''Print help''' if command: argument = command[0] c = self.get_command(ctx, argument) print(c.get_help(ctx)) else: print(ctx.parent.get_help()) self.add_command(_help) async def process(f, *args, **params): if asyncio.iscoroutinefunction(f): return await f(*args, **params) return f(*args, **params) def timeit(f): @wraps(f) async def wrapper(*args, **params): start = time.time() result = await process(f, *args, **params) for_human = seconds_to_human(time.time() - start) if config.timings: logger.info('TIMINGS %s: %s', f.__name__, for_human) return result return wrapper async def refresh_db(db): await db.refresh() @timeit async def crawl_musics(db, mf=None, concurrency=1): if mf is None: mf = Filter(youtubes=['']) musics = await db.musics(mf) with tqdm(desc='Youtube musics', total=len(musics), disable=config.quiet) as pbar: async def search(semaphore, m): async with semaphore: result = await youtube.search(m['artist'], m['title'], m['duration']) pbar.update(1) await db.set_music_youtube(m['path'], result) semaphore = asyncio.BoundedSemaphore(concurrency) requests = [asyncio.ensure_future(search(semaphore, m)) for m in musics] await asyncio.gather(*requests) await db.refresh() @timeit async def crawl_albums(db, mf=None, youtube_album='', concurrency=1): if mf is None: mf = Filter() albums = await db.albums(mf, youtube_album) with tqdm(desc='Youtube albums', total=len(albums), disable=config.quiet) as pbar: async def search(semaphore, a): async with semaphore: result = await youtube.search(a['artist'], a['name'] + ' full album', a['duration']) await db.set_album_youtube(a['name'], result) pbar.update(1) semaphore = asyncio.BoundedSemaphore(concurrency) requests = [asyncio.ensure_future(search(semaphore, a)) for a in albums] await asyncio.gather(*requests) await db.refresh() @timeit async def fullscan(db, folders=None, crawl=False): if folders is None: folders = await db.folders() folders = [f['name'] for f in folders] with click_spinner.spinner(disable=config.quiet): files = [f for f in find_files(list(folders)) if f[1].endswith(tuple(supported_formats))] size = len(files) if crawl else len(files) with tqdm(total=size, desc="Loading musics", disable=config.quiet) as pbar: async with (await db.pool).acquire() as connection: async with connection.transaction(): for f in files: m = File(f[1], f[0]) try: await db.upsert(m) except asyncpg.exceptions.CheckViolationError as e: logger.warning("Violation: %s", e) pbar.update(1) await db.refresh() async def watcher(db): logger.info('Starting to watch folders') folders = await db.folders() class MusicWatcherHandler(AIOEventHandler): async def update(self, path): for folder in folders: if path.startswith(folder['name']) and path.endswith(tuple(supported_formats)): logger.debug('Creatin/modifying DB for: %s', path) f = File(path, folder['name']) logger.debug(f.to_list()) await db.upsert(f) # await db.refresh() return async def on_modified(self, event): await self.update(event.src_path) async def on_created(self, event): await self.update(event.src_path) async def on_deleted(self, event): logger.debug('Deleting entry in DB for: %s %s', event.src_path, event.event_type) await db.delete(event.src_path) await db.refresh() async def on_moved(self, event): logger.debug('Moving entry in DB for: %s %s', event.src_path, event.event_type) await db.delete(event.src_path) await self.update(event.dest_path) evh = MusicWatcherHandler() from watchdog.observers import Observer observer = Observer() for f in folders: logger.info('Watching: %s', f['name']) observer.schedule(evh, f['name'], recursive=True) observer.start() try: while True: await asyncio.sleep(0.5) except KeyboardInterrupt: observer.stop() observer.join() def add_options(options): def _add_options(func): for option in reversed(options): func = option(func) return func return _add_options def coro(f): f = asyncio.coroutine(f) @wraps(f) def wrapper(*args, **kwargs): loop = asyncio.get_event_loop() return loop.run_until_complete(f(*args, **kwargs)) return wrapper def drier(f): @wraps(f) async def wrapper(*args, **kwargs): if config.dry: args = [str(a) for a in args] + ["%s=%s" % (k, v) for (k, v) in kwargs.items()] logger.info('DRY RUN: %s(%s)', f.__name__, ','.join(args)) await asyncio.sleep(0) else: return await process(f, *args, **kwargs) return wrapper PK!7  musicbot/lib/lib.pyimport os import re import sys import logging import humanfriendly from timeit import default_timer as timer logger = logging.getLogger(__name__) output_types = ["list", "json"] default_output_type = 'json' # taken from distutilS def str2bool(val): val = val.lower() if val in ('y', 'yes', 't', 'true', 'on', '1'): return 1 elif val in ('n', 'no', 'f', 'false', 'off', '0'): return 0 raise ValueError("invalid truth value %r" % (val,)) def bytes_to_human(b): return humanfriendly.format_size(b) def seconds_to_human(s): import datetime return str(datetime.timedelta(seconds=s)) def empty_dirs(root_dir, recursive=True): empty_dirs = [] for root, dirs, files in os.walk(root_dir, topdown=False): if recursive: all_subs_empty = True for sub in dirs: full_sub = os.path.join(root, sub) if full_sub not in empty_dirs: all_subs_empty = False break else: all_subs_empty = (not dirs) if all_subs_empty and is_empty(files): empty_dirs.append(root) yield root def is_empty(files): return len(files) == 0 class Benchmark: def __init__(self, msg): self.msg = msg def __enter__(self): self.start = timer() return self def __exit__(self, *args): t = timer() - self.start logger.info("%s : %0.3g seconds", self.msg, t) self.time = t class LazyProperty: def __init__(self, fget): self.fget = fget self.func_name = fget.__name__ def __get__(self, obj, cls): if obj is None: return None value = self.fget(obj) setattr(obj, self.func_name, value) return value def raise_limits(): import resource try: _, hard = resource.getrlimit(resource.RLIMIT_NOFILE) logger.info("Current limits, soft and hard : %s %s", _, hard) resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard)) return True except Exception as e: logger.critical('You may need to check ulimit parameter: %s', e) return False def restart(): python = sys.executable os.execl(python, python, * sys.argv) def find_files(directories): directories = [os.path.abspath(d) for d in directories] for directory in directories: for root, _, files in os.walk(directory): if '.zfs' in root: continue for basename in files: filename = os.path.join(root, basename) yield (directory, filename) def scantree(path): for entry in os.scandir(path): if entry.is_dir(follow_symlinks=False): yield from scantree(entry.path) else: yield entry def filecount(path): return len(list(scantree(path))) def all_files(directory): for root, _, files in os.walk(directory): for basename in files: yield os.path.join(root, basename) def first(iterable, default=None): if iterable: if isinstance(iterable, str): return iterable for item in iterable: return item return default def num(s): try: return int(s) except ValueError: return float(s) def duration_to_seconds(duration): if re.match(r'\d+s', duration): return int(duration[:-1]) if re.match(r'\d+m', duration): return int(duration[:-1]) * 60 if re.match(r'\d+h', duration): return int(duration[:-1]) * 3600 raise ValueError(duration) def seconds_to_str(duration): import datetime return str(datetime.timedelta(seconds=duration)) default_checks = ['keywords', 'strict_title', 'title', 'path', 'genre', 'album', 'artist', 'rating', 'number'] PK!!Ԁmusicbot/lib/mfilter.pyimport attr import click import logging import json logger = logging.getLogger(__name__) rating_choices = [x * 0.5 for x in range(0, 11)] min_int = 0 max_int = 2147483647 default_id = 0 default_name = '' default_filter = None default_relative = False default_shuffle = False default_youtubes = [] default_no_youtubes = [] supported_formats = ["mp3", "flac"] default_formats = [] default_no_formats = [] default_genres = [] default_no_genres = [] default_limit = max_int default_min_duration = min_int default_max_duration = max_int default_min_size = min_int default_max_size = max_int default_min_rating = 0.0 default_max_rating = 5.0 default_keywords = [] default_no_keywords = [] default_artists = [] default_no_artists = [] default_titles = [] default_no_titles = [] default_albums = [] default_no_albums = [] @attr.s(frozen=True) class Filter: id = attr.ib(default=default_id) name = attr.ib(default=default_name) relative = attr.ib(default=default_relative) shuffle = attr.ib(default=default_shuffle) youtubes = attr.ib(default=default_youtubes) no_youtubes = attr.ib(default=default_no_youtubes) formats = attr.ib(default=default_formats) no_formats = attr.ib(default=default_no_formats) limit = attr.ib(default=default_limit) genres = attr.ib(default=default_genres) no_genres = attr.ib(default=default_no_genres) genres = attr.ib(default=default_genres) no_genres = attr.ib(default=default_no_genres) min_duration = attr.ib(default=default_min_duration) max_duration = attr.ib(default=default_max_duration) min_size = attr.ib(default=default_min_size) max_size = attr.ib(default=default_max_size) min_rating = attr.ib(default=default_min_rating, validator=attr.validators.in_(rating_choices)) max_rating = attr.ib(default=default_max_rating, validator=attr.validators.in_(rating_choices)) keywords = attr.ib(default=default_keywords) no_keywords = attr.ib(default=default_no_keywords) artists = attr.ib(default=default_artists) no_artists = attr.ib(default=default_no_artists) titles = attr.ib(default=default_titles) no_titles = attr.ib(default=default_no_titles) albums = attr.ib(default=default_albums) no_albums = attr.ib(default=default_no_albums) def __attrs_post_init__(self): if self.min_rating > self.max_rating: raise ValueError("Invalid minimum ({}) or maximum ({}) rating".format(self.min_rating, self.max_rating)) if self.min_duration > self.max_duration: raise ValueError("Invalid minimum ({}) or maximum ({}) duration".format(self.min_duration, self.max_duration)) is_bad_formats = set(self.formats).intersection(self.no_formats) is_bad_artists = set(self.artists).intersection(self.no_artists) is_bad_genres = set(self.genres).intersection(self.no_genres) is_bad_albums = set(self.albums).intersection(self.no_albums) is_bad_titles = set(self.titles).intersection(self.no_titles) is_bad_keywords = set(self.keywords).intersection(self.no_keywords) not_empty_set = is_bad_formats or is_bad_artists or is_bad_genres or is_bad_albums or is_bad_titles or is_bad_keywords if not_empty_set: raise ValueError("You can't have duplicates value in filters {}".format(self)) logger.debug('Filter: %s', self) def __repr__(self): return json.dumps(self.to_list()) def diff(self): '''Print only differences with default filter''' myself = vars(self) default = vars(Filter()) return {k: myself[k] for k in myself if default[k] != myself[k] and k != 'name'} def common(self): '''Print common values with default filter''' myself = vars(self) default = vars(Filter()) return {k: myself[k] for k in myself if default[k] == myself[k] and k != 'name'} def to_list(self): my_list = [self.id, self.name, self.min_duration, self.max_duration, self.min_size, self.max_size, self.min_rating, self.max_rating, self.artists, self.no_artists, self.albums, self.no_albums, self.titles, self.no_titles, self.genres, self.no_genres, self.formats, self.no_formats, self.keywords, self.no_keywords, self.shuffle, self.relative, self.limit, self.youtubes, self.no_youtubes] return my_list options = [ click.option('--limit', envvar='MB_LIMIT', help='Fetch a maximum limit of music', default=default_limit), click.option('--youtubes', envvar='MB_YOUTUBES', help='Select musics with a youtube link', default=default_youtubes), click.option('--no-youtubes', envvar='MB_NO_YOUTUBES', help='Select musics without youtube link', default=default_no_youtubes), click.option('--formats', envvar='MB_FORMATS', help='Select musics with file format', multiple=True, default=default_formats), click.option('--no-formats', envvar='MB_NO_FORMATS', help='Filter musics without format', multiple=True, default=default_no_formats), click.option('--keywords', envvar='MB_KEYWORDS', help='Select musics with keywords', multiple=True, default=default_keywords), click.option('--no-keywords', envvar='MB_NO_KEYWORDS', help='Filter musics without keywords', multiple=True, default=default_no_keywords), click.option('--artists', envvar='MB_ARTISTS', help='Select musics with artists', multiple=True, default=default_artists), click.option('--no-artists', envvar='MB_NO_ARTISTS', help='Filter musics without artists', multiple=True, default=default_no_artists), click.option('--albums', envvar='MB_ALBUMS', help='Select musics with albums', multiple=True, default=default_albums), click.option('--no-albums', envvar='MB_NO_ALBUMS', help='Filter musics without albums', multiple=True, default=default_no_albums), click.option('--titles', envvar='MB_TITLES', help='Select musics with titles', multiple=True, default=default_titles), click.option('--no-titles', envvar='MB_NO_TITLES', help='Filter musics without titless', multiple=True, default=default_no_titles), click.option('--genres', envvar='MB_GENRES', help='Select musics with genres', multiple=True, default=default_genres), click.option('--no-genres', envvar='MB_NO_GENRES', help='Filter musics without genres', multiple=True, default=default_no_genres), click.option('--min-duration', envvar='MB_MIN_DURATION', help='Minimum duration filter (hours:minutes:seconds)', default=default_min_duration), click.option('--max-duration', envvar='MB_MAX_DURATION', help='Maximum duration filter (hours:minutes:seconds))', default=default_max_duration), click.option('--min-size', envvar='MB_MIN_SIZE', help='Minimum file size filter (in bytes)', default=default_min_size), click.option('--max-size', envvar='MB_MAX_SIZE', help='Maximum file size filter (in bytes)', default=default_max_size), click.option('--min-rating', envvar='MB_MIN_RATING', help='Minimum rating', default=default_min_rating, show_default=True), click.option('--max-rating', envvar='MB_MAX_RATING', help='Maximum rating', default=default_max_rating, show_default=True), click.option('--relative', envvar='MB_RELATIVE', help='Generate relatives paths', default=default_relative, is_flag=True), click.option('--shuffle', envvar='MB_SHUFFLE', help='Randomize selection', default=default_shuffle, is_flag=True), ] PK!)Vyymusicbot/lib/persistence.pyimport click import aioredis import os import logging logger = logging.getLogger(__name__) DEFAULT_REDIS_ADDRESS = 'redis://localhost' DEFAULT_REDIS_DB = 0 DEFAULT_REDIS_PASSWORD = None options = [ click.option('--redis-address', envvar='MB_REDIS_ADDRESS', help='Redis URI', default=DEFAULT_REDIS_ADDRESS, show_default=True), click.option('--redis-db', envvar='MB_REDIS_DB', help='Redis index DB', default=DEFAULT_REDIS_DB, show_default=True), click.option('--redis-password', envvar='MB_REDIS_PASSWORD', help='Redis password', default=DEFAULT_REDIS_PASSWORD), ] class Persistence: def __init__(self, redis_address=None, redis_database=None, redis_password=None): self.address = redis_address or os.getenv('MB_REDIS_ADDRESS', DEFAULT_REDIS_ADDRESS) self.database = redis_database or os.getenv('MB_REDIS_DB', str(DEFAULT_REDIS_DB)) self.password = redis_password or os.getenv('MB_REDIS_PASSWORD', DEFAULT_REDIS_PASSWORD) logger.debug('REDIS: %s %s %s', self.address, self.database, self.password) async def connect(self): self.conn = await aioredis.create_connection(self.address, db=self.database, password=self.password) async def execute(self, command, *args, **kwargs): return await self.conn.execute(command, *args, **kwargs) async def close(self): self.conn.close() await self.conn.wait_closed() PK!77musicbot/lib/server.pyimport time import os import click import logging from .web import helpers as webhelpers from . import lib from aiocache import caches from datetime import datetime from apscheduler.schedulers.asyncio import AsyncIOScheduler from sanic_openapi import swagger_blueprint, openapi_blueprint from . import helpers from .config import config from .web.api import api_v1 from .web.collection import collection from .web.app import app, db from .web.config import webconfig # from .web.limiter import limiter logger = logging.getLogger(__name__) config.set() MB_HTTP_USER = 'MB_HTTP_USER' MB_HTTP_SERVER = 'MB_HTTP_SERVER' MB_HTTP_PW = 'MB_HTTP_PASSWORD' MB_HTTP_HOST = 'MB_HTTP_HOST' MB_HTTP_PORT = 'MB_HTTP_PORT' MB_HTTP_WORKERS = 'MB_HTTP_WORKERS' DEFAULT_HTTP_USER = 'musicbot' DEFAULT_HTTP_SERVER = 'musicbot.ovh' DEFAULT_HTTP_PASSWORD = helpers.random_password(size=10) DEFAULT_HTTP_HOST = '127.0.0.1' DEFAULT_HTTP_PORT = 8000 DEFAULT_HTTP_WORKERS = 1 options = [ click.option('--http-host', envvar=MB_HTTP_HOST, help='Host interface to listen on', default=DEFAULT_HTTP_HOST, show_default=True), click.option('--http-server', envvar=MB_HTTP_SERVER, help='Server name to use in links', default=DEFAULT_HTTP_SERVER, show_default=True), click.option('--http-port', envvar=MB_HTTP_PORT, help='HTTP port to listen on', default=DEFAULT_HTTP_PORT, show_default=True), click.option('--http-workers', envvar=MB_HTTP_WORKERS, help='Number of HTTP workers (not tested)', default=DEFAULT_HTTP_WORKERS, show_default=True), click.option('--http-user', envvar=MB_HTTP_USER, help='HTTP Basic auth user', default=DEFAULT_HTTP_USER, show_default=True), click.option('--http-password', envvar=MB_HTTP_PW, help='HTTP Basic auth password', default=DEFAULT_HTTP_PASSWORD, show_default=False), ] def server(): if webconfig.no_auth: return app.config.HTTP_SERVER return app.config.HTTP_USER + ':' + app.config.HTTP_PASSWORD + '@' + app.config.HTTP_SERVER app.blueprint(collection) app.blueprint(api_v1) app.blueprint(openapi_blueprint) app.blueprint(swagger_blueprint) webhelpers.env.globals['get_flashed_messages'] = webhelpers.get_flashed_messages webhelpers.env.globals['url_for'] = app.url_for webhelpers.env.globals['server'] = server webhelpers.env.globals['bytes_to_human'] = lib.bytes_to_human webhelpers.env.globals['seconds_to_human'] = lib.seconds_to_human webhelpers.env.globals['download_title'] = webhelpers.download_title webhelpers.env.globals['request_time'] = lambda: lib.seconds_to_human(time.time() - webhelpers.env.globals['request_start_time']) session = {} app.config.HTTP_SERVER = DEFAULT_HTTP_SERVER app.config.HTTP_USER = DEFAULT_HTTP_USER app.config.HTTP_PASSWORD = DEFAULT_HTTP_PASSWORD app.config.WTF_CSRF_SECRET_KEY = helpers.random_password(size=12) app.config.SCHEDULER = None app.config.LISTENER = None app.config.CONCURRENCY = 1 app.config.CRAWL = False app.config.CONFIG = config app.config.API_VERSION = '1.0.0' app.config.API_TITLE = 'Musicbot API' app.config.API_DESCRIPTION = 'Musicbot API' app.config.API_TERMS_OF_SERVICE = 'Use with caution!' app.config.API_PRODUCES_CONTENT_TYPES = ['application/json'] app.config.API_CONTACT_EMAIL = 'crunchengine@gmail.com' # CLOSE DB GRACEFULLY @app.listener('after_server_stop') async def close_db(app, loop): await db.close() # AUTHENTICATION @app.listener('before_server_start') def init_authentication(app, loop): if webconfig.no_auth: logger.debug('Authentication disabled') else: logger.debug('Authentication enabled') user = os.getenv('MB_HTTP_USER', app.config.HTTP_USER) password = os.getenv('MB_HTTP_PASSWORD', app.config.HTTP_PASSWORD) webhelpers.env.globals['auth'] = {'user': user, 'password': password} # CACHE INVALIDATION def invalidate_cache(connection, pid, channel, payload): logger.debug('Received notification: %s %s %s', pid, channel, payload) cache = caches.get('default') app.loop.create_task(cache.delete(payload)) @app.listener('before_server_start') async def init_cache_invalidator(app, loop): if webconfig.server_cache: logger.debug('Cache invalidator activated') app.config.LISTENER = await (await db.pool).acquire() await app.config.LISTENER.add_listener('cache_invalidator', invalidate_cache) else: logger.debug('Cache invalidator disabled') # FILE WATCHER @app.listener('before_server_start') def start_watcher(app, loop): if webconfig.watcher: logger.debug('File watcher enabled') app.config.watcher_task = loop.create_task(helpers.watcher(db)) else: logger.debug('File watcher disabled') @app.listener('before_server_stop') def stop_watcher(app, loop): if webconfig.watcher: app.config.watcher_task.cancel() # APS SCHEDULER @app.listener('before_server_start') def start_scheduler(app, loop): if webconfig.autoscan: logger.debug('Autoscan enabled') app.config.SCHEDULER = AsyncIOScheduler({'event_loop': loop}) app.config.SCHEDULER.add_job(helpers.refresh_db, 'interval', [db], minutes=15) app.config.SCHEDULER.add_job(helpers.fullscan, 'cron', [db], hour=3) app.config.SCHEDULER.add_job(helpers.crawl_musics, 'cron', [db], hour=4) app.config.SCHEDULER.add_job(helpers.crawl_albums, 'cron', [db], hour=5) app.config.SCHEDULER.start() else: logger.debug('Autoscan disabled') @app.listener('before_server_stop') def stop_scheduler(app, loop): if webconfig.autoscan: app.config.SCHEDULER.shutdown(wait=False) # REQUEST TIMER @app.middleware('request') def before(request): webhelpers.env.globals['request_start_time'] = time.time() request['session'] = session # BROWSER CACHE @app.middleware('response') def after(request, response): if webconfig.client_cache: logger.debug('Browser cache enabled') else: logger.info('Browser cache disabled') if response is not None: response.headers['Last-Modified'] = datetime.now() response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '-1' @app.route("/") @webhelpers.basicauth async def get_root(request): return await webhelpers.template('index.html') app.static('/static', './lib/web/templates/static') PK!musicbot/lib/web/__init__.pyPK![b'Ar r musicbot/lib/web/api.pyfrom sanic import response, Blueprint from aiocache import cached, SimpleMemoryCache from aiocache.serializers import PickleSerializer from . import helpers from .mfilter import WebFilter from .app import db api_v1 = Blueprint('api_v1', strict_slashes=True, url_prefix='/v1') # from .limiter import limiter # limiter.limit("2 per hour")(api_v1) @api_v1.route('/stats') @helpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer()) async def stats(request): '''Music library statistics, APIv1''' mf = WebFilter(request) stats = await db.stats(mf, json=True) return response.HTTPResponse(stats, content_type="application/json") @api_v1.route("/folders") @helpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer()) async def folders(request): '''Get filters''' folders = await db.folders(json=True) return response.HTTPResponse(folders, content_type="application/json") @api_v1.route("/filters") @helpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer()) async def filters(request): '''Get filters''' filters = await db.filters(json=True) return response.HTTPResponse(filters, content_type="application/json") @api_v1.route('/musics') @helpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer()) async def musics(request): '''List musics''' mf = await helpers.get_filter(request) musics = await db.musics(mf, json=True) return response.HTTPResponse(musics, content_type="application/json") @api_v1.route("/playlist") @helpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer()) async def playlist(request): '''Generate a playlist, APIv1''' mf = await helpers.get_filter(request) musics = await db.musics(mf, json=True) return response.HTTPResponse(musics, content_type="application/json") @api_v1.route('/artists') @helpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer()) async def artists(request): '''List artists''' mf = await helpers.get_filter(request) artists = await db.artists(mf, json=True) return response.HTTPResponse(artists, content_type="application/json") @api_v1.route('/genres') @helpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer()) async def genres(request): '''List artists''' mf = await helpers.get_filter(request) genres = await db.genres(mf, json=True) return response.HTTPResponse(genres, content_type="application/json") @api_v1.route('/albums') @helpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer()) async def albums(request): '''List albums''' mf = await helpers.get_filter(request) albums = await db.albums(mf, json=True) return response.HTTPResponse(albums, content_type="application/json") @api_v1.route('/keywords') @helpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer()) async def keywords(request): '''Get keywords, APIv1''' mf = await helpers.get_filter(request) keywords = await db.keywords(mf, json=True) return response.HTTPResponse(keywords, content_type="application/json") PK!AkBmusicbot/lib/web/app.pyfrom sanic import Sanic from sanic.log import LOGGING_CONFIG_DEFAULTS from ..collection import Collection del LOGGING_CONFIG_DEFAULTS['loggers']['root'] app = Sanic(name='musicbot', strict_slashes=True) app.config.DB = Collection() db = app.config.DB PK!d""musicbot/lib/web/collection.pyimport asyncpg import asyncio import logging from tqdm import tqdm from sanic import Blueprint, response from aiocache import cached, SimpleMemoryCache from aiocache.serializers import PickleSerializer from aiocache.plugins import HitMissRatioPlugin, TimingPlugin from . import forms from . import helpers as webhelpers from .. import mfilter, lib, file, helpers from ..config import config from .app import db logger = logging.getLogger(__name__) collection = Blueprint('collection', strict_slashes=True, url_prefix='/collection') @collection.route('/schedule') @webhelpers.basicauth async def schedule(request): async def do(): await helpers.fullscan(db) await helpers.crawl_musics(db) await helpers.crawl_albums(db) await helpers.refresh_db(db) asyncio.ensure_future(do()) return await webhelpers.template('schedule.html') @collection.route('/rescan') @webhelpers.basicauth async def rescan(request): return await webhelpers.template('rescan.html') @collection.route('/refresh') @webhelpers.basicauth async def refresh(request): await db.refresh() return await webhelpers.template('refresh.html') @collection.route('/youtube') @webhelpers.basicauth async def youtube(request): mf = await webhelpers.get_filter(request) asyncio.ensure_future(helpers.crawl_musics(db, mf, 10)) return response.redirect('/') @collection.websocket('/progression') @webhelpers.basicauth async def progression(request, ws): logger.debug('Getting folders') folders = await db.folders_name() logger.debug('Scanning folders: %s', folders) files = [f for f in lib.find_files(folders) if f[1].endswith(tuple(mfilter.supported_formats))] current = 0 percentage = 0 total = len(files) logger.debug('Number of files: %s', total) with tqdm(total=total, desc="Loading musics", disable=config.quiet) as pbar: logger.debug('Reading files') for f in files: try: m = file.File(f[1], f[0]) await db.upsert(m) pbar.update(1) current += 1 current_percentage = int(current / total * 100) if current_percentage > percentage: percentage = current_percentage await ws.send(str(percentage)) except asyncpg.exceptions.CheckViolationError as e: logger.warning("Violation: %s", e) await db.refresh() @collection.get('/stats') @webhelpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer(), key='stats') async def stats(request): '''Music library statistics''' mf = await webhelpers.get_filter(request) stats = await db.stats(mf) return await webhelpers.template('stats.html', stats=stats, mf=mf) @collection.get('/search') @webhelpers.basicauth async def search(request): '''Search through library''' return await webhelpers.template('search.html') @collection.get('/results') @webhelpers.basicauth async def results(request): '''Results of search''' q = request.args.get('q') return await webhelpers.template('results.html', q=q) @collection.route('/generate') @webhelpers.basicauth async def generate(request): '''Generate a playlist step by step''' # precedent = request.form mf = await webhelpers.get_filter(request) if request.args.get('play', False): musics = await db.musics(mf) return await webhelpers.template('player.html', musics=musics, mf=mf) if request.args.get('zip', False): musics = await db.musics(mf) return webhelpers.zip(musics) if request.args.get('m3u', False): musics = await db.musics(mf) return await webhelpers.m3u(musics) records = await db.form(mf) form = forms.FilterForm(obj=records) form.initialize(records) return await webhelpers.template('generate.html', form=form, mf=mf) @collection.route('/consistency') @webhelpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer()) async def consistency(request): '''Consistency''' return response.text('not implemented') @collection.route('/folders') @webhelpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer(), key='folders') async def folders(request): '''Get filters''' folders = await db.folders() return await webhelpers.template('folders.html', folders=folders) @collection.route('/filters') @webhelpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer(), key='filters') async def filters(request): '''Get filters''' filters = await db.filters() webfilters = [mfilter.Filter(**dict(f)) for f in filters] return await webhelpers.template('filters.html', filters=webfilters) @collection.route('/keywords') @webhelpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer(), key='keywords') async def keywords(request): '''Get keywords''' mf = await webhelpers.get_filter(request) keywords = await db.keywords(mf) return await webhelpers.template('keywords.html', keywords=keywords, mf=mf) @collection.route('/genres') @webhelpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer(), key='genres') async def genres(request): '''List artists''' mf = await webhelpers.get_filter(request) genres = await db.genres(mf) return await webhelpers.template("genres.html", genres=genres, mf=mf) @collection.route('/artists') @webhelpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer(), key='artists') async def artists(request): '''List artists''' mf = await webhelpers.get_filter(request) artists = await db.artists(mf) return await webhelpers.template("artists.html", artists=artists, mf=mf) @collection.route('/albums') @webhelpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer(), key='albums') async def albums(request): '''List albums''' mf = await webhelpers.get_filter(request) albums = await db.albums(mf) return await webhelpers.template("albums.html", albums=albums, mf=mf) @collection.route('/musics') @webhelpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer(), key='musics') async def musics(request): '''List musics''' mf = await webhelpers.get_filter(request) musics = await db.musics(mf) return await webhelpers.template("musics.html", musics=musics, mf=mf) @collection.route('/music', methods=['GET', 'POST']) @webhelpers.basicauth async def music(request): '''Show music''' music_id = request.args.get('id', None) form = forms.MusicForm(request) if request.method == 'GET': music = await db.music(int(music_id)) return await webhelpers.template("music.html", form=music) if request.method == 'POST' and form.validate(): await db.update_music(request.args) return await webhelpers.template("music.html", form=form) @collection.route('/download') @webhelpers.basicauth async def download(request): '''Download a track''' music = await webhelpers.get_music(request) return webhelpers.send_file(music, name=webhelpers.download_title(music), attachment='attachment') @collection.route('/listen') @webhelpers.basicauth async def listen(request): '''Listen a track''' music = await webhelpers.get_music(request) return webhelpers.send_file(music=music, name=webhelpers.download_title(music), attachment='inline') @collection.route('/m3u') @webhelpers.basicauth @cached(cache=SimpleMemoryCache, serializer=PickleSerializer()) async def m3u(request): '''Download m3u''' mf = await webhelpers.get_filter(request) musics = await db.musics(mf) name = request.args.get('name', 'playlist') return await webhelpers.m3u(musics, name) @collection.route('/zip') @webhelpers.basicauth async def zip_musics(request): '''Generate a playlist''' mf = await webhelpers.get_filter(request) musics = await db.musics(mf) if not musics == 0: return response.text('Empty playlist') name = request.args.get('name', 'archive') return webhelpers.zip_musics(musics, name) async def gen_playlist(request): mf = await webhelpers.get_filter(request) musics = await db.musics(mf) return await webhelpers.template('player.html', musics=musics, mf=mf) @cached(cache=SimpleMemoryCache, serializer=PickleSerializer(), plugins=[HitMissRatioPlugin(), TimingPlugin()]) async def cached_call(f, request): return await f(request) @collection.route('/player') @webhelpers.basicauth async def player(request): '''Play a playlist in browser''' if request.args.get('shuffle', False): logger.debug('Shuffled playlist, not using cache') return await gen_playlist(request) return await cached_call(gen_playlist, request) PK!bTTmusicbot/lib/web/config.pyimport click import os import logging from .. import lib logger = logging.getLogger(__name__) DEFAULT_DEV = False DEFAULT_WATCHER = False DEFAULT_AUTOSCAN = False DEFAULT_SERVER_CACHE = False DEFAULT_CLIENT_CACHE = False DEFAULT_NO_AUTH = False options = [ click.option('--dev', envvar='MB_DEV', help='Watch for source file modification', default=DEFAULT_DEV, is_flag=True), click.option('--watcher', envvar='MB_WATCHER', help='Watch for music file modification', default=DEFAULT_WATCHER, is_flag=True), click.option('--autoscan', envvar='MB_AUTOSCAN', help='Enable auto scan background job', default=DEFAULT_AUTOSCAN, is_flag=True), click.option('--server-cache', envvar='MB_SERVER_CACHE', help='Activate server cache system', default=DEFAULT_SERVER_CACHE, is_flag=True), click.option('--client-cache', envvar='MB_CLIENT_CACHE', help='Activate client cache system', default=DEFAULT_CLIENT_CACHE, is_flag=True), click.option('--no-auth', envvar='MB_NO_AUTH', help='Disable authentication system', default=DEFAULT_NO_AUTH, is_flag=True), ] class WebConfig: def __init__(self, **kwargs): self.set(**kwargs) def set(self, dev=None, autoscan=None, watcher=None, client_cache=None, server_cache=None, no_auth=None): self.dev = dev or lib.str2bool(os.getenv('MB_DEV', str(DEFAULT_DEV))) self.watcher = watcher or lib.str2bool(os.getenv('MB_WATCHER', str(DEFAULT_WATCHER))) self.autoscan = autoscan or lib.str2bool(os.getenv('MB_AUTOSCAN', str(DEFAULT_AUTOSCAN))) self.server_cache = server_cache or lib.str2bool(os.getenv('MB_SERVER_CACHE', str(DEFAULT_SERVER_CACHE))) self.client_cache = client_cache or lib.str2bool(os.getenv('MB_CLIENT_CACHE', str(DEFAULT_CLIENT_CACHE))) self.no_auth = no_auth or lib.str2bool(os.getenv('MB_NO_AUTH', str(DEFAULT_NO_AUTH))) logger.debug('Webconfig: %s', self) def __repr__(self): return 'dev:{} watcher:{} autoscan:{} server_cache:{} client_cache:{} no_auth:{}'.format(self.dev, self.watcher, self.autoscan, self.server_cache, self.client_cache, self.no_auth) webconfig = WebConfig() PK!h^musicbot/lib/web/forms.pyfrom sanic_wtf import SanicForm from wtforms import HiddenField, IntegerField, BooleanField, StringField, SelectField, SelectMultipleField, SubmitField from .. import mfilter rating_choices = [(x * 0.5, x * 0.5) for x in range(0, 11)] formats_choices = [(x, x) for x in mfilter.default_formats] class MusicForm(SanicForm): id = HiddenField('Id') title = StringField('Title') artist = StringField('Artist') album = StringField('Album') genre = StringField('Genre') youtube = StringField('YouTube') number = IntegerField('Number') keywords = StringField('Keywords') rating = SelectField('Rating', default=0.0, choices=rating_choices, coerce=float) submit = SubmitField(label='Save') class FilterForm(SanicForm): shuffle = BooleanField(label="Shuffle?", default=False) youtube = SelectField(label='YouTube', default="2", choices=[("2", "Yes and No"), ("1", "Yes"), ("0", "No")]) min_rating = SelectField(label='Minimum rating', default=0.0, choices=rating_choices, coerce=float) max_rating = SelectField(label='Maximum rating', default=5.0, choices=rating_choices, coerce=float) formats = SelectMultipleField(label='Formats', default=[], choices=formats_choices) no_formats = SelectMultipleField(label='No formats', default=[], choices=formats_choices) limit = IntegerField(label='Limit', default=2147483647) min_duration = IntegerField(label='Min duration', default=0) max_duration = IntegerField(label='Max duration', default=2147483647) min_size = IntegerField(label='Min size', default=0) max_size = IntegerField(label='Max size', default=2147483647) genres = SelectMultipleField(label='Genres', default=[], choices=[]) no_genres = SelectMultipleField(label='No genres', default=[], choices=[]) artists = SelectMultipleField(label='Artists', default=[], choices=[]) no_artists = SelectMultipleField(label='No artists', default=[], choices=[]) keywords = SelectMultipleField(label='Keywords', default=[], choices=[]) no_keywords = SelectMultipleField(label='No keywords', default=[], choices=[]) titles = SelectMultipleField(label='Titles', default=[], choices=[]) no_titles = SelectMultipleField(label='No titles', default=[], choices=[]) albums = SelectMultipleField(label='Albums', default=[], choices=[]) no_albums = SelectMultipleField(label='No albums', default=[], choices=[]) form = SubmitField(label='Form') play = SubmitField(label='Play') m3u = SubmitField(label='m3u') zip = SubmitField(label='Zip') def initialize(self, records): genres_choices = [(x, x) for x in records['genres']] no_genres_choices = genres_choices self.genres.choices = genres_choices self.no_genres.choices = no_genres_choices artists_choices = [(x, x) for x in records['artists']] self.artists.choices = artists_choices self.no_artists.choices = artists_choices keywords_choices = [(x, x) for x in records['keywords']] self.keywords.choices = keywords_choices self.no_keywords.choices = keywords_choices titles_choices = [(x, x) for x in records['titles']] self.titles.choices = titles_choices self.no_titles.choices = titles_choices albums_choices = [(x, x) for x in records['albums']] self.albums.choices = albums_choices self.no_albums.choices = albums_choices # def __init__(self, records, precedent, *args, **kwargs): # # only_csrf = MultiDict([('csrf_token', precedent.get('csrf_token'))]) # # super(FlaskForm, self).__init__(only_csrf, *args, **kwargs) # # super(SanicForm, self).__init__(precedent, *args, **kwargs) PK!ڼ!musicbot/lib/web/helpers.pyimport os import base64 import logging import asyncio from urllib.parse import quote from sanic import response from jinja2 import Environment, FileSystemLoader from functools import wraps from .mfilter import WebFilter from .config import webconfig from .app import db logger = logging.getLogger(__name__) THIS_DIR = os.path.dirname(os.path.abspath(__file__)) env = Environment(extensions=['jinja2.ext.loopcontrols'], loader=FileSystemLoader(os.path.join(THIS_DIR, 'templates')), enable_async=True, autoescape=True) async def get_filter(request, **kwargs): filter_name = request.args.get('filter', None) d = kwargs if filter_name is not None: d = dict(await db.get_filter(filter_name)) return WebFilter(request, **d) async def get_music(request): mf = await get_filter(request, limit=1) musics = await db.musics(mf) if not musics: return ('music not found', 404) return musics[0] async def m3u(musics, name='playlist'): headers = {} headers['Content-Disposition'] = 'attachment; filename={}'.format(name + '.m3u') return await template("m3u.html", headers=headers, musics=musics) def zip_musics(musics, name='archive'): headers = {} headers['X-Archive-Files'] = 'zip' headers['Content-Disposition'] = 'attachment; filename={}'.format(name + '.zip') # see mod_zip documentation :p lines = [' '.join(['-', str(m['size']), quote("/sendfile" + m['path'][len(m['folder']):]), os.path.join(m['artist'], m['album'], os.path.basename(m['path']))]) for m in musics] body = '\n'.join(lines) logger.debug(body) return response.HTTPResponse(headers=headers, body=body) def send_file(music, name, attachment): logger.debug("sending file: %s", music['path']) headers = {} headers['Content-Description'] = 'File Transfer' # headers['Cache-Control'] = 'no-cache' headers['Cache-Control'] = 'public, must-revalidate' if music['path'].endswith('.flac'): headers['Content-Type'] = 'audio/flac' else: headers['Content-Type'] = 'audio/mpeg' headers['Content-Disposition'] = '{}; filename={}'.format(attachment, quote(name)) headers['Accept-Ranges'] = 'bytes' headers['Content-Length'] = music['size'] headers['Content-Transfer-Encoding'] = 'binary' headers['X-Accel-Buffering'] = 'no' server_path = "/sendfile" + music['path'][len(music['folder']):] logger.debug('server_path: %s', server_path) headers['X-Accel-Redirect'] = server_path return response.HTTPResponse(headers=headers) def basename(path): return os.path.basename(path) def get_flashed_messages(): return () def download_title(m): _, extension = os.path.splitext(m['path']) return m['artist'] + ' - ' + m['album'] + ' - ' + m['title'] + extension def check_auth(h): auth = env.globals['auth'] s = auth['user'].encode() + b':' + auth['password'].encode() basic_hash = base64.b64encode(s) basic = b'Basic ' + basic_hash return basic == h.encode() async def template(tpl, headers=None, **kwargs): template = env.get_template(tpl) rendered_template = await template.render_async(**kwargs) return response.html(rendered_template, headers=headers) def basicauth(f): @wraps(f) async def wrapper(request, *args, **kwargs): if webconfig.no_auth: if asyncio.iscoroutinefunction(f): return await f(request, *args, **kwargs) return f(request, *args, **kwargs) headers = {} is_authorized = False if 'Authorization' not in request.headers: headers['WWW-Authenticate'] = 'Basic realm="musicbot"' else: auth = request.headers['Authorization'] is_authorized = check_auth(auth) if not is_authorized: logger.debug('Authorization denied') return response.json({'status': 'not_authorized'}, headers=headers, status=401) logger.debug('Authorization granted') if asyncio.iscoroutinefunction(f): return await f(request, *args, **kwargs) return f(request, *args, **kwargs) return wrapper PK!s'musicbot/lib/web/limiter.pyfrom sanic_limiter import Limiter, get_remote_address from .app import app limiter = Limiter(app, global_limits=['1 per hour', '10 per day'], key_func=get_remote_address) PK!Z--musicbot/lib/web/mfilter.pyimport logging import attr from ..mfilter import Filter from ..lib import num logger = logging.getLogger(__name__) # @attr.s(frozen=True) class WebFilter(Filter): def __init__(self, request, **kwargs): for kw in request.args: if kw not in attr.fields_dict(Filter): continue default_value = attr.fields_dict(Filter)[kw].default if kw in ['name']: kwargs[kw] = request.args.get(kw, default_value) elif kw in ['youtubes', 'no_youtubes', 'formats', 'no_formats', 'artists', 'no_artists', 'genres', 'no_genres', 'albums', 'no_albums', 'titles', 'no_titles', 'keywords', 'no_keywords']: kwargs[kw] = request.args.getlist(kw, default_value) elif kw in ['min_rating', 'max_rating']: kwargs[kw] = float(request.args.get(kw, default_value)) elif kw in ['shuffle', 'relative']: kwargs[kw] = bool(num(request.args.get(kw, default_value))) elif kw in ['limit', 'min_size', 'max_size', 'min_duration', 'min_duration']: kwargs[kw] = int(num(request.args.get(kw, default_value))) else: logger.warning('Keyword argument not known: %s', kw) super().__init__(**kwargs) logger.debug('WebFilter: %s', self) PK!&&musicbot/lib/web/templates/albums.html{% extends "layout.html" %} {% block title %}Albums{% endblock %} {% block body %} {% for a in albums: %} {% endfor %}
Album Artist Musics Play YouTube Download Info
{{ a.name }} {{ a.artist }} musics play {{ a.youtube }} m3u / zip stats
{% endblock %} PK!E 'musicbot/lib/web/templates/artists.html{% extends "layout.html" %} {% block title %}Artists{% endblock %} {% block body %} {% for a in artists: %} {% endfor %}
Artist Musics / Albums Play Download Info
{{ a.name }} musics / albums all / 4+ / 4.5+ / 5.0 / best m3u / zip stats
{% endblock %} PK!#J'musicbot/lib/web/templates/filters.html{% extends "layout.html" %} {% block title %}Filters{% endblock %} {% block body %} {% for f in filters: %} {% endfor %}
Filter Diff Common Links Play
{{ f.name }} {{ f.diff() }} {{ f.common() }} musics / artists / albums play
{% endblock %} PK!p|'musicbot/lib/web/templates/folders.html{% extends "layout.html" %} {% block title %}Folders{% endblock %} {% block body %} {% for folder in folders: %} {{ folder }}
{% endfor %} {% for f in folders: %} {% endfor %}
Path
{{ f.name }}
{% endblock %} PK!1(musicbot/lib/web/templates/generate.html{% extends "layout.html" %} {% block title %}Generate{% endblock %} {% block body %}

Generate

{{ form.csrf_token }} {# {{ form.min_rating.label }} {{ form.min_rating(size=1) }}
{{ form.max_rating.label }} {{ form.max_rating(size=1) }}
{{ form.min_size.label }} {{ form.min_size(size=10) }}
{{ form.max_size.label }} {{ form.max_size(size=10) }}
{{ form.min_duration.label }} {{ form.min_duration(size=10) }}
{{ form.max_duration.label }} {{ form.max_duration(size=10) }}
{{ form.limit.label }} {{ form.limit(size=10) }}
{{ form.youtube.label }} {{ form.youtube(size=1) }}
{{ form.shuffle.label }} {{ form.shuffle(size=1) }}
{{ form.formats.label }}
{{ form.formats(size=2) }}
{{ form.no_formats.label }}
{{ form.no_formats(size=2) }}
#} {{ form.genres.label }}
{{ form.genres(size=5) }}
{{ form.no_genres.label }}
{{ form.no_genres(size=5) }}
{{ form.artists.label }}
{{ form.artists(size=5) }}
{{ form.no_artists.label }}
{{ form.no_artists(size=5) }}
{{ form.keywords.label }}
{{ form.keywords(size=5) }}
{{ form.no_keywords.label }}
{{ form.no_keywords(size=5) }}
{{ form.titles.label }}
{{ form.titles(size=5) }}
{{ form.no_titles.label }}
{{ form.no_titles(size=5) }}
{{ form.albums.label }}
{{ form.albums(size=5) }}
{{ form.no_albums.label }}
{{ form.no_albums(size=5) }}
{{ form.form }} - {{ form.play }} - {{ form.zip }} - {{ form.m3u }}
{% endblock %} PK!x^F?**&musicbot/lib/web/templates/genres.html{% extends "layout.html" %} {% block title %}Genres{% endblock %} {% block body %} {% for g in genres: %} {% endfor %}
Genre Artists Albums Play Download Info
{{ g.name }} artists albums all / 4+ / 4.5+ / 5.0 / best m3u / zip stats
{% endblock %} PK!: cc%musicbot/lib/web/templates/index.html{% extends "layout.html" %} {% block body %}

Welcome!

{% endblock %} PK!"}7(musicbot/lib/web/templates/keywords.html{% extends "layout.html" %} {% block title %}Keywords{% endblock %} {% block body %} {% for k in keywords: %} {% endfor %}
Keyword Artists Genres Albums Musics Play Download Info
{{ k.name }} artists genres albums musics all / 4+ / 4.5+ / 5.0 m3u / zip stats
{% endblock %} PK!P%P%&musicbot/lib/web/templates/layout.html {% block title %}MusicBot{% endblock %}
{% with messages = get_flashed_messages() %} {% if messages|count %} {% endif %} {% endwith %} {% block body %}{% endblock %}
PK!/j#musicbot/lib/web/templates/m3u.html#EXTM3U {% for m in musics: %}#EXTINF:{{ m.duration }},{{ m.artist }} - {{ m.title }} {{ url_for('collection.download', _scheme='https', _external=True, _server=server(), artists=m.artist, albums=m.album, titles=m.title) | safe }} {% endfor %} PK!ŭ%musicbot/lib/web/templates/music.html{% extends "layout.html" %} {% block title %}Music{% endblock %} {% block body %}
{{ form.csrf_token }} {{ form.id }} {{ form.artist.label }}
{{ form.artist }}
{{ form.genre.label }}
{{ form.genre }}
{{ form.album.label }}
{{ form.album }}
{{ form.number.label }}
{{ form.number }}
{{ form.title.label }}
{{ form.title }}
{{ form.rating.label }}
{{ form.rating }}
{{ form.keywords.label }}
{{ form.keywords }}
{{ form.youtube.label }}
{{ form.youtube }}
{{ form.submit }}
{% endblock %} PK!  &musicbot/lib/web/templates/musics.html{% extends "layout.html" %} {% block title %}Musics{% endblock %} {% block body %} {% for m in musics: %} {% endfor %}
Artist Genre Album Track Title Rating Keywords Folder Duration Size YouTube Actions
{{ m.artist }} {{ m.genre }} {{ m.album }} {{ m.number }} {{ m.title }} {{ m.rating }} {{ m.keywords}} {{ m.folder }} {{ seconds_to_human(m.duration) }} {{ bytes_to_human(m.size) }} {%if m.youtube != "not found" %}{{ m.youtube }}{%else%}Not found{% endif %} play / download / show
{% endblock %} PK!f&musicbot/lib/web/templates/player.html{% extends "layout.html" %} {% block body %} {% endblock %} PK!R||'musicbot/lib/web/templates/refresh.html{% extends "layout.html" %} {% block title %}Refresh DB{% endblock %} {% block body %} DB View refreshed {% endblock %} PK!(Q&musicbot/lib/web/templates/rescan.html{% extends "layout.html" %} {% block title %}Rescan{% endblock %} {% block body %}
0%
{% endblock %} PK!3?%%'musicbot/lib/web/templates/results.htmlHello world you searched for {{ q }} PK!(musicbot/lib/web/templates/schedule.html{% extends "layout.html" %} {% block title %}Schedule jobs{% endblock %} {% block body %} All jobs scheduled for now ! {% endblock %} PK!W&musicbot/lib/web/templates/search.html
PK!:)i-musicbot/lib/web/templates/static/favicon.icoPNG  IHDR szz cHRMz%u0`:o_FbKGDC pHYs   vpAg qIDATXõkL}_b.C @I&Pu*UF]RUڇiI8ۦJ>LDidi7EY  I=n` 8vK|9y<Ѐ@Te }e`=pJ) @)RjmiBHSet`DaRh !# +EJI3ǟ2Ou|FrykΖe!E0if%,D\\o -*-JoS]S0քVWW9wKKK!D&M).-8@pʅZݵCVw}'eQ_)ǖ={WR\\] nCǎ 3Bz%h=8#d>^|'6]oݣ&&&())06+.+[C,EMx]l'_* s_jz[qERCCCܾ}u}DEu#$W!& 𔒒N^$o]= :i255 `x0d999ZwB8@FaU^JUْ:|C((*Lʒ2Ӥv@HXݥebv%42 f:Mx&덲~Iy7lBFiY'&Dӷ~BJ)(~L$"8>A* 2=ׄic#DؿkRCWWP4ׄJ&~Q@F !f1I0IJ>25`8/LyNMmp~:H]7|!֞D؝%[񛷸tgΩZM)EQQgGqW~{jb oؙ}/"߫#//˲Rb&.vuH%S Diu*` 8"i&n} &PJL&q\|>Μ9ӧ74MJo^cvr -π 2]񦦴htNxx [S@STͬ7n3TYzTXtSoftwarexMLOMLLV03ҳP007074R44SHI/-./H,JD(53ҳOOKI(Nt.k!zTXtThumb::Document::Pagesx322 !zTXtThumb::Image::heightx3412)82 zTXtThumb::Image::Widthx3412-g"zTXtThumb::MimetypexMLO/K{x_9G zTXtThumb::MTimex346300047 5zTXtThumb::SizexڳгNmVzTXtThumb::URIxKI h@QIENDB`PK!].musicbot/lib/web/templates/static/musicbot.csshtml { position: relative; min-height: 100%; } body { margin-bottom: 30px; } .container-music { margin-top: 60px; } .table-condensed>thead>tr>th, .table-condensed>tbody>tr>th, .table-condensed>tfoot>tr>th, .table-condensed>thead>tr>td, .table-condensed>tbody>tr>td, .table-condensed>tfoot>tr>td{ padding: 0.1rem; font-size: 0.75rem; } #player { width: 100%; } audio { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } ul#playlist li { padding-top: 0.25rem; padding-bottom: 0.25rem; font-size: 0.75rem; } ul#playlist li:hover { background:#333; } ul#playlist li.active a { color:#000000; text-decoration:none; } .footer { position: absolute; bottom: 0; width: 100%; height: 30px; line-height: 30px; background-color: #f5f5f5; } /* #myProgress { width: 100%; background-color: grey; } #myBar { width: 1%; height: 30px; background-color: green; } */ PK!+6[-musicbot/lib/web/templates/static/musicbot.js/* eslint semi: ["error", "always"] */ /* global $, URI */ $(document).ready(function () { var current = URI(window.location); var params = current.search(true); // SHUFFLE var shuffleParam = 'shuffle'; var shuffleInput = 'input#shuffle'; if (params.hasOwnProperty(shuffleParam)) { $(shuffleInput).prop('checked', params[shuffleParam]); } $(shuffleInput).change(function () { var current = URI(window.location); if (this.checked) { current.addSearch(shuffleParam, '1'); } else { current.removeSearch(shuffleParam); } window.location = current.href(); }); // LOOP var loopParam = 'loop'; var loopInput = 'input#loop'; if (params.hasOwnProperty(loopParam)) { $(loopInput).prop('checked', params[loopParam]); } $(loopInput).change(function () { var current = URI(window.location); if (this.checked) { current.addSearch(loopParam, '1'); } else { current.removeSearch(loopParam); } window.location = current.href(); }); // AUTOPLAY var autoplayParam = 'autoplay'; var autoplayInput = 'input#autoplay'; if (params.hasOwnProperty(autoplayParam)) { $(autoplayInput).prop('checked', params[autoplayParam]); } $(autoplayInput).change(function () { var current = URI(window.location); if (this.checked) { current.addSearch(autoplayParam, '1'); } else { current.removeSearch(autoplayParam); } window.location = current.href(); }); // EXCLUDE LIVES var noKeywordsParam = 'no_keywords'; var excludeLivesInput = 'input#lives'; if (params.hasOwnProperty(noKeywordsParam)) { $(excludeLivesInput).prop('checked', params[noKeywordsParam].includes('live')); } $(excludeLivesInput).change(function () { var current = URI(window.location); if (this.checked) { current.addSearch(noKeywordsParam, 'live'); } else { current.removeSearch(noKeywordsParam); } window.location = current.href(); }); // LIMIT var limitParam = 'limit'; var limitInput = 'input#limit'; if (params.hasOwnProperty(limitParam) && params[limitParam].length) { $(limitInput).prop('value', params[limitParam]); } $(limitInput).on('change', function () { var current = URI(window.location); var params = current.search(true); if (this.value !== 0) { if (params.hasOwnProperty(limitParam)) { if (params[limitParam] === this.value) { return; } } current.setSearch(limitParam, this.value); } else { current.removeSearch(limitParam); } window.location = current.href(); }); // VOLUME var volumeParam = 'volume'; var volumeInput = 'input#volume'; if (params.hasOwnProperty(volumeParam) && params[volumeParam].length) { $(volumeInput).prop('value', params[volumeParam]); } $(volumeInput).on('change', function () { var current = URI(window.location); var params = current.search(true); if (this.value !== 0) { if (params.hasOwnProperty(volumeParam)) { if (params[volumeParam] === this.value) { return; } } current.setSearch(volumeParam, this.value); } else { current.removeSearch(volumeParam); } window.location = current.href(); }); // LINK BUILD $('a').on('click contextmenu', function (event) { var next = URI(this.href); var a = this; if ($(shuffleInput).prop('checked')) { next.setSearch(shuffleParam, '1'); } if ($(autoplayInput).prop('checked')) { next.setSearch(autoplayParam, '1'); } if (params.hasOwnProperty(limitParam) && params[limitParam].length && $(limitParam).prop('value') !== 0) { next.setSearch(limitParam, $(limitInput).prop('value')); } if (params.hasOwnProperty(volumeParam) && params[volumeParam].length && $(volumeParam).prop('value') !== 0) { next.setSearch(volumeParam, $(volumeInput).prop('value')); } a.href = next.href(); }); }); PK!r r +musicbot/lib/web/templates/static/player.js/* eslint semi: ["error", "always"] */ /* global $, URI */ $(document).ready(function () { var currentUri = URI(window.location); var params = currentUri.search(true); var current = 0; var audio = $('#player'); var playlist = $('#playlist'); var tracks = playlist.find('li a'); var loop = false; var len = tracks.length - 1; function run (link, player) { player.src = link.attr('href'); console.log('Setting title to' + link.text()); document.title = link.text(); var par = link.parent(); par.addClass('active').siblings().removeClass('active'); audio[0].load(); var playPromise = audio[0].play(); if (playPromise !== undefined) { playPromise.then(_ => { console.log('Autoplay is ok'); }) .catch(error => { console.log('Autoplay issue: ', error); }); } } var first = tracks.first(); if (first) { console.log('Setting title to' + first.text()); document.title = first.text(); } // check loop var loopParam = 'loop'; if (params.hasOwnProperty(loopParam)) { console.log('Enable loop'); loop = true; } else { console.log('Disable loop'); loop = false; } // check volume var volumeParam = 'volume'; if (params.hasOwnProperty(volumeParam)) { console.log('Custom volume'); audio[0].volume = params[volumeParam] / 100; } else { console.log('Default volume'); audio[0].volume = 1; } // check autoplay var autoplayParam = 'autoplay'; if (params.hasOwnProperty(autoplayParam)) { var playPromise = audio[0].play(); if (playPromise !== undefined) { playPromise.then(_ => { console.log('Autoplay is ok.'); }) .catch(error => { console.log('Autoplay issue: ', error); }); } } playlist.find('a').click(function (e) { e.preventDefault(); var link = $(this); current = link.parent().index(); run(link, audio[0]); }); audio[0].addEventListener('ended', function (e) { current++; if (current > len) { audio[0].pause(); console.log('Last song ended'); if (loop) { console.log('Loop enabled, return to first song'); current = 0; var link = playlist.find('a')[current]; run($(link), audio[0]); } } else { var currentLink = playlist.find('a')[current]; run($(currentLink), audio[0]); } }); }); PK!jcm[[%musicbot/lib/web/templates/stats.html{% extends "layout.html" %} {% block body %}

Stats

Musics: {{ stats.musics }}
Artists: {{ stats.artists }}
Albums: {{ stats.albums }}
Genres: {{ stats.genres }}
Keywords: {{ stats.keywords }}
Size: {{ bytes_to_human(stats.size) }}
Duration: {{ seconds_to_human(stats.duration) }}
{% endblock %} PK! musicbot/lib/youtube.pyimport logging import isodate import ujson import aiohttp logger = logging.getLogger(__name__) logging.getLogger('googleapiclient.discovery_cache').setLevel(logging.ERROR) logging.getLogger('googleapiclient.discovery').setLevel(logging.CRITICAL) DEVELOPER_KEY = "AIzaSyAm3XZ3OYj-GlNIHk-YFvpzrtvQ3ZalAoI" YOUTUBE_API_SERVICE_NAME = "youtube" YOUTUBE_API_VERSION = "v3" def youtube_duration(dur): if 0 <= dur <= 4 * 60: return "short" if 4 * 60 < dur <= 20 * 60: return "medium" if dur > 20 * 60: return "long" return "any" # pylint: disable-msg=too-many-locals async def search(artist, title, duration): try: string = ' '.join([artist, title]) parsingChannelUrl = "https://www.googleapis.com/youtube/v3/search" parsingChannelHeader = {'cache-control': "no-cache"} parsingChannelQueryString = {"part": "id,snippet", "maxResults": "10", "key": DEVELOPER_KEY, "type": "video", "q": string, "safeSearch": "none", "videoDuration": youtube_duration(duration)} parsingChannel = None async with aiohttp.ClientSession() as session: async with session.get(parsingChannelUrl, headers=parsingChannelHeader, params=parsingChannelQueryString) as resp: parsingChannel = await resp.read() parsingChannelItems = ujson.loads(parsingChannel).get("items") if parsingChannelItems is None or not parsingChannelItems: return 'not found' VideoIds = ",".join(str(x.get("id").get("videoId")) for x in parsingChannelItems) parsingVideoUrl = "https://www.googleapis.com/youtube/v3/videos" parsingVideoHeader = {'cache-control': "no-cache"} parsingVideoQueryString = {"part": 'id,snippet,contentDetails', "id": VideoIds, "key": DEVELOPER_KEY} parsingVideo = None async with aiohttp.ClientSession() as session: async with session.get(parsingVideoUrl, headers=parsingVideoHeader, params=parsingVideoQueryString) as resp: parsingVideo = await resp.read() results = ujson.loads(parsingVideo).get("items") if results is None: return 'not found' mapping = {r["id"]: isodate.parse_duration(r["contentDetails"]["duration"]).total_seconds() for r in results} logger.debug("duration: %s, mapping: %s", duration, mapping) key = min(mapping, key=lambda k: abs(mapping[k] - duration)) url = "https://www.youtube.com/watch?v={}".format(key) logger.debug("Most relevant: %s %s %s", key, mapping[key], url) return url except Exception as e: logger.debug(e) logger.debug('Cannot find video for artist: %s title: %s duration: %s', artist, title, duration) return 'error' PK!RLaamusicbot/musicbot#!/usr/bin/env python3 # -*- coding: utf-8 -*- import click import click_completion import os import logging from click_repl import register_repl from attrdict import AttrDict from lib import helpers, config if os.path.islink(__file__): myself = os.readlink(__file__) else: myself = __file__ bin_folder = os.path.dirname(myself) commands_folder = 'commands' plugin_folder = os.path.join(bin_folder, commands_folder) CONTEXT_SETTINGS = {'auto_envvar_prefix': 'MUSICBOT', 'help_option_names': ['-h', '--help']} logger = logging.getLogger('musicbot') def custom_startswith(string, incomplete): """A custom completion matching that supports case insensitive matching""" if os.environ.get('_MUSICBOT_CASE_INSENSITIVE_COMPLETE'): string = string.lower() incomplete = incomplete.lower() return string.startswith(incomplete) click_completion.startswith = custom_startswith click_completion.init() class SubCommandLineInterface(helpers.GroupWithHelp): def list_commands(self, ctx): rv = [] for filename in os.listdir(plugin_folder): if filename.endswith('.py') and '__init__' not in filename: rv.append(filename[:-3]) all_commands = rv + super().list_commands(ctx) all_commands.sort() return all_commands def get_command(self, ctx, name): ns = {} fn = os.path.join(plugin_folder, name + '.py') try: with open(fn) as f: code = compile(f.read(), fn, 'exec') ns['__name__'] = '{}.{}'.format(commands_folder, name) eval(code, ns, ns) except FileNotFoundError: return super().get_command(ctx, name) return ns['cli'] @click.group(cls=SubCommandLineInterface, context_settings=CONTEXT_SETTINGS) @click.version_option("1.0") @helpers.add_options(config.options) @click.pass_context def cli(ctx, **kwargs): """Music swiss knife, new gen.""" ctx.obj = AttrDict ctx.obj.folder = bin_folder config.config.set(**kwargs) ctx.obj.config = config.config if __name__ == '__main__': register_repl(cli) cli() PK!Hr(3)musicbot-0.0.1.dist-info/entry_points.txtN+I/N.,()--LN/1` <..PK!HW"TTmusicbot-0.0.1.dist-info/WHEEL A н#J."jm)Afb~ ڡ5 G7hiޅF4+-3ڦ/̖?XPK!HdfŅ!musicbot-0.0.1.dist-info/METADATAis6~N$7NT[1ulxR$Լ%O&mMΐ3IDĻH">x+a=}38!0ԛb4 1N{9 ]GtAlВD8$/JO%t>p<%98fb:\_N>t0M*f#3\`x {ϟ=={{x; lIDw$iR~q ~`^I>*8f)avz Cac?׈Ef:5Ѣe 687bpC`|IRB WAr>ޝ^^ó裏Gh4rjș,>Qõ}\ +09'䆊%:޺O%X`iwט>5pred.A_)yA\ "VBl@@s̸CPx%('OEtȥb Remx0y3h?(An% f `[LA=\k1 k"H6_ʹ϶w klHp8 'n=GЋ5m!Q$xH@Tƶd ӮBG}0?x̀hAm1D2þSJxu?/ 7R:ZāOZpGrnՀȬٓ F ׬G`UTp0>8a+.}O`M0&5Yju6l&O*} U?uyw4l+3m2)We/0^4HG?5CT 8T=+9se0.깤-f0S +s-U@K`~od%S}Wl'/ȤQQDp06rU:$"{.*Ԧ]/RT]7VLUMSJBk=5 ER4>y5 x+*=6j|4#jkYlkd(yXE@cկlw0!}UCώ;/UŻbYª8*⪄Jw7y5*"+&M|K]ګv1Jjy;Lb,KIHug V9Iڰdg#Ct jYurR䵢 kYAQ[yAU 쩜7Ͼ~U)s|Tr]sFzUBXS\+{{{% t i΁xO"AUIڜ(j-bU+[YU!uMDP v1gKIq=cIX8RXā#"'i)zQX1\Ռej@5 a!ң (x9_@T9P!ց yʁ c\Q4GЯiJxMҔ :@+ _L30tS,C`UzNj`^^PPT-IN9@ $kEO9J!~ØHyUvg1=O(>\_Y\ƅxhK\_/R|˘ubhDEf@_2 +'gYSGY}$I;@ :跣B- y!aњՃ;|.A]@JC)"Wsʾ{pwos T>0,ZT;~_C%u C12g##' 5#u@[k}H81. % 60(i%W[ kMɆT [UU+Z)n'N(tl9uDjʈO͏ "XMl#b#ۊݽjqߒwBsNqLW&k)u횴m\}'stf]A_F?33cy.OMN]G)] /M0 &J3]kv7ZhoCm ѷ!6DJs&9)B&Jw]!2Su4#T^-ymL^-zfmyz~W2cػFfmjקFz}ӞiO]x~Kb>y2A:΢`޶Q!_2B|3]ɾzqMAØ곬dJhG'0ɕ?ό1}HXP+5Aw__1#Kh4U8 %,yJT<=0N wP}}e"/[hYZݹ=q&N/JnnwI1,+rU:P2G(qy:ąhqe=:;ʳ<ޟO[*_׉}#RWnz_kϰw!UONg ^230}<ߢo찙] sh}=#;AFlbV:#a X5T݌IZQb/b/bp&,M)?ܜ,YOhG˃UT4#`}e wQzfS\S㰣aiҠϻϻ}nwǗduoN=o7oA {nvv5f+["4I0!'n&h&RO=:{E!١$zdsy'riM|Y(h"J:w *fK[4,d#iFOz$"7:Jhؙ`Wߑ)tJ'z J5ŷF}JMi¾Oo[(W2׆L'q 8>qj(+i^4E]fP١h53o ا bU=vf+8SǑJf~P?)ܦweCm&;֘m=X'GPvbYj|-4 WZmpg/n3ON;LN ˬ^U-u==xq`Q$wG7m+voKAoKm-%.o&QȽ3P&ni$7Juu qrT_@D_@4 @ݟkGE Q/vaՃS}R%g~%1ZI[qP>jmw|pqEFk˯lN[N_=u[ /M1bbmE.bRWҨqVjjFʚ%K~)/@mZ{`be1c!]$+ݒ1}Ud'E%ݤr'.Fv*jDPK!H OW~ musicbot-0.0.1.dist-info/RECORDɒXE-<,zc1A}}YYR:Ջds{{3eu (r o EĿ0BI;+[n<Wt6cW<_D kE d }Ne>ALK!FMN{m8+{պH 8;^y`r̟۬ut{GDU5["AF[lWG!B$ G8m>qeŠHU"@Ѱ(,RV4(DO8|bT>gAHާ@p`c* /+ȓ,(KLX2DE@&(4m!Q{G$(xWG NVyux(fƏu夲#Wg˜Bɉī9)h}>E pwyK;<.I uc ww4>ug-UC,c0uczfA;P D31TdEyRdJ(pps]hFGw'\y6ܻĭI؇qUN~`q#zP]Fs]]hv'.C#M\rlP/0*m?oIi } KPV!i {9nj®9V"'2+H)ý2}@LE:"δ 8VfZ\ke8etKI]-Hr=3lE~ tϒ){'X<9cs{T`̺y֊}`d2 3CZgb]@IL]gS9-uF?˷AȒ8!kCP3.U7H2Q!܃EQ%QR5M%`Gx 7,Ÿ}˕hm.CDx#恤l5́VNFYEn^8-Կy?ݠ3g'M7]0,x h,#?L_Eμ|>1V!2ekQdG0AR_+!̛̇pBG͋>EH0DՇ% p9@o49ru%_hjy*{l /\ʻ`\n,>]%@Ge8 Vo vN>E׌7=a2/M ﲉx5x~ck{ 6uP#QC\Rcd;m1}m+Yx+b 9]PHcH\eqAmQ~[<¸s/0,jl)(b=+j$t8Aیk}r58X,Ȋ#D[GE/Ȍ\m!]b6Dp[E[j'ul!6K0 ~7ϝۢ2)C#lځ5sM/VQ5Y ɥ jPnWDȸX|nikxș@F!ׇ+.("58bmjhTĴzBߠodcͺi; kwe}@0-УDЊtu,tCh/,<[/h%%lo+b] VĺOs=3Mo P_܎#"c]ݲ#Cgt:,p+޺.muBǑg#PK!musicbot/__init__.pyPK!2musicbot/commands/__init__.pyPK!Fccmmusicbot/commands/completion.pyPK!'6/ musicbot/commands/config.pyPK!y= I musicbot/commands/consistency.pyPK!3 musicbot/commands/db.pyPK!!musicbot/commands/file.pyPK!Z(]ttmusicbot/commands/folder.pyPK!:K} } +musicbot/commands/playlist.pyPK!2. . w5musicbot/commands/server.pyPK!>musicbot/commands/stats.pyPK!ZXY$Bmusicbot/commands/tag.pyPK!fz,,Gmusicbot/commands/task.pyPK!1VJmusicbot/commands/youtube.pyPK!mPmusicbot/lib/__init__.pyPK!u8cPmusicbot/lib/collection.pyPK!nJ ||pmusicbot/lib/config.pyPK!hu33musicbot/lib/database.pyPK!s>musicbot/lib/file.pyPK!@Wmusicbot/lib/helpers.pyPK!7  musicbot/lib/lib.pyPK!!ԀWmusicbot/lib/mfilter.pyPK!)Vyy musicbot/lib/persistence.pyPK!77musicbot/lib/server.pyPK!)musicbot/lib/web/__init__.pyPK![b'Ar r cmusicbot/lib/web/api.pyPK!AkB %musicbot/lib/web/app.pyPK!d""=&musicbot/lib/web/collection.pyPK!bTT#Imusicbot/lib/web/config.pyPK!h^Qmusicbot/lib/web/forms.pyPK!ڼ!y`musicbot/lib/web/helpers.pyPK!s';qmusicbot/lib/web/limiter.pyPK!Z-- rmusicbot/lib/web/mfilter.pyPK!&&wmusicbot/lib/web/templates/albums.htmlPK!E '|musicbot/lib/web/templates/artists.htmlPK!#J'musicbot/lib/web/templates/filters.htmlPK!p|'musicbot/lib/web/templates/folders.htmlPK!1(dmusicbot/lib/web/templates/generate.htmlPK!x^F?**&<musicbot/lib/web/templates/genres.htmlPK!: cc%musicbot/lib/web/templates/index.htmlPK!"}7(Pmusicbot/lib/web/templates/keywords.htmlPK!P%P%&9musicbot/lib/web/templates/layout.htmlPK!/j#musicbot/lib/web/templates/m3u.htmlPK!ŭ%musicbot/lib/web/templates/music.htmlPK!  &musicbot/lib/web/templates/musics.htmlPK!f&4musicbot/lib/web/templates/player.htmlPK!R||'Smusicbot/lib/web/templates/refresh.htmlPK!(Q&musicbot/lib/web/templates/rescan.htmlPK!3?%%'musicbot/lib/web/templates/results.htmlPK!(musicbot/lib/web/templates/schedule.htmlPK!W&Rmusicbot/lib/web/templates/search.htmlPK!:)i-omusicbot/lib/web/templates/static/favicon.icoPK!]._musicbot/lib/web/templates/static/musicbot.cssPK!+6[-umusicbot/lib/web/templates/static/musicbot.jsPK!r r +Emusicbot/lib/web/templates/static/player.jsPK!jcm[[%musicbot/lib/web/templates/stats.htmlPK! musicbot/lib/youtube.pyPK!RLaa큾 musicbot/musicbotPK!Hr(3)Nmusicbot-0.0.1.dist-info/entry_points.txtPK!HW"TTmusicbot-0.0.1.dist-info/WHEELPK!HdfŅ!Mmusicbot-0.0.1.dist-info/METADATAPK!H OW~ $musicbot-0.0.1.dist-info/RECORDPK>>/