diff --git a/.gitignore b/.gitignore index 59324bd41..b846f4b9f 100644 --- a/.gitignore +++ b/.gitignore @@ -121,6 +121,5 @@ data/close.pl ssl/input.pl data/datadir.pl data/502Task.pl -plugins/qbittorrent/ data/default.pl data/backup.pl diff --git a/plugins/qbittorrent/LICENSE b/plugins/qbittorrent/LICENSE new file mode 100644 index 000000000..b89fade39 --- /dev/null +++ b/plugins/qbittorrent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Mr Chen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/qbittorrent/README.md b/plugins/qbittorrent/README.md new file mode 100644 index 000000000..6ae4b8a7b --- /dev/null +++ b/plugins/qbittorrent/README.md @@ -0,0 +1,14 @@ +# mw-qbittorrent +mdserver-web|qbittorrent管理 + + +### 安装过程 + +``` +* 先进行压缩 `cd mw-qbittorrent && zip qbittorrent.zip -r ./* ` +* 在mdserver-web点击`添加插件` +``` + +### 截图 + +[![截图1](/screenshot/ss1.jpg)](/screenshot/ss1.jpg) \ No newline at end of file diff --git a/plugins/qbittorrent/conf/qb.conf b/plugins/qbittorrent/conf/qb.conf new file mode 100755 index 000000000..765541ada --- /dev/null +++ b/plugins/qbittorrent/conf/qb.conf @@ -0,0 +1,35 @@ +[db] +DB_HOST = 127.0.0.1 +DB_PORT = 3306 +DB_USER = qbittorrent +DB_PASS = qbittorrent +DB_NAME = qbittorrent + +[qb] +QB_HOST = 127.0.0.1 +QB_PORT = 8080 +QB_USER = admin +QB_PWD = adminadmin + + +[file] +FILE_TO={$SERVER_PATH}/tmp +FILE_TRANSFER_TO={$SERVER_PATH}/tmp +FILE_OWN=www +FILE_GROUP=www +FILE_ENC_SWITCH=0 +FILE_API_URL=http://v.demo.com/api/key/id/{$KEY} +FILE_ASYNC_SWITCH=0 + +[task] +TASK_SIZE_LIMIT=1 +TASK_RATE=4 +TASK_COMPLETED_RATE=10 +TASK_DEBUG=0 + +[setting] +QUEUE_SWITCH = 1 +MAX_ACTIVE_UPLOADS = 1 +MAX_ACTIVE_TORRENTS = 10 +MAX_ACTIVE_DOWNLOADS = 10 + diff --git a/plugins/qbittorrent/conf/qb.sql b/plugins/qbittorrent/conf/qb.sql new file mode 100755 index 000000000..08baac3a7 --- /dev/null +++ b/plugins/qbittorrent/conf/qb.sql @@ -0,0 +1,34 @@ +CREATE TABLE `pl_hash_list` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` text NOT NULL, + `info_hash` varchar(40) NOT NULL, + `length` bigint(20) NOT NULL, + `status` char(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `create_time` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `info_hash` (`info_hash`), + KEY `create_time` (`create_time`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +CREATE TABLE `pl_hash_file` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `pid` bigint(20) NOT NULL, + `name` text NOT NULL, + `m3u8` varchar(40) NOT NULL, + `key` char(40) NULL DEFAULT '', + `length` bigint(20) NULL DEFAULT 0, + `create_time` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `create_time` (`create_time`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +CREATE TABLE `pl_hash_queue` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `info_hash` char(40) NOT NULL, + `length` bigint(20) NOT NULL DEFAULT 0, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`), + INDEX `length`(`length`) USING BTREE, + KEY `created_at` (`created_at`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/plugins/qbittorrent/ico.png b/plugins/qbittorrent/ico.png new file mode 100755 index 000000000..99808f644 Binary files /dev/null and b/plugins/qbittorrent/ico.png differ diff --git a/plugins/qbittorrent/index.html b/plugins/qbittorrent/index.html new file mode 100755 index 000000000..45229c8d3 --- /dev/null +++ b/plugins/qbittorrent/index.html @@ -0,0 +1,23 @@ +
+
+
+

服务

+

自启动

+

导入SQL

+

配置

+

异步脚本

+

日志

+

列表

