From 5ea5e22e13ddb4ce9c7ecb09516c0d848aadbc78 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Mon, 10 Jul 2017 12:20:11 +0800 Subject: [PATCH 01/88] update log system --- youtube_dl_webui/__main__.py | 7 ++++++ youtube_dl_webui/core.py | 13 ++++++---- youtube_dl_webui/db.py | 6 +++-- youtube_dl_webui/logging.json | 46 +++++++++++++++++++++++++++++++++++ youtube_dl_webui/worker.py | 13 ++++++---- 5 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 youtube_dl_webui/logging.json diff --git a/youtube_dl_webui/__main__.py b/youtube_dl_webui/__main__.py index db6b495..ff9eb32 100644 --- a/youtube_dl_webui/__main__.py +++ b/youtube_dl_webui/__main__.py @@ -5,6 +5,8 @@ import sys import os.path +import json +import logging.config if __package__ is None and not hasattr(sys, 'frozen'): path = os.path.realpath(os.path.abspath(__file__)) @@ -14,5 +16,10 @@ import youtube_dl_webui if __name__ == '__main__': + # Setup logger + with open('logging.json') as f: + logging_conf = json.load(f) + logging.config.dictConfig(logging_conf) + youtube_dl_webui.main() diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 882f4ba..74fc8d0 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -3,6 +3,7 @@ import json import os +import logging from multiprocessing import Process, Queue from collections import deque @@ -19,11 +20,13 @@ from .server import Server from .worker import Worker + class Core(object): exerpt_keys = ['tid', 'state', 'percent', 'total_bytes', 'title', 'eta', 'speed'] valid_opts = ['proxy', 'format'] def __init__(self, args=None): + self.logger = logging.getLogger('ydl_webui') self.cmd_args = {} self.conf = {'server': {}, 'ydl': {}} self.rq = Queue() @@ -40,10 +43,10 @@ def __init__(self, args=None): dl_dir = self.conf['download_dir'] try: os.makedirs(dl_dir, exist_ok=True) - print(dl_dir) + self.logger.info("Download dir: %s", dl_dir) os.chdir(dl_dir) except PermissionError: - print('[ERROR] Permission Error of download_dir: {}'.format(dl_dir)) + self.logger.critical('Permission Error for download dir: %s', dl_dir) exit(1) self.launch_unfinished() @@ -60,7 +63,7 @@ def run(self): elif data_from == 'worker': ret = self.worker_request(data) else: - print(data) + logger.debug(data) def launch_unfinished(self): @@ -159,7 +162,7 @@ def load_conf_file(self): with open(self.cmd_args['conf']) as f: conf_dict = json.load(f) except FileNotFoundError as e: - print("Config file ({}) doesn't exist".format(self.cmd_args['conf'])) + self.logger.critical("Config file (%s) doesn't exist", self.cmd_args['conf']) exit(1) general = conf_dict.get('general', None) @@ -323,6 +326,6 @@ def worker_request(self, data): d = data['data'] if d['type'] == 'invalid_url': - print("Can't start downloading {}, url is invalid".format(d['url'])) + self.logger.error("Can't start downloading {}, url is invalid".format(d['url'])) self.db.set_state(tid, 'invalid') diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index d56d8d1..103e658 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -4,6 +4,7 @@ import json import os import sqlite3 +import logging from hashlib import sha1 from time import time @@ -16,12 +17,13 @@ class DataBase(object): def __init__(self, db_path): + self.logger = logging.getLogger('ydl_webui') if os.path.exists(db_path) and not os.path.isfile(db_path): - print('[ERROR] The db_path: {} is not a regular file'.format(db_path)) + self.logger.error('The db_path: %s is not a regular file', db_path) raise Exception('The db_path is not valid') if os.path.exists(db_path) and not os.access(db_path, os.W_OK): - print('[ERROR] The db_path: {} is not writable'.format(db_path)) + self.logger.error('The db_path: %s is not writable', db_path) raise Exception('The db_path is not valid') # first time to create db diff --git a/youtube_dl_webui/logging.json b/youtube_dl_webui/logging.json new file mode 100644 index 0000000..e62c27b --- /dev/null +++ b/youtube_dl_webui/logging.json @@ -0,0 +1,46 @@ +{ + "version": 1, + "disable_existing_loggers": false, + "formatters": { + "simple": { + "format": "%(levelname)s - %(filename)s:%(lineno)d - %(message)s" + } + }, + + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://sys.stdout" + }, + + "info_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "simple", + "filename": "info.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + }, + + "error_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "ERROR", + "formatter": "simple", + "filename": "errors.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + } + }, + + "loggers": { + "ydl_webui": { + "level": "DEBUG", + "handlers": ["console"], + "propagate": "no" + } + } +} diff --git a/youtube_dl_webui/worker.py b/youtube_dl_webui/worker.py index 69c069e..32e93cb 100644 --- a/youtube_dl_webui/worker.py +++ b/youtube_dl_webui/worker.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import re +import logging from youtube_dl import YoutubeDL from youtube_dl import DownloadError @@ -15,6 +16,7 @@ class ydl_hook(object): def __init__(self, tid, wqueue): + self.logger = logging.getLogger('ydl_webui') self.tid = tid self.wq = wqueue self.wqd = deepcopy(WQ_DICT) @@ -24,7 +26,7 @@ def __init__(self, tid, wqueue): def finished(self, d): - print('finished status') + self.logger.debug('finished status') d['_percent_str'] = '100%' d['speed'] = '0' d['elapsed'] = 0 @@ -35,12 +37,12 @@ def finished(self, d): def downloading(self, d): - print('downloading status') + self.logger.debug('downloading status') return d def error(self, d): - print('error status') + self.logger.debug('error status') d['_percent_str'] = '100%' return d @@ -123,6 +125,7 @@ def invalid_url(self, url): class Worker(Process): def __init__(self, tid, wqueue, param=None, ydl_opts=None, first_run=False): super(Worker, self).__init__() + self.logger = logging.getLogger('ydl_webui') self.tid = tid self.wq = wqueue self.param = param @@ -155,7 +158,7 @@ def run(self): wqd['data'] = info_dict self.wq.put(wqd) - print('start downloading ...') + self.logger.info('start downloading ...') ydl.download([self.url]) except DownloadError as e: # url error @@ -164,7 +167,7 @@ def run(self): def stop(self): - print('Terminating Process ...') + self.logger.info('Terminating Process ...') self.terminate() self.join() From a1cff73e383aea320e245cc7717965c582a9b45a Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 11 Jul 2017 09:49:18 +0800 Subject: [PATCH 02/88] refactor --- youtube_dl_webui/core.py | 74 +++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 74fc8d0..fab5c1f 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -27,15 +27,21 @@ class Core(object): def __init__(self, args=None): self.logger = logging.getLogger('ydl_webui') - self.cmd_args = {} + + # options from command line + self.cmdl_args_dict = {} + # options read from configuration file + self.conf_file_dict = {} + # configuration options combined cmdl_args_dict with conf_file_dict. self.conf = {'server': {}, 'ydl': {}} + self.rq = Queue() self.wq = Queue() self.worker = {} - self.load_cmd_args(args) + self.load_cmdl_args(args) self.load_conf_file() - self.override_conf() + self.cmdl_override_conf_file() self.server = Server(self.wq, self.rq, self.conf['server']['host'], self.conf['server']['port']) self.db = DataBase(self.conf['db_path']) @@ -151,70 +157,66 @@ def cancel_worker(self, tid): del self.worker[tid] - def load_cmd_args(self, args): - self.cmd_args['conf'] = args.get('config', None) - self.cmd_args['host'] = args.get('host', None) - self.cmd_args['port'] = args.get('port', None) + def load_cmdl_args(self, args): + self.cmdl_args_dict['conf'] = args.get('config') + self.cmdl_args_dict['host'] = args.get('host') + self.cmdl_args_dict['port'] = args.get('port') def load_conf_file(self): try: - with open(self.cmd_args['conf']) as f: - conf_dict = json.load(f) + with open(self.cmdl_args_dict['conf']) as f: + self.conf_file_dict = json.load(f) except FileNotFoundError as e: - self.logger.critical("Config file (%s) doesn't exist", self.cmd_args['conf']) + self.logger.critical("Config file (%s) doesn't exist", self.cmdl_args_dict['conf']) exit(1) - general = conf_dict.get('general', None) - self.load_general_conf(general) - - server_conf = conf_dict.get('server', None) - self.load_server_conf(server_conf) - - ydl_opts = conf_dict.get('youtube_dl', None) - self.load_ydl_conf(ydl_opts) + self.load_general_conf(self.conf_file_dict) + self.load_server_conf(self.conf_file_dict) + self.load_ydl_conf(self.conf_file_dict) - def load_general_conf(self, general): - valid_conf = [ ['download_dir', '~/Downloads/youtube-dl', expanduser], - ['db_path', '~/.conf/youtube-dl-webui/db.db', expanduser], - ['task_log_size', 10, None], + def load_general_conf(self, conf_file_dict): + # field1: key, field2: default value, field3: function to process the value + valid_conf = [ ['download_dir', '~/Downloads/youtube-dl', expanduser], + ['db_path', '~/.conf/youtube-dl-webui/db.db', expanduser], + ['task_log_size', 10, None], ] - general = {} if general is None else general + general_conf = conf_file_dict.get('general', {}) for conf in valid_conf: if conf[2] is None: - self.conf[conf[0]] = general.get(conf[0], conf[1]) + self.conf[conf[0]] = general_conf.get(conf[0], conf[1]) else: - self.conf[conf[0]] = conf[2](general.get(conf[0], conf[1])) + self.conf[conf[0]] = conf[2](general_conf.get(conf[0], conf[1])) - def load_server_conf(self, server_conf): - valid_conf = [ ('host', '127.0.0.1'), - ('port', '5000') + def load_server_conf(self, conf_file_dict): + valid_conf = [ ['host', '127.0.0.1'], + ['port', '5000' ] ] - server_conf = {} if server_conf is None else server_conf + server_conf = conf_file_dict.get('server', {}) for pair in valid_conf: self.conf['server'][pair[0]] = server_conf.get(pair[0], pair[1]) - def load_ydl_conf(self, ydl_opts): - ydl_opts = {} if ydl_opts is None else ydl_opts + def load_ydl_conf(self, conf_file_dict): + ydl_opts = conf_file_dict.get('youtube_dl', {}) for opt in Core.valid_opts: if opt in ydl_opts: self.conf['ydl'][opt] = ydl_opts.get(opt, None) - def override_conf(self): - if self.cmd_args['host'] is not None: - self.conf['server']['host'] = self.cmd_args['host'] + def cmdl_override_conf_file(self): + if self.cmdl_args_dict['host'] is not None: + self.conf['server']['host'] = self.cmdl_args_dict['host'] - if self.cmd_args['port'] is not None: - self.conf['server']['port'] = self.cmd_args['port'] + if self.cmdl_args_dict['port'] is not None: + self.conf['server']['port'] = self.cmdl_args_dict['port'] def server_request(self, data): From 13bac5ee00163c72dbd972f283c9bc1817c888bc Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 11 Jul 2017 10:12:32 +0800 Subject: [PATCH 03/88] refactor --- youtube_dl_webui/core.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index fab5c1f..dfc8670 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -42,6 +42,7 @@ def __init__(self, args=None): self.load_cmdl_args(args) self.load_conf_file() self.cmdl_override_conf_file() + self.logger.debug("configuration: \n%s", json.dumps(self.conf, indent=4)) self.server = Server(self.wq, self.rq, self.conf['server']['host'], self.conf['server']['port']) self.db = DataBase(self.conf['db_path']) @@ -191,6 +192,8 @@ def load_general_conf(self, conf_file_dict): else: self.conf[conf[0]] = conf[2](general_conf.get(conf[0], conf[1])) + self.logger.debug("general_config: %s", json.dumps(self.conf)) + def load_server_conf(self, conf_file_dict): valid_conf = [ ['host', '127.0.0.1'], @@ -202,13 +205,21 @@ def load_server_conf(self, conf_file_dict): for pair in valid_conf: self.conf['server'][pair[0]] = server_conf.get(pair[0], pair[1]) + self.logger.debug("server_config: %s", json.dumps(self.conf['server'])) + def load_ydl_conf(self, conf_file_dict): + valid_opts = [ ['proxy', None, ], + ['format', 'bestaudio/best' ] + ] + ydl_opts = conf_file_dict.get('youtube_dl', {}) - for opt in Core.valid_opts: - if opt in ydl_opts: - self.conf['ydl'][opt] = ydl_opts.get(opt, None) + for opt in valid_opts: + if opt[0] in ydl_opts: + self.conf['ydl'][opt[0]] = ydl_opts.get(opt[0], opt[1]) + + self.logger.debug("global ydl_opts: %s", json.dumps(self.conf['ydl'])) def cmdl_override_conf_file(self): From 69bf01481e6c0b25a6172788e3bd6388343890bc Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 11 Jul 2017 11:07:43 +0800 Subject: [PATCH 04/88] combine task ydl_opts with global ydl_opts --- example_config.json | 4 ++-- youtube_dl_webui/core.py | 15 +++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/example_config.json b/example_config.json index 28464f7..26f920a 100644 --- a/example_config.json +++ b/example_config.json @@ -1,7 +1,7 @@ { "general": { - "download_dir": "~/Downloads/youtube_dl", - "db_path": "~/Documents/youtube_dl_webui.db", + "download_dir": "/tmp/youtube_dl", + "db_path": "/tmp/youtube_dl_webui.db", "download_log_size": 10 }, "server": { diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index dfc8670..f29e13a 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -86,6 +86,8 @@ def create_task(self, param, ydl_opts): if param['url'].strip() == '': raise KeyError + valid_ydl_opts = {k: ydl_opts[k] for k in ydl_opts if k in self.conf['ydl']} + tid = self.db.create_task(param, ydl_opts) return tid @@ -128,7 +130,9 @@ def launch_worker(self, tid, log_list, param=None, ydl_opts={}, first_run=False) self.worker[tid]['log'].appendleft({'time': int(time()), 'type': 'debug', 'msg': 'Task starts...'}) self.db.update_log(tid, self.worker[tid]['log']) - opts = self.add_ydl_conf_file_opts(ydl_opts) + # Merge global ydl_opts with local opts + opts = {k: ydl_opts[k] if k in ydl_opts else self.conf['ydl'][k] for k in self.conf['ydl']} + self.logger.debug("ydl_opts(%s): %s" %(tid, json.dumps(opts))) # launch worker process w = Worker(tid, self.rq, param=param, ydl_opts=opts, first_run=first_run) @@ -136,15 +140,6 @@ def launch_worker(self, tid, log_list, param=None, ydl_opts={}, first_run=False) self.worker[tid]['obj'] = w - def add_ydl_conf_file_opts(self, ydl_opts={}): - conf_opts = self.conf.get('ydl', {}) - - # filter out unvalid options - d = {k: ydl_opts[k] for k in ydl_opts if k in Core.valid_opts} - - return {**conf_opts, **d} - - def cancel_worker(self, tid): if tid not in self.worker: raise TaskPausedError('task not running') From e89f1aa5b140affb41a8189a90fa9af962135d37 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 18 Jul 2017 08:49:19 +0800 Subject: [PATCH 05/88] some refactor --- test/sample_info_dict.json | 509 +++++++++++++++++++++++++++++++++++++ youtube_dl_webui/core.py | 1 - youtube_dl_webui/worker.py | 3 + 3 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 test/sample_info_dict.json diff --git a/test/sample_info_dict.json b/test/sample_info_dict.json new file mode 100644 index 0000000..964a961 --- /dev/null +++ b/test/sample_info_dict.json @@ -0,0 +1,509 @@ +{ + "id": "C0DPdy98e4c", + "uploader": "Simon Yapp", + "uploader_id": "simonyapp", + "uploader_url": "http://www.youtube.com/user/simonyapp", + "upload_date": "20070221", + "license": "Standard YouTube License", + "creator": null, + "title": "TEST VIDEO", + "alt_title": null, + "thumbnail": "https://i.ytimg.com/vi/C0DPdy98e4c/hqdefault.jpg", + "description": "COUNTING LEADER AND TONE", + "categories": [ + "Film & Animation" + ], + "tags": [ + "TONES", + "AND", + "BARS", + "Countdown", + "Black & White", + "Sync Flashes", + "Sync", + "Test Testing", + "Test", + "Testing", + "54321", + "Numbers", + "Quality", + "Call", + "Funny" + ], + "subtitles": {}, + "automatic_captions": {}, + "duration": 18, + "age_limit": 0, + "annotations": null, + "chapters": null, + "webpage_url": "https://www.youtube.com/watch?v=C0DPdy98e4c", + "view_count": 631615, + "like_count": 284, + "dislike_count": 136, + "average_rating": 3.70476198196, + "formats": [ + { + "format_id": "171", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=29360&expire=1500360321&ipbits=0&pl=20&mime=audio%2Fwebm&key=yt6&lmt=1449553631233094&dur=17.550&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=4295A306DEC3776B38C5EDC35BC5F210EA87A818.0D1EB6715CF637D025483145683804267B3231FC&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Ckeepalive%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&ms=au&itag=171&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&keepalive=yes&initcwndbps=4687500&ratebypass=yes", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "webm", + "acodec": "vorbis", + "format_note": "DASH audio", + "abr": 128, + "filesize": 29360, + "tbr": 15.169, + "vcodec": "none", + "format": "171 - audio only (DASH audio)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "format_id": "249", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=44319&expire=1500360321&ipbits=0&pl=20&mime=audio%2Fwebm&key=yt6&lmt=1449553634943807&dur=17.561&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=4738F97E310715C9F835DCB7DA594103CA4588D0.2CD30E586E2DB40E35F0017383C3A9C13324E85E&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Ckeepalive%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&ms=au&itag=249&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&keepalive=yes&initcwndbps=4687500&ratebypass=yes", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "webm", + "format_note": "DASH audio", + "acodec": "opus", + "abr": 50, + "filesize": 44319, + "tbr": 25.171, + "vcodec": "none", + "format": "249 - audio only (DASH audio)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "format_id": "250", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=60843&expire=1500360321&ipbits=0&pl=20&mime=audio%2Fwebm&key=yt6&lmt=1449553631221728&dur=17.561&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=4DAAD89E8BAD7FE27491368D68DD3C958FCBAB35.7E9984DF100BC91CD24CB8AC08BA4448907BF06B&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Ckeepalive%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&ms=au&itag=250&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&keepalive=yes&initcwndbps=4687500&ratebypass=yes", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "webm", + "format_note": "DASH audio", + "acodec": "opus", + "abr": 70, + "filesize": 60843, + "tbr": 38.126, + "vcodec": "none", + "format": "250 - audio only (DASH audio)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "ext": "m4a", + "format_note": "DASH audio", + "acodec": "mp4a.40.5", + "abr": 48, + "container": "m4a_dash", + "format_id": "139", + "url": "https://r2---sn-npoe7n7y.googlevideo.com/videoplayback?id=0b40cf772f7c7b87&itag=139&source=youtube&requiressl=yes&ei=JFptWYPCOM2iowO9vJ2gAg&ms=au&mv=m&initcwndbps=4687500&pl=20&mm=31&mn=sn-npoe7n7y&ratebypass=yes&mime=audio/mp4&gir=yes&clen=107617&lmt=1407412299911186&dur=17.692&signature=0D4C08A6002FC75CA56A7F6B5204159E646183B5.9160D27A6787B478FD224F31EE95A6E76634BA2F&key=dg_yt0&mt=1500338629&ip=188.166.189.227&ipbits=0&expire=1500360325&sparams=ip,ipbits,expire,id,itag,source,requiressl,ei,ms,mv,initcwndbps,pl,mm,mn,ratebypass,mime,gir,clen,lmt,dur", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/dash/ei/JFptWYPCOM2iowO9vJ2gAg/ms/au/source/youtube/mv/m/initcwndbps/4687500/id/0b40cf772f7c7b87/signature/DF7193EA58FB61FF0031F74EC45D7AFA33D1989C.C1413087AE8A73E28F01389A016D712AF90AE0C1/requiressl/yes/key/yt6/mt/1500338629/ipbits/0/playback_host/r2---sn-npoe7n7y.googlevideo.com/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/hfr/1/sparams/as%2Cei%2Chfr%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Cplayback_host%2Crequiressl%2Csource%2Cexpire/itag/0/pl/20/ip/188.166.189.227/mm/31/mn/sn-npoe7n7y/expire/1500360325", + "width": null, + "height": null, + "tbr": 48.766, + "asr": 22050, + "fps": null, + "language": null, + "filesize": 107617, + "vcodec": "none", + "format": "139 - audio only (DASH audio)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "format_id": "251", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=87201&expire=1500360321&ipbits=0&pl=20&mime=audio%2Fwebm&key=yt6&lmt=1449553631097350&dur=17.561&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=34D343CC9F92A25183098C65B17AD0E29A23F0BA.D24ACC27812FBD3D965A25DE8A2EE6199E142079&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Ckeepalive%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&ms=au&itag=251&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&keepalive=yes&initcwndbps=4687500&ratebypass=yes", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "webm", + "format_note": "DASH audio", + "acodec": "opus", + "abr": 160, + "filesize": 87201, + "tbr": 54.92, + "vcodec": "none", + "format": "251 - audio only (DASH audio)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "ext": "m4a", + "format_note": "DASH audio", + "acodec": "mp4a.40.2", + "abr": 128, + "container": "m4a_dash", + "format_id": "140", + "url": "https://r2---sn-npoe7n7y.googlevideo.com/videoplayback?id=0b40cf772f7c7b87&itag=140&source=youtube&requiressl=yes&ei=JFptWYPCOM2iowO9vJ2gAg&ms=au&mv=m&initcwndbps=4687500&pl=20&mm=31&mn=sn-npoe7n7y&ratebypass=yes&mime=audio/mp4&gir=yes&clen=283612&lmt=1407412007036143&dur=17.600&signature=05D9A38C49198320F23B7D6B73DFDA10ED71E907.6BD5C1C6FE79D1BF88B09250E782CB59C150512F&key=dg_yt0&mt=1500338629&ip=188.166.189.227&ipbits=0&expire=1500360325&sparams=ip,ipbits,expire,id,itag,source,requiressl,ei,ms,mv,initcwndbps,pl,mm,mn,ratebypass,mime,gir,clen,lmt,dur", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/dash/ei/JFptWYPCOM2iowO9vJ2gAg/ms/au/source/youtube/mv/m/initcwndbps/4687500/id/0b40cf772f7c7b87/signature/DF7193EA58FB61FF0031F74EC45D7AFA33D1989C.C1413087AE8A73E28F01389A016D712AF90AE0C1/requiressl/yes/key/yt6/mt/1500338629/ipbits/0/playback_host/r2---sn-npoe7n7y.googlevideo.com/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/hfr/1/sparams/as%2Cei%2Chfr%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Cplayback_host%2Crequiressl%2Csource%2Cexpire/itag/0/pl/20/ip/188.166.189.227/mm/31/mn/sn-npoe7n7y/expire/1500360325", + "width": null, + "height": null, + "tbr": 128.936, + "asr": 44100, + "fps": null, + "language": null, + "filesize": 283612, + "vcodec": "none", + "format": "140 - audio only (DASH audio)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "format_id": "278", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=53464&expire=1500360321&ipbits=0&pl=20&mime=video%2Fwebm&key=yt6&lmt=1449553649956267&dur=16.560&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=A0506DBD49F0B81516D76D9EFFF6818210A2B0FD.E0528EAB38A903BA7711A65BCD07800525F789C2&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Ckeepalive%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&ms=au&itag=278&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&keepalive=yes&initcwndbps=4687500&ratebypass=yes", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "webm", + "height": 144, + "format_note": "144p", + "container": "webm", + "vcodec": "vp9", + "filesize": 53464, + "tbr": 27.6, + "width": 192, + "fps": 13, + "acodec": "none", + "format": "278 - 192x144 (144p)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "format_id": "242", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=134629&expire=1500360321&ipbits=0&pl=20&mime=video%2Fwebm&key=yt6&lmt=1449553650009148&dur=16.560&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=C2829800714165B6A35CB7D5CA748075B4C76AE4.8C8A89B9A1FA070C79C999210B543D962A64AEEC&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Ckeepalive%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&ms=au&itag=242&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&keepalive=yes&initcwndbps=4687500&ratebypass=yes", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "webm", + "height": 240, + "format_note": "240p", + "vcodec": "vp9", + "filesize": 134629, + "tbr": 69.371, + "width": 320, + "fps": 25, + "acodec": "none", + "format": "242 - 320x240 (240p)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "format_id": "243", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=205692&expire=1500360321&ipbits=0&pl=20&mime=video%2Fwebm&key=yt6&lmt=1449553649954993&dur=16.560&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=34D3D7CDEAA3935B438E016E08BCA1682D8656A3.26984685DEF9E3AB8C35D14A268953A438FD6742&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Ckeepalive%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&ms=au&itag=243&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&keepalive=yes&initcwndbps=4687500&ratebypass=yes", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "webm", + "height": 360, + "format_note": "360p", + "vcodec": "vp9", + "filesize": 205692, + "tbr": 106.515, + "width": 480, + "fps": 25, + "acodec": "none", + "format": "243 - 480x360 (360p)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "ext": "mp4", + "height": 144, + "format_note": "DASH video", + "vcodec": "avc1.4d400c", + "format_id": "160", + "url": "https://r2---sn-npoe7n7y.googlevideo.com/videoplayback?id=0b40cf772f7c7b87&itag=160&source=youtube&requiressl=yes&ei=JFptWYPCOM2iowO9vJ2gAg&ms=au&mv=m&initcwndbps=4687500&pl=20&mm=31&mn=sn-npoe7n7y&ratebypass=yes&mime=video/mp4&gir=yes&clen=222037&lmt=1407412023621420&dur=16.521&signature=27DA5FF73F12625AB8CAD91331D10934E8B27F18.8EC4FEA84BBF56EC683FB6EC4CC724222307A3EE&key=dg_yt0&mt=1500338629&ip=188.166.189.227&ipbits=0&expire=1500360325&sparams=ip,ipbits,expire,id,itag,source,requiressl,ei,ms,mv,initcwndbps,pl,mm,mn,ratebypass,mime,gir,clen,lmt,dur", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/dash/ei/JFptWYPCOM2iowO9vJ2gAg/ms/au/source/youtube/mv/m/initcwndbps/4687500/id/0b40cf772f7c7b87/signature/DF7193EA58FB61FF0031F74EC45D7AFA33D1989C.C1413087AE8A73E28F01389A016D712AF90AE0C1/requiressl/yes/key/yt6/mt/1500338629/ipbits/0/playback_host/r2---sn-npoe7n7y.googlevideo.com/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/hfr/1/sparams/as%2Cei%2Chfr%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Cplayback_host%2Crequiressl%2Csource%2Cexpire/itag/0/pl/20/ip/188.166.189.227/mm/31/mn/sn-npoe7n7y/expire/1500360325", + "width": 192, + "tbr": 111.357, + "asr": null, + "fps": 13, + "language": null, + "filesize": 222037, + "acodec": "none", + "format": "160 - 192x144 (DASH video)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "ext": "mp4", + "height": 360, + "format_note": "DASH video", + "vcodec": "avc1.4d4015", + "format_id": "134", + "url": "https://r2---sn-npoe7n7y.googlevideo.com/videoplayback?id=0b40cf772f7c7b87&itag=134&source=youtube&requiressl=yes&ei=JFptWYPCOM2iowO9vJ2gAg&ms=au&mv=m&initcwndbps=4687500&pl=20&mm=31&mn=sn-npoe7n7y&ratebypass=yes&mime=video/mp4&gir=yes&clen=266447&lmt=1407413074718193&dur=16.521&signature=409387A4A163E3A5163C039F48E7D1BC5DA803AE.44196CCC975890F37DDFC52E89FEDD43C97684C4&key=dg_yt0&mt=1500338629&ip=188.166.189.227&ipbits=0&expire=1500360325&sparams=ip,ipbits,expire,id,itag,source,requiressl,ei,ms,mv,initcwndbps,pl,mm,mn,ratebypass,mime,gir,clen,lmt,dur", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/dash/ei/JFptWYPCOM2iowO9vJ2gAg/ms/au/source/youtube/mv/m/initcwndbps/4687500/id/0b40cf772f7c7b87/signature/DF7193EA58FB61FF0031F74EC45D7AFA33D1989C.C1413087AE8A73E28F01389A016D712AF90AE0C1/requiressl/yes/key/yt6/mt/1500338629/ipbits/0/playback_host/r2---sn-npoe7n7y.googlevideo.com/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/hfr/1/sparams/as%2Cei%2Chfr%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Cplayback_host%2Crequiressl%2Csource%2Cexpire/itag/0/pl/20/ip/188.166.189.227/mm/31/mn/sn-npoe7n7y/expire/1500360325", + "width": 480, + "tbr": 132.011, + "asr": null, + "fps": 25, + "language": null, + "filesize": 266447, + "acodec": "none", + "format": "134 - 480x360 (DASH video)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "format_id": "244", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=294311&expire=1500360321&ipbits=0&pl=20&mime=video%2Fwebm&key=yt6&lmt=1449553649983144&dur=16.560&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=538963ED13399B7E16D6B3FDC6978A5627DF1613.C423AFF165DD588745D3C53BDF44F5BF1EF9EC89&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Ckeepalive%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&ms=au&itag=244&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&keepalive=yes&initcwndbps=4687500&ratebypass=yes", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "webm", + "height": 480, + "format_note": "480p", + "vcodec": "vp9", + "filesize": 294311, + "tbr": 153.643, + "width": 640, + "fps": 25, + "acodec": "none", + "format": "244 - 640x480 (480p)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "ext": "mp4", + "height": 480, + "format_note": "DASH video", + "vcodec": "avc1.4d401e", + "format_id": "135", + "url": "https://r2---sn-npoe7n7y.googlevideo.com/videoplayback?id=0b40cf772f7c7b87&itag=135&source=youtube&requiressl=yes&ei=JFptWYPCOM2iowO9vJ2gAg&ms=au&mv=m&initcwndbps=4687500&pl=20&mm=31&mn=sn-npoe7n7y&ratebypass=yes&mime=video/mp4&gir=yes&clen=399465&lmt=1407412695180201&dur=16.521&signature=1A54913CB546677286EAA9990A35B7D12A51491F.29CA2DDFE51A4272D78C6695898688FC4CBA4F7B&key=dg_yt0&mt=1500338629&ip=188.166.189.227&ipbits=0&expire=1500360325&sparams=ip,ipbits,expire,id,itag,source,requiressl,ei,ms,mv,initcwndbps,pl,mm,mn,ratebypass,mime,gir,clen,lmt,dur", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/dash/ei/JFptWYPCOM2iowO9vJ2gAg/ms/au/source/youtube/mv/m/initcwndbps/4687500/id/0b40cf772f7c7b87/signature/DF7193EA58FB61FF0031F74EC45D7AFA33D1989C.C1413087AE8A73E28F01389A016D712AF90AE0C1/requiressl/yes/key/yt6/mt/1500338629/ipbits/0/playback_host/r2---sn-npoe7n7y.googlevideo.com/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/hfr/1/sparams/as%2Cei%2Chfr%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Cplayback_host%2Crequiressl%2Csource%2Cexpire/itag/0/pl/20/ip/188.166.189.227/mm/31/mn/sn-npoe7n7y/expire/1500360325", + "width": 640, + "tbr": 198.973, + "asr": null, + "fps": 25, + "language": null, + "filesize": 399465, + "acodec": "none", + "format": "135 - 640x480 (DASH video)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "ext": "mp4", + "height": 240, + "format_note": "DASH video", + "vcodec": "avc1.4d400d", + "format_id": "133", + "url": "https://r2---sn-npoe7n7y.googlevideo.com/videoplayback?id=0b40cf772f7c7b87&itag=133&source=youtube&requiressl=yes&ei=JFptWYPCOM2iowO9vJ2gAg&ms=au&mv=m&initcwndbps=4687500&pl=20&mm=31&mn=sn-npoe7n7y&ratebypass=yes&mime=video/mp4&gir=yes&clen=503705&lmt=1407412011429135&dur=16.521&signature=028FF5A4C975CBA782D38ED7796BDF56618597C1.538CF5A7DC9A2CAF15C383457179A6D2F0E404AD&key=dg_yt0&mt=1500338629&ip=188.166.189.227&ipbits=0&expire=1500360325&sparams=ip,ipbits,expire,id,itag,source,requiressl,ei,ms,mv,initcwndbps,pl,mm,mn,ratebypass,mime,gir,clen,lmt,dur", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/dash/ei/JFptWYPCOM2iowO9vJ2gAg/ms/au/source/youtube/mv/m/initcwndbps/4687500/id/0b40cf772f7c7b87/signature/DF7193EA58FB61FF0031F74EC45D7AFA33D1989C.C1413087AE8A73E28F01389A016D712AF90AE0C1/requiressl/yes/key/yt6/mt/1500338629/ipbits/0/playback_host/r2---sn-npoe7n7y.googlevideo.com/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/hfr/1/sparams/as%2Cei%2Chfr%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Cplayback_host%2Crequiressl%2Csource%2Cexpire/itag/0/pl/20/ip/188.166.189.227/mm/31/mn/sn-npoe7n7y/expire/1500360325", + "width": 320, + "tbr": 247.817, + "asr": null, + "fps": 25, + "language": null, + "filesize": 503705, + "acodec": "none", + "format": "133 - 320x240 (DASH video)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "format_id": "17", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=187750&expire=1500360321&ipbits=0&pl=20&mime=video%2F3gpp&key=yt6&lmt=1386391067396178&dur=17.600&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=DA04DD6BF11E0862D1F5F62937616A16010FBD90.3B56D0D71664903B9C34D84B74F3A06559E68D0E&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&ms=au&itag=17&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&initcwndbps=4687500&ratebypass=yes", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "3gp", + "width": 176, + "height": 144, + "acodec": "mp4a.40.2", + "abr": 24, + "vcodec": "mp4v.20.3", + "resolution": "176x144", + "format_note": "small", + "format": "17 - 176x144 (small)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "format_id": "36", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=450783&expire=1500360321&ipbits=0&pl=20&mime=video%2F3gpp&key=yt6&lmt=1427259060409196&dur=17.600&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=6731D2BA8D13248320B7FD685FF5BBDBE1ABE2FF.9887F983CB663F3321BDC4679E5E1D81DC945AA4&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&ms=au&itag=36&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&initcwndbps=4687500&ratebypass=yes", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "3gp", + "width": 320, + "acodec": "mp4a.40.2", + "vcodec": "mp4v.20.3", + "resolution": "320x240", + "height": 240, + "format_note": "small", + "format": "36 - 320x240 (small)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "format_id": "18", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=552999&expire=1500360321&ipbits=0&pl=20&mime=video%2Fmp4&key=yt6&lmt=1407413068816851&dur=17.600&ratebypass=yes&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=A82A64F390C00EE813FEE22DA63178864B568423.8173FE1A80DDC357FADBB1C1C9E9CF515DDF8DFC&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Cratebypass%2Crequiressl%2Csource%2Cexpire&ms=au&itag=18&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&initcwndbps=4687500", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "mp4", + "width": 480, + "height": 360, + "acodec": "mp4a.40.2", + "abr": 96, + "vcodec": "avc1.42001E", + "resolution": "480x360", + "format_note": "medium", + "format": "18 - 480x360 (medium)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + }, + { + "format_id": "43", + "url": "https://r2---sn-npoeene7.googlevideo.com/videoplayback?clen=287596&expire=1500360321&ipbits=0&pl=20&mime=video%2Fwebm&key=yt6&lmt=1298445916327233&dur=0.000&ratebypass=yes&source=youtube&id=o-AN7Ly6egFnKgINhasKpm9BakU4BNG5d-yQfMMgL5ar2f&signature=03B5A4F15BF00BBD03834800DB8333081BA95117.345D5B56C5420E1C12C272B332A4CF3F25A665BD&gir=yes&requiressl=yes&ip=188.166.189.227&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Cratebypass%2Crequiressl%2Csource%2Cexpire&ms=au&itag=43&ei=IVptWfnHAoXKowPt8rOwAQ&mv=m&mt=1500338629&mn=sn-npoeene7&mm=31&initcwndbps=4687500", + "player_url": "/yts/jsbin/player-vflL_WLGI/en_US/base.js", + "ext": "webm", + "width": 640, + "height": 360, + "acodec": "vorbis", + "abr": 128, + "vcodec": "vp8.0", + "resolution": "640x360", + "format_note": "medium", + "format": "43 - 640x360 (medium)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } + } + ], + "is_live": null, + "start_time": null, + "end_time": null, + "series": null, + "season_number": null, + "episode_number": null, + "extractor": "youtube", + "webpage_url_basename": "watch", + "extractor_key": "Youtube", + "playlist": null, + "playlist_index": null, + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/C0DPdy98e4c/hqdefault.jpg", + "id": "0" + } + ], + "display_id": "C0DPdy98e4c", + "requested_subtitles": null, + "ext": "m4a", + "format_note": "DASH audio", + "acodec": "mp4a.40.2", + "abr": 128, + "container": "m4a_dash", + "format_id": "140", + "url": "https://r2---sn-npoe7n7y.googlevideo.com/videoplayback?id=0b40cf772f7c7b87&itag=140&source=youtube&requiressl=yes&ei=JFptWYPCOM2iowO9vJ2gAg&ms=au&mv=m&initcwndbps=4687500&pl=20&mm=31&mn=sn-npoe7n7y&ratebypass=yes&mime=audio/mp4&gir=yes&clen=283612&lmt=1407412007036143&dur=17.600&signature=05D9A38C49198320F23B7D6B73DFDA10ED71E907.6BD5C1C6FE79D1BF88B09250E782CB59C150512F&key=dg_yt0&mt=1500338629&ip=188.166.189.227&ipbits=0&expire=1500360325&sparams=ip,ipbits,expire,id,itag,source,requiressl,ei,ms,mv,initcwndbps,pl,mm,mn,ratebypass,mime,gir,clen,lmt,dur", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/dash/ei/JFptWYPCOM2iowO9vJ2gAg/ms/au/source/youtube/mv/m/initcwndbps/4687500/id/0b40cf772f7c7b87/signature/DF7193EA58FB61FF0031F74EC45D7AFA33D1989C.C1413087AE8A73E28F01389A016D712AF90AE0C1/requiressl/yes/key/yt6/mt/1500338629/ipbits/0/playback_host/r2---sn-npoe7n7y.googlevideo.com/as/fmp4_audio_clear%2Cfmp4_sd_hd_clear/hfr/1/sparams/as%2Cei%2Chfr%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Cplayback_host%2Crequiressl%2Csource%2Cexpire/itag/0/pl/20/ip/188.166.189.227/mm/31/mn/sn-npoe7n7y/expire/1500360325", + "width": null, + "height": null, + "tbr": 128.936, + "asr": 44100, + "fps": null, + "language": null, + "filesize": 283612, + "vcodec": "none", + "format": "140 - audio only (DASH audio)", + "protocol": "https", + "http_headers": { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-us,en;q=0.5" + } +} + diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index f29e13a..d7008a5 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -23,7 +23,6 @@ class Core(object): exerpt_keys = ['tid', 'state', 'percent', 'total_bytes', 'title', 'eta', 'speed'] - valid_opts = ['proxy', 'format'] def __init__(self, args=None): self.logger = logging.getLogger('ydl_webui') diff --git a/youtube_dl_webui/worker.py b/youtube_dl_webui/worker.py index 32e93cb..cb2a6c6 100644 --- a/youtube_dl_webui/worker.py +++ b/youtube_dl_webui/worker.py @@ -3,6 +3,7 @@ import re import logging +import json from youtube_dl import YoutubeDL from youtube_dl import DownloadError @@ -150,6 +151,8 @@ def run(self): if self.first_run: info_dict = ydl.extract_info(self.url, download=False) + # self.logger.debug(json.dumps(info_dict, indent=4)) + info_dict['description'] = info_dict['description'].replace('\n', '
'); wqd = deepcopy(WQ_DICT) From 4290826ab2ce11eec2dce2aaf94ac74353111fdf Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 20 Jul 2017 09:08:37 +0800 Subject: [PATCH 06/88] add interface to del files --- youtube_dl_webui/core.py | 6 +++--- youtube_dl_webui/db.py | 14 +++++++++++++- youtube_dl_webui/server.py | 2 ++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index d7008a5..04a855a 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -106,7 +106,7 @@ def pause_task(self, tid): self.cancel_worker(tid) - def delete_task(self, tid): + def delete_task(self, tid, del_data=False): try: self.cancel_worker(tid) except TaskInexistenceError as e: @@ -114,7 +114,7 @@ def delete_task(self, tid): except: pass - self.db.delete_task(tid) + self.db.delete_task(tid, del_data=del_data) def launch_worker(self, tid, log_list, param=None, ydl_opts={}, first_run=False): @@ -244,7 +244,7 @@ def server_request(self, data): if data['command'] == 'delete': try: - self.delete_task(data['tid']) + self.delete_task(data['tid'], del_data=data['del_data']) except TaskInexistenceError: return msg_task_inexistence_error diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index 103e658..5134705 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -156,12 +156,22 @@ def start_task(self, tid, ignore_state=False): return json.loads(row['log']) - def delete_task(self, tid): + def delete_task(self, tid, del_data=False): self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) row = self.db.fetchone() if row is None: raise TaskInexistenceError('') + if del_data: + if row['tmpfilename'] != '': + self.logger.debug('Delete tmp file: %s' %(row['tmpfilename'])) + os.remove(row['tmpfilename']) + elif row['filename'] != '': + self.logger.debug('Delete file: %s' %(row['filename'])) + os.remove(row['filename']) + else: + self.logger.debug('No file to delete') + self.db.execute('DELETE FROM task_status WHERE tid=(?)', (tid, )) self.db.execute('DELETE FROM task_info WHERE tid=(?)', (tid, )) self.db.execute('DELETE FROM task_param WHERE tid=(?)', (tid, )) @@ -255,6 +265,8 @@ def progress_update(self, tid, d): else: d['total_bytes'] = '0' + self.logger.debug("update filename=%s, tmpfilename=%s" %(d['filename'], d['tmpfilename'])) + self.db.execute("UPDATE task_status SET " "percent=:percent, filename=:filename, " "tmpfilename=:tmpfilename, downloaded_bytes=:downloaded_bytes, " diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index 453f4ca..103d916 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -66,6 +66,8 @@ def delete_task(tid): wqd['command'] = 'delete' wqd['tid'] = tid + wqd['del_data'] = not not request.args.get('del_data', False) + WQ.put(wqd) return json.dumps(RQ.get()) From 3f2e325a472c88cabc487e5e4bff7c99bbab3962 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 15 Aug 2017 10:31:41 +0800 Subject: [PATCH 07/88] add test page --- youtube_dl_webui/server.py | 7 +++++ youtube_dl_webui/static/css/test.css | 9 +++++++ .../static/js/jquery-3.2.1.slim.min.js | 4 +++ youtube_dl_webui/templates/test/index.html | 27 +++++++++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 youtube_dl_webui/static/css/test.css create mode 100644 youtube_dl_webui/static/js/jquery-3.2.1.slim.min.js create mode 100644 youtube_dl_webui/templates/test/index.html diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index 103d916..1f494e5 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -108,6 +108,13 @@ def query_task(tid): return json.dumps(RQ.get()) +### +# test cases +### +@app.route('/test/') +def test(case): + return render_template('test/{}.html'.format(case)) + class Server(Process): def __init__(self, rqueue, wqueue, host, port): diff --git a/youtube_dl_webui/static/css/test.css b/youtube_dl_webui/static/css/test.css new file mode 100644 index 0000000..aba8e62 --- /dev/null +++ b/youtube_dl_webui/static/css/test.css @@ -0,0 +1,9 @@ +body { + font-family: "Courier New", Courier, monospace; +} + +.row { + margin-bottom: 5px; + height: 25px; + line-height: 25px; +} diff --git a/youtube_dl_webui/static/js/jquery-3.2.1.slim.min.js b/youtube_dl_webui/static/js/jquery-3.2.1.slim.min.js new file mode 100644 index 0000000..105d00e --- /dev/null +++ b/youtube_dl_webui/static/js/jquery-3.2.1.slim.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a); +}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S),a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}}),r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var _a,ab=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return T(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?_a:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),_a={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=ab[b]||r.find.attr;ab[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=ab[g],ab[g]=e,e=null!=c(a,b,d)?g:null,ab[g]=f),e}});var bb=/^(?:input|select|textarea|button)$/i,cb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function db(a){var b=a.match(L)||[];return b.join(" ")}function eb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,eb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=eb(c),d=1===c.nodeType&&" "+db(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=db(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,eb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=eb(c),d=1===c.nodeType&&" "+db(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=db(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,eb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=eb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+db(eb(c))+" ").indexOf(b)>-1)return!0;return!1}});var fb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(fb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:db(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var gb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!gb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,gb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var hb=/\[\]$/,ib=/\r?\n/g,jb=/^(?:submit|button|image|reset|file)$/i,kb=/^(?:input|select|textarea|keygen)/i;function lb(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||hb.test(a)?d(a,e):lb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d); +});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)lb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)lb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&kb.test(this.nodeName)&&!jb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ib,"\r\n")}}):{name:b.name,value:c.replace(ib,"\r\n")}}).get()}}),r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="
",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=C.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=qa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),b=f.ownerDocument,c=b.documentElement,e=b.defaultView,{top:d.top+e.pageYOffset-c.clientTop,left:d.left+e.pageXOffset-c.clientLeft}):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),B(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||ra})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return T(this,function(a,d,e){var f;return r.isWindow(a)?f=a:9===a.nodeType&&(f=a.defaultView),void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Pa(o.pixelPosition,function(a,c){if(c)return c=Oa(a,b),Ma.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return T(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.holdReady=function(a){a?r.readyWait++:r.ready(!0)},r.isArray=Array.isArray,r.parseJSON=JSON.parse,r.nodeName=B,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var mb=a.jQuery,nb=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=nb),b&&a.jQuery===r&&(a.jQuery=mb),r},b||(a.jQuery=a.$=r),r}); diff --git a/youtube_dl_webui/templates/test/index.html b/youtube_dl_webui/templates/test/index.html new file mode 100644 index 0000000..e3712e8 --- /dev/null +++ b/youtube_dl_webui/templates/test/index.html @@ -0,0 +1,27 @@ + + + + Test cases + + + + + +
+ Task control +
+ +
+
+
+
+
+ +
+
+ Delete File +
+
+ + + From d884cb65726037e7842f57e34b681f3d4b1fb15c Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 15 Aug 2017 11:19:24 +0800 Subject: [PATCH 08/88] update test html --- .../static/js/jquery-3.2.1.min.js | 4 ++ .../static/js/jquery-3.2.1.slim.min.js | 4 -- youtube_dl_webui/templates/test/index.html | 46 +++++++++++++++++-- 3 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 youtube_dl_webui/static/js/jquery-3.2.1.min.js delete mode 100644 youtube_dl_webui/static/js/jquery-3.2.1.slim.min.js diff --git a/youtube_dl_webui/static/js/jquery-3.2.1.min.js b/youtube_dl_webui/static/js/jquery-3.2.1.min.js new file mode 100644 index 0000000..644d35e --- /dev/null +++ b/youtube_dl_webui/static/js/jquery-3.2.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), +a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" +
+
Task control
- +
-
+
- +
Delete File
+ From 19a0167e1ac85e7a44a9310f835d482b4b973c8f Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 16 Aug 2017 08:20:30 +0800 Subject: [PATCH 09/88] update test page --- youtube_dl_webui/templates/test/index.html | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/youtube_dl_webui/templates/test/index.html b/youtube_dl_webui/templates/test/index.html index 8a49a39..a940378 100644 --- a/youtube_dl_webui/templates/test/index.html +++ b/youtube_dl_webui/templates/test/index.html @@ -20,13 +20,13 @@
- Delete File + Delete File
From 83a4307b02d2b3560537f90fc76d43023d7128f5 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 16 Aug 2017 09:07:44 +0800 Subject: [PATCH 10/88] update test page --- youtube_dl_webui/static/css/test.css | 19 +++++++++ youtube_dl_webui/templates/test/index.html | 46 ++++++++++++++++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/youtube_dl_webui/static/css/test.css b/youtube_dl_webui/static/css/test.css index aba8e62..f7f23da 100644 --- a/youtube_dl_webui/static/css/test.css +++ b/youtube_dl_webui/static/css/test.css @@ -7,3 +7,22 @@ body { height: 25px; line-height: 25px; } + +.left { + float: left; + display: block; + min-width: 60px; + width: 160px; + max-width: 200px; + text-align: right; + margin-right: 10px; +} + +.right { + float: left; + margin-right: 10px; +} + +input[type=text] { + width: 350px; +} diff --git a/youtube_dl_webui/templates/test/index.html b/youtube_dl_webui/templates/test/index.html index a940378..29e72df 100644 --- a/youtube_dl_webui/templates/test/index.html +++ b/youtube_dl_webui/templates/test/index.html @@ -11,18 +11,58 @@
Task control
- + + +
-
+ +
+
- Delete File + Del: + + Delete File
+
+
+ Settings +
+ -- General -- +
+
+ + +
+
+ + +
+
+ + +
+
+
+ -- youtubedl -- +
+
+ + +
+
+ + +
+
+ +
+
From fc7eb347008a55d039bcbc171341d160d479cd74 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Fri, 18 Aug 2017 08:17:29 +0800 Subject: [PATCH 18/88] add Flask codes for configuration page --- youtube_dl_webui/server.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index 1f494e5..5ee3a4f 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -108,6 +108,20 @@ def query_task(tid): return json.dumps(RQ.get()) +@app.route('/config', methods=['GET', 'POST']) +def get_config(): + wqd = deepcopy(WQ_DICT) + wqd['command'] = 'config' + + if request.method == 'POST': + wqd['act'] = 'update' + else: + wqd['act'] = 'get' + + WQ.put(wqd) + return json.dumps(RQ.get()) + + ### # test cases ### From 0424eccd90ccb76c6af73a5d638a20bb4ce2a92f Mon Sep 17 00:00:00 2001 From: d0u9 Date: Fri, 18 Aug 2017 08:21:35 +0800 Subject: [PATCH 19/88] update test html to support post configuraton --- youtube_dl_webui/templates/test/index.html | 136 +++++++++++++-------- 1 file changed, 88 insertions(+), 48 deletions(-) diff --git a/youtube_dl_webui/templates/test/index.html b/youtube_dl_webui/templates/test/index.html index 5c40967..6b83b85 100644 --- a/youtube_dl_webui/templates/test/index.html +++ b/youtube_dl_webui/templates/test/index.html @@ -77,65 +77,105 @@ From 37143c3669bceede176b88e7f551ef3348fa8375 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Fri, 18 Aug 2017 08:45:05 +0800 Subject: [PATCH 20/88] make request content type to json --- youtube_dl_webui/templates/test/index.html | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/youtube_dl_webui/templates/test/index.html b/youtube_dl_webui/templates/test/index.html index 6b83b85..3574617 100644 --- a/youtube_dl_webui/templates/test/index.html +++ b/youtube_dl_webui/templates/test/index.html @@ -141,14 +141,15 @@ console.debug(data); - $.post(host + '/config', - JSON.stringify(data), - function(data, status) { - console.log(data); - if (data['status'] == 'success') { - $("#tid").val(data['tid']); - } - }, 'json'); + $.ajax({url: host + '/config', + type: 'POST', + data: JSON.stringify(data), + contentType: 'application/json', + success: function(result) { + console.log(result); + }, + dataType: 'json' + }); }); $(document).ready(function(){ From 8a93f28142199e1abbd4e146ada272cd4327403a Mon Sep 17 00:00:00 2001 From: d0u9 Date: Fri, 18 Aug 2017 09:38:40 +0800 Subject: [PATCH 21/88] update server.pu --- youtube_dl_webui/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index 5ee3a4f..288732b 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -115,6 +115,7 @@ def get_config(): if request.method == 'POST': wqd['act'] = 'update' + wqd['param'] = request.get_json() else: wqd['act'] = 'get' From eae9e32f4f426c6c4845adeeeb441396d65f4c52 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Mon, 21 Aug 2017 09:16:36 +0800 Subject: [PATCH 22/88] Change the ydl_opts merging stage when task creating. To keep the task's ydl_opts consistent even after alerting the global ydl_opts, we change the chance to merge task's ydl_opts with global ydl_opts to the time when task is created. In this way, the task's ydl_opts is a snapshot of the current task is creating instead of varing from time ot time. --- youtube_dl_webui/core.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index b504c0e..83f640d 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -85,9 +85,14 @@ def create_task(self, param, ydl_opts): if param['url'].strip() == '': raise KeyError - valid_ydl_opts = {k: ydl_opts[k] for k in ydl_opts if k in self.conf['ydl']} + opts = {} + for key, val in self.conf['ydl'].items(): + if key in ydl_opts: + opts[key] = ydl_opts[key] + else: + opts[key] = self.conf['ydl'][key] - tid = self.db.create_task(param, ydl_opts) + tid = self.db.create_task(param, opts) return tid @@ -129,12 +134,10 @@ def launch_worker(self, tid, log_list, param=None, ydl_opts={}, first_run=False) self.worker[tid]['log'].appendleft({'time': int(time()), 'type': 'debug', 'msg': 'Task starts...'}) self.db.update_log(tid, self.worker[tid]['log']) - # Merge global ydl_opts with local opts - opts = {k: ydl_opts[k] if k in ydl_opts else self.conf['ydl'][k] for k in self.conf['ydl']} - self.logger.debug("ydl_opts(%s): %s" %(tid, json.dumps(opts))) + self.logger.debug("ydl_opts(%s): %s" %(tid, json.dumps(ydl_opts))) # launch worker process - w = Worker(tid, self.rq, param=param, ydl_opts=opts, first_run=first_run) + w = Worker(tid, self.rq, param=param, ydl_opts=ydl_opts, first_run=first_run) w.start() self.worker[tid]['obj'] = w From 14465d7367c6a109faad04a6102b2589868aa6cc Mon Sep 17 00:00:00 2001 From: sky Date: Mon, 21 Aug 2017 17:02:54 +0800 Subject: [PATCH 23/88] delete task --- youtube_dl_webui/static/js/global.js | 22 +++++++++++++++++++++- youtube_dl_webui/templates/index.html | 12 +++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/youtube_dl_webui/static/js/global.js b/youtube_dl_webui/static/js/global.js index 63b7ba6..40e7a21 100644 --- a/youtube_dl_webui/static/js/global.js +++ b/youtube_dl_webui/static/js/global.js @@ -7,11 +7,12 @@ var videoDownload = (function (Vue, extendAM){ videoList: [], videoListCopy: [], showModal: false, + modalType: 'addTask', // tablist: ['status', 'details', 'file24s', 'peers', 'options'], tablist: ['Status', 'Details', 'Log'], showTab: 'Status', stateCounter: { all: 0, downloading: 0, finished: 0, paused: 0, invalid: 0}, - modalData: { url: '' }, + modalData: { url: '' , removeFile: false }, currentSelected: null, taskDetails: {}, taskInfoUrl: null, @@ -47,10 +48,16 @@ var videoDownload = (function (Vue, extendAM){ showAddTaskModal: function(){ this.modalData.url = ''; this.showModal = true; + this.modalType = 'addTask'; this.$nextTick(function(){ this.$refs.url.focus(); }); }, + showRemoveTaskModal: function(){ + this.modalData.removeFile = false; + this.showModal = true; + this.modalType = 'removeTask'; + }, addTask: function(){ var _self = this; var url = _self.headPath + 'task'; @@ -60,9 +67,22 @@ var videoDownload = (function (Vue, extendAM){ _self.showAlertToast(err, 'error'); }); }, + modalConfirmHandler: function(){ + switch(modalType){ + case 'addTask': + this.addTask(); + break; + case 'deleteTask': + this.removeTask(); + break; + } + } removeTask: function(){ var _self = this; var url = _self.headPath + 'task/tid/' + (_self.videoList[_self.currentSelected] && _self.videoList[_self.currentSelected].tid); + if(_self.modalData.removeFile){ + url += '?del_data=true'; + } Vue.http.delete(url).then(function(res){ _self.showAlertToast('Task Delete', 'info'); _self.videoList.splice(_self.currentSelected, _self.currentSelected+1); diff --git a/youtube_dl_webui/templates/index.html b/youtube_dl_webui/templates/index.html index cd61d91..171492f 100644 --- a/youtube_dl_webui/templates/index.html +++ b/youtube_dl_webui/templates/index.html @@ -45,7 +45,7 @@
- +
@@ -109,8 +109,14 @@ -
URL to download
-
+ +
From 7437cf6604df74fd6e23bab821ddde366642a4d2 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 22 Aug 2017 17:14:02 +0800 Subject: [PATCH 24/88] prepare to separate config to a standalone class --- youtube_dl_webui/core.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 83f640d..4bd6124 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -227,6 +227,10 @@ def cmdl_override_conf_file(self): self.conf['server']['port'] = self.cmdl_args_dict['port'] + def update_config(self, config): + print(config) + + def server_request(self, data): msg_internal_error = {'status': 'error', 'errmsg': 'Internal Error'} msg_task_existence_error = {'status': 'error', 'errmsg': 'URL is already added'} @@ -308,6 +312,7 @@ def server_request(self, data): if data['act'] == 'get': return {'status': 'success', 'config': self.conf} elif data['act'] == 'update': + self.update_config(data['param']) return {'status': 'success'} else: return {'status': 'error', 'errmsg': 'invalid query'} From 2c5346d0876f4fa37491e385e709f295bba760a6 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 30 Aug 2017 12:12:30 +0800 Subject: [PATCH 25/88] add config.py for configuration options --- youtube_dl_webui/config.py | 101 +++++++++++++++++++++++++++++++++++++ youtube_dl_webui/core.py | 6 +++ 2 files changed, 107 insertions(+) create mode 100644 youtube_dl_webui/config.py diff --git a/youtube_dl_webui/config.py b/youtube_dl_webui/config.py new file mode 100644 index 0000000..688044b --- /dev/null +++ b/youtube_dl_webui/config.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import logging + +from os.path import expanduser + +class conf_base(object): + _valid_fields = [ + # (key, default_val, type, validate_regex, call_function) + ] + + _conf = {} + + def __init__(self, conf_json): + self.load(conf_json) + + def load(self, conf_json): + for field in self._valid_fields: + key = field[0] + dft_val = field[1] + val_type = field[2] + vld_regx = field[3] + func = field[4] + + # More check can be made here + if key in conf_json: + self._conf[key] = conf_json[key] if func is None else func(conf_json[key]) + elif dft_val is not None: + self._conf[key] = dft_val if func is None else func(conf_json[key]) + + def get_val(self, key): + return self._conf[key] + + +class ydl_conf(conf_base): + _valid_fields = [ + ('proxy', None, 'string', None, None), + ('format', None, 'string', None, None), + ] + + def __init__(self, conf_json={}): + self.logger = logging.getLogger('ydl_webui') + super(ydl_conf, self).__init__(conf_json) + + +class svr_conf(conf_base): + _valid_fields = [ + ('host', '127.0.0.1', 'string', None, None), + ('port', '5000', 'string', None, None), + ] + + def __init__(self, conf_json={}): + self.logger = logging.getLogger('ydl_webui') + super(ydl_conf, self).__init__(conf_json) + + +class gen_conf(conf_base): + _valid_fields = [ + #(key, default_val, type, validate_regex, call_function) + ('download_dir', '~/Downloads/youtube-dl', 'string', '', expanduser), + ('db_path', '~/.conf/ydl_webui.db', 'string', '', expanduser), + ('task_log_size', 10, 'int', '', None), + ] + + def __init__(self, conf_json={}): + self.logger = logging.getLogger('ydl_webui') + super(ydl_conf, self).__init__(conf_json) + + +class conf(object): + _valid_fields = set(('ydl', 'svr', 'gen')) + + ydl_conf = None + svr_conf = None + gen_conf = None + + def __init__(self, conf={}): + self.logger = logging.getLogger('ydl_webui') + self.load(conf) + + def load(self, conf): + if not isinstance(conf, dict): + self.logger.debug("input parameter(conf) is not an instance of dict") + return + + + + + + + + + + + + + + + + diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 4bd6124..73e4931 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -20,6 +20,7 @@ from .server import Server from .worker import Worker +from .config import ydl_conf, conf class Core(object): exerpt_keys = ['tid', 'state', 'percent', 'total_bytes', 'title', 'eta', 'speed'] @@ -27,6 +28,11 @@ class Core(object): def __init__(self, args=None): self.logger = logging.getLogger('ydl_webui') + y = ydl_conf({'proxy':123, 'ff': 33}) + c = conf([]) + + # exit(1) + # options from command line self.cmdl_args_dict = {} # options read from configuration file From cc9b4fabebc0dd3bb50e29d5bea6572ea7782268 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 30 Aug 2017 13:09:21 +0800 Subject: [PATCH 26/88] update config.py --- youtube_dl_webui/config.py | 69 ++++++++++++++++++++++++++------------ youtube_dl_webui/core.py | 5 ++- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/youtube_dl_webui/config.py b/youtube_dl_webui/config.py index 688044b..63ee25f 100644 --- a/youtube_dl_webui/config.py +++ b/youtube_dl_webui/config.py @@ -6,16 +6,14 @@ from os.path import expanduser class conf_base(object): - _valid_fields = [ - # (key, default_val, type, validate_regex, call_function) - ] - - _conf = {} - - def __init__(self, conf_json): - self.load(conf_json) - - def load(self, conf_json): + def __init__(self, valid_fields, conf_dict): + # each item in the _valid_fields is a tuple represents + # (key, default_val, type, validate_regex, call_function) + self._valid_fields = valid_fields + self._conf = {} + self.load(conf_dict) + + def load(self, conf_dict): for field in self._valid_fields: key = field[0] dft_val = field[1] @@ -24,35 +22,43 @@ def load(self, conf_json): func = field[4] # More check can be made here - if key in conf_json: - self._conf[key] = conf_json[key] if func is None else func(conf_json[key]) + if key in conf_dict: + self._conf[key] = conf_dict[key] if func is None else func(conf_json.get(key, dft_val)) elif dft_val is not None: - self._conf[key] = dft_val if func is None else func(conf_json[key]) + self._conf[key] = dft_val if func is None else func(conf_dict.get(key, dft_val)) + def get_val(self, key): return self._conf[key] + def dict(self): + return self._conf + class ydl_conf(conf_base): _valid_fields = [ + #(key, default_val, type, validate_regex, call_function) ('proxy', None, 'string', None, None), ('format', None, 'string', None, None), ] - def __init__(self, conf_json={}): + def __init__(self, conf_dict={}): self.logger = logging.getLogger('ydl_webui') - super(ydl_conf, self).__init__(conf_json) + + super(ydl_conf, self).__init__(self._valid_fields, conf_dict) class svr_conf(conf_base): _valid_fields = [ + #(key, default_val, type, validate_regex, call_function) ('host', '127.0.0.1', 'string', None, None), ('port', '5000', 'string', None, None), ] - def __init__(self, conf_json={}): + def __init__(self, conf_dict={}): self.logger = logging.getLogger('ydl_webui') - super(ydl_conf, self).__init__(conf_json) + + super(svr_conf, self).__init__(self._valid_fields, conf_dict) class gen_conf(conf_base): @@ -63,13 +69,14 @@ class gen_conf(conf_base): ('task_log_size', 10, 'int', '', None), ] - def __init__(self, conf_json={}): + def __init__(self, conf_dict={}): self.logger = logging.getLogger('ydl_webui') - super(ydl_conf, self).__init__(conf_json) + + super(gen_conf, self).__init__(self._valid_fields, conf_dict) class conf(object): - _valid_fields = set(('ydl', 'svr', 'gen')) + _valid_fields = set(('youtube_dl', 'server', 'general')) ydl_conf = None svr_conf = None @@ -81,9 +88,29 @@ def __init__(self, conf={}): def load(self, conf): if not isinstance(conf, dict): - self.logger.debug("input parameter(conf) is not an instance of dict") + self.logger.error("input parameter(conf) is not an instance of dict") return + for f in self._valid_fields: + if f == 'youtube_dl': + self.ydl_conf = ydl_conf(conf.get(f, {})) + elif f == 'server': + self.svr_conf = svr_conf(conf.get(f, {})) + elif f == 'general': + self.gen_conf = gen_conf(conf.get(f, {})) + + def dict(self): + d = {} + for f in self._valid_fields: + if f == 'youtube_dl': + d[f] = self.ydl_conf.dict() + elif f == 'server': + d[f] = self.svr_conf.dict() + elif f == 'general': + d[f] = self.gen_conf.dict() + + return d + diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 73e4931..9e21ea2 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -28,10 +28,9 @@ class Core(object): def __init__(self, args=None): self.logger = logging.getLogger('ydl_webui') - y = ydl_conf({'proxy':123, 'ff': 33}) - c = conf([]) + c = conf({'proxy':123, 'ff': 33}) - # exit(1) + #exit(1) # options from command line self.cmdl_args_dict = {} From bd5e5c84d9fefd4c6cd5e3c10042deca44c9bee5 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 30 Aug 2017 14:15:19 +0800 Subject: [PATCH 27/88] separate configuration codes --- youtube_dl_webui/__init__.py | 4 +- youtube_dl_webui/config.py | 34 ++++++++---- youtube_dl_webui/core.py | 102 ++++++++--------------------------- 3 files changed, 50 insertions(+), 90 deletions(-) diff --git a/youtube_dl_webui/__init__.py b/youtube_dl_webui/__init__.py index 0c3a0ac..572cb12 100644 --- a/youtube_dl_webui/__init__.py +++ b/youtube_dl_webui/__init__.py @@ -9,7 +9,7 @@ def getopt(argv): parser = ArgumentParser(description='Another webui for youtube-dl') - parser.add_argument('-c', '--config', metavar="CONFIG_FILE", help="config file", default="/etc/youtube-dl-webui.conf") + parser.add_argument('-c', '--config', metavar="CONFIG_FILE", help="config file") parser.add_argument('--host', metavar="ADDR", help="the address server listens on") parser.add_argument('--port', metavar="PORT", help="the port server listens on") @@ -23,5 +23,5 @@ def main(argv=None): print("-----------------------------------") cmd_args = getopt(argv) - core = Core(args=cmd_args) + core = Core(cmd_args=cmd_args) core.run() diff --git a/youtube_dl_webui/config.py b/youtube_dl_webui/config.py index 63ee25f..9923c39 100644 --- a/youtube_dl_webui/config.py +++ b/youtube_dl_webui/config.py @@ -23,7 +23,7 @@ def load(self, conf_dict): # More check can be made here if key in conf_dict: - self._conf[key] = conf_dict[key] if func is None else func(conf_json.get(key, dft_val)) + self._conf[key] = conf_dict[key] if func is None else func(conf_dict.get(key, dft_val)) elif dft_val is not None: self._conf[key] = dft_val if func is None else func(conf_dict.get(key, dft_val)) @@ -31,6 +31,9 @@ def load(self, conf_dict): def get_val(self, key): return self._conf[key] + def __getitem__(self, key): + return self.get_val(key) + def dict(self): return self._conf @@ -82,22 +85,22 @@ class conf(object): svr_conf = None gen_conf = None - def __init__(self, conf={}): + def __init__(self, conf_dict={}, cmd_args={}): self.logger = logging.getLogger('ydl_webui') - self.load(conf) + self.load(conf_dict) - def load(self, conf): - if not isinstance(conf, dict): - self.logger.error("input parameter(conf) is not an instance of dict") + def load(self, conf_dict): + if not isinstance(conf_dict, dict): + self.logger.error("input parameter(conf_dict) is not an instance of dict") return for f in self._valid_fields: if f == 'youtube_dl': - self.ydl_conf = ydl_conf(conf.get(f, {})) + self.ydl_conf = ydl_conf(conf_dict.get(f, {})) elif f == 'server': - self.svr_conf = svr_conf(conf.get(f, {})) + self.svr_conf = svr_conf(conf_dict.get(f, {})) elif f == 'general': - self.gen_conf = gen_conf(conf.get(f, {})) + self.gen_conf = gen_conf(conf_dict.get(f, {})) def dict(self): d = {} @@ -111,6 +114,19 @@ def dict(self): return d + def __getitem__(self, key): + if key not in self._valid_fields: + raise KeyError(key) + + if key == 'youtube_dl': + return self.ydl_conf + elif key == 'server': + return self.svr_conf + elif key == 'general': + return self.gen_conf + else: + raise KeyError(key) + diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 9e21ea2..b010384 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -22,31 +22,39 @@ from .config import ydl_conf, conf + +def load_conf_from_file(cmd_args): + logger = logging.getLogger('ydl_webui') + + conf_file = cmd_args.get('config', None) + logger.info('load config file (%s)' %(conf_file)) + + if cmd_args is None or conf_file is None: + return ({}, {}) + + try: + with open(expanduser(conf_file)) as f: + return (json.load(f), cmd_args) + except FileNotFoundError as e: + logger.critical("Config file (%s) doesn't exist", conf_file) + exit(1) + + class Core(object): exerpt_keys = ['tid', 'state', 'percent', 'total_bytes', 'title', 'eta', 'speed'] - def __init__(self, args=None): + def __init__(self, cmd_args=None): self.logger = logging.getLogger('ydl_webui') - c = conf({'proxy':123, 'ff': 33}) - - #exit(1) - - # options from command line - self.cmdl_args_dict = {} - # options read from configuration file - self.conf_file_dict = {} - # configuration options combined cmdl_args_dict with conf_file_dict. - self.conf = { 'server': {}, 'ydl': {}, 'general': {} } + self.logger.debug('cmd_args = %s' %(cmd_args)) + conf_dict, cmd_args = load_conf_from_file(cmd_args) + self.conf = conf(conf_dict=conf_dict, cmd_args=cmd_args) self.rq = Queue() self.wq = Queue() self.worker = {} - self.load_cmdl_args(args) - self.load_conf_file() - self.cmdl_override_conf_file() - self.logger.debug("configuration: \n%s", json.dumps(self.conf, indent=4)) + self.logger.debug("configuration: \n%s", json.dumps(self.conf.dict(), indent=4)) self.server = Server(self.wq, self.rq, self.conf['server']['host'], self.conf['server']['port']) self.db = DataBase(self.conf['general']['db_path']) @@ -160,70 +168,6 @@ def cancel_worker(self, tid): del self.worker[tid] - def load_cmdl_args(self, args): - self.cmdl_args_dict['conf'] = args.get('config') - self.cmdl_args_dict['host'] = args.get('host') - self.cmdl_args_dict['port'] = args.get('port') - - - def load_conf_file(self): - try: - with open(self.cmdl_args_dict['conf']) as f: - self.conf_file_dict = json.load(f) - except FileNotFoundError as e: - self.logger.critical("Config file (%s) doesn't exist", self.cmdl_args_dict['conf']) - exit(1) - - self.load_general_conf(self.conf_file_dict) - self.load_server_conf(self.conf_file_dict) - self.load_ydl_conf(self.conf_file_dict) - - - def load_general_conf(self, conf_file_dict): - # field1: key, field2: default value, field3: function to process the value - valid_conf = [ ['download_dir', '~/Downloads/youtube-dl', expanduser], - ['db_path', '~/.conf/youtube-dl-webui/db.db', expanduser], - ['task_log_size', 10, None], - ] - - general_conf = conf_file_dict.get('general', {}) - - for conf in valid_conf: - if conf[2] is None: - self.conf['general'][conf[0]] = general_conf.get(conf[0], conf[1]) - else: - self.conf['general'][conf[0]] = conf[2](general_conf.get(conf[0], conf[1])) - - self.logger.debug("general_config: %s", json.dumps(self.conf)) - - - def load_server_conf(self, conf_file_dict): - valid_conf = [ ['host', '127.0.0.1'], - ['port', '5000' ] - ] - - server_conf = conf_file_dict.get('server', {}) - - for pair in valid_conf: - self.conf['server'][pair[0]] = server_conf.get(pair[0], pair[1]) - - self.logger.debug("server_config: %s", json.dumps(self.conf['server'])) - - - def load_ydl_conf(self, conf_file_dict): - valid_opts = [ ['proxy', None, ], - ['format', 'bestaudio/best' ] - ] - - ydl_opts = conf_file_dict.get('youtube_dl', {}) - - for opt in valid_opts: - if opt[0] in ydl_opts: - self.conf['ydl'][opt[0]] = ydl_opts.get(opt[0], opt[1]) - - self.logger.debug("global ydl_opts: %s", json.dumps(self.conf['ydl'])) - - def cmdl_override_conf_file(self): if self.cmdl_args_dict['host'] is not None: self.conf['server']['host'] = self.cmdl_args_dict['host'] From 8f816c443af8b437d8fd6b26efcaa767da12f613 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 31 Aug 2017 09:16:56 +0800 Subject: [PATCH 28/88] make cmdline arguments overwrite configuration file --- youtube_dl_webui/config.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/youtube_dl_webui/config.py b/youtube_dl_webui/config.py index 9923c39..e11d211 100644 --- a/youtube_dl_webui/config.py +++ b/youtube_dl_webui/config.py @@ -34,6 +34,12 @@ def get_val(self, key): def __getitem__(self, key): return self.get_val(key) + def set_val(self, key, val): + self._conf[key] = val + + def __setitem__(self, key, val): + self.set_val(key, val) + def dict(self): return self._conf @@ -87,8 +93,19 @@ class conf(object): def __init__(self, conf_dict={}, cmd_args={}): self.logger = logging.getLogger('ydl_webui') + self.cmd_args = cmd_args self.load(conf_dict) + def cmd_args_override(self): + _cat_dict = {'host': 'server', + 'port': 'server'} + + for key, val in self.cmd_args.items(): + if key not in _cat_dict: + continue + sub_conf = self.get_val(_cat_dict[key]) + sub_conf.set_val(key, val) + def load(self, conf_dict): if not isinstance(conf_dict, dict): self.logger.error("input parameter(conf_dict) is not an instance of dict") @@ -102,6 +119,9 @@ def load(self, conf_dict): elif f == 'general': self.gen_conf = gen_conf(conf_dict.get(f, {})) + # override configurations by cmdline arguments + self.cmd_args_override() + def dict(self): d = {} for f in self._valid_fields: @@ -114,7 +134,7 @@ def dict(self): return d - def __getitem__(self, key): + def get_val(self, key): if key not in self._valid_fields: raise KeyError(key) @@ -127,6 +147,9 @@ def __getitem__(self, key): else: raise KeyError(key) + def __getitem__(self, key): + return self.get_val(key) + From 3ef3762c2161d321f5edfc95140ec30bd686412d Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 31 Aug 2017 14:45:51 +0800 Subject: [PATCH 29/88] refactor task --- youtube_dl_webui/core.py | 9 ++++ youtube_dl_webui/db.py | 56 +++++++++++++++++++-- youtube_dl_webui/schema.sql | 4 +- youtube_dl_webui/task.py | 99 +++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 youtube_dl_webui/task.py diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index b010384..02a5544 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -21,6 +21,7 @@ from .worker import Worker from .config import ydl_conf, conf +from .task import TaskManager, Task def load_conf_from_file(cmd_args): @@ -50,6 +51,14 @@ def __init__(self, cmd_args=None): conf_dict, cmd_args = load_conf_from_file(cmd_args) self.conf = conf(conf_dict=conf_dict, cmd_args=cmd_args) + self.db = DataBase(self.conf['general']['db_path']) + self.task_manager = TaskManager(self.db) + + # tid = self.task_manager.new_task('ix212xx', {'proxy': '12.12.12.12'}) + # self.task_manager.start_task(tid) + + # exit(1) + self.rq = Queue() self.wq = Queue() self.worker = {} diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index 5134705..f3302a3 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -14,6 +14,7 @@ from .utils import TaskInexistenceError from .utils import TaskPausedError from .utils import TaskRunningError +from .task import url2tid class DataBase(object): def __init__(self, db_path): @@ -69,7 +70,7 @@ def get_param(self, tid): def get_opts(self, tid): self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?) and state not in (?,?)', - (tid, state_index['finished'], state_index['invalid'])) + (tid, state_index['finished'], state_index['invalid'])) row = self.db.fetchone() if row is None: @@ -78,8 +79,8 @@ def get_opts(self, tid): return json.loads(row['opt']) - def get_ydl_opts(self, tid): - self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?)', (tid, )) + # def get_ydl_opts(self, tid): + # self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?)', (tid, )) def create_task(self, param, ydl_opts): @@ -282,3 +283,52 @@ def progress_update(self, tid, d): self.db.execute('UPDATE task_info SET finish_time=? WHERE tid=(?)', (time(), tid)) self.conn.commit() + + + + def get_ydl_opts(self, tid): + self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?) and state not in (?,?)', + (tid, state_index['finished'], state_index['invalid'])) + row = self.db.fetchone() + + if row is None: + raise TaskInexistenceError('task does not exist') + + return json.loads(row['opt']) + + def get_stat(self, tid): + self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) + row = self.db.fetchone() + + if row is None: + raise TaskInexistenceError('task does not exist') + + return dict(row) + + def get_info(self, tid): + self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) + row = self.db.fetchone() + + if row is None: + raise TaskInexistenceError('task does not exist') + + return dict(row) + + def new_task(self, url, ydl_opts): + tid = url2tid(url) + + self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) + if self.db.fetchone() is not None: + raise TaskExistenceError('Task exists') + + ydl_opts_str = json.dumps(ydl_opts) + + self.db.execute('INSERT INTO task_status (tid, url) VALUES (?, ?)', (tid, url)) + self.db.execute('INSERT INTO task_info (tid, url, create_time) VALUES (?, ?, ?)', + (tid, url, time())) + self.db.execute('INSERT INTO task_ydl_opt (tid, url, opt) VALUES (?, ?, ?)', + (tid, url, ydl_opts_str)) + self.conn.commit() + + return tid + diff --git a/youtube_dl_webui/schema.sql b/youtube_dl_webui/schema.sql index 5fc6900..f758fca 100644 --- a/youtube_dl_webui/schema.sql +++ b/youtube_dl_webui/schema.sql @@ -8,8 +8,8 @@ CREATE TABLE task_param ( DROP TABLE IF EXISTS task_info; CREATE TABLE task_info ( tid TEXT PRIMARY KEY NOT NULL, - state INTEGER NOT NULL DEFAULT 2, url TEXT NOT NULL, + state INTEGER NOT NULL DEFAULT 2, title TEXT NOT NULL DEFAULT '', create_time REAL DEFAULT 0.0, finish_time REAL DEFAULT 0.0, @@ -27,6 +27,7 @@ CREATE TABLE task_info ( DROP TABLE IF EXISTS task_status; CREATE TABLE task_status ( tid TEXT PRIMARY KEY NOT NULL, + url TEXT NOT NULL, state INTEGER NOT NULL DEFAULT 2, percent TEXT NOT NULL DEFAULT '0.0%', filename TEXT NOT NULL DEFAULT '', @@ -45,6 +46,7 @@ CREATE TABLE task_status ( DROP TABLE IF EXISTS task_ydl_opt; CREATE TABLE task_ydl_opt ( tid TEXT PRIMARY KEY NOT NULL, + url TEXT NOT NULL, state INTEGER NOT NULL DEFAULT 2, opt TEXT NOT NULL DEFAULT '{}' ); diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py new file mode 100644 index 0000000..e11e30a --- /dev/null +++ b/youtube_dl_webui/task.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from hashlib import sha1 + +from .config import ydl_conf +from .utils import TaskInexistenceError +from .utils import TaskRunningError +from .utils import TaskExistenceError +from .utils import TaskPausedError + +def url2tid(url): + return sha1(url.encode()).hexdigest() + +class Task(object): + + def __init__(self, tid, db, ydl_opts={}, info={}, status={}): + self.tid = tid + self._db = db + self.ydl_conf = ydl_conf(ydl_opts) + self.info = info + self.status = status + + def start(self): + pass + + def pause(self): + pass + + def halt(self): + pass + + def finish(self): + pass + + +class TaskManager(object): + """ + Tasks are categorized into two types, active type and inactive type. + + Tasks in active type are which in downloading, pausing state. These tasks + associate with a Task instance in memory. However, inactive type tasks + are in invalid state or finished state, which only have database recoards + but memory instance. + """ + + def __init__(self, db): + self._db = db + + # all the active type tasks can be referenced from self._tasks_dict or + # self._tasks_set. + self._tasks_dict = {} + self._tasks_set = set(()) + + def new_task(self, url, ydl_opts={}): + """Create a new task and put it in inactive type""" + + return self._db.new_task(url, ydl_opts) + + def start_task(self, tid, ignore_state=False, first_run=False): + """make an inactive type task into active type""" + + if tid in self._tasks_set: + task = self._tasks_dict[tid] + task.start() + return task + + try: + ydl_opts = self._db.get_ydl_opts(tid) + info = self._db.get_info(tid) + status = self._db.get_stat(tid) + except TaskInexistenceError as e: + raise TaskInexistenceError(e.msg) + + task = Task(tid, self._db, ydl_opts=ydl_opts, info=info, status=status) + self._tasks_set.add(task) + self._tasks_dict[tid] = task + + task.start() + + return task + + def pause_task(self, tid): + task = self._tasks_dict[tid] + task.pause() + + def halt_task(self, tid): + task = self._tasks_dict[tid] + task.stop() + + def finish_task(self, tid): + task = self._tasks_dict[tid] + task.finish() + + + + + + From 89a3ebe58e1db03fdb8641d43f9aa5f573707584 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Fri, 1 Sep 2017 15:42:41 +0800 Subject: [PATCH 30/88] msg temp --- youtube_dl_webui/core.py | 19 +++++++++- youtube_dl_webui/msg.py | 75 ++++++++++++++++++++++++++++++++++++++ youtube_dl_webui/server.py | 20 ++++++++-- youtube_dl_webui/utils.py | 6 +++ 4 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 youtube_dl_webui/msg.py diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 02a5544..daab4bf 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -22,6 +22,7 @@ from .config import ydl_conf, conf from .task import TaskManager, Task +from .msg import MsgMgr def load_conf_from_file(cmd_args): @@ -57,7 +58,23 @@ def __init__(self, cmd_args=None): # tid = self.task_manager.new_task('ix212xx', {'proxy': '12.12.12.12'}) # self.task_manager.start_task(tid) - # exit(1) + def pp(m, event, data): + print(m) + print(event) + print(data) + m.svr_put('reply') + + + self.msg_mgr = MsgMgr() + self.msg_mgr.reg_event('hello', pp) + + m = self.msg_mgr.get_msg_handler('server') + + self.server = Server(None, None, None, None, m) + self.server.start() + self.msg_mgr.run() + + exit(1) self.rq = Queue() self.wq = Queue() diff --git a/youtube_dl_webui/msg.py b/youtube_dl_webui/msg.py new file mode 100644 index 0000000..8a0a853 --- /dev/null +++ b/youtube_dl_webui/msg.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import logging + +from multiprocessing import Process, Queue + +from .utils import uuid + +class Msg(object): + _svrQ = Queue() + + def __init__(self, uuid, cliQ=False): + self.uuid = uuid + if cliQ is True: + self._cliQ = Queue() + else: + self._cliQ = None + + def put(self, event, data): + payload = {'__uuid__': self.uuid, '__event__': event, '__data__': data} + Msg._svrQ.put(payload) + + def get(self): + raw_msg = self._cliQ.get() + uuid = raw_msg['__uuid__'] + data = raw_msg['__data__'] + + return (uuid, data) + + def svr_put(self, data): + payload = {'__uuid__': self.uuid, '__data__': data} + self._cliQ.put(payload) + + @classmethod + def svr_get(cls): + raw_msg = cls._svrQ.get() + print(raw_msg) + uuid = raw_msg['__uuid__'] + event = raw_msg['__event__'] + data = raw_msg['__data__'] + + return (uuid, event, data) + + +class MsgMgr(object): + + def __init__(self): + self.logger = logging.getLogger('ydl_webui') + self._cb_dict = {} + self._msg_dict = {} + + def get_msg_handler(self, cli_name=None): + if cli_name is not None: + m = Msg(uuid=cli_name, cliQ=True) + self._msg_dict[cli_name] = m + return m + else: + uuid = uuid() + m = Msg(uuid=uuid) + self._msg_dict[uuid] = m + return m + + def reg_event(self, event, callback): + self._cb_dict[event] = callback + + def run(self): + while True: + uuid, event, data = Msg.svr_get() + + cb_func = self._cb_dict[event] + cb_func(self._msg_dict[uuid], event, data) + + + diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index 288732b..6759944 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -9,6 +9,10 @@ from multiprocessing import Process from copy import deepcopy +from .msg import Msg + +MSG = None + app = Flask(__name__) RQ = None @@ -19,7 +23,12 @@ @app.route('/') def index(): - return render_template('index.html') + MSG.put('hello', 'data') + k = MSG.get() + print(k) + # print('index') + return 'good' + # return render_template('index.html') @app.route('/task', methods=['POST']) @@ -132,7 +141,7 @@ def test(case): class Server(Process): - def __init__(self, rqueue, wqueue, host, port): + def __init__(self, rqueue, wqueue, host, port, m): super(Server, self).__init__() self.rq = rqueue self.wq = wqueue @@ -145,7 +154,12 @@ def __init__(self, rqueue, wqueue, host, port): self.host = host self.port = port + global MSG + MSG = m + def run(self): - app.run(host=self.host, port=self.port, use_reloader=False) + # self.m.put('hello', 'data') + # app.run(host=self.host, port=self.port, use_reloader=False) + app.run(host='0.0.0.0', port=5000, use_reloader=False) diff --git a/youtube_dl_webui/utils.py b/youtube_dl_webui/utils.py index fff0ab1..9285ef7 100644 --- a/youtube_dl_webui/utils.py +++ b/youtube_dl_webui/utils.py @@ -1,9 +1,15 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import uuid + state_index={'all': 0, 'downloading': 1, 'paused': 2, 'finished': 3, 'invalid': 4} state_name=['all', 'downloading', 'paused', 'finished', 'invalid'] +def uuid(): + return uuid.uuid4().hex + + class YoutubeDLWebUI(Exception): """Base exception for YoutubeDL errors.""" pass From 3f10d185aac6c1383c601143824a7c7a20ae88ee Mon Sep 17 00:00:00 2001 From: d0u9 Date: Fri, 1 Sep 2017 16:08:52 +0800 Subject: [PATCH 31/88] msg is ok --- youtube_dl_webui/core.py | 12 ++--- youtube_dl_webui/msg.py | 89 ++++++++++++++++++++------------------ youtube_dl_webui/server.py | 4 +- 3 files changed, 53 insertions(+), 52 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index daab4bf..76d42ed 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -58,19 +58,19 @@ def __init__(self, cmd_args=None): # tid = self.task_manager.new_task('ix212xx', {'proxy': '12.12.12.12'}) # self.task_manager.start_task(tid) - def pp(m, event, data): - print(m) + def pp(svr, event, data): + print(svr) print(event) print(data) - m.svr_put('reply') + svr.put('sdf') self.msg_mgr = MsgMgr() - self.msg_mgr.reg_event('hello', pp) + self.msg_mgr.reg_event('event', pp) - m = self.msg_mgr.get_msg_handler('server') + cli = self.msg_mgr.new_cli('server') - self.server = Server(None, None, None, None, m) + self.server = Server(None, None, None, None, cli) self.server.start() self.msg_mgr.run() diff --git a/youtube_dl_webui/msg.py b/youtube_dl_webui/msg.py index 8a0a853..083f9c7 100644 --- a/youtube_dl_webui/msg.py +++ b/youtube_dl_webui/msg.py @@ -7,69 +7,72 @@ from .utils import uuid -class Msg(object): - _svrQ = Queue() +class MsgBase(object): - def __init__(self, uuid, cliQ=False): - self.uuid = uuid - if cliQ is True: - self._cliQ = Queue() - else: - self._cliQ = None + def __init__(self, getQ, putQ): + self.getQ = getQ + self.putQ = putQ - def put(self, event, data): - payload = {'__uuid__': self.uuid, '__event__': event, '__data__': data} - Msg._svrQ.put(payload) - def get(self): - raw_msg = self._cliQ.get() - uuid = raw_msg['__uuid__'] - data = raw_msg['__data__'] +class SvrMsg(MsgBase): + + def __init__(self, getQ, putQ): + super(SvrMsg, self).__init__(getQ, putQ) - return (uuid, data) + def put(self, data): + payload = {'__data__': data} + self.putQ.put(payload) - def svr_put(self, data): - payload = {'__uuid__': self.uuid, '__data__': data} - self._cliQ.put(payload) - @classmethod - def svr_get(cls): - raw_msg = cls._svrQ.get() - print(raw_msg) - uuid = raw_msg['__uuid__'] - event = raw_msg['__event__'] - data = raw_msg['__data__'] +class CliMsg(MsgBase): - return (uuid, event, data) + def __init__(self, uuid, getQ, putQ): + super(CliMsg, self).__init__(getQ, putQ) + self.uuid = uuid + + def put(self, event, data): + payload = {'__uuid__': self.uuid, '__event__': event, '__data__': data} + self.putQ.put(payload) + + def get(self): + raw_msg = self.getQ.get() + return raw_msg['__data__'] class MsgMgr(object): + _svrQ = Queue() + _cli_dict = {} + _evnt_cb_dict = {} def __init__(self): - self.logger = logging.getLogger('ydl_webui') - self._cb_dict = {} - self._msg_dict = {} + pass - def get_msg_handler(self, cli_name=None): + def new_cli(self, cli_name=None): + uuid = None if cli_name is not None: - m = Msg(uuid=cli_name, cliQ=True) - self._msg_dict[cli_name] = m - return m + uuid = cli_name + cli = CliMsg(cli_name, Queue(), self._svrQ) else: uuid = uuid() - m = Msg(uuid=uuid) - self._msg_dict[uuid] = m - return m + cli = cliMsg(uuid, None, self._svrQ) + + self._cli_dict[uuid] = cli - def reg_event(self, event, callback): - self._cb_dict[event] = callback + return cli + + def reg_event(self, event, cb_func): + self._evnt_cb_dict[event] = cb_func def run(self): while True: - uuid, event, data = Msg.svr_get() - - cb_func = self._cb_dict[event] - cb_func(self._msg_dict[uuid], event, data) + raw_msg = self._svrQ.get() + uuid = raw_msg['__uuid__'] + evnt = raw_msg['__event__'] + data = raw_msg['__data__'] + cli = self._cli_dict[uuid] + cb = self._evnt_cb_dict[evnt] + svr = SvrMsg(cli.putQ, cli.getQ) + cb(svr, evnt, data) diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index 6759944..f942ed1 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -9,8 +9,6 @@ from multiprocessing import Process from copy import deepcopy -from .msg import Msg - MSG = None app = Flask(__name__) @@ -23,7 +21,7 @@ @app.route('/') def index(): - MSG.put('hello', 'data') + MSG.put('event', 'data') k = MSG.get() print(k) # print('index') From 796212dc4bf32b7df9887275658ab58045cd70dd Mon Sep 17 00:00:00 2001 From: d0u9 Date: Fri, 1 Sep 2017 16:14:09 +0800 Subject: [PATCH 32/88] msg update --- youtube_dl_webui/msg.py | 7 ++++--- youtube_dl_webui/utils.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/youtube_dl_webui/msg.py b/youtube_dl_webui/msg.py index 083f9c7..0ecd024 100644 --- a/youtube_dl_webui/msg.py +++ b/youtube_dl_webui/msg.py @@ -5,7 +5,7 @@ from multiprocessing import Process, Queue -from .utils import uuid +from .utils import new_uuid class MsgBase(object): @@ -53,8 +53,8 @@ def new_cli(self, cli_name=None): uuid = cli_name cli = CliMsg(cli_name, Queue(), self._svrQ) else: - uuid = uuid() - cli = cliMsg(uuid, None, self._svrQ) + uuid = new_uuid() + cli = CliMsg(uuid, None, self._svrQ) self._cli_dict[uuid] = cli @@ -67,6 +67,7 @@ def run(self): while True: raw_msg = self._svrQ.get() uuid = raw_msg['__uuid__'] + print(uuid) evnt = raw_msg['__event__'] data = raw_msg['__data__'] diff --git a/youtube_dl_webui/utils.py b/youtube_dl_webui/utils.py index 9285ef7..ccf9586 100644 --- a/youtube_dl_webui/utils.py +++ b/youtube_dl_webui/utils.py @@ -6,8 +6,8 @@ state_index={'all': 0, 'downloading': 1, 'paused': 2, 'finished': 3, 'invalid': 4} state_name=['all', 'downloading', 'paused', 'finished', 'invalid'] -def uuid(): - return uuid.uuid4().hex +def new_uuid(): + return str(uuid.uuid4().hex) class YoutubeDLWebUI(Exception): From be01e8a6074e8ec418aed185abb19d8a3aab2bc1 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Fri, 1 Sep 2017 16:19:16 +0800 Subject: [PATCH 33/88] prepare to repalce msg --- youtube_dl_webui/core.py | 24 ++++++++++++------------ youtube_dl_webui/server.py | 14 ++++---------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 76d42ed..15277e5 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -58,23 +58,23 @@ def __init__(self, cmd_args=None): # tid = self.task_manager.new_task('ix212xx', {'proxy': '12.12.12.12'}) # self.task_manager.start_task(tid) - def pp(svr, event, data): - print(svr) - print(event) - print(data) - svr.put('sdf') + # def pp(svr, event, data): + # print(svr) + # print(event) + # print(data) + # svr.put('sdf') - self.msg_mgr = MsgMgr() - self.msg_mgr.reg_event('event', pp) + # self.msg_mgr = MsgMgr() + # self.msg_mgr.reg_event('event', pp) - cli = self.msg_mgr.new_cli('server') + # cli = self.msg_mgr.new_cli('server') - self.server = Server(None, None, None, None, cli) - self.server.start() - self.msg_mgr.run() + # self.server = Server(None, None, None, None, cli) + # self.server.start() + # self.msg_mgr.run() - exit(1) + # exit(1) self.rq = Queue() self.wq = Queue() diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index f942ed1..5a307d4 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -21,12 +21,7 @@ @app.route('/') def index(): - MSG.put('event', 'data') - k = MSG.get() - print(k) - # print('index') - return 'good' - # return render_template('index.html') + return render_template('index.html') @app.route('/task', methods=['POST']) @@ -139,7 +134,7 @@ def test(case): class Server(Process): - def __init__(self, rqueue, wqueue, host, port, m): + def __init__(self, rqueue, wqueue, host, port, m=None): super(Server, self).__init__() self.rq = rqueue self.wq = wqueue @@ -156,8 +151,7 @@ def __init__(self, rqueue, wqueue, host, port, m): MSG = m def run(self): - # self.m.put('hello', 'data') - # app.run(host=self.host, port=self.port, use_reloader=False) - app.run(host='0.0.0.0', port=5000, use_reloader=False) + app.run(host=self.host, port=self.port, use_reloader=False) + # app.run(host='0.0.0.0', port=5000, use_reloader=False) From 5beeb113f0e2e6f6f60f23edafcc6a0ffd22605b Mon Sep 17 00:00:00 2001 From: d0u9 Date: Mon, 4 Sep 2017 08:11:34 +0800 Subject: [PATCH 34/88] integrate msg --- youtube_dl_webui/core.py | 83 ++++++++++++++++++++++++++------ youtube_dl_webui/msg.py | 1 - youtube_dl_webui/server.py | 96 ++++++++++++++------------------------ youtube_dl_webui/task.py | 3 +- 4 files changed, 106 insertions(+), 77 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 15277e5..225bc23 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -24,6 +24,53 @@ from .task import TaskManager, Task from .msg import MsgMgr +class WebMsgDispatcher(object): + InternalErrorMsg = {'status': 'error', 'errmsg': 'Internal Error'} + TaskExistenceErrorMsg = {'status': 'error', 'errmsg': 'URL is already added'} + TaskInexistenceErrorMsg = {'status': 'error', 'errmsg': 'Task does not exist'} + UrlErrorMsg = {'status': 'error', 'errmsg': 'URL is invalid'} + + _task_mgr = None + + @classmethod + def init(cls, task_mgr): + cls._task_mgr = task_mgr + + @classmethod + def create_event(cls, svr, event, data): + print(data) + svr.put({'status': 'success', 'tid': 123}) + + @classmethod + def delete_event(cls, svr, event, data): + svr.put({}) + + @classmethod + def manipulate_event(cls, svr, event, data): + svr.put({}) + + @classmethod + def query_event(cls, svr, event, data): + svr.put({}) + + @classmethod + def list_event(cls, svr, event, data): + svr.put({}) + + @classmethod + def state_event(cls, svr, event, data): + svr.put({}) + + @classmethod + def config_event(cls, svr, event, data): + svr.put({}) + + +class WorkMsgDispatcher(object): + + def init(cls, __unknow__=None): + pass + def load_conf_from_file(cmd_args): logger = logging.getLogger('ydl_webui') @@ -49,32 +96,40 @@ def __init__(self, cmd_args=None): self.logger = logging.getLogger('ydl_webui') self.logger.debug('cmd_args = %s' %(cmd_args)) + conf_dict, cmd_args = load_conf_from_file(cmd_args) self.conf = conf(conf_dict=conf_dict, cmd_args=cmd_args) + self.logger.debug("configuration: \n%s", json.dumps(self.conf.dict(), indent=4)) + + + self.msg_mgr = MsgMgr() + web_cli = self.msg_mgr.new_cli('server') + task_cli = self.msg_mgr.new_cli() self.db = DataBase(self.conf['general']['db_path']) - self.task_manager = TaskManager(self.db) + self.task_mgr = TaskManager(self.db, task_cli) # tid = self.task_manager.new_task('ix212xx', {'proxy': '12.12.12.12'}) # self.task_manager.start_task(tid) - # def pp(svr, event, data): - # print(svr) - # print(event) - # print(data) - # svr.put('sdf') + WebMsgDispatcher.init(self.task_mgr) + WorkMsgDispatcher.init(self.task_mgr) - # self.msg_mgr = MsgMgr() - # self.msg_mgr.reg_event('event', pp) + self.msg_mgr.reg_event('create', WebMsgDispatcher.create_event) + self.msg_mgr.reg_event('delete', WebMsgDispatcher.delete_event) + self.msg_mgr.reg_event('manipulate', WebMsgDispatcher.manipulate_event) + self.msg_mgr.reg_event('query', WebMsgDispatcher.query_event) + self.msg_mgr.reg_event('list', WebMsgDispatcher.list_event) + self.msg_mgr.reg_event('state', WebMsgDispatcher.state_event) + self.msg_mgr.reg_event('config', WebMsgDispatcher.config_event) - # cli = self.msg_mgr.new_cli('server') + self.server = Server(web_cli, self.conf['server']['host'], self.conf['server']['port']) + self.server.start() - # self.server = Server(None, None, None, None, cli) - # self.server.start() - # self.msg_mgr.run() + self.msg_mgr.run() - # exit(1) + exit(1) self.rq = Queue() self.wq = Queue() @@ -82,7 +137,7 @@ def __init__(self, cmd_args=None): self.logger.debug("configuration: \n%s", json.dumps(self.conf.dict(), indent=4)) - self.server = Server(self.wq, self.rq, self.conf['server']['host'], self.conf['server']['port']) + self.server = Server(self.wq, self.rq, self.conf['server']['host'], self.conf['server']['port'], web_cli) self.db = DataBase(self.conf['general']['db_path']) dl_dir = self.conf['general']['download_dir'] diff --git a/youtube_dl_webui/msg.py b/youtube_dl_webui/msg.py index 0ecd024..dfdc1cc 100644 --- a/youtube_dl_webui/msg.py +++ b/youtube_dl_webui/msg.py @@ -67,7 +67,6 @@ def run(self): while True: raw_msg = self._svrQ.get() uuid = raw_msg['__uuid__'] - print(uuid) evnt = raw_msg['__event__'] data = raw_msg['__data__'] diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index 5a307d4..99130f6 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -26,103 +26,84 @@ def index(): @app.route('/task', methods=['POST']) def add_task(): - wqd = deepcopy(WQ_DICT) - wqd['command'] = 'create' - wqd['param'] = {'url': request.form['url']} - wqd['ydl_opts'] = {} + payload = {'url': request.form['url'], 'ydl_opts': {}} - WQ.put(wqd) - return json.dumps(RQ.get()) + MSG.put('create', payload) + return json.dumps(MSG.get()) @app.route('/task/list', methods=['GET']) def list_task(): - wqd = deepcopy(WQ_DICT) - wqd['command'] = 'list' - + payload = {} exerpt = request.args.get('exerpt', None) if exerpt is None: - wqd['exerpt'] = True + payload['exerpt'] = True else: - wqd['exerpt'] = False + payload['exerpt'] = False - state = request.args.get('state', 'all') - wqd['state'] = state + payload['state'] = request.args.get('state', 'all') - WQ.put(wqd) - return json.dumps(RQ.get()) + MSG.put('list', payload) + return json.dumps(MSG.get()) @app.route('/task/state_counter', methods=['GET']) def list_state(): - wqd = deepcopy(WQ_DICT) - wqd['command'] = 'state' - - WQ.put(wqd) - return json.dumps(RQ.get()) + MSG.put('state', None) + return json.dumps(MSG.get()) @app.route('/task/tid/', methods=['DELETE']) def delete_task(tid): - wqd = deepcopy(WQ_DICT) - wqd['command'] = 'delete' - wqd['tid'] = tid - - wqd['del_data'] = not not request.args.get('del_data', False) + payload = {} + payload['tid'] = tid + payload['del_data'] = not not request.args.get('del_data', False) - WQ.put(wqd) - return json.dumps(RQ.get()) + MSG.put('delete', payload) + return json.dumps(MSG.get()) @app.route('/task/tid/', methods=['PUT']) def manipulate_task(tid): - wqd = deepcopy(WQ_DICT) - wqd['command'] = 'manipulate' - wqd['tid'] = tid + payload = {} + payload['tid'] = tid act = request.args.get('act', None) - if act == 'pause': - wqd['act'] = 'pause' + payload['act'] = 'pause' elif act == 'resume': - wqd['act'] = 'resume' + payload['act'] = 'resume' else: return json.dumps(MSG_INVALID_REQUEST) - WQ.put(wqd) - return json.dumps(RQ.get()) + MSG.put('manipulate', payload) + return json.dumps(MSG.get()) @app.route('/task/tid//status', methods=['GET']) def query_task(tid): - wqd = deepcopy(WQ_DICT) - wqd['command'] = 'query' - wqd['tid'] = tid + payload['tid'] = tid exerpt = request.args.get('exerpt', None) - if exerpt is None: - wqd['exerpt'] = False + payload['exerpt'] = False else: - wqd['exerpt'] = True + payload['exerpt'] = True - WQ.put(wqd) - return json.dumps(RQ.get()) + MSG.put('query', payload) + return json.dumps(MSG.get()) @app.route('/config', methods=['GET', 'POST']) def get_config(): - wqd = deepcopy(WQ_DICT) - wqd['command'] = 'config' - if request.method == 'POST': - wqd['act'] = 'update' - wqd['param'] = request.get_json() + payload['act'] = 'update' + payload['param'] = request.get_json() else: - wqd['act'] = 'get' + payload['act'] = 'get' - WQ.put(wqd) - return json.dumps(RQ.get()) + MSG.put('config', payload) + return json.dumps(MSG.get()) ### @@ -134,22 +115,15 @@ def test(case): class Server(Process): - def __init__(self, rqueue, wqueue, host, port, m=None): + def __init__(self, msg_cli, host, port): super(Server, self).__init__() - self.rq = rqueue - self.wq = wqueue - global RQ - global WQ - RQ = rqueue - WQ = wqueue + global MSG + MSG = msg_cli self.host = host self.port = port - global MSG - MSG = m - def run(self): app.run(host=self.host, port=self.port, use_reloader=False) # app.run(host='0.0.0.0', port=5000, use_reloader=False) diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index e11e30a..8d0cb79 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -44,8 +44,9 @@ class TaskManager(object): but memory instance. """ - def __init__(self, db): + def __init__(self, db, msg_cli): self._db = db + self._msg_cli = msg_cli # all the active type tasks can be referenced from self._tasks_dict or # self._tasks_set. From c85427b60baebb5f02574458e4b1a9b17961559d Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 5 Sep 2017 15:33:53 +0800 Subject: [PATCH 35/88] test task class interface --- youtube_dl_webui/core.py | 55 ++++++++++++++++++++++++++++++---------- youtube_dl_webui/msg.py | 9 ++++--- youtube_dl_webui/task.py | 36 +++++++++++++++++++------- 3 files changed, 73 insertions(+), 27 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 225bc23..995710a 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -25,6 +25,9 @@ from .msg import MsgMgr class WebMsgDispatcher(object): + logger = logging.getLogger('ydl_webui') + + SuccessMsg = {'status': 'success'} InternalErrorMsg = {'status': 'error', 'errmsg': 'Internal Error'} TaskExistenceErrorMsg = {'status': 'error', 'errmsg': 'URL is already added'} TaskInexistenceErrorMsg = {'status': 'error', 'errmsg': 'Task does not exist'} @@ -37,32 +40,56 @@ def init(cls, task_mgr): cls._task_mgr = task_mgr @classmethod - def create_event(cls, svr, event, data): - print(data) - svr.put({'status': 'success', 'tid': 123}) + def create_event(cls, svr, event, data, task_mgr): + cls.logger.debug('url = %s' %(data['url'])) + try: + tid = task_mgr.new_task(data['url'], {'proxy': '12.12.12.12'}) + except TaskExistenceError: + svr.put(cls.TaskExistenceErrorMsg) + return + + task = task_mgr.start_task(tid) + print(task) + + svr.put({'status': 'success', 'tid': tid}) @classmethod - def delete_event(cls, svr, event, data): - svr.put({}) + def delete_event(cls, svr, event, data, task_mgr): + tid, del_data = data['tid'], data['del_data'] + + task_mgr.delete_task(tid) + + svr.put(cls.SuccessMsg) @classmethod - def manipulate_event(cls, svr, event, data): - svr.put({}) + def manipulate_event(cls, svr, event, data, task_mgr): + cls.logger.debug('manipulation event') + tid, act = data['tid'], data['act'] + + ret_val = cls.InternalErrorMsg + if act == 'pause': + task_mgr.pause_task(tid) + ret_val = cls.SuccessMsg + elif act == 'resume': + task_mgr.start_task(tid) + ret_val = cls.SuccessMsg + + svr.put(ret_val) @classmethod - def query_event(cls, svr, event, data): + def query_event(cls, svr, event, data, arg): svr.put({}) @classmethod - def list_event(cls, svr, event, data): + def list_event(cls, svr, event, data, arg): svr.put({}) @classmethod - def state_event(cls, svr, event, data): + def state_event(cls, svr, event, data, arg): svr.put({}) @classmethod - def config_event(cls, svr, event, data): + def config_event(cls, svr, event, data, arg): svr.put({}) @@ -116,9 +143,9 @@ def __init__(self, cmd_args=None): WebMsgDispatcher.init(self.task_mgr) WorkMsgDispatcher.init(self.task_mgr) - self.msg_mgr.reg_event('create', WebMsgDispatcher.create_event) - self.msg_mgr.reg_event('delete', WebMsgDispatcher.delete_event) - self.msg_mgr.reg_event('manipulate', WebMsgDispatcher.manipulate_event) + self.msg_mgr.reg_event('create', WebMsgDispatcher.create_event, self.task_mgr) + self.msg_mgr.reg_event('delete', WebMsgDispatcher.delete_event, self.task_mgr) + self.msg_mgr.reg_event('manipulate', WebMsgDispatcher.manipulate_event, self.task_mgr) self.msg_mgr.reg_event('query', WebMsgDispatcher.query_event) self.msg_mgr.reg_event('list', WebMsgDispatcher.list_event) self.msg_mgr.reg_event('state', WebMsgDispatcher.state_event) diff --git a/youtube_dl_webui/msg.py b/youtube_dl_webui/msg.py index dfdc1cc..caba0ba 100644 --- a/youtube_dl_webui/msg.py +++ b/youtube_dl_webui/msg.py @@ -60,8 +60,8 @@ def new_cli(self, cli_name=None): return cli - def reg_event(self, event, cb_func): - self._evnt_cb_dict[event] = cb_func + def reg_event(self, event, cb_func, arg=None): + self._evnt_cb_dict[event] = (cb_func, arg) def run(self): while True: @@ -71,8 +71,9 @@ def run(self): data = raw_msg['__data__'] cli = self._cli_dict[uuid] - cb = self._evnt_cb_dict[evnt] + cb = self._evnt_cb_dict[evnt][0] + arg = self._evnt_cb_dict[evnt][1] svr = SvrMsg(cli.putQ, cli.getQ) - cb(svr, evnt, data) + cb(svr, evnt, data, arg) diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 8d0cb79..011fb15 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import logging + from hashlib import sha1 from .config import ydl_conf @@ -22,15 +24,16 @@ def __init__(self, tid, db, ydl_opts={}, info={}, status={}): self.status = status def start(self): - pass + print('---- task start ----') def pause(self): - pass + print('---- task pause ----') def halt(self): - pass + print('---- task halt ----') def finish(self): + print('---- task finish ----') pass @@ -45,6 +48,7 @@ class TaskManager(object): """ def __init__(self, db, msg_cli): + self.logger = logging.getLogger('ydl_webui') self._db = db self._msg_cli = msg_cli @@ -82,19 +86,33 @@ def start_task(self, tid, ignore_state=False, first_run=False): return task def pause_task(self, tid): + self.logger.debug('task paused (%s)' %(tid)) task = self._tasks_dict[tid] task.pause() def halt_task(self, tid): - task = self._tasks_dict[tid] - task.stop() - - def finish_task(self, tid): - task = self._tasks_dict[tid] - task.finish() + self.logger.debug('task halted (%s)' %(tid)) + if tid in self._tasks_dict: + task = self._tasks_dict[tid] + task.halt() + del self._tasks_dict[tid] + def finish_task(self, tid): + self.logger.debug('task finished (%s)' %(tid)) + if tid in self._tasks_dict: + task = self._tasks_dict[tid] + task.finish() + del self._tasks_dict[tid] + def delete_task(self, tid, del_data=False): + self.logger.debug('task deleted (%s)' %(tid)) + if tid in self._tasks_dict: + task = self._tasks_dict[tid] + task.halt() + del self._tasks_dict[tid] + if del_data: + pass From 6d88cff6b131aa7f519c6f2bb111c5236255955e Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 5 Sep 2017 15:34:24 +0800 Subject: [PATCH 36/88] test task class interface --- youtube_dl_webui/core.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 995710a..8f93ada 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -136,10 +136,6 @@ def __init__(self, cmd_args=None): self.db = DataBase(self.conf['general']['db_path']) self.task_mgr = TaskManager(self.db, task_cli) - # tid = self.task_manager.new_task('ix212xx', {'proxy': '12.12.12.12'}) - # self.task_manager.start_task(tid) - - WebMsgDispatcher.init(self.task_mgr) WorkMsgDispatcher.init(self.task_mgr) From 122818f8bb939ef21d1fa37411e30e8a6e92a5ce Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 5 Sep 2017 15:35:39 +0800 Subject: [PATCH 37/88] test task class interface --- youtube_dl_webui/task.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 011fb15..7b2bb7a 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -16,9 +16,8 @@ def url2tid(url): class Task(object): - def __init__(self, tid, db, ydl_opts={}, info={}, status={}): + def __init__(self, tid, ydl_opts={}, info={}, status={}): self.tid = tid - self._db = db self.ydl_conf = ydl_conf(ydl_opts) self.info = info self.status = status @@ -77,7 +76,7 @@ def start_task(self, tid, ignore_state=False, first_run=False): except TaskInexistenceError as e: raise TaskInexistenceError(e.msg) - task = Task(tid, self._db, ydl_opts=ydl_opts, info=info, status=status) + task = Task(tid, ydl_opts=ydl_opts, info=info, status=status) self._tasks_set.add(task) self._tasks_dict[tid] = task From 379dcce72354ce534c4ed706fae592a50febdf95 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 5 Sep 2017 15:39:40 +0800 Subject: [PATCH 38/88] test task class interface --- youtube_dl_webui/task.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 7b2bb7a..1796a62 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -51,10 +51,7 @@ def __init__(self, db, msg_cli): self._db = db self._msg_cli = msg_cli - # all the active type tasks can be referenced from self._tasks_dict or - # self._tasks_set. self._tasks_dict = {} - self._tasks_set = set(()) def new_task(self, url, ydl_opts={}): """Create a new task and put it in inactive type""" @@ -64,7 +61,7 @@ def new_task(self, url, ydl_opts={}): def start_task(self, tid, ignore_state=False, first_run=False): """make an inactive type task into active type""" - if tid in self._tasks_set: + if tid in self._tasks_dict: task = self._tasks_dict[tid] task.start() return task @@ -77,7 +74,6 @@ def start_task(self, tid, ignore_state=False, first_run=False): raise TaskInexistenceError(e.msg) task = Task(tid, ydl_opts=ydl_opts, info=info, status=status) - self._tasks_set.add(task) self._tasks_dict[tid] = task task.start() From 90ec846addb3581c3ce114ba8d706c919865acef Mon Sep 17 00:00:00 2001 From: d0u9 Date: Tue, 5 Sep 2017 16:03:01 +0800 Subject: [PATCH 39/88] refactor delete interface --- REST_API.txt | 2 +- youtube_dl_webui/core.py | 10 +++++--- youtube_dl_webui/db.py | 51 +++++++++++++++++++++----------------- youtube_dl_webui/server.py | 2 +- youtube_dl_webui/task.py | 33 +++++++++++++----------- 5 files changed, 56 insertions(+), 42 deletions(-) diff --git a/REST_API.txt b/REST_API.txt index 8f8a1dd..36a922d 100644 --- a/REST_API.txt +++ b/REST_API.txt @@ -7,7 +7,7 @@ | 1 | POST | /task | create a new download task | | 2 | PUT | /task/tid/?act=pause | pause a task with its tid | | 3 | PUT | /task/tid/?act=resume | resume download | -| 4 | DELETE | /task/tid/?del_data=true | delete a task and its data | +| 4 | DELETE | /task/tid/?del_file=true | delete a task and its data | | 5 | DELETE | /task/tid/ | delete a task and keep data | | 6 | GET | /task/tid//status | get the full status of a task | | 7 | GET | /task/tid//status?exerpt=true | get the status exerpt of a task | diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 8f93ada..ad9f352 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -49,15 +49,19 @@ def create_event(cls, svr, event, data, task_mgr): return task = task_mgr.start_task(tid) - print(task) svr.put({'status': 'success', 'tid': tid}) @classmethod def delete_event(cls, svr, event, data, task_mgr): - tid, del_data = data['tid'], data['del_data'] + tid = data['tid'] + del_file = True if data['del_data'] == 'true' else False - task_mgr.delete_task(tid) + try: + task_mgr.delete_task(tid, del_file) + except TaskInexistenceError: + svr.put(cls.TaskInexistenceErrorMsg) + return svr.put(cls.SuccessMsg) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index f3302a3..73ab907 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -157,29 +157,6 @@ def start_task(self, tid, ignore_state=False): return json.loads(row['log']) - def delete_task(self, tid, del_data=False): - self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) - row = self.db.fetchone() - if row is None: - raise TaskInexistenceError('') - - if del_data: - if row['tmpfilename'] != '': - self.logger.debug('Delete tmp file: %s' %(row['tmpfilename'])) - os.remove(row['tmpfilename']) - elif row['filename'] != '': - self.logger.debug('Delete file: %s' %(row['filename'])) - os.remove(row['filename']) - else: - self.logger.debug('No file to delete') - - self.db.execute('DELETE FROM task_status WHERE tid=(?)', (tid, )) - self.db.execute('DELETE FROM task_info WHERE tid=(?)', (tid, )) - self.db.execute('DELETE FROM task_param WHERE tid=(?)', (tid, )) - self.db.execute('DELETE FROM task_ydl_opt WHERE tid=(?)', (tid, )) - self.conn.commit() - - def query_task(self, tid): self.db.execute('SELECT * FROM task_status, task_info WHERE task_status.tid=(?) and task_info.tid=(?)', (tid, tid)) row = self.db.fetchone() @@ -286,6 +263,13 @@ def progress_update(self, tid, d): + + + + + + + def get_ydl_opts(self, tid): self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?) and state not in (?,?)', (tid, state_index['finished'], state_index['invalid'])) @@ -332,3 +316,24 @@ def new_task(self, url, ydl_opts): return tid + def delete_task(self, tid): + """ return the tmp file or file downloaded """ + self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) + row = self.db.fetchone() + if row is None: + raise TaskInexistenceError('') + + dl_file = None + if row['tmpfilename'] != '': + dl_file = row['tmpfilename'] + elif row['filename'] != '': + dl_file = row['filename'] + + self.db.execute('DELETE FROM task_status WHERE tid=(?)', (tid, )) + self.db.execute('DELETE FROM task_info WHERE tid=(?)', (tid, )) + self.db.execute('DELETE FROM task_ydl_opt WHERE tid=(?)', (tid, )) + self.conn.commit() + + return dl_file + + diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index 99130f6..11af99b 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -57,7 +57,7 @@ def list_state(): def delete_task(tid): payload = {} payload['tid'] = tid - payload['del_data'] = not not request.args.get('del_data', False) + payload['del_file'] = not not request.args.get('del_file', False) MSG.put('delete', payload) return json.dumps(MSG.get()) diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 1796a62..176c070 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import logging +import os from hashlib import sha1 @@ -61,20 +62,19 @@ def new_task(self, url, ydl_opts={}): def start_task(self, tid, ignore_state=False, first_run=False): """make an inactive type task into active type""" + task = None if tid in self._tasks_dict: task = self._tasks_dict[tid] - task.start() - return task + else: + try: + ydl_opts = self._db.get_ydl_opts(tid) + info = self._db.get_info(tid) + status = self._db.get_stat(tid) + except TaskInexistenceError as e: + raise TaskInexistenceError(e.msg) - try: - ydl_opts = self._db.get_ydl_opts(tid) - info = self._db.get_info(tid) - status = self._db.get_stat(tid) - except TaskInexistenceError as e: - raise TaskInexistenceError(e.msg) - - task = Task(tid, ydl_opts=ydl_opts, info=info, status=status) - self._tasks_dict[tid] = task + task = Task(tid, ydl_opts=ydl_opts, info=info, status=status) + self._tasks_dict[tid] = task task.start() @@ -101,7 +101,7 @@ def finish_task(self, tid): task.finish() del self._tasks_dict[tid] - def delete_task(self, tid, del_data=False): + def delete_task(self, tid, del_file=False): self.logger.debug('task deleted (%s)' %(tid)) if tid in self._tasks_dict: @@ -109,5 +109,10 @@ def delete_task(self, tid, del_data=False): task.halt() del self._tasks_dict[tid] - if del_data: - pass + try: + dl_file = self._db.delete_task(tid) + except TaskInexistenceError as e: + raise TaskInexistenceError(e.msg) + + if del_file and dl_file is not None: + os.remove(dl_file) From 2008ae4c25b7e817501eb24aafa789b65c72bfc7 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 08:28:49 +0800 Subject: [PATCH 40/88] more refactors --- youtube_dl_webui/db.py | 6 ++++++ youtube_dl_webui/task.py | 21 +++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index 73ab907..4599bcf 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -336,4 +336,10 @@ def delete_task(self, tid): return dl_file + def pause_task(self, tid): + self.logger.debug("db pause_task()") + + def finish_task(self, tid): + self.logger.debug("db finish_task()") + diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 176c070..10143f3 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -82,32 +82,41 @@ def start_task(self, tid, ignore_state=False, first_run=False): def pause_task(self, tid): self.logger.debug('task paused (%s)' %(tid)) + + if tid not in self._tasks_dict: + raise TaskInexistenceError + task = self._tasks_dict[tid] task.pause() + self._db.pause_task(tid) def halt_task(self, tid): self.logger.debug('task halted (%s)' %(tid)) if tid in self._tasks_dict: task = self._tasks_dict[tid] - task.halt() del self._tasks_dict[tid] + task.halt() def finish_task(self, tid): self.logger.debug('task finished (%s)' %(tid)) - if tid in self._tasks_dict: - task = self._tasks_dict[tid] - task.finish() - del self._tasks_dict[tid] + if tid not in self._tasks_dict: + raise TaskInexistenceError + + task = self._tasks_dict[tid] + del self._tasks_dict[tid] + task.finish() + + self._db.finish_task(tid) def delete_task(self, tid, del_file=False): self.logger.debug('task deleted (%s)' %(tid)) if tid in self._tasks_dict: task = self._tasks_dict[tid] - task.halt() del self._tasks_dict[tid] + task.halt() try: dl_file = self._db.delete_task(tid) From 3ebf1e0898ca87b9551049e8d1ed7ade5688de3c Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 10:39:39 +0800 Subject: [PATCH 41/88] db manipulations for start and pause task is OK --- youtube_dl_webui/db.py | 56 +++++++++++++++++++++++++++++++++++++--- youtube_dl_webui/task.py | 7 ++++- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index 4599bcf..e438c69 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -45,6 +45,16 @@ def __init__(self, db_path): self.db = db self.conn = conn + # Get tables + self.tables = {} + self.db.execute("SELECT name FROM sqlite_master WHERE type='table'") + for row in self.db.fetchall(): + self.tables[row['name']] = None + + for table in self.tables: + c = self.conn.execute('SELECT * FROM {}'.format(table)) + self.tables[table] = [desc[0] for desc in c.description] + def get_unfinished(self): self.db.execute('SELECT tid FROM task_status WHERE state not in (?,?)', @@ -268,7 +278,26 @@ def progress_update(self, tid, d): + def update(self, tid, val_dict={}): + for table, data in val_dict.items(): + if table not in self.tables: + self.logger.warning('table(%s) does not exist' %(table)) + continue + f, v = '', [] + for name, val in data.items(): + if name in self.tables[table]: + f = f + '{}=?,'.format(name) + v.append(val) + else: + self.logger.warning('field_name(%s) does not exist' %(name)) + else: + f = f[:-1] + + v.append(tid) + self.db.execute('UPDATE {} SET {} WHERE tid=(?)'.format(table, f), tuple(v)) + + self.conn.commit() def get_ydl_opts(self, tid): self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?) and state not in (?,?)', @@ -316,6 +345,30 @@ def new_task(self, url, ydl_opts): return tid + def start_task(self, tid, start_time=time()): + state = state_index['downloading'] + db_data = { + 'task_info': {'state': state}, + 'task_status': {'start_time': start_time, 'state': state}, + 'task_ydl_opt': {'state': state}, + } + self.update(tid, db_data) + + def pause_task(self, tid, elapsed, pause_time=time()): + self.logger.debug("db pause_task()") + state = state_index['paused'] + db_data = { + 'task_info': {'state': state}, + 'task_status': {'pause_time': pause_time, + 'eta': 0, + 'speed': 0, + 'elapsed': elapsed, + 'state': state, + }, + 'task_ydl_opt': {'state': state}, + } + self.update(tid, db_data) + def delete_task(self, tid): """ return the tmp file or file downloaded """ self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) @@ -336,9 +389,6 @@ def delete_task(self, tid): return dl_file - def pause_task(self, tid): - self.logger.debug("db pause_task()") - def finish_task(self, tid): self.logger.debug("db finish_task()") diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 10143f3..6963add 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -5,6 +5,7 @@ import os from hashlib import sha1 +from time import time from .config import ydl_conf from .utils import TaskInexistenceError @@ -24,9 +25,11 @@ def __init__(self, tid, ydl_opts={}, info={}, status={}): self.status = status def start(self): + self.start_time = time() print('---- task start ----') def pause(self): + self.pause_time = time() print('---- task pause ----') def halt(self): @@ -77,6 +80,7 @@ def start_task(self, tid, ignore_state=False, first_run=False): self._tasks_dict[tid] = task task.start() + self._db.start_task(tid, start_time=task.start_time) return task @@ -88,7 +92,8 @@ def pause_task(self, tid): task = self._tasks_dict[tid] task.pause() - self._db.pause_task(tid) + elapsed = task.pause_time - task.start_time + task.status['elapsed'] + self._db.pause_task(tid, pause_time=task.pause_time, elapsed=elapsed) def halt_task(self, tid): self.logger.debug('task halted (%s)' %(tid)) From 9805a985b8d5dfb3a7bfcfe708551c9c09a2c906 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 10:53:59 +0800 Subject: [PATCH 42/88] db manipulation for delete and halt --- youtube_dl_webui/db.py | 39 +++++++++++++++++++++++++++++++++++---- youtube_dl_webui/task.py | 26 +++++++++++++++++--------- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index e438c69..423f91b 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -369,6 +369,41 @@ def pause_task(self, tid, elapsed, pause_time=time()): } self.update(tid, db_data) + def finish_task(self, tid, elapsed, finish_time=time()): + self.logger.debug("db finish_task()") + state = state_index['finished'] + db_data = { + 'task_info': { 'state': state, + 'finish_time': finish_time, + }, + 'task_status': {'pause_time': finish_time, + 'eta': 0, + 'speed': 0, + 'elapsed': elapsed, + 'state': state, + 'percent': '100.0%', + }, + 'task_ydl_opt': {'state': state}, + } + self.update(tid, db_data) + + def halt_task(self, tid, elapsed, halt_time=time()): + self.logger.debug('db halt_task()') + state = state_index['invalid'] + db_data = { + 'task_info': { 'state': state, + 'finish_time': finish_time, + }, + 'task_status': {'pause_time': finish_time, + 'eta': 0, + 'speed': 0, + 'elapsed': elapsed, + 'state': state, + }, + 'task_ydl_opt': {'state': state}, + } + self.update(tid, db_data) + def delete_task(self, tid): """ return the tmp file or file downloaded """ self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) @@ -389,7 +424,3 @@ def delete_task(self, tid): return dl_file - def finish_task(self, tid): - self.logger.debug("db finish_task()") - - diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 6963add..618f100 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -33,9 +33,13 @@ def pause(self): print('---- task pause ----') def halt(self): + self.pause_time = time() + self.finish_time = time() print('---- task halt ----') def finish(self): + self.pause_time = time() + self.finish_time = time() print('---- task finish ----') pass @@ -95,14 +99,6 @@ def pause_task(self, tid): elapsed = task.pause_time - task.start_time + task.status['elapsed'] self._db.pause_task(tid, pause_time=task.pause_time, elapsed=elapsed) - def halt_task(self, tid): - self.logger.debug('task halted (%s)' %(tid)) - - if tid in self._tasks_dict: - task = self._tasks_dict[tid] - del self._tasks_dict[tid] - task.halt() - def finish_task(self, tid): self.logger.debug('task finished (%s)' %(tid)) @@ -112,8 +108,20 @@ def finish_task(self, tid): task = self._tasks_dict[tid] del self._tasks_dict[tid] task.finish() + elapsed = task.finish_time - task.start_time + task.status['elapsed'] + self._db.finish_task(tid, finish_time=task.finish_time, elapsed=elapsed) - self._db.finish_task(tid) + def halt_task(self, tid): + self.logger.debug('task halted (%s)' %(tid)) + + if tid not in self._tasks_dict: + raise TaskInexistenceError + + task = self._tasks_dict[tid] + del self._tasks_dict[tid] + task.halt() + elapsed = task.finish_time - task.start_time + task.status['elapsed'] + self._db.halt_task(tid, finish_time=task.halt_time, elapsed=elapsed) def delete_task(self, tid, del_file=False): self.logger.debug('task deleted (%s)' %(tid)) From 3240242eae621b8771a6aed510829a083a981144 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 10:54:06 +0800 Subject: [PATCH 43/88] bug fix --- youtube_dl_webui/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index ad9f352..87c35a7 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -55,7 +55,7 @@ def create_event(cls, svr, event, data, task_mgr): @classmethod def delete_event(cls, svr, event, data, task_mgr): tid = data['tid'] - del_file = True if data['del_data'] == 'true' else False + del_file = True if data['del_file'] == 'true' else False try: task_mgr.delete_task(tid, del_file) From ef488769fbe38b48fce42e3502d4c7084ae3b80b Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 10:57:51 +0800 Subject: [PATCH 44/88] refactor --- youtube_dl_webui/db.py | 2 +- youtube_dl_webui/task.py | 4 +--- youtube_dl_webui/utils.py | 4 ++++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index 423f91b..cf2c454 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -14,7 +14,7 @@ from .utils import TaskInexistenceError from .utils import TaskPausedError from .utils import TaskRunningError -from .task import url2tid +from .utils import url2tid class DataBase(object): def __init__(self, db_path): diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 618f100..558e731 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -12,9 +12,7 @@ from .utils import TaskRunningError from .utils import TaskExistenceError from .utils import TaskPausedError - -def url2tid(url): - return sha1(url.encode()).hexdigest() +from .utils import url2tid class Task(object): diff --git a/youtube_dl_webui/utils.py b/youtube_dl_webui/utils.py index ccf9586..8e2a73d 100644 --- a/youtube_dl_webui/utils.py +++ b/youtube_dl_webui/utils.py @@ -10,6 +10,10 @@ def new_uuid(): return str(uuid.uuid4().hex) +def url2tid(url): + return sha1(url.encode()).hexdigest() + + class YoutubeDLWebUI(Exception): """Base exception for YoutubeDL errors.""" pass From 8c10539d115b3beb5e0a2398e5aaa264cb411e87 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 13:14:33 +0800 Subject: [PATCH 45/88] rename event --- youtube_dl_webui/core.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 87c35a7..27f59e8 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -40,7 +40,7 @@ def init(cls, task_mgr): cls._task_mgr = task_mgr @classmethod - def create_event(cls, svr, event, data, task_mgr): + def event_create(cls, svr, event, data, task_mgr): cls.logger.debug('url = %s' %(data['url'])) try: tid = task_mgr.new_task(data['url'], {'proxy': '12.12.12.12'}) @@ -53,7 +53,7 @@ def create_event(cls, svr, event, data, task_mgr): svr.put({'status': 'success', 'tid': tid}) @classmethod - def delete_event(cls, svr, event, data, task_mgr): + def event_delete(cls, svr, event, data, task_mgr): tid = data['tid'] del_file = True if data['del_file'] == 'true' else False @@ -66,7 +66,7 @@ def delete_event(cls, svr, event, data, task_mgr): svr.put(cls.SuccessMsg) @classmethod - def manipulate_event(cls, svr, event, data, task_mgr): + def event_manipulation(cls, svr, event, data, task_mgr): cls.logger.debug('manipulation event') tid, act = data['tid'], data['act'] @@ -81,19 +81,19 @@ def manipulate_event(cls, svr, event, data, task_mgr): svr.put(ret_val) @classmethod - def query_event(cls, svr, event, data, arg): + def event_query(cls, svr, event, data, arg): svr.put({}) @classmethod - def list_event(cls, svr, event, data, arg): + def event_list(cls, svr, event, data, arg): svr.put({}) @classmethod - def state_event(cls, svr, event, data, arg): + def event_state(cls, svr, event, data, arg): svr.put({}) @classmethod - def config_event(cls, svr, event, data, arg): + def event_config(cls, svr, event, data, arg): svr.put({}) @@ -143,13 +143,13 @@ def __init__(self, cmd_args=None): WebMsgDispatcher.init(self.task_mgr) WorkMsgDispatcher.init(self.task_mgr) - self.msg_mgr.reg_event('create', WebMsgDispatcher.create_event, self.task_mgr) - self.msg_mgr.reg_event('delete', WebMsgDispatcher.delete_event, self.task_mgr) - self.msg_mgr.reg_event('manipulate', WebMsgDispatcher.manipulate_event, self.task_mgr) - self.msg_mgr.reg_event('query', WebMsgDispatcher.query_event) - self.msg_mgr.reg_event('list', WebMsgDispatcher.list_event) - self.msg_mgr.reg_event('state', WebMsgDispatcher.state_event) - self.msg_mgr.reg_event('config', WebMsgDispatcher.config_event) + self.msg_mgr.reg_event('create', WebMsgDispatcher.event_create, self.task_mgr) + self.msg_mgr.reg_event('delete', WebMsgDispatcher.event_delete, self.task_mgr) + self.msg_mgr.reg_event('manipulate', WebMsgDispatcher.event_manipulation, self.task_mgr) + self.msg_mgr.reg_event('query', WebMsgDispatcher.event_query) + self.msg_mgr.reg_event('list', WebMsgDispatcher.event_list) + self.msg_mgr.reg_event('state', WebMsgDispatcher.event_state) + self.msg_mgr.reg_event('config', WebMsgDispatcher.event_config) self.server = Server(web_cli, self.conf['server']['host'], self.conf['server']['port']) self.server.start() From 53e0e9e82f875daa8027c9c932326e46e15ee42a Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 13:48:51 +0800 Subject: [PATCH 46/88] bug fix --- youtube_dl_webui/server.py | 1 + youtube_dl_webui/utils.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index 11af99b..d8dcaa7 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -82,6 +82,7 @@ def manipulate_task(tid): @app.route('/task/tid//status', methods=['GET']) def query_task(tid): + payload = {} payload['tid'] = tid exerpt = request.args.get('exerpt', None) diff --git a/youtube_dl_webui/utils.py b/youtube_dl_webui/utils.py index 8e2a73d..eb3f8a1 100644 --- a/youtube_dl_webui/utils.py +++ b/youtube_dl_webui/utils.py @@ -3,6 +3,8 @@ import uuid +from hashlib import sha1 + state_index={'all': 0, 'downloading': 1, 'paused': 2, 'finished': 3, 'invalid': 4} state_name=['all', 'downloading', 'paused', 'finished', 'invalid'] From d8b3bb6c0ca321f7684e99247c83b77ce3f6576f Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 13:49:06 +0800 Subject: [PATCH 47/88] add query interface --- youtube_dl_webui/core.py | 20 ++++++++++++++------ youtube_dl_webui/db.py | 17 +++++++++++++++++ youtube_dl_webui/task.py | 14 +++++++++++++- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 27f59e8..1ee5509 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -61,9 +61,8 @@ def event_delete(cls, svr, event, data, task_mgr): task_mgr.delete_task(tid, del_file) except TaskInexistenceError: svr.put(cls.TaskInexistenceErrorMsg) - return - - svr.put(cls.SuccessMsg) + else: + svr.put(cls.SuccessMsg) @classmethod def event_manipulation(cls, svr, event, data, task_mgr): @@ -81,11 +80,20 @@ def event_manipulation(cls, svr, event, data, task_mgr): svr.put(ret_val) @classmethod - def event_query(cls, svr, event, data, arg): - svr.put({}) + def event_query(cls, svr, event, data, task_mgr): + cls.logger.debug('query event') + tid, exerpt = data['tid'], data['exerpt'] + + try: + detail = task_mgr.query(tid, exerpt) + except TaskInexistenceError: + svr.put(cls.TaskInexistenceErrorMsg) + else: + svr.put({'status': 'success', 'detail': detail}) @classmethod def event_list(cls, svr, event, data, arg): + tid, exerpt, state = data['tid'], data['exerpt'], data['state'] svr.put({}) @classmethod @@ -146,7 +154,7 @@ def __init__(self, cmd_args=None): self.msg_mgr.reg_event('create', WebMsgDispatcher.event_create, self.task_mgr) self.msg_mgr.reg_event('delete', WebMsgDispatcher.event_delete, self.task_mgr) self.msg_mgr.reg_event('manipulate', WebMsgDispatcher.event_manipulation, self.task_mgr) - self.msg_mgr.reg_event('query', WebMsgDispatcher.event_query) + self.msg_mgr.reg_event('query', WebMsgDispatcher.event_query, self.task_mgr) self.msg_mgr.reg_event('list', WebMsgDispatcher.event_list) self.msg_mgr.reg_event('state', WebMsgDispatcher.event_state) self.msg_mgr.reg_event('config', WebMsgDispatcher.event_config) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index cf2c454..9e84648 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -424,3 +424,20 @@ def delete_task(self, tid): return dl_file + def query_task(self, tid): + self.db.execute('SELECT * FROM task_status, task_info WHERE task_status.tid=(?) and task_info.tid=(?)', (tid, tid)) + row = self.db.fetchone() + if row is None: + raise TaskInexistenceError('') + + ret = {} + for key in row.keys(): + if key == 'state': + ret[key] = state_name[row[key]] + elif key == 'log': + ret['log'] = json.loads(row['log']) + else: + ret[key] = row[key] + + return ret + diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 558e731..01738f4 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -4,7 +4,6 @@ import logging import os -from hashlib import sha1 from time import time from .config import ydl_conf @@ -51,6 +50,7 @@ class TaskManager(object): are in invalid state or finished state, which only have database recoards but memory instance. """ + ExerptKeys = ['tid', 'state', 'percent', 'total_bytes', 'title', 'eta', 'speed'] def __init__(self, db, msg_cli): self.logger = logging.getLogger('ydl_webui') @@ -136,3 +136,15 @@ def delete_task(self, tid, del_file=False): if del_file and dl_file is not None: os.remove(dl_file) + + def query(self, tid, exerpt=True): + db_ret = self._db.query_task(tid) + + detail = {} + if exerpt: + detail = {k: db_ret[k] for k in ret if k in self.ExerptKeys} + else: + detail = db_ret + + return detail + From 9c5b69306598685f92e337285a8d0774d67bd64d Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 14:35:31 +0800 Subject: [PATCH 48/88] basic interface is ok --- REST_API.txt | 1 - youtube_dl_webui/core.py | 21 ++++++++++++++------- youtube_dl_webui/db.py | 33 +++++++++++++++++++++++++++++++++ youtube_dl_webui/server.py | 4 ++-- youtube_dl_webui/task.py | 16 ++++++++++++++++ 5 files changed, 65 insertions(+), 10 deletions(-) diff --git a/REST_API.txt b/REST_API.txt index 36a922d..4c70e81 100644 --- a/REST_API.txt +++ b/REST_API.txt @@ -16,7 +16,6 @@ | 10 | GET | /task/list?exerpt=false | get task list | | 11 | GET | /task/list?state={all|downloading|finished|paused} | get task list exerpt according to the state | | 12 | GET | /task/list?exerpt=false&state={all|downloading|finished|paused} | get task list according to the state | -| 13 | GET | /task/list | get task list of all tasks | | 14 | GET | /task/state_coutner | get number of tasks in each state | | 15 | GET | /config | get the server's configuration | |----+--------+-----------------------------------------------------------------+----------------------------------------------| diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 1ee5509..71884a4 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -32,6 +32,7 @@ class WebMsgDispatcher(object): TaskExistenceErrorMsg = {'status': 'error', 'errmsg': 'URL is already added'} TaskInexistenceErrorMsg = {'status': 'error', 'errmsg': 'Task does not exist'} UrlErrorMsg = {'status': 'error', 'errmsg': 'URL is invalid'} + InvalidStateMsg = {'status': 'error', 'errmsg': 'invalid query state'} _task_mgr = None @@ -92,13 +93,19 @@ def event_query(cls, svr, event, data, task_mgr): svr.put({'status': 'success', 'detail': detail}) @classmethod - def event_list(cls, svr, event, data, arg): - tid, exerpt, state = data['tid'], data['exerpt'], data['state'] - svr.put({}) + def event_list(cls, svr, event, data, task_mgr): + exerpt, state = data['exerpt'], data['state'] + + if state not in state_name: + svr.put(cls.InvalidStateMsg) + else: + d, c = task_mgr.list(state, exerpt) + svr.put({'status': 'success', 'detail': d, 'state_counter': c}) @classmethod - def event_state(cls, svr, event, data, arg): - svr.put({}) + def event_state(cls, svr, event, data, task_mgr): + c = task_mgr.state() + svr.put({'status': 'success', 'detail': c}) @classmethod def event_config(cls, svr, event, data, arg): @@ -155,8 +162,8 @@ def __init__(self, cmd_args=None): self.msg_mgr.reg_event('delete', WebMsgDispatcher.event_delete, self.task_mgr) self.msg_mgr.reg_event('manipulate', WebMsgDispatcher.event_manipulation, self.task_mgr) self.msg_mgr.reg_event('query', WebMsgDispatcher.event_query, self.task_mgr) - self.msg_mgr.reg_event('list', WebMsgDispatcher.event_list) - self.msg_mgr.reg_event('state', WebMsgDispatcher.event_state) + self.msg_mgr.reg_event('list', WebMsgDispatcher.event_list, self.task_mgr) + self.msg_mgr.reg_event('state', WebMsgDispatcher.event_state, self.task_mgr) self.msg_mgr.reg_event('config', WebMsgDispatcher.event_config) self.server = Server(web_cli, self.conf['server']['host'], self.conf['server']['port']) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index 9e84648..f034a56 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -441,3 +441,36 @@ def query_task(self, tid): return ret + def list_task(self, state): + self.db.execute('SELECT * FROM task_status, task_info WHERE task_status.tid=task_info.tid') + rows = self.db.fetchall() + + state_counter = {'downloading': 0, 'paused': 0, 'finished': 0, 'invalid': 0} + ret_val = [] + for row in rows: + t = {} + for key in row.keys(): + if key == 'state': + s = row[key] + t[key] = state_name[s] + state_counter[state_name[s]] += 1 + elif key == 'log': + t['log'] = json.loads(row[key]) + else: + t[key] = row[key] + + if state == 'all' or t['state'] == state: + ret_val.append(t) + + return ret_val, state_counter + + def state_counter(self): + state_counter = {'downloading': 0, 'paused': 0, 'finished': 0, 'invalid': 0} + + self.db.execute('SELECT state, count(*) as NUM FROM task_status GROUP BY state') + rows = self.db.fetchall() + + for r in rows: + state_counter[state_name[r['state']]] = r['NUM'] + + return state_counter diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index d8dcaa7..51ed2b9 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -37,9 +37,9 @@ def list_task(): payload = {} exerpt = request.args.get('exerpt', None) if exerpt is None: - payload['exerpt'] = True - else: payload['exerpt'] = False + else: + payload['exerpt'] = True payload['state'] = request.args.get('state', 'all') diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 01738f4..e062721 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -148,3 +148,19 @@ def query(self, tid, exerpt=True): return detail + def list(self, state, exerpt=False): + db_ret, counter = self._db.list_task(state) + + detail = [] + if exerpt is not True: + for item in db_ret: + d = {k: item[k] for k in item if k in self.ExerptKeys} + detail.append(d) + else: + detail = db_ret + + return detail, counter + + def state(self): + return self._db.state_counter() + From 7bd1aa414cdf741c5a20affbdf41149b9447eb0a Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 14:37:38 +0800 Subject: [PATCH 49/88] purge core.py --- youtube_dl_webui/__init__.py | 2 +- youtube_dl_webui/core.py | 256 +---------------------------------- 2 files changed, 3 insertions(+), 255 deletions(-) diff --git a/youtube_dl_webui/__init__.py b/youtube_dl_webui/__init__.py index 572cb12..c719839 100644 --- a/youtube_dl_webui/__init__.py +++ b/youtube_dl_webui/__init__.py @@ -24,4 +24,4 @@ def main(argv=None): cmd_args = getopt(argv) core = Core(cmd_args=cmd_args) - core.run() + core.start() diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 71884a4..a0445f8 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -167,260 +167,8 @@ def __init__(self, cmd_args=None): self.msg_mgr.reg_event('config', WebMsgDispatcher.event_config) self.server = Server(web_cli, self.conf['server']['host'], self.conf['server']['port']) - self.server.start() - - self.msg_mgr.run() - - exit(1) - self.rq = Queue() - self.wq = Queue() - self.worker = {} - - self.logger.debug("configuration: \n%s", json.dumps(self.conf.dict(), indent=4)) - - self.server = Server(self.wq, self.rq, self.conf['server']['host'], self.conf['server']['port'], web_cli) - self.db = DataBase(self.conf['general']['db_path']) - - dl_dir = self.conf['general']['download_dir'] - try: - os.makedirs(dl_dir, exist_ok=True) - self.logger.info("Download dir: %s", dl_dir) - os.chdir(dl_dir) - except PermissionError: - self.logger.critical('Permission Error for download dir: %s', dl_dir) - exit(1) - - self.launch_unfinished() + def start(self): self.server.start() - - - def run(self): - while True: - data = self.rq.get() - data_from = data.get('from', None) - if data_from == 'server': - ret = self.server_request(data) - self.wq.put(ret) - elif data_from == 'worker': - ret = self.worker_request(data) - else: - logger.debug(data) - - - def launch_unfinished(self): - tlist = self.db.get_unfinished() - for t in tlist: - self.start_task(t, ignore_state=True) - - - def create_task(self, param, ydl_opts): - if 'url' not in param: - raise KeyError - - if param['url'].strip() == '': - raise KeyError - - opts = {} - for key, val in self.conf['ydl'].items(): - if key in ydl_opts: - opts[key] = ydl_opts[key] - else: - opts[key] = self.conf['ydl'][key] - - tid = self.db.create_task(param, opts) - return tid - - - def start_task(self, tid, ignore_state=False, first_run=False): - try: - param = self.db.get_param(tid) - ydl_opts = self.db.get_opts(tid) - except TaskInexistenceError as e: - raise TaskInexistenceError(e.msg) - - log_list = self.db.start_task(tid, ignore_state) - self.launch_worker(tid, log_list, param=param, ydl_opts=ydl_opts, first_run=first_run) - - - def pause_task(self, tid): - self.cancel_worker(tid) - - - def delete_task(self, tid, del_data=False): - try: - self.cancel_worker(tid) - except TaskInexistenceError as e: - raise e - except: - pass - - self.db.delete_task(tid, del_data=del_data) - - - def launch_worker(self, tid, log_list, param=None, ydl_opts={}, first_run=False): - if tid in self.worker: - raise TaskRunningError('task already running') - - self.worker[tid] ={'obj': None, 'log': deque(maxlen=10)} - - for l in log_list: - self.worker[tid]['log'].appendleft(l) - - self.worker[tid]['log'].appendleft({'time': int(time()), 'type': 'debug', 'msg': 'Task starts...'}) - self.db.update_log(tid, self.worker[tid]['log']) - - self.logger.debug("ydl_opts(%s): %s" %(tid, json.dumps(ydl_opts))) - - # launch worker process - w = Worker(tid, self.rq, param=param, ydl_opts=ydl_opts, first_run=first_run) - w.start() - self.worker[tid]['obj'] = w - - - def cancel_worker(self, tid): - if tid not in self.worker: - raise TaskPausedError('task not running') - - w = self.worker[tid] - self.db.cancel_task(tid, log=w['log']) - w['obj'].stop() - self.worker[tid]['log'].appendleft({'time': int(time()), 'type': 'debug', 'msg': 'Task stops...'}) - self.db.update_log(tid, self.worker[tid]['log']) - - del self.worker[tid] - - - def cmdl_override_conf_file(self): - if self.cmdl_args_dict['host'] is not None: - self.conf['server']['host'] = self.cmdl_args_dict['host'] - - if self.cmdl_args_dict['port'] is not None: - self.conf['server']['port'] = self.cmdl_args_dict['port'] - - - def update_config(self, config): - print(config) - - - def server_request(self, data): - msg_internal_error = {'status': 'error', 'errmsg': 'Internal Error'} - msg_task_existence_error = {'status': 'error', 'errmsg': 'URL is already added'} - msg_task_inexistence_error = {'status': 'error', 'errmsg': 'Task does not exist'} - msg_url_error = {'status': 'error', 'errmsg': 'URL is invalid'} - if data['command'] == 'create': - try: - tid = self.create_task(data['param'], {}) - self.start_task(tid, first_run=True) - except TaskExistenceError: - return msg_task_existence_error - except TaskInexistenceError: - return msg_internal_error - except KeyError: - return msg_url_error - - return {'status': 'success', 'tid': tid} - - if data['command'] == 'delete': - try: - self.delete_task(data['tid'], del_data=data['del_data']) - except TaskInexistenceError: - return msg_task_inexistence_error - - return {'status': 'success'} - - if data['command'] == 'manipulate': - tid = data['tid'] - try: - if data['act'] == 'pause': - self.pause_task(tid) - elif data['act'] == 'resume': - self.start_task(tid) - except TaskPausedError: - return {'status': 'error', 'errmsg': 'task paused already'} - except TaskRunningError: - return {'status': 'error', 'errmsg': 'task running already'} - except TaskInexistenceError: - return msg_task_inexistence_error - - return {'status': 'success'} - - if data['command'] == 'query': - tid = data['tid'] - try: - ret = self.db.query_task(tid) - except TaskInexistenceError: - return msg_task_inexistence_error - - detail = {} - if data['exerpt'] is True: - detail = {k: ret[k] for k in ret if k in Core.exerpt_keys} - else: - detail = ret - - return {'status': 'success', 'detail': detail} - - if data['command'] == 'list': - state = data['state'] - if state not in state_name: - return {'status': 'error', 'errmsg': 'invalid query state'} - - ret, counter = self.db.list_task(state) - - detail = [] - if data['exerpt'] is True: - for each in ret: - d = {k: each[k] for k in each if k in Core.exerpt_keys} - detail.append(d) - else: - detail = ret - - return {'status': 'success', 'detail': detail, 'state_counter': counter} - - if data['command'] == 'state': - return self.db.list_state() - - if data['command'] == 'config': - if data['act'] == 'get': - return {'status': 'success', 'config': self.conf} - elif data['act'] == 'update': - self.update_config(data['param']) - return {'status': 'success'} - else: - return {'status': 'error', 'errmsg': 'invalid query'} - - - def worker_request(self, data): - tid = data['tid'] - msgtype = data['msgtype'] - - if msgtype == 'info_dict': - self.db.update_from_info_dict(tid, data['data']) - return - - if msgtype == 'log': - if tid not in self.worker: - return - - self.worker[tid]['log'].appendleft(data['data']) - self.db.update_log(tid, self.worker[tid]['log']) - - if msgtype == 'progress': - d = data['data'] - - if d['status'] == 'downloading': - self.db.progress_update(tid, d) - - if d['status'] == 'finished': - self.worker[tid]['log'].appendleft({'time': int(time()), 'type': 'debug', 'msg': 'Task is done'}) - self.cancel_worker(tid) - self.db.progress_update(tid, d) - self.db.set_state(tid, 'finished') - - if msgtype == 'fatal': - d = data['data'] - - if d['type'] == 'invalid_url': - self.logger.error("Can't start downloading {}, url is invalid".format(d['url'])) - self.db.set_state(tid, 'invalid') + self.msg_mgr.run() From f9b9467355fa4f14c8462992ef4d3644baf0708b Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 17:13:19 +0800 Subject: [PATCH 50/88] log pass communication is ok --- youtube_dl_webui/core.py | 63 +++++++++------ youtube_dl_webui/db.py | 34 +++++++- youtube_dl_webui/task.py | 66 +++++++++++++--- youtube_dl_webui/worker.py | 156 +++++++++++++++++-------------------- 4 files changed, 196 insertions(+), 123 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index a0445f8..f2aecd8 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -41,70 +41,71 @@ def init(cls, task_mgr): cls._task_mgr = task_mgr @classmethod - def event_create(cls, svr, event, data, task_mgr): + def event_create(cls, svr, event, data, args): cls.logger.debug('url = %s' %(data['url'])) try: - tid = task_mgr.new_task(data['url'], {'proxy': '12.12.12.12'}) + ydl_opts = cls._task_mgr.ydl_conf.dict() + tid = cls._task_mgr.new_task(data['url'], ydl_opts=ydl_opts) except TaskExistenceError: svr.put(cls.TaskExistenceErrorMsg) return - task = task_mgr.start_task(tid) + task = cls._task_mgr.start_task(tid, first_run=True) svr.put({'status': 'success', 'tid': tid}) @classmethod - def event_delete(cls, svr, event, data, task_mgr): + def event_delete(cls, svr, event, data, args): tid = data['tid'] del_file = True if data['del_file'] == 'true' else False try: - task_mgr.delete_task(tid, del_file) + cls._task_mgr.delete_task(tid, del_file) except TaskInexistenceError: svr.put(cls.TaskInexistenceErrorMsg) else: svr.put(cls.SuccessMsg) @classmethod - def event_manipulation(cls, svr, event, data, task_mgr): + def event_manipulation(cls, svr, event, data, args): cls.logger.debug('manipulation event') tid, act = data['tid'], data['act'] ret_val = cls.InternalErrorMsg if act == 'pause': - task_mgr.pause_task(tid) + cls._task_mgr.pause_task(tid) ret_val = cls.SuccessMsg elif act == 'resume': - task_mgr.start_task(tid) + cls._task_mgr.start_task(tid) ret_val = cls.SuccessMsg svr.put(ret_val) @classmethod - def event_query(cls, svr, event, data, task_mgr): + def event_query(cls, svr, event, data, args): cls.logger.debug('query event') tid, exerpt = data['tid'], data['exerpt'] try: - detail = task_mgr.query(tid, exerpt) + detail = cls._task_mgr.query(tid, exerpt) except TaskInexistenceError: svr.put(cls.TaskInexistenceErrorMsg) else: svr.put({'status': 'success', 'detail': detail}) @classmethod - def event_list(cls, svr, event, data, task_mgr): + def event_list(cls, svr, event, data, args): exerpt, state = data['exerpt'], data['state'] if state not in state_name: svr.put(cls.InvalidStateMsg) else: - d, c = task_mgr.list(state, exerpt) + d, c = cls._task_mgr.list(state, exerpt) svr.put({'status': 'success', 'detail': d, 'state_counter': c}) @classmethod - def event_state(cls, svr, event, data, task_mgr): - c = task_mgr.state() + def event_state(cls, svr, event, data, args): + c = cls._task_mgr.state() svr.put({'status': 'success', 'detail': c}) @classmethod @@ -114,8 +115,21 @@ def event_config(cls, svr, event, data, arg): class WorkMsgDispatcher(object): - def init(cls, __unknow__=None): - pass + _task_mgr = None + + @classmethod + def init(cls, task_mgr): + cls._task_mgr = task_mgr + + @classmethod + def event_info_dict(cls, svr, event, data, arg): + tid, info_dict = data['tid'], data['data'] + cls._task_mgr.update_info(tid, info_dict) + + @classmethod + def event_log(cls, svr, event, data, arg): + tid, log = data['tid'], data['data'] + cls._task_mgr.update_log(tid, log) def load_conf_from_file(cmd_args): @@ -153,19 +167,22 @@ def __init__(self, cmd_args=None): task_cli = self.msg_mgr.new_cli() self.db = DataBase(self.conf['general']['db_path']) - self.task_mgr = TaskManager(self.db, task_cli) + self.task_mgr = TaskManager(self.db, task_cli, self.conf) WebMsgDispatcher.init(self.task_mgr) WorkMsgDispatcher.init(self.task_mgr) - self.msg_mgr.reg_event('create', WebMsgDispatcher.event_create, self.task_mgr) - self.msg_mgr.reg_event('delete', WebMsgDispatcher.event_delete, self.task_mgr) - self.msg_mgr.reg_event('manipulate', WebMsgDispatcher.event_manipulation, self.task_mgr) - self.msg_mgr.reg_event('query', WebMsgDispatcher.event_query, self.task_mgr) - self.msg_mgr.reg_event('list', WebMsgDispatcher.event_list, self.task_mgr) - self.msg_mgr.reg_event('state', WebMsgDispatcher.event_state, self.task_mgr) + self.msg_mgr.reg_event('create', WebMsgDispatcher.event_create) + self.msg_mgr.reg_event('delete', WebMsgDispatcher.event_delete) + self.msg_mgr.reg_event('manipulate', WebMsgDispatcher.event_manipulation) + self.msg_mgr.reg_event('query', WebMsgDispatcher.event_query) + self.msg_mgr.reg_event('list', WebMsgDispatcher.event_list) + self.msg_mgr.reg_event('state', WebMsgDispatcher.event_state) self.msg_mgr.reg_event('config', WebMsgDispatcher.event_config) + self.msg_mgr.reg_event('info_dict', WorkMsgDispatcher.event_info_dict) + self.msg_mgr.reg_event('log', WorkMsgDispatcher.event_log) + self.server = Server(web_cli, self.conf['server']['host'], self.conf['server']['port']) def start(self): diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index f034a56..2aa75a5 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -229,7 +229,7 @@ def update_from_info_dict(self, tid, info_dict): self.conn.commit() - def update_log(self, tid, log): + def update_log_ob(self, tid, log): self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) row = self.db.fetchone() if row is None: @@ -287,8 +287,9 @@ def update(self, tid, val_dict={}): f, v = '', [] for name, val in data.items(): if name in self.tables[table]: - f = f + '{}=?,'.format(name) - v.append(val) + if val is not None: + f = f + '{}=(?),'.format(name) + v.append(val) else: self.logger.warning('field_name(%s) does not exist' %(name)) else: @@ -474,3 +475,30 @@ def state_counter(self): state_counter[state_name[r['state']]] = r['NUM'] return state_counter + + def update_info(self, tid, info_dict): + self.logger.debug('db update_info()') + db_data = { + 'title': info_dict['title'], + 'format': info_dict['format'], + 'ext': info_dict['ext'], + 'thumbnail': info_dict['thumbnail'], + 'duration': info_dict['duration'], + 'view_count': info_dict['view_count'], + 'like_count': info_dict['like_count'], + 'dislike_count': info_dict['dislike_count'], + 'average_rating': info_dict['average_rating'], + 'description': info_dict['description'], + } + self.update(tid, {'task_info': db_data}) + + def update_log(self, tid, log, exist_test=False): + if exist_test: + self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) + row = self.db.fetchone() + if row is None: + raise TaskInexistenceError('') + + log_str = json.dumps([l for l in log]) + self.update(tid, {'task_status': {'log': log_str}}) + diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index e062721..55fc6f9 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -3,8 +3,10 @@ import logging import os +import json from time import time +from collections import deque from .config import ydl_conf from .utils import TaskInexistenceError @@ -13,32 +15,56 @@ from .utils import TaskPausedError from .utils import url2tid +from .worker import Worker + class Task(object): - def __init__(self, tid, ydl_opts={}, info={}, status={}): + def __init__(self, tid, msg_cli, ydl_opts={}, info={}, status={}): self.tid = tid + self.ydl_opts = ydl_opts self.ydl_conf = ydl_conf(ydl_opts) self.info = info self.status = status + self.log = deque(maxlen=10) + self.msg_cli = msg_cli + + log_list = json.loads(status['log']) + for log in log_list: + self.log.appendleft(log) - def start(self): - self.start_time = time() + def start(self, first_run=False): print('---- task start ----') + tm = time() + self.start_time = tm + self.worker = Worker(self.tid, self.info['url'], + msg_cli=self.msg_cli, + ydl_opts=self.ydl_opts, + first_run=first_run) + self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task starts...'}) + self.worker.start() def pause(self): - self.pause_time = time() + tm = time() + self.pause_time = tm + self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task pauses...'}) print('---- task pause ----') def halt(self): - self.pause_time = time() - self.finish_time = time() + tm = time() + self.pause_time = tm + self.finish_time = tm + self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task stops...'}) print('---- task halt ----') def finish(self): - self.pause_time = time() - self.finish_time = time() + tm = time() + self.pause_time = tm + self.finish_time = tm + self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task finishs...'}) print('---- task finish ----') - pass + + def update_log(self, log): + self.log.appendleft(log) class TaskManager(object): @@ -52,10 +78,12 @@ class TaskManager(object): """ ExerptKeys = ['tid', 'state', 'percent', 'total_bytes', 'title', 'eta', 'speed'] - def __init__(self, db, msg_cli): + def __init__(self, db, msg_cli, conf): self.logger = logging.getLogger('ydl_webui') self._db = db self._msg_cli = msg_cli + self._conf = conf + self.ydl_conf = conf['youtube_dl'] self._tasks_dict = {} @@ -78,11 +106,12 @@ def start_task(self, tid, ignore_state=False, first_run=False): except TaskInexistenceError as e: raise TaskInexistenceError(e.msg) - task = Task(tid, ydl_opts=ydl_opts, info=info, status=status) + task = Task(tid, self._msg_cli, ydl_opts=ydl_opts, info=info, status=status) self._tasks_dict[tid] = task - task.start() + task.start(first_run=first_run) self._db.start_task(tid, start_time=task.start_time) + self._db.update_log(tid, task.log) return task @@ -96,6 +125,7 @@ def pause_task(self, tid): task.pause() elapsed = task.pause_time - task.start_time + task.status['elapsed'] self._db.pause_task(tid, pause_time=task.pause_time, elapsed=elapsed) + self._db.update_log(tid, task.log) def finish_task(self, tid): self.logger.debug('task finished (%s)' %(tid)) @@ -108,6 +138,7 @@ def finish_task(self, tid): task.finish() elapsed = task.finish_time - task.start_time + task.status['elapsed'] self._db.finish_task(tid, finish_time=task.finish_time, elapsed=elapsed) + self._db.update_log(tid, task.log) def halt_task(self, tid): self.logger.debug('task halted (%s)' %(tid)) @@ -120,6 +151,7 @@ def halt_task(self, tid): task.halt() elapsed = task.finish_time - task.start_time + task.status['elapsed'] self._db.halt_task(tid, finish_time=task.halt_time, elapsed=elapsed) + self._db.update_log(tid, task.log) def delete_task(self, tid, del_file=False): self.logger.debug('task deleted (%s)' %(tid)) @@ -164,3 +196,13 @@ def list(self, state, exerpt=False): def state(self): return self._db.state_counter() + def update_info(self, tid, info_dict): + self._db.update_info(tid, info_dict) + + def update_log(self, tid, log): + if tid not in self._tasks_dict: + raise TaskInexistenceError + + task = self._tasks_dict[tid] + task.update_log(log) + self._db.update_log(tid, task.log, exist_test=False) diff --git a/youtube_dl_webui/worker.py b/youtube_dl_webui/worker.py index cb2a6c6..2762873 100644 --- a/youtube_dl_webui/worker.py +++ b/youtube_dl_webui/worker.py @@ -13,91 +13,80 @@ from copy import deepcopy WQ_DICT = {'from': 'worker'} +MSG = None -class ydl_hook(object): - def __init__(self, tid, wqueue): +class YdlHook(object): + def __init__(self, tid, msg_cli): self.logger = logging.getLogger('ydl_webui') self.tid = tid - self.wq = wqueue - self.wqd = deepcopy(WQ_DICT) - self.wqd['tid'] = self.tid - self.wqd['msgtype'] = 'progress' - self.wqd['data'] = None + self.msg_cli = msg_cli + # self.wqd = deepcopy(WQ_DICT) + # self.wqd['tid'] = self.tid + # self.wqd['msgtype'] = 'progress' + # self.wqd['data'] = None def finished(self, d): self.logger.debug('finished status') - d['_percent_str'] = '100%' - d['speed'] = '0' - d['elapsed'] = 0 - d['eta'] = 0 - d['downloaded_bytes'] = d['total_bytes'] + # d['_percent_str'] = '100%' + # d['speed'] = '0' + # d['elapsed'] = 0 + # d['eta'] = 0 + # d['downloaded_bytes'] = d['total_bytes'] - return d + # return d def downloading(self, d): self.logger.debug('downloading status') - return d + # return d def error(self, d): self.logger.debug('error status') - d['_percent_str'] = '100%' - return d + # d['_percent_str'] = '100%' + # return d def dispatcher(self, d): - if 'total_bytes_estimate' not in d: - d['total_bytes_estimate'] = 0 - if 'tmpfilename' not in d: - d['tmpfilename'] = '' + pass + # if 'total_bytes_estimate' not in d: + # d['total_bytes_estimate'] = 0 + # if 'tmpfilename' not in d: + # d['tmpfilename'] = '' - if d['status'] == 'finished': - d = self.finished(d) - elif d['status'] == 'downloading': - d = self.downloading(d) - elif d['error'] == 'error': - d = self.error(d) + # if d['status'] == 'finished': + # d = self.finished(d) + # elif d['status'] == 'downloading': + # d = self.downloading(d) + # elif d['error'] == 'error': + # d = self.error(d) - self.wqd['data'] = d - self.wq.put(self.wqd) + # self.wqd['data'] = d + # self.wq.put(self.wqd) -class log_filter(object): - def __init__(self, tid, wqueue): +class LogFilter(object): + def __init__(self, tid, msg_cli): + self.logger = logging.getLogger('ydl_webui') self.tid = tid - self.wq = wqueue - self.wqd = deepcopy(WQ_DICT) - self.wqd['tid'] = self.tid - self.wqd['msgtype'] = 'log' - self.data = {'time': None, 'type': None, 'msg': None} - self.wqd['data'] = self.data - + self.msg_cli = msg_cli def debug(self, msg): - self.data['time'] = int(time()) - self.data['type'] = 'debug' - self.data['msg'] = self.ansi_escape(msg) - self.wq.put(self.wqd) - + self.logger.debug('debug: %s' %(self.ansi_escape(msg))) + payload = {'time': int(time()), 'type': 'debug', 'msg': self.ansi_escape(msg)} + self.msg_cli.put('log', {'tid': self.tid, 'data': payload}) def warning(self, msg): - self.data['time'] = int(time()) - self.data['type'] = 'warning' - self.data['msg'] = self.ansi_escape(msg) - self.wq.put(self.wqd) - + self.logger.debug('warning: %s' %(self.ansi_escape(msg))) + payload = {'time': int(time()), 'type': 'warning', 'msg': self.ansi_escape(msg)} + self.msg_cli.put('log', {'tid': self.tid, 'data': payload}) def error(self, msg): - self.data['time'] = int(time()) - self.data['type'] = 'error' - self.data['msg'] = self.ansi_escape(msg) - self.wq.put(self.wqd) - - self.ansi_escape(msg) - + self.logger.debug('error: %s' %(self.ansi_escape(msg))) + payload = {'time': int(time()), 'type': 'warning', 'error': self.ansi_escape(msg)} + self.msg_cli.put('log', {'tid': self.tid, 'data': payload}) def ansi_escape(self, msg): reg = r'\x1b\[([0-9,A-Z]{1,2}(;[0-9]{1,2})?(;[0-9]{3})?)?[m|K]?' @@ -106,67 +95,64 @@ def ansi_escape(self, msg): class fatal_event(object): def __init__(self, tid, wqueue): - self.tid = tid - self.wq = wqueue - self.wqd = deepcopy(WQ_DICT) - self.wqd['tid'] = self.tid - self.wqd['msgtype'] = 'fatal' - self.data = {'time': None, 'type': None, 'msg': None} - self.wqd['data'] = self.data + pass + # self.tid = tid + # self.wq = wqueue + # self.wqd = deepcopy(WQ_DICT) + # self.wqd['tid'] = self.tid + # self.wqd['msgtype'] = 'fatal' + # self.data = {'time': None, 'type': None, 'msg': None} + # self.wqd['data'] = self.data def invalid_url(self, url): - self.data['time'] = int(time()) - self.data['type'] = 'invalid_url' - self.data['url'] = url; - self.data['msg'] = 'invalid url: {}'.format(url) - self.wq.put(self.wqd) + pass + # self.data['time'] = int(time()) + # self.data['type'] = 'invalid_url' + # self.data['url'] = url; + # self.data['msg'] = 'invalid url: {}'.format(url) + # self.wq.put(self.wqd) class Worker(Process): - def __init__(self, tid, wqueue, param=None, ydl_opts=None, first_run=False): + def __init__(self, tid, url, msg_cli, ydl_opts=None, first_run=False): super(Worker, self).__init__() self.logger = logging.getLogger('ydl_webui') self.tid = tid - self.wq = wqueue - self.param = param - self.url = param['url'] + self.url = url + self.msg_cli = msg_cli self.ydl_opts = ydl_opts self.first_run = first_run - self.log_filter = log_filter(tid, self.wq) - self.ydl_hook = ydl_hook(tid, self.wq) - + self.log_filter = LogFilter(tid, msg_cli) + self.ydl_hook = YdlHook(tid, msg_cli) def intercept_ydl_opts(self): self.ydl_opts['logger'] = self.log_filter self.ydl_opts['progress_hooks'] = [self.ydl_hook.dispatcher] self.ydl_opts['noplaylist'] = "false" - def run(self): self.intercept_ydl_opts() - with YoutubeDL(self.ydl_opts) as ydl: try: if self.first_run: + print(self.url) info_dict = ydl.extract_info(self.url, download=False) - # self.logger.debug(json.dumps(info_dict, indent=4)) + # self.logger.debug(json.dumps(info_dict, indent=4)) + print(info_dict['like_count']) info_dict['description'] = info_dict['description'].replace('\n', '
'); - - wqd = deepcopy(WQ_DICT) - wqd['tid'] = self.tid - wqd['msgtype'] = 'info_dict' - wqd['data'] = info_dict - self.wq.put(wqd) + payload = {'tid': self.tid, 'data': info_dict} + self.msg_cli.put('info_dict', payload) self.logger.info('start downloading ...') - ydl.download([self.url]) + # ydl.download([self.url]) except DownloadError as e: # url error - event_handle = fatal_event(self.tid, self.wq) - event_handle.invalid_url(self.url); + # event_handle = fatal_event(self.tid, self.wq) + # event_handle.invalid_url(self.url); + pass def stop(self): From a09dadd0dbbbd677979e93fabba464e38815f03e Mon Sep 17 00:00:00 2001 From: d0u9 Date: Wed, 6 Sep 2017 17:23:03 +0800 Subject: [PATCH 51/88] invalid url process --- youtube_dl_webui/core.py | 8 ++++++++ youtube_dl_webui/task.py | 2 +- youtube_dl_webui/worker.py | 30 +++++++++--------------------- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index f2aecd8..958b88c 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -131,6 +131,14 @@ def event_log(cls, svr, event, data, arg): tid, log = data['tid'], data['data'] cls._task_mgr.update_log(tid, log) + @classmethod + def event_fatal(cls, svr, event, data, arg): + tid, data = data['tid'], data['data'] + + cls._task_mgr.update_log(tid, data) + if data['type'] == 'invalid_url': + cls._task_mgr.halt_task(tid) + def load_conf_from_file(cmd_args): logger = logging.getLogger('ydl_webui') diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 55fc6f9..8383099 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -53,7 +53,7 @@ def halt(self): tm = time() self.pause_time = tm self.finish_time = tm - self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task stops...'}) + self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task halts...'}) print('---- task halt ----') def finish(self): diff --git a/youtube_dl_webui/worker.py b/youtube_dl_webui/worker.py index 2762873..9c8d0da 100644 --- a/youtube_dl_webui/worker.py +++ b/youtube_dl_webui/worker.py @@ -93,25 +93,15 @@ def ansi_escape(self, msg): return re.sub(reg, '', msg) -class fatal_event(object): - def __init__(self, tid, wqueue): - pass - # self.tid = tid - # self.wq = wqueue - # self.wqd = deepcopy(WQ_DICT) - # self.wqd['tid'] = self.tid - # self.wqd['msgtype'] = 'fatal' - # self.data = {'time': None, 'type': None, 'msg': None} - # self.wqd['data'] = self.data - +class FatalEvent(object): + def __init__(self, tid, msg_cli): + self.tid = tid + self.msg_cli = msg_cli def invalid_url(self, url): - pass - # self.data['time'] = int(time()) - # self.data['type'] = 'invalid_url' - # self.data['url'] = url; - # self.data['msg'] = 'invalid url: {}'.format(url) - # self.wq.put(self.wqd) + self.logger.debug('fatal error: invalid url') + payload = {'time': int(time()), 'type': 'invalid_url', 'error': 'invalid url: %s' %(url)} + self.msg_cli.put('fatal', {'tid': self.tid, 'data': payload}) class Worker(Process): @@ -150,10 +140,8 @@ def run(self): # ydl.download([self.url]) except DownloadError as e: # url error - # event_handle = fatal_event(self.tid, self.wq) - # event_handle.invalid_url(self.url); - pass - + event_handler = FatalEvent(self.tid, self.msg_cli) + event_handler.invalid_url(self.url); def stop(self): self.logger.info('Terminating Process ...') From 6d2f4d7f8815ad801601421d46b8181874a28c46 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 08:32:20 +0800 Subject: [PATCH 52/88] bug fix: cmdline opt overwrite confile --- youtube_dl_webui/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl_webui/config.py b/youtube_dl_webui/config.py index e11d211..b008324 100644 --- a/youtube_dl_webui/config.py +++ b/youtube_dl_webui/config.py @@ -101,7 +101,7 @@ def cmd_args_override(self): 'port': 'server'} for key, val in self.cmd_args.items(): - if key not in _cat_dict: + if key not in _cat_dict or val is None: continue sub_conf = self.get_val(_cat_dict[key]) sub_conf.set_val(key, val) From 55a19f756e2c830e7fbe7c5e33de361d640ca394 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 09:46:16 +0800 Subject: [PATCH 53/88] simple operation is ok --- youtube_dl_webui/core.py | 10 ++++-- youtube_dl_webui/db.py | 21 +++++++++-- youtube_dl_webui/schema.sql | 1 + youtube_dl_webui/task.py | 72 +++++++++++++++++++++++++++++++------ youtube_dl_webui/worker.py | 57 +++++++++++------------------ 5 files changed, 109 insertions(+), 52 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 958b88c..9b2e4f2 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -50,7 +50,7 @@ def event_create(cls, svr, event, data, args): svr.put(cls.TaskExistenceErrorMsg) return - task = cls._task_mgr.start_task(tid, first_run=True) + task = cls._task_mgr.start_task(tid) svr.put({'status': 'success', 'tid': tid}) @@ -139,6 +139,11 @@ def event_fatal(cls, svr, event, data, arg): if data['type'] == 'invalid_url': cls._task_mgr.halt_task(tid) + @classmethod + def event_progress(cls, svr, event, data, arg): + tid, data = data['tid'], data['data'] + cls._task_mgr.progress_update(tid, data) + def load_conf_from_file(cmd_args): logger = logging.getLogger('ydl_webui') @@ -189,7 +194,8 @@ def __init__(self, cmd_args=None): self.msg_mgr.reg_event('config', WebMsgDispatcher.event_config) self.msg_mgr.reg_event('info_dict', WorkMsgDispatcher.event_info_dict) - self.msg_mgr.reg_event('log', WorkMsgDispatcher.event_log) + self.msg_mgr.reg_event('log', WorkMsgDispatcher.event_log) + self.msg_mgr.reg_event('progress', WorkMsgDispatcher.event_progress) self.server = Server(web_cli, self.conf['server']['host'], self.conf['server']['port']) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index 2aa75a5..e5a4d35 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -240,7 +240,7 @@ def update_log_ob(self, tid, log): self.conn.commit() - def progress_update(self, tid, d): + def progress_update_ob(self, tid, d): self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) row = self.db.fetchone() if row is None: @@ -320,7 +320,7 @@ def get_stat(self, tid): return dict(row) def get_info(self, tid): - self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) + self.db.execute('SELECT * FROM task_info WHERE tid=(?)', (tid, )) row = self.db.fetchone() if row is None: @@ -479,6 +479,7 @@ def state_counter(self): def update_info(self, tid, info_dict): self.logger.debug('db update_info()') db_data = { + 'valid': 1, # info_dict is updated 'title': info_dict['title'], 'format': info_dict['format'], 'ext': info_dict['ext'], @@ -502,3 +503,19 @@ def update_log(self, tid, log, exist_test=False): log_str = json.dumps([l for l in log]) self.update(tid, {'task_status': {'log': log_str}}) + def progress_update(self, tid, d, elapsed): + self.logger.debug("update filename=%s, tmpfilename=%s" %(d['filename'], d['tmpfilename'])) + + db_data = { + 'percent': d['_percent_str'], + 'filename': d['filename'], + 'tmpfilename': d['tmpfilename'], + 'downloaded_bytes': d['downloaded_bytes'], + 'total_bytes': d['total_bytes'], + 'total_bytes_estmt': d['total_bytes_estimate'], + 'speed': d['speed'], + 'eta': d['eta'], + 'elapsed': elapsed, + } + self.update(tid, {'task_status': db_data}) + diff --git a/youtube_dl_webui/schema.sql b/youtube_dl_webui/schema.sql index f758fca..5fad114 100644 --- a/youtube_dl_webui/schema.sql +++ b/youtube_dl_webui/schema.sql @@ -10,6 +10,7 @@ CREATE TABLE task_info ( tid TEXT PRIMARY KEY NOT NULL, url TEXT NOT NULL, state INTEGER NOT NULL DEFAULT 2, + valid INTEGER NOT NULL DEFAULT 0, title TEXT NOT NULL DEFAULT '', create_time REAL DEFAULT 0.0, finish_time REAL DEFAULT 0.0, diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 8383099..64b3bd0 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -14,6 +14,7 @@ from .utils import TaskExistenceError from .utils import TaskPausedError from .utils import url2tid +from .utils import state_index from .worker import Worker @@ -24,48 +25,82 @@ def __init__(self, tid, msg_cli, ydl_opts={}, info={}, status={}): self.ydl_opts = ydl_opts self.ydl_conf = ydl_conf(ydl_opts) self.info = info - self.status = status self.log = deque(maxlen=10) self.msg_cli = msg_cli + self.touch = time() + self.state = None + self.elapsed = status['elapsed'] + self.first_run = True if info['valid'] == 0 else False log_list = json.loads(status['log']) for log in log_list: self.log.appendleft(log) - def start(self, first_run=False): + def start(self): print('---- task start ----') tm = time() + self.state = state_index['downloading'] + self.start_time = tm + self.elapsed = self.elapsed + (tm - self.touch) + self.touch = tm + self.worker = Worker(self.tid, self.info['url'], msg_cli=self.msg_cli, ydl_opts=self.ydl_opts, - first_run=first_run) + first_run=self.first_run) self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task starts...'}) self.worker.start() def pause(self): tm = time() + self.state = state_index['paused'] + self.pause_time = tm + self.elapsed = self.elapsed + (tm - self.touch) + self.touch = tm + + self.worker.stop() self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task pauses...'}) print('---- task pause ----') def halt(self): tm = time() + self.state = state_index['invalid'] + self.pause_time = tm self.finish_time = tm + self.elapsed = self.elapsed + (tm - self.touch) + self.touch = tm + + self.worker.stop() self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task halts...'}) print('---- task halt ----') def finish(self): tm = time() + self.state = state_index['finished'] + self.pause_time = tm self.finish_time = tm + self.elapsed = self.elapsed + (tm - self.touch) + self.touch = tm + + self.worker.stop() self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task finishs...'}) print('---- task finish ----') + def update_info(self, info_dict): + self.first_run = False + def update_log(self, log): self.log.appendleft(log) + def progress_update(self, data): + tm = time() + self.elapsed = self.elapsed + (tm - self.touch) + self.touch = tm + class TaskManager(object): """ @@ -92,7 +127,7 @@ def new_task(self, url, ydl_opts={}): return self._db.new_task(url, ydl_opts) - def start_task(self, tid, ignore_state=False, first_run=False): + def start_task(self, tid, ignore_state=False): """make an inactive type task into active type""" task = None @@ -109,7 +144,7 @@ def start_task(self, tid, ignore_state=False, first_run=False): task = Task(tid, self._msg_cli, ydl_opts=ydl_opts, info=info, status=status) self._tasks_dict[tid] = task - task.start(first_run=first_run) + task.start() self._db.start_task(tid, start_time=task.start_time) self._db.update_log(tid, task.log) @@ -123,8 +158,7 @@ def pause_task(self, tid): task = self._tasks_dict[tid] task.pause() - elapsed = task.pause_time - task.start_time + task.status['elapsed'] - self._db.pause_task(tid, pause_time=task.pause_time, elapsed=elapsed) + self._db.pause_task(tid, pause_time=task.pause_time, elapsed=task.elapsed) self._db.update_log(tid, task.log) def finish_task(self, tid): @@ -136,8 +170,7 @@ def finish_task(self, tid): task = self._tasks_dict[tid] del self._tasks_dict[tid] task.finish() - elapsed = task.finish_time - task.start_time + task.status['elapsed'] - self._db.finish_task(tid, finish_time=task.finish_time, elapsed=elapsed) + self._db.finish_task(tid, finish_time=task.finish_time, elapsed=task.elapsed) self._db.update_log(tid, task.log) def halt_task(self, tid): @@ -149,8 +182,7 @@ def halt_task(self, tid): task = self._tasks_dict[tid] del self._tasks_dict[tid] task.halt() - elapsed = task.finish_time - task.start_time + task.status['elapsed'] - self._db.halt_task(tid, finish_time=task.halt_time, elapsed=elapsed) + self._db.halt_task(tid, finish_time=task.halt_time, elapsed=task.elapsed) self._db.update_log(tid, task.log) def delete_task(self, tid, del_file=False): @@ -197,6 +229,11 @@ def state(self): return self._db.state_counter() def update_info(self, tid, info_dict): + if tid not in self._tasks_dict: + raise TaskInexistenceError + task = self._tasks_dict[tid] + task.update_info(info_dict) + self._db.update_info(tid, info_dict) def update_log(self, tid, log): @@ -206,3 +243,16 @@ def update_log(self, tid, log): task = self._tasks_dict[tid] task.update_log(log) self._db.update_log(tid, task.log, exist_test=False) + + def progress_update(self, tid, data): + if tid not in self._tasks_dict: + raise TaskInexistenceError + task = self._tasks_dict[tid] + task.progress_update(data) + + if 'total_bytes' in data: + data['total_bytes_estmt'] = data['total_bytes'] + else: + data['total_bytes'] = '0' + + self._db.progress_update(tid, data, task.elapsed) diff --git a/youtube_dl_webui/worker.py b/youtube_dl_webui/worker.py index 9c8d0da..1aecefb 100644 --- a/youtube_dl_webui/worker.py +++ b/youtube_dl_webui/worker.py @@ -10,61 +10,44 @@ from multiprocessing import Process from time import time -from copy import deepcopy - -WQ_DICT = {'from': 'worker'} -MSG = None - class YdlHook(object): def __init__(self, tid, msg_cli): self.logger = logging.getLogger('ydl_webui') self.tid = tid self.msg_cli = msg_cli - # self.wqd = deepcopy(WQ_DICT) - # self.wqd['tid'] = self.tid - # self.wqd['msgtype'] = 'progress' - # self.wqd['data'] = None - def finished(self, d): self.logger.debug('finished status') - # d['_percent_str'] = '100%' - # d['speed'] = '0' - # d['elapsed'] = 0 - # d['eta'] = 0 - # d['downloaded_bytes'] = d['total_bytes'] - - # return d - + d['_percent_str'] = '100%' + d['speed'] = '0' + d['elapsed'] = 0 + d['eta'] = 0 + d['downloaded_bytes'] = d['total_bytes'] + return d def downloading(self, d): self.logger.debug('downloading status') - # return d - + return d def error(self, d): self.logger.debug('error status') # d['_percent_str'] = '100%' - # return d - + return d def dispatcher(self, d): - pass - # if 'total_bytes_estimate' not in d: - # d['total_bytes_estimate'] = 0 - # if 'tmpfilename' not in d: - # d['tmpfilename'] = '' - - # if d['status'] == 'finished': - # d = self.finished(d) - # elif d['status'] == 'downloading': - # d = self.downloading(d) - # elif d['error'] == 'error': - # d = self.error(d) + if 'total_bytes_estimate' not in d: + d['total_bytes_estimate'] = 0 + if 'tmpfilename' not in d: + d['tmpfilename'] = '' - # self.wqd['data'] = d - # self.wq.put(self.wqd) + if d['status'] == 'finished': + d = self.finished(d) + elif d['status'] == 'downloading': + d = self.downloading(d) + elif d['error'] == 'error': + d = self.error(d) + self.msg_cli.put('progress', {'tid': self.tid, 'data': d}) class LogFilter(object): @@ -137,7 +120,7 @@ def run(self): self.msg_cli.put('info_dict', payload) self.logger.info('start downloading ...') - # ydl.download([self.url]) + ydl.download([self.url]) except DownloadError as e: # url error event_handler = FatalEvent(self.tid, self.msg_cli) From 434bd3aeebf801d9e635536f7ad2db3192b5932e Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 09:46:25 +0800 Subject: [PATCH 54/88] bug fix --- youtube_dl_webui/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index 51ed2b9..bfc09cc 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -97,6 +97,7 @@ def query_task(tid): @app.route('/config', methods=['GET', 'POST']) def get_config(): + payload = {} if request.method == 'POST': payload['act'] = 'update' payload['param'] = request.get_json() From e434f57c490e1892f46de6f1abf0a6ea44f250f3 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 10:11:42 +0800 Subject: [PATCH 55/88] delete file --- youtube_dl_webui/core.py | 12 ++++++++++-- youtube_dl_webui/server.py | 3 ++- youtube_dl_webui/task.py | 4 +++- youtube_dl_webui/templates/test/index.html | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 9b2e4f2..694ad51 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -56,8 +56,7 @@ def event_create(cls, svr, event, data, args): @classmethod def event_delete(cls, svr, event, data, args): - tid = data['tid'] - del_file = True if data['del_file'] == 'true' else False + tid, del_file = data['tid'], data['del_file'] try: cls._task_mgr.delete_task(tid, del_file) @@ -200,6 +199,15 @@ def __init__(self, cmd_args=None): self.server = Server(web_cli, self.conf['server']['host'], self.conf['server']['port']) def start(self): + dl_dir = self.conf['general']['download_dir'] + try: + os.makedirs(dl_dir, exist_ok=True) + self.logger.info('Download dir: %s' %(dl_dir)) + os.chdir(dl_dir) + except PermissionError: + self.logger.critical('Permission error when accessing download dir') + exit(1) + self.server.start() self.msg_mgr.run() diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index bfc09cc..b5cfc9a 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -55,9 +55,10 @@ def list_state(): @app.route('/task/tid/', methods=['DELETE']) def delete_task(tid): + del_flag = request.args.get('del_file', False) payload = {} payload['tid'] = tid - payload['del_file'] = not not request.args.get('del_file', False) + payload['del_file'] = False if del_flag is False else True MSG.put('delete', payload) return json.dumps(MSG.get()) diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 64b3bd0..6c75167 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -199,7 +199,9 @@ def delete_task(self, tid, del_file=False): raise TaskInexistenceError(e.msg) if del_file and dl_file is not None: - os.remove(dl_file) + abs_dl_file = os.path.join(os.getcwd(), dl_file) + self.logger.debug('delete file: %s' %(abs_dl_file)) + os.remove(abs_dl_file) def query(self, tid, exerpt=True): db_ret = self._db.query_task(tid) diff --git a/youtube_dl_webui/templates/test/index.html b/youtube_dl_webui/templates/test/index.html index 3574617..9714bda 100644 --- a/youtube_dl_webui/templates/test/index.html +++ b/youtube_dl_webui/templates/test/index.html @@ -113,7 +113,7 @@ }); $("#del-task").click(function () { - var act = $("#del-file").is(":checked") ? '?del_data=true' : ''; + var act = $("#del-file").is(":checked") ? '?del_file=true' : ''; $.ajax({ url: host + '/task/tid/' + $("#tid").val() + act, type: 'DELETE', From 82ffee8a796ed4accf46007eb856abefbd00761c Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 10:26:30 +0800 Subject: [PATCH 56/88] purge db.py --- youtube_dl_webui/db.py | 223 ----------------------------------------- 1 file changed, 223 deletions(-) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index e5a4d35..f50a5fc 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -55,229 +55,6 @@ def __init__(self, db_path): c = self.conn.execute('SELECT * FROM {}'.format(table)) self.tables[table] = [desc[0] for desc in c.description] - - def get_unfinished(self): - self.db.execute('SELECT tid FROM task_status WHERE state not in (?,?)', - (state_index['finished'], state_index['invalid'])) - rows = self.db.fetchall() - - ret = [] - for row in rows: - ret.append(row['tid']) - - return ret - - def get_param(self, tid): - self.db.execute('SELECT * FROM task_param WHERE tid=(?) and state not in (?,?)', - (tid, state_index['finished'], state_index['invalid'])) - row = self.db.fetchone() - - if row is None: - raise TaskInexistenceError('task does not exist') - - return {'tid': row['tid'], 'url': row['url']} - - - def get_opts(self, tid): - self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?) and state not in (?,?)', - (tid, state_index['finished'], state_index['invalid'])) - row = self.db.fetchone() - - if row is None: - raise TaskInexistenceError('task does not exist') - - return json.loads(row['opt']) - - - # def get_ydl_opts(self, tid): - # self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?)', (tid, )) - - - def create_task(self, param, ydl_opts): - url = param['url'] - tid = sha1(url.encode()).hexdigest() - - self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) - if self.db.fetchone() is not None: - raise TaskExistenceError('Task exists') - - self.db.execute('INSERT INTO task_status (tid) VALUES (?)', (tid, )) - self.db.execute('INSERT INTO task_param (tid, url) VALUES (?, ?)', (tid, url)) - self.db.execute('INSERT INTO task_info (tid, url, create_time) VALUES (?, ?, ?)', - (tid, url, time())) - ydl_opt_str = json.dumps(ydl_opts) - self.db.execute('INSERT INTO task_ydl_opt (tid, opt) VALUES (?, ?)', (tid, ydl_opt_str)) - self.conn.commit() - - return tid - - - def set_state(self, tid, state): - if state not in state_index: - raise KeyError - - self.db.execute('UPDATE task_status SET state=? WHERE tid=(?)', (state_index[state], tid)) - self.db.execute('UPDATE task_param SET state=? WHERE tid=(?)', (state_index[state], tid)) - self.db.execute('UPDATE task_info SET state=? WHERE tid=(?)', (state_index[state], tid)) - self.db.execute('UPDATE task_ydl_opt SET state=? WHERE tid=(?)', (state_index[state], tid)) - self.conn.commit() - - - def cancel_task(self,tid, log=None): - self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) - row = self.db.fetchone() - if row is None: - raise TaskInexistenceError('') - - if row['state'] == state_index['paused']: - raise TaskPausedError('') - - cur_time = time() - elapsed = row['elapsed'] - start_time = row['start_time'] - elapsed += (cur_time - start_time); - - state = state_index['paused'] - self.db.execute('UPDATE task_status SET state=?, pause_time=?, elapsed=? WHERE tid=(?)', - (state, cur_time, elapsed, tid)) - self.db.execute('UPDATE task_param SET state=? WHERE tid=(?)', (state, tid)) - self.db.execute('UPDATE task_info SET state=? WHERE tid=(?)', (state, tid)) - self.db.execute('UPDATE task_ydl_opt SET state=? WHERE tid=(?)', (state, tid)) - self.conn.commit() - - self.update_log(tid, log) - - - def start_task(self, tid, ignore_state=False): - self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) - row = self.db.fetchone() - if row is None: - raise TaskInexistenceError('') - - if row['state'] == state_index['downloading'] and ignore_state is False: - raise TaskRunningError('') - - state = state_index['downloading'] - self.db.execute('UPDATE task_status SET state=?, start_time=? WHERE tid=(?)', (state, time(), tid)) - self.db.execute('UPDATE task_param SET state=? WHERE tid=(?)', (state, tid)) - self.db.execute('UPDATE task_info SET state=? WHERE tid=(?)', (state, tid)) - self.db.execute('UPDATE task_ydl_opt SET state=? WHERE tid=(?)', (state, tid)) - self.conn.commit() - - return json.loads(row['log']) - - - def query_task(self, tid): - self.db.execute('SELECT * FROM task_status, task_info WHERE task_status.tid=(?) and task_info.tid=(?)', (tid, tid)) - row = self.db.fetchone() - if row is None: - raise TaskInexistenceError('') - - ret = {} - for key in row.keys(): - if key == 'state': - ret[key] = state_name[row[key]] - elif key == 'log': - ret['log'] = json.loads(row['log']) - else: - ret[key] = row[key] - - return ret - - def list_task(self, qstate): - self.db.execute('SELECT * FROM task_status, task_info WHERE task_status.tid=task_info.tid') - rows = self.db.fetchall() - - ret = [] - state_counter = {'downloading': 0, 'paused': 0, 'finished': 0, 'invalid': 0} - if len(rows) == 0: - return ret, state_counter - - keys = set(rows[0].keys()) - for row in rows: - t = {} - for key in keys: - if key == 'state': - state = row[key] - t[key] = state_name[state] - state_counter[state_name[state]] += 1 - elif key == 'log': - t['log'] = json.loads(row['log']) - else: - t[key] = row[key] - - if qstate == 'all' or qstate == t['state']: - ret.append(t) - - return ret, state_counter - - def list_state(self): - state_counter = {'downloading': 0, 'paused': 0, 'finished': 0, 'invalid': 0} - - self.db.execute('SELECT state, count(*) as NUM FROM task_status GROUP BY state') - rows = self.db.fetchall() - - for r in rows: - state_counter[state_name[r['state']]] = r['NUM'] - - return state_counter - - - def update_from_info_dict(self, tid, info_dict): - self.db.execute('UPDATE task_info SET title=(?), format=(?), ext=(?), thumbnail=(?), duration=(?), view_count=(?), like_count=(?), dislike_count=(?), average_rating=(?), description=(?) WHERE tid=(?)', - (info_dict['title'], info_dict['format'], info_dict['ext'], info_dict['thumbnail'], info_dict['duration'], info_dict['view_count'], info_dict['like_count'], info_dict['dislike_count'], info_dict['average_rating'], info_dict['description'], tid)) - self.conn.commit() - - - def update_log_ob(self, tid, log): - self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) - row = self.db.fetchone() - if row is None: - raise TaskInexistenceError('') - - log_str = json.dumps([l for l in log]) - self.db.execute('UPDATE task_status SET log = (?) WHERE tid=(?)', (log_str, tid)) - self.conn.commit() - - - def progress_update_ob(self, tid, d): - self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) - row = self.db.fetchone() - if row is None: - raise TaskInexistenceError('') - - elapsed = row['elapsed'] + d['elapsed'] - - if 'total_bytes' in d: - d['total_bytes_estmt'] = d['total_bytes'] - else: - d['total_bytes'] = '0' - - self.logger.debug("update filename=%s, tmpfilename=%s" %(d['filename'], d['tmpfilename'])) - - self.db.execute("UPDATE task_status SET " - "percent=:percent, filename=:filename, " - "tmpfilename=:tmpfilename, downloaded_bytes=:downloaded_bytes, " - "total_bytes=:total_bytes, total_bytes_estmt=:total_bytes_estmt, " - "speed=:speed, eta=:eta, elapsed=:elapsed WHERE tid=:tid", - { "percent": d['_percent_str'], "filename": d['filename'], \ - "tmpfilename": d['tmpfilename'], "downloaded_bytes": d['downloaded_bytes'], \ - "total_bytes": d['total_bytes'], "total_bytes_estmt": d['total_bytes_estimate'], \ - "speed": d['speed'], "eta": d['eta'], \ - "elapsed": elapsed, "tid": tid - }) - - self.db.execute('UPDATE task_info SET finish_time=? WHERE tid=(?)', (time(), tid)) - self.conn.commit() - - - - - - - - - def update(self, tid, val_dict={}): for table, data in val_dict.items(): if table not in self.tables: From b9b709edb9272fced2aaeab69064d837e02dacc1 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 10:26:57 +0800 Subject: [PATCH 57/88] finish task when it finshes --- youtube_dl_webui/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 694ad51..ea373cf 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -143,6 +143,9 @@ def event_progress(cls, svr, event, data, arg): tid, data = data['tid'], data['data'] cls._task_mgr.progress_update(tid, data) + if data['status'] == 'finished': + cls._task_mgr.finish_task(tid) + def load_conf_from_file(cmd_args): logger = logging.getLogger('ydl_webui') From 1814835d6be2c502af0afa51fd529b4d604e3cab Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 10:27:10 +0800 Subject: [PATCH 58/88] bug fix --- youtube_dl_webui/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index f50a5fc..e668edc 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -227,7 +227,7 @@ def list_task(self, state): ret_val = [] for row in rows: t = {} - for key in row.keys(): + for key in set(row.keys()): if key == 'state': s = row[key] t[key] = state_name[s] From 1e7bb87e0db0edc10374907f7aed81b1db1b05e5 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 10:42:33 +0800 Subject: [PATCH 59/88] auto unfinished task when start --- youtube_dl_webui/core.py | 1 + youtube_dl_webui/db.py | 13 +++++++++++++ youtube_dl_webui/task.py | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index ea373cf..45de76b 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -211,6 +211,7 @@ def start(self): self.logger.critical('Permission error when accessing download dir') exit(1) + self.task_mgr.launch_unfinished() self.server.start() self.msg_mgr.run() diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index e668edc..9f390e3 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -296,3 +296,16 @@ def progress_update(self, tid, d, elapsed): } self.update(tid, {'task_status': db_data}) + def launch_unfinished(self): + self.db.execute('SELECT tid FROM task_status WHERE state in (?)', + (state_index['downloading'],)) + + rows = self.db.fetchall() + + ret_val = [] + for row in rows: + ret_val.append(row['tid']) + + return ret_val + + diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 6c75167..5cd7753 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -258,3 +258,10 @@ def progress_update(self, tid, data): data['total_bytes'] = '0' self._db.progress_update(tid, data, task.elapsed) + + def launch_unfinished(self): + tid_list = self._db.launch_unfinished() + + for t in tid_list: + self.start_task(t) + From 2cced480285c3dd5b4bdda5b3c19878e124f3953 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 13:52:15 +0800 Subject: [PATCH 60/88] log size can be changed --- example_config.json | 2 +- youtube_dl_webui/config.py | 2 +- youtube_dl_webui/task.py | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/example_config.json b/example_config.json index 26f920a..286c9a0 100644 --- a/example_config.json +++ b/example_config.json @@ -2,7 +2,7 @@ "general": { "download_dir": "/tmp/youtube_dl", "db_path": "/tmp/youtube_dl_webui.db", - "download_log_size": 10 + "log_size": 10 }, "server": { "host": "0.0.0.0", diff --git a/youtube_dl_webui/config.py b/youtube_dl_webui/config.py index b008324..9f20f60 100644 --- a/youtube_dl_webui/config.py +++ b/youtube_dl_webui/config.py @@ -75,7 +75,7 @@ class gen_conf(conf_base): #(key, default_val, type, validate_regex, call_function) ('download_dir', '~/Downloads/youtube-dl', 'string', '', expanduser), ('db_path', '~/.conf/ydl_webui.db', 'string', '', expanduser), - ('task_log_size', 10, 'int', '', None), + ('log_size', 10, 'int', '', None), ] def __init__(self, conf_dict={}): diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 5cd7753..fa0cdfa 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -20,12 +20,12 @@ class Task(object): - def __init__(self, tid, msg_cli, ydl_opts={}, info={}, status={}): + def __init__(self, tid, msg_cli, ydl_opts={}, info={}, status={}, log_size=10): self.tid = tid self.ydl_opts = ydl_opts self.ydl_conf = ydl_conf(ydl_opts) self.info = info - self.log = deque(maxlen=10) + self.log = deque(maxlen=log_size) self.msg_cli = msg_cli self.touch = time() self.state = None @@ -141,7 +141,8 @@ def start_task(self, tid, ignore_state=False): except TaskInexistenceError as e: raise TaskInexistenceError(e.msg) - task = Task(tid, self._msg_cli, ydl_opts=ydl_opts, info=info, status=status) + task = Task(tid, self._msg_cli, ydl_opts=ydl_opts, info=info, + status=status, log_size=self._conf['general']['log_size']) self._tasks_dict[tid] = task task.start() From f15e1f00fcce01c80034503c9af043d6d56a0aa1 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 13:59:18 +0800 Subject: [PATCH 61/88] add batch operations' API --- REST_API.txt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/REST_API.txt b/REST_API.txt index 4c70e81..636103c 100644 --- a/REST_API.txt +++ b/REST_API.txt @@ -12,12 +12,15 @@ | 6 | GET | /task/tid//status | get the full status of a task | | 7 | GET | /task/tid//status?exerpt=true | get the status exerpt of a task | | 8 | GET | /task/tid//info | get the task info | -| 9 | GET | /task/list | get task list exerpt | -| 10 | GET | /task/list?exerpt=false | get task list | -| 11 | GET | /task/list?state={all|downloading|finished|paused} | get task list exerpt according to the state | -| 12 | GET | /task/list?exerpt=false&state={all|downloading|finished|paused} | get task list according to the state | -| 14 | GET | /task/state_coutner | get number of tasks in each state | -| 15 | GET | /config | get the server's configuration | +| 9 | POST | /task/batch/pause | post a list of tasks' tid to be paused | +| 10 | POST | /task/batch/resume | post a list of tasks' tid to be resumed | +| 12 | POST | /task/batch/delete | post a list of tasks' tid to be deleted | +| 13 | GET | /task/list | get task list exerpt | +| 14 | GET | /task/list?exerpt=false | get task list | +| 15 | GET | /task/list?state={all|downloading|finished|paused} | get task list exerpt according to the state | +| 16 | GET | /task/list?exerpt=false&state={all|downloading|finished|paused} | get task list according to the state | +| 17 | GET | /task/state_coutner | get number of tasks in each state | +| 18 | GET | /config | get the server's configuration | |----+--------+-----------------------------------------------------------------+----------------------------------------------| Note: From 6913e56c46d4a6b68bc95a67c247d92bb5228cdb Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 14:55:38 +0800 Subject: [PATCH 62/88] add task batch process --- youtube_dl_webui/core.py | 41 +++++++++++++++- youtube_dl_webui/server.py | 7 +++ youtube_dl_webui/task.py | 14 +++--- youtube_dl_webui/templates/test/index.html | 54 +++++++++++++++++++++- 4 files changed, 108 insertions(+), 8 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 45de76b..3f1a59b 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -33,6 +33,7 @@ class WebMsgDispatcher(object): TaskInexistenceErrorMsg = {'status': 'error', 'errmsg': 'Task does not exist'} UrlErrorMsg = {'status': 'error', 'errmsg': 'URL is invalid'} InvalidStateMsg = {'status': 'error', 'errmsg': 'invalid query state'} + RequestErrorMsg = {'status': 'error', 'errmsg': 'request error'} _task_mgr = None @@ -70,7 +71,7 @@ def event_manipulation(cls, svr, event, data, args): cls.logger.debug('manipulation event') tid, act = data['tid'], data['act'] - ret_val = cls.InternalErrorMsg + ret_val = cls.RequestErrorMsg if act == 'pause': cls._task_mgr.pause_task(tid) ret_val = cls.SuccessMsg @@ -111,6 +112,43 @@ def event_state(cls, svr, event, data, args): def event_config(cls, svr, event, data, arg): svr.put({}) + @classmethod + def event_batch(cls, svr, event, data, arg): + act, detail = data['act'], data['detail'] + + if 'tids' not in detail: + svr.put(cls.RequestErrorMsg) + return + + tids = detail['tids'] + errors = [] + if act == 'pause': + for tid in tids: + try: + cls._task_mgr.pause_task(tid) + except TaskInexistenceError: + errors.append([tid, 'inexistence error']) + elif act == 'resume': + for tid in tids: + try: + cls._task_mgr.start_task(tid) + except TaskInexistenceError: + errors.append([tid, 'inexistence error']) + elif act == 'delete': + del_file = True if detail.get('del_file', 'false') == 'true' else False + for tid in tids: + try: + cls._task_mgr.delete_task(tid, del_file) + except TaskInexistenceError: + errors.append([tid, 'inexistence error']) + + if errors: + ret_val = {'status': 'success', 'detail': errors} + else: + ret_val = cls.SuccessMsg + + svr.put(ret_val) + class WorkMsgDispatcher(object): @@ -194,6 +232,7 @@ def __init__(self, cmd_args=None): self.msg_mgr.reg_event('list', WebMsgDispatcher.event_list) self.msg_mgr.reg_event('state', WebMsgDispatcher.event_state) self.msg_mgr.reg_event('config', WebMsgDispatcher.event_config) + self.msg_mgr.reg_event('batch', WebMsgDispatcher.event_batch) self.msg_mgr.reg_event('info_dict', WorkMsgDispatcher.event_info_dict) self.msg_mgr.reg_event('log', WorkMsgDispatcher.event_log) diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index b5cfc9a..5b82828 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -53,6 +53,13 @@ def list_state(): return json.dumps(MSG.get()) +@app.route('/task/batch/', methods=['POST']) +def task_batch(action): + payload={'act': action, 'detail': request.get_json()} + + MSG.put('batch', payload) + return json.dumps(MSG.get()) + @app.route('/task/tid/', methods=['DELETE']) def delete_task(tid): del_flag = request.args.get('del_file', False) diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index fa0cdfa..e97be5b 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -155,7 +155,7 @@ def pause_task(self, tid): self.logger.debug('task paused (%s)' %(tid)) if tid not in self._tasks_dict: - raise TaskInexistenceError + raise TaskInexistenceError('task does not exist') task = self._tasks_dict[tid] task.pause() @@ -166,7 +166,7 @@ def finish_task(self, tid): self.logger.debug('task finished (%s)' %(tid)) if tid not in self._tasks_dict: - raise TaskInexistenceError + raise TaskInexistenceError('task does not exist') task = self._tasks_dict[tid] del self._tasks_dict[tid] @@ -178,7 +178,7 @@ def halt_task(self, tid): self.logger.debug('task halted (%s)' %(tid)) if tid not in self._tasks_dict: - raise TaskInexistenceError + raise TaskInexistenceError('task does not exist') task = self._tasks_dict[tid] del self._tasks_dict[tid] @@ -233,7 +233,8 @@ def state(self): def update_info(self, tid, info_dict): if tid not in self._tasks_dict: - raise TaskInexistenceError + raise TaskInexistenceError('task does not exist') + task = self._tasks_dict[tid] task.update_info(info_dict) @@ -241,7 +242,7 @@ def update_info(self, tid, info_dict): def update_log(self, tid, log): if tid not in self._tasks_dict: - raise TaskInexistenceError + raise TaskInexistenceError('task does not exist') task = self._tasks_dict[tid] task.update_log(log) @@ -249,7 +250,8 @@ def update_log(self, tid, log): def progress_update(self, tid, data): if tid not in self._tasks_dict: - raise TaskInexistenceError + raise TaskInexistenceError('task does not exist') + task = self._tasks_dict[tid] task.progress_update(data) diff --git a/youtube_dl_webui/templates/test/index.html b/youtube_dl_webui/templates/test/index.html index 9714bda..e3fb2bd 100644 --- a/youtube_dl_webui/templates/test/index.html +++ b/youtube_dl_webui/templates/test/index.html @@ -17,7 +17,7 @@
- +
@@ -28,6 +28,15 @@ Delete File
+
+ + +
+
+ Batch Del: + + Delete File +
@@ -103,6 +112,20 @@ }); }); + $("#pause-task-batch").click(function() { + var data = { tids: [ $("#tid").val() ] }; + + $.ajax({url: host + '/task/batch/pause', + type: 'POST', + data: JSON.stringify(data), + contentType: 'application/json', + success: function(result) { + console.log(result); + }, + dataType: 'json' + }); + }); + $("#resume-task").click(function() { $.ajax({ url: host + '/task/tid/' + $("#tid").val() + '?act=resume', type: 'PUT', @@ -112,6 +135,20 @@ }); }); + $("#resume-task-batch").click(function() { + var data = { tids: [ $("#tid").val() ] }; + + $.ajax({url: host + '/task/batch/resume', + type: 'POST', + data: JSON.stringify(data), + contentType: 'application/json', + success: function(result) { + console.log(result); + }, + dataType: 'json' + }); + }); + $("#del-task").click(function () { var act = $("#del-file").is(":checked") ? '?del_file=true' : ''; @@ -123,6 +160,21 @@ }); }); + $("#del-task-batch").click(function () { + var del_flag = $("#del-file-batch").is(":checked") ? 'true' : 'false'; + var data = { tids: [ $("#tid").val() ], del_file: del_flag }; + + $.ajax({url: host + '/task/batch/delete', + type: 'POST', + data: JSON.stringify(data), + contentType: 'application/json', + success: function(result) { + console.log(result); + }, + dataType: 'json' + }); + }); + $("#conf-reset").click(function () { $.get(host + '/config', function (data, status) { From ef99ff671e36411dfab75dce117e59883c9091bb Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 15:10:16 +0800 Subject: [PATCH 63/88] fetch conf ok --- youtube_dl_webui/core.py | 15 ++++++++++++--- youtube_dl_webui/templates/test/index.html | 5 +++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index 3f1a59b..bd68164 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -36,10 +36,12 @@ class WebMsgDispatcher(object): RequestErrorMsg = {'status': 'error', 'errmsg': 'request error'} _task_mgr = None + _conf = None @classmethod - def init(cls, task_mgr): + def init(cls, conf, task_mgr): cls._task_mgr = task_mgr + cls._conf = conf @classmethod def event_create(cls, svr, event, data, args): @@ -110,7 +112,14 @@ def event_state(cls, svr, event, data, args): @classmethod def event_config(cls, svr, event, data, arg): - svr.put({}) + act = data['act'] + + ret_val = cls.RequestErrorMsg + if act == 'get': + ret_val = {'status': 'success'} + ret_val['config'] = cls._conf.dict() + + svr.put(ret_val) @classmethod def event_batch(cls, svr, event, data, arg): @@ -222,7 +231,7 @@ def __init__(self, cmd_args=None): self.db = DataBase(self.conf['general']['db_path']) self.task_mgr = TaskManager(self.db, task_cli, self.conf) - WebMsgDispatcher.init(self.task_mgr) + WebMsgDispatcher.init(self.conf, self.task_mgr) WorkMsgDispatcher.init(self.task_mgr) self.msg_mgr.reg_event('create', WebMsgDispatcher.event_create) diff --git a/youtube_dl_webui/templates/test/index.html b/youtube_dl_webui/templates/test/index.html index e3fb2bd..85bd096 100644 --- a/youtube_dl_webui/templates/test/index.html +++ b/youtube_dl_webui/templates/test/index.html @@ -72,11 +72,11 @@
-- youtubedl --
-
+
-
+
@@ -178,6 +178,7 @@ $("#conf-reset").click(function () { $.get(host + '/config', function (data, status) { + console.log(data) config = data['config']; for (let cls in config) { items = config[cls]; From 69cc1adcd34d562411e2217bdf3ece45326cac6d Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 15:44:44 +0800 Subject: [PATCH 64/88] set configuration is ok --- youtube_dl_webui/config.py | 18 +++++++++++++++++- youtube_dl_webui/core.py | 22 +++++++++++++++------- youtube_dl_webui/templates/test/index.html | 2 +- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/youtube_dl_webui/config.py b/youtube_dl_webui/config.py index 9f20f60..a0e957a 100644 --- a/youtube_dl_webui/config.py +++ b/youtube_dl_webui/config.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import logging +import json from os.path import expanduser @@ -91,8 +92,9 @@ class conf(object): svr_conf = None gen_conf = None - def __init__(self, conf_dict={}, cmd_args={}): + def __init__(self, conf_file, conf_dict={}, cmd_args={}): self.logger = logging.getLogger('ydl_webui') + self.conf_file = conf_file self.cmd_args = cmd_args self.load(conf_dict) @@ -122,6 +124,20 @@ def load(self, conf_dict): # override configurations by cmdline arguments self.cmd_args_override() + def save2file(self): + print(self.dict()) + print(self.conf_file) + if self.conf_file is not None: + try: + with open(self.conf_file, 'w') as f: + json.dump(self.dict(), f, indent=4) + except PermissionError: + return (False, 'permission error') + except FileNotFoundError: + return (False, 'can not find file') + else: + return (True, None) + def dict(self): d = {} for f in self._valid_fields: diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index bd68164..bc46f56 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -115,9 +115,17 @@ def event_config(cls, svr, event, data, arg): act = data['act'] ret_val = cls.RequestErrorMsg - if act == 'get': + if act == 'get': ret_val = {'status': 'success'} ret_val['config'] = cls._conf.dict() + elif act == 'update': + conf_dict = data['param'] + cls._conf.load(conf_dict) + suc, msg = cls._conf.save2file() + if suc: + ret_val = cls.SuccessMsg + else: + ret_val = {'status': 'error', 'errmsg': msg} svr.put(ret_val) @@ -201,11 +209,12 @@ def load_conf_from_file(cmd_args): logger.info('load config file (%s)' %(conf_file)) if cmd_args is None or conf_file is None: - return ({}, {}) + return (None, {}, {}) + abs_file = os.path.abspath(conf_file) try: - with open(expanduser(conf_file)) as f: - return (json.load(f), cmd_args) + with open(abs_file) as f: + return (abs_file, json.load(f), cmd_args) except FileNotFoundError as e: logger.critical("Config file (%s) doesn't exist", conf_file) exit(1) @@ -219,11 +228,10 @@ def __init__(self, cmd_args=None): self.logger.debug('cmd_args = %s' %(cmd_args)) - conf_dict, cmd_args = load_conf_from_file(cmd_args) - self.conf = conf(conf_dict=conf_dict, cmd_args=cmd_args) + conf_file, conf_dict, cmd_args = load_conf_from_file(cmd_args) + self.conf = conf(conf_file, conf_dict=conf_dict, cmd_args=cmd_args) self.logger.debug("configuration: \n%s", json.dumps(self.conf.dict(), indent=4)) - self.msg_mgr = MsgMgr() web_cli = self.msg_mgr.new_cli('server') task_cli = self.msg_mgr.new_cli() diff --git a/youtube_dl_webui/templates/test/index.html b/youtube_dl_webui/templates/test/index.html index 85bd096..8d5c2d2 100644 --- a/youtube_dl_webui/templates/test/index.html +++ b/youtube_dl_webui/templates/test/index.html @@ -226,7 +226,7 @@ data['general'] = general; data['server'] = server; - data['ydl'] = ydl; + data['youtube_dl'] = ydl; return data; } From 6664aa3cdb3f7044788acdefe1b6469fc7e4ccca Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 17:09:55 +0800 Subject: [PATCH 65/88] adjust sql structure --- youtube_dl_webui/schema.sql | 7 ------- 1 file changed, 7 deletions(-) diff --git a/youtube_dl_webui/schema.sql b/youtube_dl_webui/schema.sql index 5fad114..9682188 100644 --- a/youtube_dl_webui/schema.sql +++ b/youtube_dl_webui/schema.sql @@ -1,10 +1,3 @@ -DROP TABLE IF EXISTS task_param; -CREATE TABLE task_param ( - tid TEXT PRIMARY KEY NOT NULL, - state INTEGER NOT NULL DEFAULT 2, - url TEXT NOT NULL -); - DROP TABLE IF EXISTS task_info; CREATE TABLE task_info ( tid TEXT PRIMARY KEY NOT NULL, From d7dfa94f59e490e38a845ef84e7d0c67752ef3ec Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 17:20:56 +0800 Subject: [PATCH 66/88] delete print functions --- youtube_dl_webui/config.py | 2 -- youtube_dl_webui/task.py | 9 +++++---- youtube_dl_webui/worker.py | 2 -- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/youtube_dl_webui/config.py b/youtube_dl_webui/config.py index a0e957a..5a40b2e 100644 --- a/youtube_dl_webui/config.py +++ b/youtube_dl_webui/config.py @@ -125,8 +125,6 @@ def load(self, conf_dict): self.cmd_args_override() def save2file(self): - print(self.dict()) - print(self.conf_file) if self.conf_file is not None: try: with open(self.conf_file, 'w') as f: diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index e97be5b..74626f3 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -21,6 +21,7 @@ class Task(object): def __init__(self, tid, msg_cli, ydl_opts={}, info={}, status={}, log_size=10): + self.logger = logging.getLogger('ydl_webui') self.tid = tid self.ydl_opts = ydl_opts self.ydl_conf = ydl_conf(ydl_opts) @@ -37,7 +38,7 @@ def __init__(self, tid, msg_cli, ydl_opts={}, info={}, status={}, log_size=10): self.log.appendleft(log) def start(self): - print('---- task start ----') + self.logger.debug('Task starts, tid = %s' %(self.tid)) tm = time() self.state = state_index['downloading'] @@ -53,6 +54,7 @@ def start(self): self.worker.start() def pause(self): + self.logger.debug('Task pauses, tid = %s' %(self.tid)) tm = time() self.state = state_index['paused'] @@ -62,9 +64,9 @@ def pause(self): self.worker.stop() self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task pauses...'}) - print('---- task pause ----') def halt(self): + self.logger.debug('Task halts, tid = %s' %(self.tid)) tm = time() self.state = state_index['invalid'] @@ -75,9 +77,9 @@ def halt(self): self.worker.stop() self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task halts...'}) - print('---- task halt ----') def finish(self): + self.logger.debug('Task finishes, tid = %s' %(self.tid)) tm = time() self.state = state_index['finished'] @@ -88,7 +90,6 @@ def finish(self): self.worker.stop() self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task finishs...'}) - print('---- task finish ----') def update_info(self, info_dict): self.first_run = False diff --git a/youtube_dl_webui/worker.py b/youtube_dl_webui/worker.py index 1aecefb..cf6ea47 100644 --- a/youtube_dl_webui/worker.py +++ b/youtube_dl_webui/worker.py @@ -109,11 +109,9 @@ def run(self): with YoutubeDL(self.ydl_opts) as ydl: try: if self.first_run: - print(self.url) info_dict = ydl.extract_info(self.url, download=False) # self.logger.debug(json.dumps(info_dict, indent=4)) - print(info_dict['like_count']) info_dict['description'] = info_dict['description'].replace('\n', '
'); payload = {'tid': self.tid, 'data': info_dict} From c07ebd9ef65fe34f34aea215f00e42a0454151f3 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 18:39:06 +0800 Subject: [PATCH 67/88] format logging message --- youtube_dl_webui/logging.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl_webui/logging.json b/youtube_dl_webui/logging.json index e62c27b..550c9ca 100644 --- a/youtube_dl_webui/logging.json +++ b/youtube_dl_webui/logging.json @@ -3,7 +3,7 @@ "disable_existing_loggers": false, "formatters": { "simple": { - "format": "%(levelname)s - %(filename)s:%(lineno)d - %(message)s" + "format": "%(levelname)6s - %(filename)10s:%(lineno)-3d - %(message)s" } }, From bf99e57b4f7b9d1ec3b4918d64d3f50870e03eee Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 19:28:14 +0800 Subject: [PATCH 68/88] bug fixes --- youtube_dl_webui/core.py | 33 ++++++++++++++++++---------- youtube_dl_webui/db.py | 2 -- youtube_dl_webui/task.py | 27 ++++++++++++++--------- youtube_dl_webui/utils.py | 44 +------------------------------------- youtube_dl_webui/worker.py | 2 +- 5 files changed, 41 insertions(+), 67 deletions(-) diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index bc46f56..deeb46d 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -14,9 +14,8 @@ from .utils import state_name from .db import DataBase from .utils import TaskInexistenceError -from .utils import TaskRunningError from .utils import TaskExistenceError -from .utils import TaskPausedError +from .utils import TaskError from .server import Server from .worker import Worker @@ -32,8 +31,8 @@ class WebMsgDispatcher(object): TaskExistenceErrorMsg = {'status': 'error', 'errmsg': 'URL is already added'} TaskInexistenceErrorMsg = {'status': 'error', 'errmsg': 'Task does not exist'} UrlErrorMsg = {'status': 'error', 'errmsg': 'URL is invalid'} - InvalidStateMsg = {'status': 'error', 'errmsg': 'invalid query state'} - RequestErrorMsg = {'status': 'error', 'errmsg': 'request error'} + InvalidStateMsg = {'status': 'error', 'errmsg': 'Invalid query state'} + RequestErrorMsg = {'status': 'error', 'errmsg': 'Request error'} _task_mgr = None _conf = None @@ -75,11 +74,19 @@ def event_manipulation(cls, svr, event, data, args): ret_val = cls.RequestErrorMsg if act == 'pause': - cls._task_mgr.pause_task(tid) - ret_val = cls.SuccessMsg + try: + cls._task_mgr.pause_task(tid) + except TaskError as e: + ret_val = {'status': 'error', 'errmsg': e.msg} + else: + ret_val = cls.SuccessMsg elif act == 'resume': - cls._task_mgr.start_task(tid) - ret_val = cls.SuccessMsg + try: + cls._task_mgr.start_task(tid) + except TaskError as e: + ret_val = {'status': 'error', 'errmsg': e.msg} + else: + ret_val = cls.SuccessMsg svr.put(ret_val) @@ -144,20 +151,24 @@ def event_batch(cls, svr, event, data, arg): try: cls._task_mgr.pause_task(tid) except TaskInexistenceError: - errors.append([tid, 'inexistence error']) + errors.append([tid, 'Inexistence error']) + except TaskError as e: + errors.append([tid, e.msg]) elif act == 'resume': for tid in tids: try: cls._task_mgr.start_task(tid) except TaskInexistenceError: - errors.append([tid, 'inexistence error']) + errors.append([tid, 'Inexistence error']) + except TaskError as e: + errors.append([tid, e.msg]) elif act == 'delete': del_file = True if detail.get('del_file', 'false') == 'true' else False for tid in tids: try: cls._task_mgr.delete_task(tid, del_file) except TaskInexistenceError: - errors.append([tid, 'inexistence error']) + errors.append([tid, 'Inexistence error']) if errors: ret_val = {'status': 'success', 'detail': errors} diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index 9f390e3..223cd06 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -12,8 +12,6 @@ from .utils import state_index, state_name from .utils import TaskExistenceError from .utils import TaskInexistenceError -from .utils import TaskPausedError -from .utils import TaskRunningError from .utils import url2tid class DataBase(object): diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 74626f3..4b11d24 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -10,9 +10,8 @@ from .config import ydl_conf from .utils import TaskInexistenceError -from .utils import TaskRunningError from .utils import TaskExistenceError -from .utils import TaskPausedError +from .utils import TaskError from .utils import url2tid from .utils import state_index @@ -26,6 +25,7 @@ def __init__(self, tid, msg_cli, ydl_opts={}, info={}, status={}, log_size=10): self.ydl_opts = ydl_opts self.ydl_conf = ydl_conf(ydl_opts) self.info = info + self.url = info['url'] self.log = deque(maxlen=log_size) self.msg_cli = msg_cli self.touch = time() @@ -38,7 +38,7 @@ def __init__(self, tid, msg_cli, ydl_opts={}, info={}, status={}, log_size=10): self.log.appendleft(log) def start(self): - self.logger.debug('Task starts, tid = %s' %(self.tid)) + self.logger.info('Task starts, url - %s(%s)' %(self.url, self.tid)) tm = time() self.state = state_index['downloading'] @@ -54,7 +54,7 @@ def start(self): self.worker.start() def pause(self): - self.logger.debug('Task pauses, tid = %s' %(self.tid)) + self.logger.info('Task pauses, url - %s(%s)' %(self.url, self.tid)) tm = time() self.state = state_index['paused'] @@ -66,7 +66,7 @@ def pause(self): self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task pauses...'}) def halt(self): - self.logger.debug('Task halts, tid = %s' %(self.tid)) + self.logger.info('Task halts, url - %s(%s)' %(self.url, self.tid)) tm = time() self.state = state_index['invalid'] @@ -79,7 +79,7 @@ def halt(self): self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task halts...'}) def finish(self): - self.logger.debug('Task finishes, tid = %s' %(self.tid)) + self.logger.info('Task finishes, url - %s(%s)' %(self.url, self.tid)) tm = time() self.state = state_index['finished'] @@ -134,6 +134,8 @@ def start_task(self, tid, ignore_state=False): task = None if tid in self._tasks_dict: task = self._tasks_dict[tid] + if task.state == state_index['downloading']: + raise TaskError('Task is downloading') else: try: ydl_opts = self._db.get_ydl_opts(tid) @@ -156,9 +158,12 @@ def pause_task(self, tid): self.logger.debug('task paused (%s)' %(tid)) if tid not in self._tasks_dict: - raise TaskInexistenceError('task does not exist') + raise TaskError('Task is finished or invalid or inexistent') task = self._tasks_dict[tid] + if task.state == state_index['paused']: + raise TaskError('Task already paused') + task.pause() self._db.pause_task(tid, pause_time=task.pause_time, elapsed=task.elapsed) self._db.update_log(tid, task.log) @@ -170,10 +175,10 @@ def finish_task(self, tid): raise TaskInexistenceError('task does not exist') task = self._tasks_dict[tid] - del self._tasks_dict[tid] task.finish() self._db.finish_task(tid, finish_time=task.finish_time, elapsed=task.elapsed) self._db.update_log(tid, task.log) + del self._tasks_dict[tid] def halt_task(self, tid): self.logger.debug('task halted (%s)' %(tid)) @@ -182,10 +187,10 @@ def halt_task(self, tid): raise TaskInexistenceError('task does not exist') task = self._tasks_dict[tid] - del self._tasks_dict[tid] task.halt() self._db.halt_task(tid, finish_time=task.halt_time, elapsed=task.elapsed) self._db.update_log(tid, task.log) + del self._tasks_dict[tid] def delete_task(self, tid, del_file=False): self.logger.debug('task deleted (%s)' %(tid)) @@ -243,7 +248,9 @@ def update_info(self, tid, info_dict): def update_log(self, tid, log): if tid not in self._tasks_dict: - raise TaskInexistenceError('task does not exist') + # raise TaskInexistenceError('task does not exist') + self.logger.error('Task does not active, tid=%s' %(tid)) + return task = self._tasks_dict[tid] task.update_log(log) diff --git a/youtube_dl_webui/utils.py b/youtube_dl_webui/utils.py index eb3f8a1..1c5ab55 100644 --- a/youtube_dl_webui/utils.py +++ b/youtube_dl_webui/utils.py @@ -24,8 +24,7 @@ class YoutubeDLWebUI(Exception): class TaskError(YoutubeDLWebUI): """Error related to download tasks.""" def __init__(self, msg, tid=None): - if tid: - msg += ' tid={}'.format(tid) + if tid: msg += ' tid={}'.format(tid) super(TaskError, self).__init__(msg) self.msg = msg @@ -33,47 +32,6 @@ def __init__(self, msg, tid=None): def __str__(self): return repr(self.msg) -class TaskPausedError(TaskError): - def __init__(self, msg, tid=None, url=None, state=None): - msg = 'Task running error' - if tid: - msg += ' tid={}'.format(tid) - if url: - msg += ' url={}'.format(url) - if state: - msg += ' state={}'.format(state) - - super(TaskPausedError, self).__init__(msg) - self.msg = msg - - -class TaskRunningError(TaskError): - def __init__(self, msg, tid=None, url=None, state=None): - msg = 'Task running error' - if tid: - msg += ' tid={}'.format(tid) - if url: - msg += ' url={}'.format(url) - if state: - msg += ' state={}'.format(state) - - super(TaskRunningError, self).__init__(msg) - self.msg = msg - - -class TaskFinishedError(TaskError): - def __init__(self, msg, tid=None, url=None, state=None): - msg = 'Task already finished' - if tid: - msg += ' tid={}'.format(tid) - if url: - msg += ' url={}'.format(url) - if state: - msg += ' state={}'.format(state) - - super(TaskFinishedError, self).__init__(msg) - self.msg = msg - class TaskInexistenceError(TaskError): def __init__(self, msg, tid=None, url=None, state=None): diff --git a/youtube_dl_webui/worker.py b/youtube_dl_webui/worker.py index cf6ea47..26d9d95 100644 --- a/youtube_dl_webui/worker.py +++ b/youtube_dl_webui/worker.py @@ -117,7 +117,7 @@ def run(self): payload = {'tid': self.tid, 'data': info_dict} self.msg_cli.put('info_dict', payload) - self.logger.info('start downloading ...') + self.logger.info('start downloading, url - %s' %(self.url)) ydl.download([self.url]) except DownloadError as e: # url error From 44014c995c771ba8bc033660a0a9e7064a0c7666 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Thu, 7 Sep 2017 19:43:16 +0800 Subject: [PATCH 69/88] more bug fixes --- youtube_dl_webui/config.py | 15 --------------- youtube_dl_webui/core.py | 3 ++- youtube_dl_webui/db.py | 8 ++++---- youtube_dl_webui/task.py | 4 ++-- youtube_dl_webui/worker.py | 5 +++-- 5 files changed, 11 insertions(+), 24 deletions(-) diff --git a/youtube_dl_webui/config.py b/youtube_dl_webui/config.py index 5a40b2e..071d13a 100644 --- a/youtube_dl_webui/config.py +++ b/youtube_dl_webui/config.py @@ -164,18 +164,3 @@ def get_val(self, key): def __getitem__(self, key): return self.get_val(key) - - - - - - - - - - - - - - - diff --git a/youtube_dl_webui/core.py b/youtube_dl_webui/core.py index deeb46d..b13566c 100644 --- a/youtube_dl_webui/core.py +++ b/youtube_dl_webui/core.py @@ -201,7 +201,7 @@ def event_fatal(cls, svr, event, data, arg): tid, data = data['tid'], data['data'] cls._task_mgr.update_log(tid, data) - if data['type'] == 'invalid_url': + if data['type'] == 'fatal': cls._task_mgr.halt_task(tid) @classmethod @@ -265,6 +265,7 @@ def __init__(self, cmd_args=None): self.msg_mgr.reg_event('info_dict', WorkMsgDispatcher.event_info_dict) self.msg_mgr.reg_event('log', WorkMsgDispatcher.event_log) self.msg_mgr.reg_event('progress', WorkMsgDispatcher.event_progress) + self.msg_mgr.reg_event('fatal', WorkMsgDispatcher.event_fatal) self.server = Server(web_cli, self.conf['server']['host'], self.conf['server']['port']) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index 223cd06..470a1f3 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -76,8 +76,8 @@ def update(self, tid, val_dict={}): self.conn.commit() def get_ydl_opts(self, tid): - self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?) and state not in (?,?)', - (tid, state_index['finished'], state_index['invalid'])) + self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?) and state not in (?)', + (tid, state_index['finished'])) row = self.db.fetchone() if row is None: @@ -168,9 +168,9 @@ def halt_task(self, tid, elapsed, halt_time=time()): state = state_index['invalid'] db_data = { 'task_info': { 'state': state, - 'finish_time': finish_time, + 'finish_time': halt_time, }, - 'task_status': {'pause_time': finish_time, + 'task_status': {'pause_time': halt_time, 'eta': 0, 'speed': 0, 'elapsed': elapsed, diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index 4b11d24..c76b792 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -70,7 +70,7 @@ def halt(self): tm = time() self.state = state_index['invalid'] - self.pause_time = tm + self.halt_time = tm self.finish_time = tm self.elapsed = self.elapsed + (tm - self.touch) self.touch = tm @@ -188,7 +188,7 @@ def halt_task(self, tid): task = self._tasks_dict[tid] task.halt() - self._db.halt_task(tid, finish_time=task.halt_time, elapsed=task.elapsed) + self._db.halt_task(tid, halt_time=task.halt_time, elapsed=task.elapsed) self._db.update_log(tid, task.log) del self._tasks_dict[tid] diff --git a/youtube_dl_webui/worker.py b/youtube_dl_webui/worker.py index 26d9d95..4cb4cec 100644 --- a/youtube_dl_webui/worker.py +++ b/youtube_dl_webui/worker.py @@ -68,7 +68,7 @@ def warning(self, msg): def error(self, msg): self.logger.debug('error: %s' %(self.ansi_escape(msg))) - payload = {'time': int(time()), 'type': 'warning', 'error': self.ansi_escape(msg)} + payload = {'time': int(time()), 'type': 'warning', 'msg': self.ansi_escape(msg)} self.msg_cli.put('log', {'tid': self.tid, 'data': payload}) def ansi_escape(self, msg): @@ -78,12 +78,13 @@ def ansi_escape(self, msg): class FatalEvent(object): def __init__(self, tid, msg_cli): + self.logger = logging.getLogger('ydl_webui') self.tid = tid self.msg_cli = msg_cli def invalid_url(self, url): self.logger.debug('fatal error: invalid url') - payload = {'time': int(time()), 'type': 'invalid_url', 'error': 'invalid url: %s' %(url)} + payload = {'time': int(time()), 'type': 'fatal', 'msg': 'invalid url: %s' %(url)} self.msg_cli.put('fatal', {'tid': self.tid, 'data': payload}) From 1fafa5034acc938a0cc911a723d7357b47219202 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Fri, 8 Sep 2017 15:38:45 +0800 Subject: [PATCH 70/88] small fix --- youtube_dl_webui/server.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/youtube_dl_webui/server.py b/youtube_dl_webui/server.py index 5b82828..acc8415 100644 --- a/youtube_dl_webui/server.py +++ b/youtube_dl_webui/server.py @@ -13,10 +13,6 @@ app = Flask(__name__) -RQ = None -WQ = None - -WQ_DICT = {'from': 'server'} MSG_INVALID_REQUEST = {'status': 'error', 'errmsg': 'invalid request'} @app.route('/') From 1924bd487043b002fb5b0c338102237053cb0b1a Mon Sep 17 00:00:00 2001 From: sky Date: Sat, 9 Sep 2017 22:25:05 +0800 Subject: [PATCH 71/88] update file delete function --- youtube_dl_webui/static/js/global.js | 48 +++++++++++++++------------ youtube_dl_webui/templates/index.html | 29 ++++++++++------ 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/youtube_dl_webui/static/js/global.js b/youtube_dl_webui/static/js/global.js index 40e7a21..674f174 100644 --- a/youtube_dl_webui/static/js/global.js +++ b/youtube_dl_webui/static/js/global.js @@ -7,7 +7,7 @@ var videoDownload = (function (Vue, extendAM){ videoList: [], videoListCopy: [], showModal: false, - modalType: 'addTask', + modalType: 'addTask', // tablist: ['status', 'details', 'file24s', 'peers', 'options'], tablist: ['Status', 'Details', 'Log'], showTab: 'Status', @@ -48,49 +48,55 @@ var videoDownload = (function (Vue, extendAM){ showAddTaskModal: function(){ this.modalData.url = ''; this.showModal = true; - this.modalType = 'addTask'; + this.modalType = 'addTask'; this.$nextTick(function(){ this.$refs.url.focus(); }); }, - showRemoveTaskModal: function(){ - this.modalData.removeFile = false; - this.showModal = true; - this.modalType = 'removeTask'; - }, + execFunction: function(){ + switch(this.modalType) { + case 'addTask': + this.addTask(); + break; + case 'removeTask': + this.removeTask(); + break; + } + }, + showRemoveTaskModal: function(){ + this.modalData.removeFile = false; + this.showModal = true; + this.modalType = 'removeTask'; + }, addTask: function(){ var _self = this; var url = _self.headPath + 'task'; - Vue.http.post(url, _self.modalData, {emulateJSON: true}).then(function(res){ _self.showModal = false; + Vue.http.post(url, _self.modalData, {emulateJSON: true}).then(function(res){ + _self.showModal = false; that.getTaskList(); }, function(err){ _self.showAlertToast(err, 'error'); }); }, - modalConfirmHandler: function(){ - switch(modalType){ - case 'addTask': - this.addTask(); - break; - case 'deleteTask': - this.removeTask(); - break; - } - } removeTask: function(){ var _self = this; var url = _self.headPath + 'task/tid/' + (_self.videoList[_self.currentSelected] && _self.videoList[_self.currentSelected].tid); - if(_self.modalData.removeFile){ - url += '?del_data=true'; - } + if(_self.modalData.removeFile){ + url += '?del_data=true'; + } Vue.http.delete(url).then(function(res){ _self.showAlertToast('Task Delete', 'info'); _self.videoList.splice(_self.currentSelected, _self.currentSelected+1); + _self.showModal = false; that.getTaskList(); }, function(err){ _self.showAlertToast(err, 'error'); }); }, + removeData: function(){ + this.modalData.removeFile = true; + this.removeTask(); + }, pauseTask: function(){ var _self = this; var url = _self.headPath + 'task/tid/' + (_self.videoList[_self.currentSelected] && _self.videoList[_self.currentSelected].tid) + '?act=pause'; diff --git a/youtube_dl_webui/templates/index.html b/youtube_dl_webui/templates/index.html index 171492f..487f279 100644 --- a/youtube_dl_webui/templates/index.html +++ b/youtube_dl_webui/templates/index.html @@ -29,10 +29,10 @@ @@ -108,15 +108,22 @@
- - - + + +
From 22d783cb27fb37c01d97359159be4cfa99b82caa Mon Sep 17 00:00:00 2001 From: d0u9 Date: Sat, 9 Sep 2017 22:42:16 +0800 Subject: [PATCH 72/88] raise error when resume a finished task --- youtube_dl_webui/db.py | 3 +-- youtube_dl_webui/task.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/youtube_dl_webui/db.py b/youtube_dl_webui/db.py index 470a1f3..cadcb04 100644 --- a/youtube_dl_webui/db.py +++ b/youtube_dl_webui/db.py @@ -76,8 +76,7 @@ def update(self, tid, val_dict={}): self.conn.commit() def get_ydl_opts(self, tid): - self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?) and state not in (?)', - (tid, state_index['finished'])) + self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?)', (tid, )) row = self.db.fetchone() if row is None: diff --git a/youtube_dl_webui/task.py b/youtube_dl_webui/task.py index c76b792..eecd720 100644 --- a/youtube_dl_webui/task.py +++ b/youtube_dl_webui/task.py @@ -144,6 +144,9 @@ def start_task(self, tid, ignore_state=False): except TaskInexistenceError as e: raise TaskInexistenceError(e.msg) + if status['state'] == state_index['finished']: + raise TaskError('Task is finished') + task = Task(tid, self._msg_cli, ydl_opts=ydl_opts, info=info, status=status, log_size=self._conf['general']['log_size']) self._tasks_dict[tid] = task From 72c01bdbbb8f1d321f27047ebaba9183732a6a36 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Sat, 23 Sep 2017 23:05:23 +0800 Subject: [PATCH 73/88] fix restful api --- youtube_dl_webui/static/js/global.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl_webui/static/js/global.js b/youtube_dl_webui/static/js/global.js index 674f174..e30af33 100644 --- a/youtube_dl_webui/static/js/global.js +++ b/youtube_dl_webui/static/js/global.js @@ -82,7 +82,7 @@ var videoDownload = (function (Vue, extendAM){ var _self = this; var url = _self.headPath + 'task/tid/' + (_self.videoList[_self.currentSelected] && _self.videoList[_self.currentSelected].tid); if(_self.modalData.removeFile){ - url += '?del_data=true'; + url += '?del_file=true'; } Vue.http.delete(url).then(function(res){ _self.showAlertToast('Task Delete', 'info'); From 650f290d167680f1bf1223890c5e3e4d1137c4b7 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Sun, 24 Sep 2017 14:09:27 +0800 Subject: [PATCH 74/88] add dockerfile --- docker/Dockerfile | 43 ++++++++++++++++++++++++++ docker/README.md | 60 +++++++++++++++++++++++++++++++++++++ docker/default_config.json | 13 ++++++++ docker/docker-entrypoint.sh | 16 ++++++++++ 4 files changed, 132 insertions(+) create mode 100644 docker/Dockerfile create mode 100644 docker/README.md create mode 100644 docker/default_config.json create mode 100755 docker/docker-entrypoint.sh diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..ea3761b --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,43 @@ +# For ubuntu, the latest tag points to the LTS version, since that is +# recommended for general use. +FROM python:3.6-slim + +# grab gosu for easy step-down from root +ENV GOSU_VERSION 1.10 +RUN set -x \ + && buildDeps=' \ + unzip \ + ca-certificates \ + dirmngr \ + wget \ + ' \ + && apt-get update && apt-get install -y --no-install-recommends $buildDeps \ + && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture)" \ + && wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture).asc" \ + && export GNUPGHOME="$(mktemp -d)" \ + && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ + && gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu \ + && rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc \ + && chmod +x /usr/local/bin/gosu \ + && gosu nobody true + +# install youtube-dl-webui +ENV YOUTUBE_DL_WEBUI_SOURCE /usr/src/youtube_dl_webui +WORKDIR $YOUTUBE_DL_WEBUI_SOURCE + +RUN : \ + && pip install --no-cache-dir youtube-dl flask \ + && wget -O youtube-dl-webui.zip https://github.com/d0u9/youtube-dl-webui/archive/web_dev.zip \ + && unzip youtube-dl-webui.zip \ + && cd youtube-dl-webui*/ \ + && cp -r ./* $YOUTUBE_DL_WEBUI_SOURCE/ \ + && ln -s $YOUTUBE_DL_WEBUI_SOURCE/example_config.json /etc/youtube-dl-webui.json \ + && cd .. && rm -rf youtube-dl-webui* \ + && apt-get purge -y --auto-remove wget unzip dirmngr \ + && rm -fr /var/lib/apt/lists/* + +COPY docker-entrypoint.sh /usr/local/bin +COPY default_config.json /config.json +ENTRYPOINT ["docker-entrypoint.sh"] + +CMD ["python", "-m", "youtube_dl_webui"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..8896b52 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,60 @@ +# youtube-dl-webui + +--- + +Visit [GitHub](https://github.com/d0u9/youtube-dl-webui) for more details. + + +## Install + +1. From DockerHUB + + docker pull d0u9/youtube-dl-webui + + For china users, aliyun docker repo is more preferable: + + docker pull registry.cn-hangzhou.aliyuncs.com/master/youtube-dl-webui + + +2. From DockerFile + + cd /tmp + docker build -f -t youtube-dl-webui . + +## Usage + +1. Run container + + docker run -d \ + --name \ + -e PGID= \ + -e PUID= \ + -e PORT=port \ + -e CONF_FILE= \ + -v : \ + -p : \ + -v : \ + d0u9/youtube-dl-webui + + +2. Automatically start container after booting + + Create `/etc/systemd/system/docker-youtube_dl_webui.service`, and fill + with the contents below: + + [Unit] + Description=youtube-dl downloader + Requires=docker.service + After=docker.service + + [Service] + Restart=always + ExecStart=/usr/bin/docker start -a + ExecStop=/usr/bin/docker stop -t 2 + + [Install] + WantedBy=default.target + +--- + + diff --git a/docker/default_config.json b/docker/default_config.json new file mode 100644 index 0000000..245b1a9 --- /dev/null +++ b/docker/default_config.json @@ -0,0 +1,13 @@ +{ + "general": { + "download_dir": "/tmp/youtube_dl", + "db_path": "/tmp/youtube_dl_webui.db", + "log_size": 10 + }, + "server": { + "host": "0.0.0.0", + "port": 5000 + }, + "youtube_dl": { + } +} diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 0000000..da1ef02 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +pgid=${PGID:-$(id -u nobody)} +puid=${PUID:-$(id -g nobody)} + +conf=${CONF_FILE:-"/config.json"} +host=${HOST:-"0.0.0.0"} +port=${PORT:-5000} + + +if [[ "$*" == python*-m*youtube_dl_webui* ]]; then + exec gosu $puid:$pgid "$@" -c $conf --host $host --port $port +fi + +exec "$@" From bcfb9678e596950c55f832c5410552e47dc3df46 Mon Sep 17 00:00:00 2001 From: d0u9 Date: Mon, 25 Sep 2017 09:19:37 +0800 Subject: [PATCH 75/88] remove docker file --- docker/Dockerfile | 43 -------------------------- docker/README.md | 60 ------------------------------------- docker/default_config.json | 13 -------- docker/docker-entrypoint.sh | 16 ---------- 4 files changed, 132 deletions(-) delete mode 100644 docker/Dockerfile delete mode 100644 docker/README.md delete mode 100644 docker/default_config.json delete mode 100755 docker/docker-entrypoint.sh diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index ea3761b..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -# For ubuntu, the latest tag points to the LTS version, since that is -# recommended for general use. -FROM python:3.6-slim - -# grab gosu for easy step-down from root -ENV GOSU_VERSION 1.10 -RUN set -x \ - && buildDeps=' \ - unzip \ - ca-certificates \ - dirmngr \ - wget \ - ' \ - && apt-get update && apt-get install -y --no-install-recommends $buildDeps \ - && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture)" \ - && wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture).asc" \ - && export GNUPGHOME="$(mktemp -d)" \ - && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ - && gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu \ - && rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc \ - && chmod +x /usr/local/bin/gosu \ - && gosu nobody true - -# install youtube-dl-webui -ENV YOUTUBE_DL_WEBUI_SOURCE /usr/src/youtube_dl_webui -WORKDIR $YOUTUBE_DL_WEBUI_SOURCE - -RUN : \ - && pip install --no-cache-dir youtube-dl flask \ - && wget -O youtube-dl-webui.zip https://github.com/d0u9/youtube-dl-webui/archive/web_dev.zip \ - && unzip youtube-dl-webui.zip \ - && cd youtube-dl-webui*/ \ - && cp -r ./* $YOUTUBE_DL_WEBUI_SOURCE/ \ - && ln -s $YOUTUBE_DL_WEBUI_SOURCE/example_config.json /etc/youtube-dl-webui.json \ - && cd .. && rm -rf youtube-dl-webui* \ - && apt-get purge -y --auto-remove wget unzip dirmngr \ - && rm -fr /var/lib/apt/lists/* - -COPY docker-entrypoint.sh /usr/local/bin -COPY default_config.json /config.json -ENTRYPOINT ["docker-entrypoint.sh"] - -CMD ["python", "-m", "youtube_dl_webui"] diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index 8896b52..0000000 --- a/docker/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# youtube-dl-webui - ---- - -Visit [GitHub](https://github.com/d0u9/youtube-dl-webui) for more details. - - -## Install - -1. From DockerHUB - - docker pull d0u9/youtube-dl-webui - - For china users, aliyun docker repo is more preferable: - - docker pull registry.cn-hangzhou.aliyuncs.com/master/youtube-dl-webui - - -2. From DockerFile - - cd /tmp - docker build -f -t youtube-dl-webui . - -## Usage - -1. Run container - - docker run -d \ - --name \ - -e PGID= \ - -e PUID= \ - -e PORT=port \ - -e CONF_FILE= \ - -v : \ - -p : \ - -v : \ - d0u9/youtube-dl-webui - - -2. Automatically start container after booting - - Create `/etc/systemd/system/docker-youtube_dl_webui.service`, and fill - with the contents below: - - [Unit] - Description=youtube-dl downloader - Requires=docker.service - After=docker.service - - [Service] - Restart=always - ExecStart=/usr/bin/docker start -a - ExecStop=/usr/bin/docker stop -t 2 - - [Install] - WantedBy=default.target - ---- - - diff --git a/docker/default_config.json b/docker/default_config.json deleted file mode 100644 index 245b1a9..0000000 --- a/docker/default_config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "general": { - "download_dir": "/tmp/youtube_dl", - "db_path": "/tmp/youtube_dl_webui.db", - "log_size": 10 - }, - "server": { - "host": "0.0.0.0", - "port": 5000 - }, - "youtube_dl": { - } -} diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh deleted file mode 100755 index da1ef02..0000000 --- a/docker/docker-entrypoint.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -set -e - -pgid=${PGID:-$(id -u nobody)} -puid=${PUID:-$(id -g nobody)} - -conf=${CONF_FILE:-"/config.json"} -host=${HOST:-"0.0.0.0"} -port=${PORT:-5000} - - -if [[ "$*" == python*-m*youtube_dl_webui* ]]; then - exec gosu $puid:$pgid "$@" -c $conf --host $host --port $port -fi - -exec "$@" From ea9ed44726ee56fae3ed4612bb382c7e3fa476af Mon Sep 17 00:00:00 2001 From: d0u9 Date: Mon, 25 Sep 2017 15:34:24 +0800 Subject: [PATCH 76/88] add about button --- .../static/css/modalComponent.css | 16 +++++- youtube_dl_webui/static/js/global.js | 4 ++ youtube_dl_webui/templates/index.html | 54 +++++++++++++------ 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/youtube_dl_webui/static/css/modalComponent.css b/youtube_dl_webui/static/css/modalComponent.css index fcc787f..9c21e9d 100644 --- a/youtube_dl_webui/static/css/modalComponent.css +++ b/youtube_dl_webui/static/css/modalComponent.css @@ -16,7 +16,7 @@ } .modal-container { - width: 400px; + width: 500px; margin: 0px auto; padding: 20px; background-color: #fff; @@ -27,8 +27,10 @@ } .modal-header div { + font-weight: bold; + font-size: 20px; margin-top: 0; - color: #42b983; + color: #020202; } .modal-body div { @@ -38,6 +40,11 @@ align-items: center; } +.modal-body label { + min-width: 60px; + max-width: 100px; +} + .modal-body input { font-size: 15px; display: block; @@ -45,6 +52,11 @@ padding: 5px; } +.modal-body .caption { + width: 80px; + font-weight: bold; +} + .modal-footer { display: flex; justify-content: center; diff --git a/youtube_dl_webui/static/js/global.js b/youtube_dl_webui/static/js/global.js index e30af33..dffbc4e 100644 --- a/youtube_dl_webui/static/js/global.js +++ b/youtube_dl_webui/static/js/global.js @@ -117,6 +117,10 @@ var videoDownload = (function (Vue, extendAM){ _self.showAlertToast(err, 'error'); }); }, + about: function() { + this.showModal = true; + this.modalType = 'about'; + }, selected: function(index){ var _self = this; this.currentSelected = index; diff --git a/youtube_dl_webui/templates/index.html b/youtube_dl_webui/templates/index.html index 487f279..4336749 100644 --- a/youtube_dl_webui/templates/index.html +++ b/youtube_dl_webui/templates/index.html @@ -48,6 +48,7 @@ +
@@ -109,23 +110,42 @@
- - - - + + + + +