From a82ba2f5b5caecef4a6854a4eca0b683fdfaa3f1 Mon Sep 17 00:00:00 2001 From: Shurelol Date: Tue, 7 Feb 2023 22:20:50 +0800 Subject: [PATCH] =?UTF-8?q?fix=20=E5=A2=9E=E5=8A=A0=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E9=99=90=E9=80=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/conf/systemconfig.py | 4 +- app/downloader/client/_base.py | 7 ++ app/downloader/client/aria2.py | 6 + app/downloader/client/client115.py | 6 + app/downloader/client/pikpak.py | 6 + app/downloader/client/qbittorrent.py | 15 +++ app/downloader/client/transmission.py | 27 ++++ app/downloader/downloader.py | 19 +++ app/helper/security_helper.py | 21 ++-- app/mediaserver/client/_base.py | 7 ++ app/mediaserver/client/emby.py | 20 +++ app/mediaserver/client/jellyfin.py | 6 + app/mediaserver/client/plex.py | 6 + app/mediaserver/media_server.py | 16 ++- app/speedlimiter.py | 173 ++++++++++++++++++++++++++ run.py | 3 + web/action.py | 3 + web/main.py | 15 ++- web/templates/macro/svg.html | 31 +++-- web/templates/setting/downloader.html | 93 +++++++++++++- 20 files changed, 456 insertions(+), 28 deletions(-) create mode 100644 app/speedlimiter.py diff --git a/app/conf/systemconfig.py b/app/conf/systemconfig.py index be2c60306..fc3c2fd23 100644 --- a/app/conf/systemconfig.py +++ b/app/conf/systemconfig.py @@ -16,7 +16,9 @@ class SystemConfig: # 自动获取Cookie的用户信息 "CookieUserInfo": {}, # 用户自定义CSS/JavsScript - "CustomScript": {} + "CustomScript": {}, + # 播放限速设置 + "SpeedLimit": {} } def __init__(self): diff --git a/app/downloader/client/_base.py b/app/downloader/client/_base.py index 05e1c22e3..9084f9d4e 100644 --- a/app/downloader/client/_base.py +++ b/app/downloader/client/_base.py @@ -143,3 +143,10 @@ def get_downloading_progress(self): 获取下载进度 """ pass + + @abstractmethod + def set_speed_limit(self, **kwargs): + """ + 设置速度限制 + """ + pass diff --git a/app/downloader/client/aria2.py b/app/downloader/client/aria2.py index 11532c35a..6b3388ebd 100644 --- a/app/downloader/client/aria2.py +++ b/app/downloader/client/aria2.py @@ -159,3 +159,9 @@ def get_downloading_progress(self, **kwargs): 'progress': progress }) return DispTorrents + + def set_speed_limit(self, **kwargs): + """ + 设置速度限制 + """ + pass diff --git a/app/downloader/client/client115.py b/app/downloader/client/client115.py index 7abd86de5..8e7d62501 100644 --- a/app/downloader/client/client115.py +++ b/app/downloader/client/client115.py @@ -133,3 +133,9 @@ def get_downloading_progress(self, **kwargs): 'progress': progress }) return DispTorrents + + def set_speed_limit(self, **kwargs): + """ + 设置速度限制 + """ + pass diff --git a/app/downloader/client/pikpak.py b/app/downloader/client/pikpak.py index cfc8bb63b..501fe31df 100644 --- a/app/downloader/client/pikpak.py +++ b/app/downloader/client/pikpak.py @@ -145,3 +145,9 @@ def get_downloading_progress(self, **kwargs): 'noprogress': True }) return DispTorrents + + def set_speed_limit(self, **kwargs): + """ + 设置速度限制 + """ + pass diff --git a/app/downloader/client/qbittorrent.py b/app/downloader/client/qbittorrent.py index 399ab6f60..da9adc0a7 100644 --- a/app/downloader/client/qbittorrent.py +++ b/app/downloader/client/qbittorrent.py @@ -513,3 +513,18 @@ def get_downloading_progress(self, tag=None): 'progress': progress }) return DispTorrents + + def set_speed_limit(self, download_limit=None, upload_limit=None): + """ + 设置速度限制 + """ + if not self.qbc: + return + try: + if self.qbc.transfer.upload_limit != upload_limit: + self.qbc.transfer.upload_limit = upload_limit + if self.qbc.transfer.download_limit != download_limit: + self.qbc.transfer.download_limit = download_limit + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False diff --git a/app/downloader/client/transmission.py b/app/downloader/client/transmission.py index 289abf3dc..d70504705 100644 --- a/app/downloader/client/transmission.py +++ b/app/downloader/client/transmission.py @@ -497,3 +497,30 @@ def get_downloading_progress(self, tag=None): 'progress': progress }) return DispTorrents + + def set_speed_limit(self, download_limit=None, upload_limit=None): + """ + 设置速度限制 + """ + if not self.trc: + return + try: + session = self.trc.get_session() + download_limit_enabled = True if download_limit else False + upload_limit_enabled = True if upload_limit else False + if download_limit_enabled == session.speed_limit_down_enabled and \ + upload_limit_enabled == session.speed_limit_up_enabled and \ + download_limit == session.speed_limit_down and \ + upload_limit == session.speed_limit_up: + return + self.trc.set_session( + speed_limit_down=download_limit if download_limit != session.speed_limit_down + else session.speed_limit_down, + speed_limit_up=upload_limit if upload_limit != session.speed_limit_up + else session.speed_limit_up, + speed_limit_down_enabled=download_limit_enabled, + speed_limit_up_enabled=upload_limit_enabled + ) + except Exception as err: + ExceptionUtils.exception_traceback(err) + return False diff --git a/app/downloader/downloader.py b/app/downloader/downloader.py index 6da5e32d3..7a6834ee2 100644 --- a/app/downloader/downloader.py +++ b/app/downloader/downloader.py @@ -1062,3 +1062,22 @@ def get_default_download_setting(self): if not self._download_setting.get(default_download_setting): default_download_setting = "-1" return default_download_setting + + def set_speed_limit(self, downloader, download_limit=None, upload_limit=None): + """ + 设置速度限制 + """ + if not downloader: + return [] + _client = self.__get_client(downloader) + try: + download_limit = int(download_limit) if download_limit else 0 + except Exception as err: + ExceptionUtils.exception_traceback(err) + download_limit = 0 + try: + upload_limit = int(upload_limit) if upload_limit else 0 + except Exception as err: + ExceptionUtils.exception_traceback(err) + upload_limit = 0 + _client.set_speed_limit(download_limit=download_limit, upload_limit=upload_limit) diff --git a/app/helper/security_helper.py b/app/helper/security_helper.py index 6c251558a..cc6ec84b8 100644 --- a/app/helper/security_helper.py +++ b/app/helper/security_helper.py @@ -17,19 +17,19 @@ def __init__(self): self.synology_webhook_allow_ip = security.get('synology_webhook_allow_ip') or {} def check_mediaserver_ip(self, ip): - return self.webhook_allow_access(self.media_server_webhook_allow_ip, ip) + return self.allow_access(self.media_server_webhook_allow_ip, ip) def check_telegram_ip(self, ip): - return self.webhook_allow_access(self.telegram_webhook_allow_ip, ip) + return self.allow_access(self.telegram_webhook_allow_ip, ip) def check_synology_ip(self, ip): - return self.webhook_allow_access(self.synology_webhook_allow_ip, ip) + return self.allow_access(self.synology_webhook_allow_ip, ip) def check_slack_ip(self, ip): - return self.webhook_allow_access({"ipve": "127.0.0.1"}, ip) + return self.allow_access({"ipve": "127.0.0.1"}, ip) @staticmethod - def webhook_allow_access(allow_ips, ip): + def allow_access(allow_ips, ip): """ 判断IP是否合法 :param allow_ips: 充许的IP范围 {"ipv4":, "ipv6":} @@ -39,14 +39,19 @@ def webhook_allow_access(allow_ips, ip): return True try: ipaddr = ipaddress.ip_address(ip) - if ipaddr.version == 4 or ipaddr.ipv4_mapped: + if ipaddr.version == 4: if not allow_ips.get('ipv4'): return True allow_ipv4s = allow_ips.get('ipv4').split(",") for allow_ipv4 in allow_ipv4s: - if ipaddr.version == 4 and ipaddr in ipaddress.ip_network(allow_ipv4): + if ipaddr in ipaddress.ip_network(allow_ipv4): return True - if ipaddr.ipv4_mapped and ipaddr.ipv4_mapped in ipaddress.ip_network(allow_ipv4): + elif ipaddr.ipv4_mapped: + if not allow_ips.get('ipv4'): + return True + allow_ipv4s = allow_ips.get('ipv4').split(",") + for allow_ipv4 in allow_ipv4s: + if ipaddr.ipv4_mapped in ipaddress.ip_network(allow_ipv4): return True else: if not allow_ips.get('ipv6'): diff --git a/app/mediaserver/client/_base.py b/app/mediaserver/client/_base.py index fd9934274..7674beba6 100644 --- a/app/mediaserver/client/_base.py +++ b/app/mediaserver/client/_base.py @@ -99,3 +99,10 @@ def get_items(self, parent): :param parent: 上一级的ID """ pass + + @abstractmethod + def get_playing_sessions(self): + """ + 获取正在播放的会话 + """ + pass diff --git a/app/mediaserver/client/emby.py b/app/mediaserver/client/emby.py index 26809a9bf..8da8720a9 100644 --- a/app/mediaserver/client/emby.py +++ b/app/mediaserver/client/emby.py @@ -470,3 +470,23 @@ def get_items(self, parent): ExceptionUtils.exception_traceback(e) log.error(f"【{self.server_type}】连接Users/Items出错:" + str(e)) yield {} + + def get_playing_sessions(self): + """ + 获取正在播放的会话 + """ + if not self._host or not self._apikey: + return [] + playing_sessions = [] + req_url = "%semby/Sessions?api_key=%s" % (self._host, self._apikey) + try: + res = RequestUtils().get_res(req_url) + if res and res.status_code == 200: + sessions = res.json() + for session in sessions: + if session.get("NowPlayingItem"): + playing_sessions.append(session) + return playing_sessions + except Exception as e: + ExceptionUtils.exception_traceback(e) + return [] diff --git a/app/mediaserver/client/jellyfin.py b/app/mediaserver/client/jellyfin.py index 6d51da2f6..4d511fdd5 100644 --- a/app/mediaserver/client/jellyfin.py +++ b/app/mediaserver/client/jellyfin.py @@ -416,3 +416,9 @@ def get_items(self, parent): ExceptionUtils.exception_traceback(e) log.error(f"【{self.server_type}】连接Users/Items出错:" + str(e)) yield {} + + def get_playing_sessions(self): + """ + 获取正在播放的会话 + """ + pass diff --git a/app/mediaserver/client/plex.py b/app/mediaserver/client/plex.py index 29fcb0399..abc9fe515 100644 --- a/app/mediaserver/client/plex.py +++ b/app/mediaserver/client/plex.py @@ -208,3 +208,9 @@ def get_items(self, parent): except Exception as err: ExceptionUtils.exception_traceback(err) yield {} + + def get_playing_sessions(self): + """ + 获取正在播放的会话 + """ + pass diff --git a/app/mediaserver/media_server.py b/app/mediaserver/media_server.py index 4c88294d6..c11fc2d43 100644 --- a/app/mediaserver/media_server.py +++ b/app/mediaserver/media_server.py @@ -229,10 +229,18 @@ def get_mediasync_status(self): def get_iteminfo(self, itemid): """ - 根据ItemId从媒体服务器查询项目详情 - :param itemid: 在Emby中的ID - :return: 图片对应在TMDB中的URL - """ + 根据ItemId从媒体服务器查询项目详情 + :param itemid: 在Emby中的ID + :return: 图片对应在TMDB中的URL + """ if not self.server: return None return self.server.get_iteminfo(itemid) + + def get_playing_sessions(self): + """ + 获取正在播放的会话 + """ + if not self.server: + return None + return self.server.get_playing_sessions() diff --git a/app/speedlimiter.py b/app/speedlimiter.py new file mode 100644 index 000000000..af7d91ee4 --- /dev/null +++ b/app/speedlimiter.py @@ -0,0 +1,173 @@ +from app.conf import SystemConfig +from app.downloader import Downloader +from app.mediaserver import MediaServer +from app.utils import ExceptionUtils +from app.utils.commons import singleton +from app.utils.types import DownloaderType, MediaServerType +from app.helper.security_helper import SecurityHelper +from apscheduler.schedulers.background import BackgroundScheduler +from config import Config + +import log + + +@singleton +class SpeedLimiter: + downloader = None + mediaserver = None + limit_enabled = False + limit_flag = False + qb_limit = False + qb_download_limit = 0 + qb_upload_limit = 0 + tr_limit = False + tr_download_limit = 0 + tr_upload_limit = 0 + unlimited_ips = {"ipv4": "0.0.0.0/0", "ipv6": "::/0"} + + _scheduler = None + + def __init__(self): + self.init_config() + + def init_config(self): + self.downloader = Downloader() + self.mediaserver = MediaServer() + + config = SystemConfig().get_system_config("SpeedLimit") + if config: + try: + self.qb_download_limit = int(float(config.get("qb_download") or 0))*1024 + self.qb_upload_limit = int(float(config.get("qb_upload") or 0))*1024 + except Exception as e: + ExceptionUtils.exception_traceback(e) + self.qb_limit = True if self.qb_download_limit or self.qb_upload_limit else False + try: + self.tr_download_limit = int(float(config.get("tr_download") or 0)) + self.tr_upload_limit = int(float(config.get("tr_upload") or 0)) + except Exception as e: + ExceptionUtils.exception_traceback(e) + self.tr_limit = True if self.tr_download_limit or self.tr_upload_limit else False + self.limit_enabled = True if self.qb_limit or self.tr_limit else False + self.unlimited_ips["ipv4"] = config.get("ipv4") or "0.0.0.0/0" + self.unlimited_ips["ipv6"] = config.get("ipv6") or "::/0" + else: + self.limit_enabled = False + # 移出现有任务 + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + ExceptionUtils.exception_traceback(e) + # 启动限速任务 + if self.limit_enabled: + self._scheduler = BackgroundScheduler(timezone=Config().get_timezone()) + self._scheduler.add_job(func=self.__check_playing_sessions, + args=[self.mediaserver.get_type(), True], + trigger='interval', + seconds=300) + self._scheduler.print_jobs() + self._scheduler.start() + log.info("播放限速服务启动") + + def __start(self): + """ + 开始限速 + """ + if self.qb_limit: + self.downloader.set_speed_limit( + downloader=DownloaderType.QB, + download_limit=self.qb_download_limit, + upload_limit=self.qb_upload_limit + ) + log.info(f"【SpeedLimiter】Qbittorrent下载器开始限速") + if self.tr_limit: + self.downloader.set_speed_limit( + downloader=DownloaderType.TR, + download_limit=self.tr_download_limit, + upload_limit=self.tr_upload_limit + ) + log.info(f"【SpeedLimiter】Transmission下载器开始限速") + self.limit_flag = True + + def __stop(self): + """ + 停止限速 + """ + if self.qb_limit: + self.downloader.set_speed_limit( + downloader=DownloaderType.QB, + download_limit=0, + upload_limit=0 + ) + log.info(f"【SpeedLimiter】Qbittorrent下载器停止限速") + if self.tr_limit: + self.downloader.set_speed_limit( + downloader=DownloaderType.TR, + download_limit=0, + upload_limit=0 + ) + log.info(f"【SpeedLimiter】Transmission下载器停止限速") + self.limit_flag = False + + def emby_action(self, message): + """ + 检查emby Webhook消息 + """ + if self.limit_enabled and message.get("Event") in ["playback.start", "playback.stop"]: + self.__check_playing_sessions(mediaserver_type=MediaServerType.EMBY, time_check=False) + + def jellyfin_action(self, message): + """ + 检查jellyfin Webhook消息 + """ + pass + + def plex_action(self, message): + """ + 检查plex Webhook消息 + """ + pass + + def __check_playing_sessions(self, mediaserver_type, time_check=False): + """ + 检查是否限速 + """ + if mediaserver_type != self.mediaserver.get_type(): + return + playing_sessions = self.mediaserver.get_playing_sessions() + limit_flag = False + match mediaserver_type: + case MediaServerType.EMBY: + for session in playing_sessions: + if not SecurityHelper.allow_access(self.unlimited_ips, session.get("RemoteEndPoint")) \ + and "Video" in session.get("PlayableMediaTypes"): + limit_flag = True + break + case MediaServerType.JELLYFIN: + pass + case MediaServerType.PLEX: + pass + case _: + pass + if time_check: + if limit_flag: + self.__start() + else: + self.__stop() + else: + if not self.limit_flag and limit_flag: + self.__start() + elif self.limit_flag and not limit_flag: + self.__stop() + else: + pass + + + + + + diff --git a/run.py b/run.py index d117b45aa..44297b123 100644 --- a/run.py +++ b/run.py @@ -43,6 +43,7 @@ from app.scheduler import run_scheduler, restart_scheduler from app.sync import run_monitor, restart_monitor from app.torrentremover import TorrentRemover +from app.speedlimiter import SpeedLimiter from check_config import update_config, check_config from version import APP_VERSION @@ -119,6 +120,8 @@ def start_service(): RssChecker() # 启动自动删种服务 TorrentRemover() + # 启动播放限速服务 + SpeedLimiter() # 加载索引器配置 IndexerHelper() # 初始化浏览器 diff --git a/web/action.py b/web/action.py index 28585ac9a..508546b6c 100644 --- a/web/action.py +++ b/web/action.py @@ -37,6 +37,7 @@ from app.subtitle import Subtitle from app.sync import Sync, stop_monitor from app.torrentremover import TorrentRemover +from app.speedlimiter import SpeedLimiter from app.utils import StringUtils, EpisodeFormat, RequestUtils, PathUtils, \ SystemUtils, ExceptionUtils, Torrent from app.utils.types import RmtMode, OsType, SearchType, DownloaderType, SyncType, MediaType @@ -4328,6 +4329,8 @@ def __set_system_config(data): return {"code": 1} try: SystemConfig().set_system_config(key=key, value=value) + if key == "SpeedLimit": + SpeedLimiter().init_config() return {"code": 0} except Exception as e: ExceptionUtils.exception_traceback(e) diff --git a/web/main.py b/web/main.py index 02f117309..0fea8f230 100644 --- a/web/main.py +++ b/web/main.py @@ -23,13 +23,14 @@ from app.conf import ModuleConf, SystemConfig from app.downloader import Downloader from app.filter import Filter -from app.helper import SecurityHelper, MetaHelper, ChromeHelper +from app.helper import SecurityHelper, MetaHelper, ChromeHelper, ThreadHelper from app.indexer import Indexer from app.media.meta import MetaInfo from app.mediaserver import WebhookEvent from app.message import Message from app.rsschecker import RssChecker from app.sites import Sites +from app.speedlimiter import SpeedLimiter from app.subscribe import Subscribe from app.sync import Sync from app.torrentremover import TorrentRemover @@ -1017,6 +1018,7 @@ def douban(): def downloader(): return render_template("setting/downloader.html", Config=Config().get_config(), + SpeedLimitConf=SystemConfig().get_system_config("SpeedLimit") or {}, DownloaderConf=ModuleConf.DOWNLOADER_CONF) @@ -1304,11 +1306,12 @@ def plex_webhook(): return '不允许的IP地址请求' request_json = json.loads(request.form.get('payload', {})) log.debug("收到Plex Webhook报文:%s" % str(request_json)) - WebhookEvent().plex_action(request_json) + ThreadHelper().start_thread(WebhookEvent().plex_action, (request_json,)) + ThreadHelper().start_thread(SpeedLimiter().plex_action, (request_json,)) return 'Ok' -# Emby Webhook +# Jellyfin Webhook @App.route('/jellyfin', methods=['POST']) def jellyfin_webhook(): if not SecurityHelper().check_mediaserver_ip(request.remote_addr): @@ -1316,7 +1319,8 @@ def jellyfin_webhook(): return '不允许的IP地址请求' request_json = request.get_json() log.debug("收到Jellyfin Webhook报文:%s" % str(request_json)) - WebhookEvent().jellyfin_action(request_json) + ThreadHelper().start_thread(WebhookEvent().jellyfin_action, (request_json,)) + ThreadHelper().start_thread(SpeedLimiter().jellyfin_action, (request_json,)) return 'Ok' @@ -1328,7 +1332,8 @@ def emby_webhook(): return '不允许的IP地址请求' request_json = json.loads(request.form.get('data', {})) log.debug("收到Emby Webhook报文:%s" % str(request_json)) - WebhookEvent().emby_action(request_json) + ThreadHelper().start_thread(WebhookEvent().emby_action, (request_json,)) + ThreadHelper().start_thread(SpeedLimiter().emby_action, (request_json,)) return 'Ok' diff --git a/web/templates/macro/svg.html b/web/templates/macro/svg.html index 56fc49590..8a9fcfeaa 100644 --- a/web/templates/macro/svg.html +++ b/web/templates/macro/svg.html @@ -1035,12 +1035,27 @@ {% macro code_dots(class) %} - - - - - - - - + + + + + + + + +{% endmacro %} + + + +{% macro forbid(class) %} + + + + + + {% endmacro %} \ No newline at end of file diff --git a/web/templates/setting/downloader.html b/web/templates/setting/downloader.html index 4852497e4..45219fecb 100644 --- a/web/templates/setting/downloader.html +++ b/web/templates/setting/downloader.html @@ -15,9 +15,16 @@

{{ SVG.folders() }} 下载目录 - + {{ SVG.folders() }} + + {{ SVG.forbid() }} + 播放限速 + + + {{ SVG.forbid() }} + {{ SVG.settings() }} 下载设置 @@ -180,6 +187,78 @@ +