+ +
+
+
+
+
+
+ +
+ \ No newline at end of file diff --git a/plugins/qbittorrent/index.py b/plugins/qbittorrent/index.py new file mode 100755 index 000000000..c7b6abbe3 --- /dev/null +++ b/plugins/qbittorrent/index.py @@ -0,0 +1,357 @@ +# coding: utf-8 + +import time +import random +import os +import json +import re +import sys + +sys.path.append(os.getcwd() + "/class/core") +import public + +reload(sys) +sys.setdefaultencoding('utf8') + +sys.path.append('/usr/local/lib/python2.7/site-packages') + + +app_debug = False +if public.isAppleSystem(): + app_debug = True + + +def getPluginName(): + return 'qbittorrent' + + +def getPluginDir(): + return public.getPluginDir() + '/' + getPluginName() + + +def getServerDir(): + return public.getServerDir() + '/' + getPluginName() + + +def getInitDFile(): + if app_debug: + return '/tmp/' + getPluginName() + return '/etc/init.d/' + getPluginName() + + +def getArgs(): + args = sys.argv[2:] + tmp = {} + args_len = len(args) + + if args_len == 1: + t = args[0].strip('{').strip('}') + t = t.split(':') + tmp[t[0]] = t[1] + elif args_len > 1: + for i in range(len(args)): + t = args[i].split(':') + tmp[t[0]] = t[1] + + return tmp + + +def checkArgs(data, ck=[]): + for i in range(len(ck)): + if not ck[i] in data: + return (False, public.returnJson(False, '参数:(' + ck[i] + ')没有!')) + return (True, public.returnJson(True, 'ok')) + + +def getInitDTpl(): + path = getPluginDir() + "/init.d/" + getPluginName() + ".tpl" + return path + + +def getSqlFile(): + file = getPluginDir() + "/conf/qb.sql" + return file + + +def getRsyncShell(): + file = getServerDir() + "/workers/rsync.sh" + return file + + +def getConf(): + file = getServerDir() + "/qb.conf" + return file + + +def getRunLog(): + file = getServerDir() + "/logs.pl" + return file + + +def contentReplace(content): + service_path = public.getServerDir() + content = content.replace('{$SERVER_PATH}', service_path) + return content + + +def initDreplace(): + + ddir = getServerDir() + '/workers' + if not os.path.exists(ddir): + sdir = getPluginDir() + '/workers' + public.execShell('cp -rf ' + sdir + ' ' + getServerDir()) + + cfg = getServerDir() + '/qb.conf' + if not os.path.exists(cfg): + cfg_tpl = getPluginDir() + '/conf/qb.conf' + content = public.readFile(cfg_tpl) + content = contentReplace(content) + public.writeFile(cfg, content) + + file_tpl = getInitDTpl() + service_path = os.path.dirname(os.getcwd()) + + initD_path = getServerDir() + '/init.d' + if not os.path.exists(initD_path): + os.mkdir(initD_path) + file_bin = initD_path + '/' + getPluginName() + + # initd replace + if not os.path.exists(file_bin): + content = public.readFile(file_tpl) + content = contentReplace(content) + public.writeFile(file_bin, content) + public.execShell('chmod +x ' + file_bin) + + return file_bin + + +def status(): + data = public.execShell( + "ps -ef|grep qbittorrent_worker | grep -v grep | awk '{print $2}'") + if data[0] == '': + return 'stop' + return 'start' + + +def start(): + + cmd = "ps -ef | grep qbittorrent-nox |grep -v grep |awk '{print $2}'" + ret = public.execShell(cmd) + if ret[0] == '': + public.execShell('qbittorrent-nox -d') + + file = initDreplace() + + data = public.execShell(file + ' start') + if data[1] == '': + return 'ok' + return data[1] + + +def stop(): + file = initDreplace() + data = public.execShell(file + ' stop') + # cmd = "ps -ef | grep qbittorrent-nox |grep -v grep |awk '{print $2}' | xargs kill" + # public.execShell(cmd) + if data[1] == '': + return 'ok' + return data[1] + + +def restart(): + file = initDreplace() + data = public.execShell(file + ' restart') + if data[1] == '': + return 'ok' + return data[1] + + +def reload(): + file = initDreplace() + data = public.execShell(file + ' reload') + if data[1] == '': + return 'ok' + return data[1] + + +def initdStatus(): + if not app_debug: + if public.isAppleSystem(): + return "Apple Computer does not support" + + initd_bin = getInitDFile() + if os.path.exists(initd_bin): + return 'ok' + return 'fail' + + +def initdInstall(): + import shutil + if not app_debug: + if public.isAppleSystem(): + return "Apple Computer does not support" + + mysql_bin = initDreplace() + initd_bin = getInitDFile() + shutil.copyfile(mysql_bin, initd_bin) + public.execShell('chmod +x ' + initd_bin) + public.execShell('chkconfig --add ' + getPluginName()) + return 'ok' + + +def initdUinstall(): + if not app_debug: + if public.isAppleSystem(): + return "Apple Computer does not support" + initd_bin = getInitDFile() + os.remove(initd_bin) + public.execShell('chkconfig --del ' + getPluginName()) + return 'ok' + + +def matchData(reg, content): + tmp = re.search(reg, content).groups() + return tmp[0] + + +def getDbConfInfo(): + cfg = getConf() + content = public.readFile(cfg) + data = {} + data['DB_HOST'] = matchData("DB_HOST\s*=\s(.*)", content) + data['DB_USER'] = matchData("DB_USER\s*=\s(.*)", content) + data['DB_PORT'] = matchData("DB_PORT\s*=\s(.*)", content) + data['DB_PASS'] = matchData("DB_PASS\s*=\s(.*)", content) + data['DB_NAME'] = matchData("DB_NAME\s*=\s(.*)", content) + return data + + +def getQbConf(): + cfg = getConf() + content = public.readFile(cfg) + data = {} + data['QB_HOST'] = matchData("QB_HOST\s*=\s(.*)", content) + data['QB_PORT'] = matchData("QB_PORT\s*=\s(.*)", content) + data['QB_USER'] = matchData("QB_USER\s*=\s(.*)", content) + data['QB_PWD'] = matchData("QB_PWD\s*=\s(.*)", content) + return data + + +def pMysqlDb(): + data = getDbConfInfo() + conn = mysql.mysql() + conn.setHost(data['DB_HOST']) + conn.setUser(data['DB_USER']) + conn.setPwd(data['DB_PASS']) + conn.setPort(int(data['DB_PORT'])) + conn.setDb(data['DB_NAME']) + return conn + + +def pQbClient(): + from qbittorrent import Client + info = getQbConf() + url = 'http://' + info['QB_HOST'] + ':' + info['QB_PORT'] + '/' + qb = Client(url) + qb.login(info['QB_USER'], info['QB_PWD']) + return qb + + +def getQbUrl(): + info = getQbConf() + url = 'http://' + info['QB_HOST'] + ':' + info['QB_PORT'] + '/' + return public.returnJson(True, 'ok', url) + + +def qbList(): + args = getArgs() + # data = checkArgs(args, ['type']) + # if not data[0]: + # return data[1] + args_type = '' + if 'type' in args: + args_type = args['type'] + + f = ['downloading', 'completed'] + tfilter = '' + if args_type in f: + tfilter = args['type'] + try: + qb = pQbClient() + torrents = qb.torrents(filter=tfilter) + data = {} + data['type'] = tfilter + data['torrents'] = torrents + return public.returnJson(True, 'ok', data) + except Exception as e: + return public.returnJson(False, str(e)) + + +def qbDel(): + args = getArgs() + data = checkArgs(args, ['hash']) + if not data[0]: + return data[1] + qb = pQbClient() + data = qb.delete(args['hash']) + return public.returnJson(True, '操作成功!', data) + + +def qbAdd(): + args = getArgs() + data = checkArgs(args, ['hash']) + if not data[0]: + return data[1] + url = 'magnet:?xt=urn:btih:' + args['hash'] + qb = pQbClient() + data = qb.download_from_link(url) + return public.returnJson(True, '操作成功!', data) + + +def test(): + qb = pQbClient() + # magnet_link = "magnet:?xt=urn:btih:57a0ec92a61c60585f1b7a206a75798aa69285a5" + # print qb.download_from_link(magnet_link) + torrents = qb.torrents(filter='downloading') + for torrent in torrents: + print public.returnJson(False, torrent) + +if __name__ == "__main__": + func = sys.argv[1] + if func == 'status': + print status() + elif func == 'start': + print start() + elif func == 'stop': + print stop() + elif func == 'restart': + print restart() + elif func == 'reload': + print reload() + elif func == 'initd_status': + print initdStatus() + elif func == 'initd_install': + print initdInstall() + elif func == 'initd_uninstall': + print initdUinstall() + elif func == 'get_sql': + print getSqlFile() + elif func == 'rsync_shell': + print getRsyncShell() + elif func == 'conf': + print getConf() + elif func == 'get_run_Log': + print getRunLog() + elif func == 'qb_list': + print qbList() + elif func == 'qb_del': + print qbDel() + elif func == 'qb_add': + print qbAdd() + elif func == 'qb_url': + print getQbUrl() + elif func == 'test': + print test() + else: + print 'error' diff --git a/plugins/qbittorrent/info.json b/plugins/qbittorrent/info.json new file mode 100755 index 000000000..c77d22c60 --- /dev/null +++ b/plugins/qbittorrent/info.json @@ -0,0 +1,18 @@ +{ + "sort": 7, + "ps": "一个新的轻量级BitTorrent客户端", + "name": "qbittorrent", + "title": "qBittorrent", + "shell": "install.sh", + "versions":["4.1.5"], + "updates":["4.1.5"], + "tip": "soft", + "checks": "server/qbittorrent", + "path": "server/qbittorrent", + "display": 1, + "author": "Zend", + "date": "2017-04-01", + "home": "https://www.qbittorrent.org", + "type": 0, + "pid": "5" +} \ No newline at end of file diff --git a/plugins/qbittorrent/init.d/qbittorrent.tpl b/plugins/qbittorrent/init.d/qbittorrent.tpl new file mode 100755 index 000000000..a6fb06707 --- /dev/null +++ b/plugins/qbittorrent/init.d/qbittorrent.tpl @@ -0,0 +1,46 @@ +#!/bin/sh +# chkconfig: 2345 55 25 +# description: qbittorrent Service + +### BEGIN INIT INFO +# Provides: qbittorrent +# Required-Start: $all +# Required-Stop: $all +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: starts qbittorrent +# Description: starts the MDW-Web +### END INIT INFO + + +qb_start(){ + cd {$SERVER_PATH}/qbittorrent/workers + nohup python qbittorrent_worker.py > {$SERVER_PATH}/qbittorrent/logs.pl 2>&1 & + echo "qbittorrent started" +} + +qb_stop(){ + echo "Stopping ..." + #ps -ef | grep qbittorrent-nox-bin | grep -v grep | awk '{print $2}' | xargs kill -9 + ps -ef | grep "qbittorrent_worker.py" | grep -v grep | awk '{print $2}' | xargs kill -9 + echo "qbittorrent stopped" +} + + +case "$1" in + start) + qb_start + ;; + stop) + qb_stop + ;; + restart|reload) + qb_stop + sleep 0.3 + qb_start + ;; + *) + echo "Please use start or stop as first argument" + ;; +esac + diff --git a/plugins/qbittorrent/install.sh b/plugins/qbittorrent/install.sh new file mode 100755 index 000000000..f72dfa7b6 --- /dev/null +++ b/plugins/qbittorrent/install.sh @@ -0,0 +1,74 @@ +#!/bin/bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH + +curPath=`pwd` +rootPath=$(dirname "$curPath") +rootPath=$(dirname "$rootPath") +serverPath=$(dirname "$rootPath") +sysName=`uname` + +install_tmp=${rootPath}/tmp/bt_install.pl + +#https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz + +Install_mac_ffmpeg() +{ + if [ ! -f $serverPath/source/ffmpeg-20180702-3c4af57-macos64-static.zip ];then + wget -O $serverPath/source/ffmpeg-20180702-3c4af57-macos64-static.zip https://ffmpeg.zeranoe.com/builds/macos64/static/ffmpeg-20180702-3c4af57-macos64-static.zip + fi + + if [ ! -d $serverPath/lib/ffmpeg ];then + cd $serverPath/source && tar -xvf $serverPath/source/ffmpeg-20180702-3c4af57-macos64-static.zip + mv ffmpeg-20180702-3c4af57-macos64-static $serverPath/lib/ffmpeg + fi +} + +Install_linux_ffmpeg() +{ + if [ ! -f $serverPath/source/ffmpeg-release-amd64-static.tar.xz ];then + wget -O $serverPath/source/ffmpeg-release-amd64-static.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz + fi + + if [ ! -d $serverPath/lib/ffmpeg ];then + cd $serverPath/source && tar -xvf $serverPath/source/ffmpeg-release-amd64-static.tar.xz + mv ffmpeg-4.1.2-amd64-static $serverPath/lib/ffmpeg + fi +} + +Install_qbittorrent() +{ + if [ $sysName == 'Darwin' ]; then + Install_mac_ffmpeg + else + yum -y install qbittorrent-nox + #qbittorrent-nox -d + + Install_linux_ffmpeg + fi + + pip install python-qbittorrent==0.2 + + echo '正在安装脚本文件...' > $install_tmp + mkdir -p $serverPath/qbittorrent + + QB_DIR=${serverPath}/source/qbittorrent + mkdir -p $QB_DIR + + echo '4.1.5' > $serverPath/qbittorrent/version.pl + echo '安装完成' > $install_tmp +} + +Uninstall_qbittorrent() +{ + rm -rf $serverPath/qbittorrent + which yum && yum -y remove qbittorrent-nox + echo "Uninstall_qbittorrent" > $install_tmp +} + +action=$1 +if [ "${1}" == 'install' ];then + Install_qbittorrent +else + Uninstall_qbittorrent +fi diff --git a/plugins/qbittorrent/js/qbittorrent.js b/plugins/qbittorrent/js/qbittorrent.js new file mode 100755 index 000000000..e00249172 --- /dev/null +++ b/plugins/qbittorrent/js/qbittorrent.js @@ -0,0 +1,186 @@ +function qbPostMin(method, args, callback){ + + var req_data = {}; + req_data['name'] = 'qbittorrent'; + req_data['func'] = method; + + if (typeof(args) != 'undefined' && args!=''){ + req_data['args'] = JSON.stringify(args); + } + + $.post('/plugins/run', req_data, function(data) { + if (!data.status){ + layer.msg(data.msg,{icon:0,time:2000,shade: [0.3, '#000']}); + return; + } + + if(typeof(callback) == 'function'){ + callback(data); + } + },'json'); +} + +function qbPost(method, args, callback){ + var loadT = layer.msg('正在获取...', { icon: 16, time: 0, shade: 0.3 }); + qbPostMin(method,args,function(data){ + layer.close(loadT); + if(typeof(callback) == 'function'){ + callback(data); + } + }); +} + +function showHideHash(obj){ + var a = "glyphicon-eye-open"; + var b = "glyphicon-eye-close"; + + if($(obj).hasClass(a)){ + $(obj).removeClass(a).addClass(b); + $(obj).prev().text($(obj).prev().attr('data-pw')) + } else{ + $(obj).removeClass(b).addClass(a); + $(obj).prev().text($(obj).attr('data-pw')); + } +} + + +function copyText(password){ + var clipboard = new ClipboardJS('#bt_copys'); + clipboard.on('success', function (e) { + layer.msg('复制成功',{icon:1,time:2000}); + }); + + clipboard.on('error', function (e) { + layer.msg('复制失败,浏览器不兼容!',{icon:2,time:2000}); + }); + $("#bt_copys").attr('data-clipboard-text',password); + $("#bt_copys").click(); +} + +function getLocalTime(nS) { + return new Date(parseInt(nS) * 1000).toLocaleString().replace(/:\d{1,2}$/,' '); +} + + + +function qbAdd(){ + + var loadOpen = layer.open({ + type: 1, + title: '添加资源', + area: '400px', + content:"
\ +
\ +
\ +
\ +
\ + \ + \ +
\ +
" + }); + + $('#qb_close').click(function(){ + layer.close(loadOpen); + }); + + $('#qb_ok').click(function(){ + var hash = $('#qb_hash').val(); + qbPost('qb_add', {hash:hash}, function(data){ + + var rdata = $.parseJSON(data.data); + if (rdata['status']){ + showMsg(rdata.msg, function(){ + qbList(); + },{icon:1,time:2000,shade: [0.3, '#000']}); + layer.close(loadOpen); + } else { + layer.msg(rdata.msg,{icon:2,time:2000,shade: [0.3, '#000']}); + } + }); + }); +} + + +function qbDel(hash){ + qbPost('qb_del', {hash:hash}, function(data){ + var rdata = $.parseJSON(data.data); + if (rdata['status']){ + layer.msg(rdata.msg,{icon:1,time:2000,shade: [0.3, '#000']}); + } else { + layer.msg(rdata.msg,{icon:2,time:2000,shade: [0.3, '#000']}); + } + }); +} + +function qbListFind(){ + var qbs = $('#qb_selected').val(); + if ( qbs == '0' ){ + qbList(); + } else { + qbList(qbs); + } +} + +function openAdminUrl(){ + qbPost('qb_url', '', function(data){ + var rdata = $.parseJSON(data.data); + window.open(rdata.data); + }); +} + +function qbList(search){ + var _data = {}; + _data['test'] ='yes'; + if(typeof(search) != 'undefined'){ + _data['type'] = search; + } + + qbPost('qb_list', _data, function(data){ + + var rdata = $.parseJSON(data.data); + if (!rdata['status']){ + layer.msg(rdata.msg,{icon:0,time:2000,shade: [0.3, '#000']}); + return; + } + + content = ''; + content += ''; + + content += '
'; + content += ''; + content += ''; + content += ''; + content += ''; + content += ''; + + content += ''; + + ulist = rdata.data.torrents; + for (i in ulist){ + content += ''+ + ''+ + ''; + } + + content += ''; + content += '
种子(hash)添加时间操作(添加 | 管理)
'+ + ''+ulist[i]['hash'].substr(0,3)+'' + + ''+ + ''+ + ''+getLocalTime(ulist[i]['added_on'])+'删除
'; + + $(".soft-man-con").html(content); + + var type = rdata.data.type; + if (type == ''){ + $("#qb_selected option[value='0']").attr("selected", true); + } else { + $("#qb_selected option[value='"+type+"']").attr("selected", true); + } + }); +} diff --git a/plugins/qbittorrent/screenshot/ss1.jpg b/plugins/qbittorrent/screenshot/ss1.jpg new file mode 100644 index 000000000..807c51660 Binary files /dev/null and b/plugins/qbittorrent/screenshot/ss1.jpg differ diff --git a/plugins/qbittorrent/workers/qbittorrent_worker.py b/plugins/qbittorrent/workers/qbittorrent_worker.py new file mode 100755 index 000000000..d44e688f6 --- /dev/null +++ b/plugins/qbittorrent/workers/qbittorrent_worker.py @@ -0,0 +1,719 @@ +#!/usr/bin/env python +# encoding: utf-8 +""" +下载检测 +""" + +''' +pl_hash_list 表字段 status说明 + +0 视频资源 - 正常 +1 不含有视频资源 +2 3小时下载未反应 +4 3个月未下载完成 +''' + +import hashlib +import os +import time +import datetime +import traceback +import sys +import json +import socket +import threading +from hashlib import sha1 +from random import randint +from struct import unpack +from socket import inet_ntoa +from threading import Timer, Thread +from time import sleep + +reload(sys) +sys.setdefaultencoding("utf8") + +sys.path.append('/usr/local/lib/python2.7/site-packages') + + +def formatTime(): + return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + + +def getRunDir(): + return os.getcwd() + + +def getRootDir(): + return os.path.dirname(os.path.dirname(getRunDir())) + + +def toSize(size): + # 字节单位转换 + d = ('b', 'KB', 'MB', 'GB', 'TB') + s = d[0] + for b in d: + if size < 1024: + return str(round(size, 2)) + ' ' + b + size = float(size) / 1024.0 + s = b + return str(round(size, 2)) + ' ' + b + +# import pygeoip +import MySQLdb as mdb + +from configparser import ConfigParser +cp = ConfigParser() +cp.read("../qb.conf") +section_db = cp.sections()[0] +DB_HOST = cp.get(section_db, "DB_HOST") +DB_USER = cp.get(section_db, "DB_USER") +DB_PORT = cp.getint(section_db, "DB_PORT") +DB_PASS = cp.get(section_db, "DB_PASS") +DB_NAME = cp.get(section_db, "DB_NAME") + + +section_qb = cp.sections()[1] +QB_HOST = cp.get(section_qb, "QB_HOST") +QB_PORT = cp.get(section_qb, "QB_PORT") +QB_USER = cp.get(section_qb, "QB_USER") +QB_PWD = cp.get(section_qb, "QB_PWD") + +section_file = cp.sections()[2] +FILE_TO = cp.get(section_file, "FILE_TO") +FILE_TRANSFER_TO = cp.get(section_file, "FILE_TRANSFER_TO") +FILE_OWN = cp.get(section_file, "FILE_OWN") +FILE_GROUP = cp.get(section_file, "FILE_GROUP") +FILE_ENC_SWITCH = cp.get(section_file, "FILE_ENC_SWITCH") +FILE_API_URL = cp.get(section_file, "FILE_API_URL") +FILE_ASYNC_SWITCH = cp.get(section_file, "FILE_ASYNC_SWITCH") + + +section_task = cp.sections()[3] +TASK_SIZE_LIMIT = cp.get(section_task, "TASK_SIZE_LIMIT") +TASK_RATE = cp.getint(section_task, "TASK_RATE") +TASK_COMPLETED_RATE = cp.getint(section_task, "TASK_COMPLETED_RATE") +TASK_DEBUG = cp.getint(section_task, "TASK_DEBUG") + +section_setting = cp.sections()[4] +QUEUE_SWITCH = cp.get(section_setting, "QUEUE_SWITCH") +MAX_ACTIVE_UPLOADS = cp.getint(section_setting, "MAX_ACTIVE_UPLOADS") +MAX_ACTIVE_TORRENTS = cp.getint(section_setting, "MAX_ACTIVE_TORRENTS") +MAX_ACTIVE_DOWNLOADS = cp.getint(section_setting, "MAX_ACTIVE_DOWNLOADS") + +rooDir = getRootDir() +ffmpeg_cmd = rooDir + "/lib/ffmpeg/ffmpeg" +if not os.path.exists(ffmpeg_cmd): + ffmpeg_cmd = rooDir + "/lib/ffmpeg/bin/ffmpeg" + + +class downloadBT(Thread): + + __db_err = None + + def __init__(self): + Thread.__init__(self) + self.setDaemon(True) + self.qb = self.qb() + + self.qb.set_preferences(max_active_uploads=MAX_ACTIVE_UPLOADS) + self.qb.set_preferences(max_active_torrents=MAX_ACTIVE_TORRENTS) + self.qb.set_preferences(max_active_downloads=MAX_ACTIVE_DOWNLOADS) + + _has_suffix = ['mp4', 'rmvb', 'flv', 'avi', + 'mpg', 'mkv', 'wmv', 'avi', 'rm'] + has_suffix = [] + for x in range(len(_has_suffix)): + has_suffix.append('.' + _has_suffix[x]) + has_suffix.append('.' + _has_suffix[x].upper()) + self.has_suffix = has_suffix + + def __conn(self): + try: + self.dbconn = mdb.connect( + DB_HOST, DB_USER, DB_PASS, DB_NAME, port=DB_PORT, charset='utf8') + self.dbconn.autocommit(False) + self.dbcurr = self.dbconn.cursor() + self.dbcurr.execute('SET NAMES utf8') + return True + except Exception as e: + self.__db_err = e + return False + + def __close(self): + self.dbcurr.close() + self.dbconn.close() + + def query(self, sql): + # 执行SQL语句返回数据集 + if not self.__conn(): + return self.__db_err + try: + self.dbcurr.execute(sql) + result = self.dbcurr.fetchall() + # print result + # 将元组转换成列表 + data = map(list, result) + self.__close() + return data + except Exception, ex: + return ex + + def execute(self, sql): + if not self.__conn(): + return self.__db_err + try: + result = self.dbcurr.execute(sql) + self.dbconn.commit() + self.__close() + return result + except Exception, ex: + return ex + + def qb(self): + from qbittorrent import Client + url = 'http://' + QB_HOST + ':' + QB_PORT + '/' + qb = Client(url) + qb.login(QB_USER, QB_PWD) + return qb + + def execShell(self, cmdstring, cwd=None, timeout=None, shell=True): + import subprocess + if shell: + cmdstring_list = cmdstring + else: + cmdstring_list = shlex.split(cmdstring) + if timeout: + end_time = datetime.datetime.now() + datetime.timedelta(seconds=timeout) + + sub = subprocess.Popen(cmdstring_list, cwd=cwd, stdin=subprocess.PIPE, + shell=shell, bufsize=4096, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + while sub.poll() is None: + time.sleep(0.1) + if timeout: + if end_time <= datetime.datetime.now(): + raise Exception("Timeout:%s" % cmdstring) + + return sub.communicate() + + def md5(self, str): + # 生成MD5 + try: + m = hashlib.md5() + m.update(str) + return m.hexdigest() + except: + return False + + def readFile(self, filename): + # 读文件内容 + try: + fp = open(filename, 'r') + fBody = fp.read() + fp.close() + return fBody + except: + return False + + def get_transfer_ts_file(self, to): + return FILE_TRANSFER_TO + '/' + to + '.ts' + + def get_transfer_mp4_file(self, to): + return FILE_TRANSFER_TO + '/' + to + '.mp4' + + def get_transfer_m3u5_dir(self, dirname, fname): + return FILE_TO + '/m3u8/' + dirname + '/' + fname + + def get_transfer_hash_dir(self, dirname): + return FILE_TO + '/m3u8/' + dirname + + def fg_transfer_mp4_cmd(self, sfile, dfile): + cmd = ffmpeg_cmd + ' -y -i "' + sfile + \ + '" -threads 1 -preset veryslow -crf 28 -c:v libx264 -strict -2 ' + dfile + return cmd + + def fg_transfer_ts_cmd(self, file, to_file): + cmd = ffmpeg_cmd + ' -y -i ' + file + \ + ' -s 480x360 -vcodec copy -acodec copy -vbsf h264_mp4toannexb ' + to_file + return cmd + + def fg_m3u8_cmd(self, ts_file, m3u8_file, to_file): + cmd = ffmpeg_cmd + ' -y -i ' + ts_file + ' -c copy -map 0 -f segment -segment_list ' + \ + m3u8_file + ' -segment_time 3 ' + to_file + return cmd + + def fg_m3u8enc_cmd(self, ts_file, m3u8_file, to_file, enc_dir): + cmd = ffmpeg_cmd + ' -y -i ' + ts_file + ' -threads 1 -strict -2 -hls_time 3 -hls_key_info_file ' + \ + enc_dir + '/enc.keyinfo.txt -hls_playlist_type vod -hls_segment_filename ' + \ + to_file + ' ' + m3u8_file + return cmd + + def debug(self, msg): + return formatTime() + ":" + msg + + def get_lock_file(self, to): + return '/tmp/mdw_qb_' + to + '.lock' + + def lock(self, sign): + l = self.get_lock_file(sign) + self.execShell('touch ' + l) + + def unlock(self, sign): + l = self.get_lock_file(sign) + self.execShell('rm -rf ' + l) + + def islock(self, sign): + l = self.get_lock_file(sign) + if os.path.exists(l): + return True + return False + + def ffmpeg_file_sync(self): + if FILE_ASYNC_SWITCH == '1': + runDir = getRunDir() + sign = 'sync' + + print 'file_sync... start' + if self.islock(sign): + print self.debug('sync doing,already lock it!!!') + else: + self.lock(sign) + + r = self.execShell('sh -x ' + runDir + '/rsync.sh') + print self.debug('file_sync:' + r[0]) + print self.debug('file_sync_error:' + r[1]) + self.unlock(sign) + print 'file_sync... end' + + def ffmpeg_del_file(self, mp4, ts, m3u8_dir): + print self.debug('delete middle file ... start' + mp4) + self.execShell('rm -rf ' + mp4) + self.execShell('rm -rf ' + ts) + + if os.path.exists(m3u8_dir): + self.execShell('rm -rf ' + m3u8_dir) + + print self.debug('delete middle file ... end' + ts) + + def ffmpeg_del_hfile(self, shash_dir): + print self.debug('delete middle hash dir ... start' + shash_dir) + if os.path.exists(shash_dir): + self.execShell('rm -rf ' + shash_dir) + + print self.debug('delete middle hash dir ... end' + shash_dir) + + def ffmpeg(self, file=''): + if not os.path.exists(FILE_TRANSFER_TO): + self.execShell('mkdir -p ' + FILE_TRANSFER_TO) + + fname = os.path.basename(file) + shash = self.sign_torrent['hash'] + md5file = self.md5(file) + if not os.path.exists(file): + print formatTime(), 'file not exists:', file + return + print self.debug('source file ' + file) + + mp4file = self.get_transfer_mp4_file(md5file) + cmd_mp4 = self.fg_transfer_mp4_cmd(file, mp4file) + if not os.path.exists(mp4file): + print self.debug('cmd_mp4:' + cmd_mp4) + os.system(cmd_mp4) + else: + print self.debug('mp4 exists:' + mp4file) + + if not os.path.exists(mp4file): + print self.debug('mp4 not exists') + return + + tsfile = self.get_transfer_ts_file(md5file) + cmd_ts = self.fg_transfer_ts_cmd(mp4file, tsfile) + if not os.path.exists(tsfile): + print self.debug('cmd_ts:' + cmd_ts) + os.system(cmd_ts) + else: + print self.debug('data_ts exists:' + mp4file) + + if not os.path.exists(tsfile): + print self.debug('ts not exists') + return + + md5Fname = self.md5(fname) + m3u8_dir = self.get_transfer_m3u5_dir(shash, md5Fname) + if not os.path.exists(m3u8_dir): + self.execShell('mkdir -p ' + m3u8_dir) + + m3u8_file = m3u8_dir + '/index.m3u8' + tofile = m3u8_dir + '/%010d.ts' + print self.debug('tofile:' + tofile) + # 加密m3u8 + if FILE_ENC_SWITCH != '0': + enc_dir = '/tmp/qb_m3u8' + cmd = self.fg_m3u8enc_cmd(tsfile, m3u8_file, tofile, enc_dir) + if os.path.exists(m3u8_file): + print self.debug('cmd_m3u8_enc exists:' + m3u8_file) + print self.debug('cmd_m3u8_enc:' + cmd) + self.ffmpeg_file_sync() + self.ffmpeg_del_file(mp4file, tsfile, m3u8_dir) + return + + self.execShell('mkdir -p ' + enc_dir) + self.execShell('openssl rand -base64 16 > ' + + enc_dir + '/enc.key') + self.execShell('rm -rf ' + enc_dir + '/enc.keyinfo.txt') + + try: + fid = self.add_hash(fname, md5Fname) + except Exception as e: + print 'add_hash_enc:' + str(e) + return + fid = self.add_hash(fname, md5Fname) + key = self.readFile(enc_dir + '/enc.key').strip() + self.set_hashfile_key(fid, key) + + # FILE_API_URL + url = FILE_API_URL.replace('{$KEY}', fid) + enc_url = 'echo ' + url + ' >> ' + enc_dir + '/enc.keyinfo.txt' + self.execShell(enc_url) + enc_path = 'echo ' + enc_dir + '/enc.key >> ' + enc_dir + '/enc.keyinfo.txt' + self.execShell(enc_path) + enc_iv = 'openssl rand -hex 16 >> ' + enc_dir + '/enc.keyinfo.txt' + self.execShell(enc_iv) + + os.system(cmd) + else: + + if os.path.exists(m3u8_file): + print self.debug('m3u8 exists:' + tofile) + self.ffmpeg_file_sync() + self.ffmpeg_del_file(mp4file, tsfile, m3u8_dir) + return + + cmd_m3u8 = self.fg_m3u8_cmd(tsfile, m3u8_file, tofile) + print self.debug('cmd_m3u8:' + cmd_m3u8) + os.system(cmd_m3u8) + + try: + self.add_hash(fname, md5Fname) + except Exception as e: + print 'add_hash', str(e) + + self.execShell('chown -R ' + FILE_OWN + ':' + + FILE_GROUP + ' ' + m3u8_dir) + self.execShell('chmod -R 755 ' + m3u8_dir) + + self.ffmpeg_file_sync() + self.ffmpeg_del_file(mp4file, tsfile, m3u8_dir) + + def get_bt_size(self, torrent): + total_size = '0' + if 'size' in torrent: + total_size = str(torrent['size']) + + if 'total_size' in torrent: + total_size = str(torrent['total_size']) + return total_size + + def get_hashlist_id(self): + ct = formatTime() + + total_size = self.get_bt_size(self.sign_torrent) + + shash = self.sign_torrent['hash'] + sname = self.sign_torrent['name'] + sname = mdb.escape_string(sname) + + info = self.query( + "select id from pl_hash_list where info_hash='" + shash + "'") + if len(info) > 0: + pid = str(info[0][0]) + else: + print 'insert into pl_hash_list data' + pid = self.execute("insert into pl_hash_list (`name`,`info_hash`,`length`,`create_time`) values('" + + sname + "','" + shash + "','" + total_size + "','" + ct + "')") + return pid + + def set_hashlist_status(self, torrent, status): + ct = formatTime() + + shash = torrent['hash'] + + info = self.query( + "select id from pl_hash_list where info_hash='" + shash + "'") + if len(info) > 0: + print 'set_hashlist_status update' + usql = "update pl_hash_list set `status`='" + \ + str(status) + "' where info_hash='" + shash + "'" + self.execute(usql) + else: + print 'set_hashlist_status insert' + total_size = self.get_bt_size(torrent) + sname = torrent['name'] + sname = mdb.escape_string(sname) + return self.execute("insert into pl_hash_list (`name`,`info_hash`,`length`,`status`,`create_time`) values('" + + sname + "','" + shash + "','" + total_size + "','" + str(status) + "','" + ct + "')") + + def get_hashfile_id(self, fname, m3u8_name, pid): + ct = formatTime() + + info = self.query( + "select id from pl_hash_file where name='" + fname + "' and pid='" + pid + "'") + if len(info) == 0: + print 'insert into pl_hash_file data !' + fid = self.execute("insert into pl_hash_file (`pid`,`name`,`m3u8`,`create_time`) values('" + + pid + "','" + fname + "','" + m3u8_name + "','" + ct + "')") + else: + print fname, ':', m3u8_name, 'already is exists!' + fid = str(info[0][0]) + return fid + + def set_hashfile_key(self, fid, key): + self.execute("update pl_hash_file set `key`='" + + mdb.escape_string(key) + "' where id=" + fid) + + def add_queue(self, shash, size): + ct = formatTime() + + info = self.query( + "select id from pl_hash_queue where info_hash='" + shash + "'") + if len(info) == 0: + sql = "insert into pl_hash_queue (`info_hash`,`length`,`created_at`,`updated_at`) values('" + \ + shash + "','" + str(size) + "','" + ct + "','" + ct + "')" + return self.execute(sql) + else: + print 'queue:', shash, 'already is exists!' + + def add_hash(self, fname, m3u8_name): + print '-------------------------add_hash---start-----------------------' + + pid = self.get_hashlist_id() + fid = self.get_hashfile_id(fname, m3u8_name, pid) + + print '-------------------------add_hash---end--------------------------' + + return fid + + def file_arr(self, path, filters=['.DS_Store']): + file_list = [] + flist = os.listdir(path) + + for i in range(len(flist)): + # 下载缓存文件过滤 + if flist[i] == '.unwanted': + continue + + file_path = os.path.join(path, flist[i]) + if flist[i] in filters: + continue + if os.path.isdir(file_path): + tmp = self.file_arr(file_path, filters) + file_list.extend(tmp) + else: + file_list.append(file_path) + return file_list + + def find_dir_video(self, path): + flist = self.file_arr(path) + video = [] + for i in range(len(flist)): + if self.is_video(flist[i]): + video.append(flist[i]) + return video + + def video_do(self, path): + if os.path.isfile(path): + if self.is_video(path): + self.ffmpeg(path) + else: + vlist = self.find_dir_video(path) + for v in vlist: + self.ffmpeg(v) + return '' + + def is_video(self, path): + t = os.path.splitext(path) + if t[1] in self.has_suffix: + return True + return False + + def non_download(self, torrent): + flist = self.qb.get_torrent_files(torrent['hash']) + is_video = False + for pos in range(len(flist)): + file = torrent['save_path'] + flist[pos]['name'] + if not self.is_video(file): + self.qb.set_file_priority(torrent['hash'], pos, 0) + else: + is_video = True + + # is video + if not is_video: + self.set_status(torrent, 1) + + def set_status(self, torrent, status): + self.set_hashlist_status(torrent, status) + if TASK_DEBUG == 0 and status != 0: + self.qb.delete_permanently(torrent['hash']) + + def is_downloading(self, torrent): + if torrent['name'] == torrent['hash']: + return True + else: + return False + + def is_nondownload_overtime(self, torrent, sec): + ct = time.time() + use_time = int(ct) - int(torrent['added_on']) + + flist = self.qb.get_torrent_files(torrent['hash']) + # print flist + flist_len = len(flist) + # 没有获取种子信息 + # print 'ddd:',flist_len,use_time,sec + if flist_len == 0 and use_time > sec: + self.set_status(torrent, 2) + return True + + is_video_download = False + + # 获取了种子信息,但是没有下载 + for pos in range(len(flist)): + file = torrent['save_path'] + flist[pos]['name'] + if self.is_video(file): + if flist[pos]['progress'] != '0': + is_video_download = True + + if not is_video_download and use_time > sec: + self.set_status(torrent, 3) + return True + return False + + def is_downloading_overtime(self, torrent, sec): + ct = time.time() + use_time = int(ct) - int(torrent['added_on']) + if use_time > sec: + self.set_status(torrent, 4) + return True + return False + + def is_downloading_overlimit(self, torrent): + sz = self.get_bt_size(torrent) + ct = formatTime() + size_limit = float(TASK_SIZE_LIMIT) * 1024 * 1024 * 1024 + size_limit = int(size_limit) + print 'is_downloading_overlimit:', toSize(sz), toSize(size_limit) + if int(sz) > int(size_limit): + print 'overlimit sz:' + sz + self.add_queue(torrent['hash'], str(sz)) + self.qb.delete_permanently(torrent['hash']) + + def check_task(self): + while True: + torrents = self.qb.torrents(filter='downloading') + tlen = len(torrents) + if tlen > 0: + print "downloading torrents count:", tlen + for torrent in torrents: + if self.is_nondownload_overtime(torrent, 5 * 60): + pass + elif self.is_downloading_overtime(torrent, 7 * 24 * 60 * 60): + pass + elif self.is_downloading(torrent): + pass + elif self.is_downloading_overlimit(torrent): + pass + else: + self.non_download(torrent) + print torrent['name'], ' task downloading!' + else: + print self.debug("no downloading task!") + time.sleep(TASK_RATE) + + def completed(self): + while True: + + torrents = self.qb.torrents(filter='completed') + tlen = len(torrents) + print "completed torrents count:", tlen + if tlen > 0: + for torrent in torrents: + self.sign_torrent = torrent + path = torrent['save_path'] + torrent['name'] + path = path.encode() + try: + self.video_do(path) + + hash_dir = self.get_transfer_hash_dir(torrent['hash']) + self.ffmpeg_del_hfile(hash_dir) + if TASK_DEBUG == 0: + self.qb.delete_permanently(torrent['hash']) + except Exception as e: + print formatTime(), str(e) + print self.debug("done task!") + else: + print self.debug("no completed task!") + time.sleep(TASK_COMPLETED_RATE) + + def add_hash_task(self, shash): + url = 'magnet:?xt=urn:btih:' + shash + self.qb.download_from_link(url) + print self.debug('queue add_hash_task is ok ... ') + + def queue(self): + while True: + if QUEUE_SWITCH == '1': + print self.debug("------------ do queue task start! ---------------") + + setting = self.qb.preferences() + torrents = self.qb.torrents() + tlen = len(torrents) + # print tlen, setting['max_active_torrents'] + add = int(setting['max_active_torrents']) - tlen + + if add == 0: + print self.debug('the download queue is full ... ') + else: + size_limit = float(TASK_SIZE_LIMIT) * 1024 * 1024 * 1024 + size_limit = int(size_limit) + size_sql_where = '' + size_sql = '' + if size_limit != 0: + size_sql = ',`length` desc ' + size_sql_where = 'where `length`<=' + str(size_limit) + + sql = "select * from pl_hash_queue " + size_sql_where + \ + " order by created_at desc " + \ + size_sql + " limit " + str(add) + info = self.query(sql) + info_len = len(info) + + if info_len == 0: + print self.debug('queue data is empty ... ') + else: + for x in range(info_len): + self.add_hash_task(info[x][1]) + self.execute( + 'delete from pl_hash_queue where id=' + str(info[x][0])) + print self.debug("------------ do queue task end ! ---------------") + time.sleep(TASK_RATE) + + +def test(): + while True: + print self.debug("no download task!") + time.sleep(1) + test() + +if __name__ == "__main__": + + dl = downloadBT() + + import threading + check_task = threading.Thread(target=dl.check_task) + check_task.start() + + completed = threading.Thread(target=dl.completed) + completed.start() + + queue = threading.Thread(target=dl.queue) + queue.start() diff --git a/plugins/qbittorrent/workers/rsync.sh b/plugins/qbittorrent/workers/rsync.sh new file mode 100755 index 000000000..311108f21 --- /dev/null +++ b/plugins/qbittorrent/workers/rsync.sh @@ -0,0 +1,5 @@ +#!/bin/bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH + +echo 'hello world'