from app.downloader import Downloader from app.mediaserver import MediaServer from app.plugins import EventHandler from app.plugins.modules._base import _IPluginModule from app.utils import ExceptionUtils from app.utils.types import DownloaderType, MediaServerType, EventType from app.helper.security_helper import SecurityHelper from apscheduler.schedulers.background import BackgroundScheduler from config import Config import log class SpeedLimiter(_IPluginModule): # 插件名称 module_name = "播放限速" # 插件描述 module_desc = "媒体服务器开始播放时,自动对下载器进行限速。" # 插件图标 module_icon = "SpeedLimiter.jpg" # 主题色 module_color = "bg-blue" # 插件版本 module_version = "1.0" # 插件作者 module_author = "Shurelol" # 插件配置项ID前缀 module_config_prefix = "speedlimit_" # 加载顺序 module_order = 1 # 私有属性 _downloader = None _mediaserver = None _scheduler = None # 限速开关 _limit_enabled = False _limit_flag = False # QB _qb_limit = False _qb_download_limit = 0 _qb_upload_limit = 0 _qb_upload_ratio = 0 # TR _tr_limit = False _tr_download_limit = 0 _tr_upload_limit = 0 _tr_upload_ratio = 0 # 不限速地址 _unlimited_ips = {"ipv4": "0.0.0.0/0", "ipv6": "::/0"} # 自动限速 _auto_limit = False # 总速宽 _bandwidth = 0 @staticmethod def get_fields(): return [ # 同一板块 { 'type': 'div', 'content': [ # 同一行 [ { 'title': 'Qbittorrent', 'required': "", 'tooltip': '媒体服务器播放时对Qbittorrent下载器进行限速,不限速地址范围除外,0或留空不启用', 'type': 'text', 'content': [ { 'id': 'qb_upload', 'placeholder': '上传限速,KB/s' }, { 'id': 'qb_download', 'placeholder': '下载限速,KB/s' } ] }, { 'title': 'Transmission', 'required': "", 'tooltip': '媒体服务器播放时对Transmission下载器进行限速,不限速地址范围除外,0或留空不启用', 'type': 'text', 'content': [ { 'id': 'tr_upload', 'placeholder': '上传限速,KB/s' }, { 'id': 'tr_download', 'placeholder': '下载限速,KB/s' } ] }, { 'title': '不限速地址范围', 'required': 'required', 'tooltip': '以下地址范围不进行限速处理,一般配置为局域网地址段;多个地址段用,号分隔,配置为0.0.0.0/0,::/0则不做限制', 'type': 'text', 'content': [ { 'id': 'ipv4', 'placeholder': '192.168.1.0/24', }, { 'id': 'ipv6', 'placeholder': 'FE80::/10', } ] } ] ] }, { 'type': 'details', 'summary': '自动限速设置', 'tooltip': '设置后根据上行带宽及剩余比例自动计算限速数值', 'content': [ # 同一行 [ { 'title': '上行带宽', 'required': "", 'type': 'text', 'tooltip': '设置后将根据上行带宽、剩余比例、分配比例自动计算限速数值,否则使用Qbittorrent、Transmisson设定的限速数值', 'content': [ { 'id': 'bandwidth', 'placeholder': 'Mbps,留空不启用自动限速' }, ] }, { 'title': '剩余比例', 'required': "", 'tooltip': '上行带宽扣除播放媒体比特率后,乘以剩余比例为剩余带宽分配给下载器,最大为1', 'type': 'text', 'content': [ { 'id': 'residual_ratio', 'placeholder': '0.5' } ] }, { 'title': '分配比例', 'required': "", 'tooltip': 'Qbittorrent与Transmission下载器分配剩余带宽比例,如Qbittorrent下载器无需上传限速,可设为0:x(x可为任意正整数)', 'type': 'text', 'content': [ { 'id': 'allocation', 'placeholder': '1:1' } ] } ] ] } ] def init_config(self, config=None): self._downloader = Downloader() self._mediaserver = MediaServer() # 读取配置 if config: try: # 总带宽 self._bandwidth = int(float(config.get("bandwidth") or 0)) * 1000000 # 剩余比例 residual_ratio = float(config.get("residual_ratio") or 1) if residual_ratio > 1: residual_ratio = 1 # 分配比例 allocation = (config.get("allocation") or "1:1").split(":") if len(allocation) != 2 or not str(allocation[0]).isdigit() or not str(allocation[-1]).isdigit(): allocation = ["1", "1"] # QB上传限速 self._qb_upload_ratio = round( int(allocation[0]) / (int(allocation[-1]) + int(allocation[0])) * residual_ratio, 2) # TR上传限速 self._tr_upload_ratio = round( int(allocation[-1]) / (int(allocation[-1]) + int(allocation[0])) * residual_ratio, 2) except Exception as e: ExceptionUtils.exception_traceback(e) self._bandwidth = 0 self._qb_upload_ratio = 0 self._tr_upload_ratio = 0 # 自动限速开关 self._auto_limit = True if self._bandwidth and (self._qb_upload_ratio or self._tr_upload_ratio) else False try: # QB下载限速 self._qb_download_limit = int(float(config.get("qb_download") or 0)) * 1024 # QB上传限速 self._qb_upload_limit = int(float(config.get("qb_upload") or 0)) * 1024 except Exception as e: ExceptionUtils.exception_traceback(e) self._qb_download_limit = 0 self._qb_upload_limit = 0 # QB限速开关 self._qb_limit = True if self._qb_download_limit or self._qb_upload_limit or self._auto_limit else False try: # TR上传限速 self._tr_download_limit = int(float(config.get("tr_download") or 0)) # TR下载限速 self._tr_upload_limit = int(float(config.get("tr_upload") or 0)) except Exception as e: self._tr_download_limit = 0 self._tr_upload_limit = 0 ExceptionUtils.exception_traceback(e) # TR限速开关 self._tr_limit = True if self._tr_download_limit or self._tr_upload_limit or self._auto_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 # 移出现有任务 self.stop_service() # 启动限速任务 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 ) if not self._limit_flag: log.info(f"【Plugin】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 ) if not self._limit_flag: log.info(f"【Plugin】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 ) if self._limit_flag: log.info(f"【Plugin】Qbittorrent下载器停止限速") if self._tr_limit: self._downloader.set_speed_limit( downloader=DownloaderType.TR, download_limit=0, upload_limit=0 ) if self._limit_flag: log.info(f"【Plugin】Transmission下载器停止限速") self._limit_flag = False @EventHandler.register(EventType.EmbyWebhook) def emby_action(self, event): """ 检查emby Webhook消息 """ if self._limit_enabled and event.event_data.get("Event") in ["playback.start", "playback.stop"]: self.__check_playing_sessions(_mediaserver_type=MediaServerType.EMBY, time_check=False) @EventHandler.register(EventType.JellyfinWebhook) def jellyfin_action(self, event): """ 检查jellyfin Webhook消息 """ if self._limit_enabled and event.event_data.get("NotificationType") in ["PlaybackStart", "PlaybackStop"]: self.__check_playing_sessions(_mediaserver_type=MediaServerType.JELLYFIN, time_check=False) @EventHandler.register(EventType.PlexWebhook) def plex_action(self, event): """ 检查plex Webhook消息 """ if self._limit_enabled and event.event_data.get("event") in ["media.play", "media.stop"]: self.__check_playing_sessions(_mediaserver_type=MediaServerType.PLEX, time_check=False) def __check_playing_sessions(self, _mediaserver_type, time_check=False): """ 检查是否限速 """ def __calc_limit(_total_bit_rate): """ 计算限速 """ if not _total_bit_rate: return False if self._auto_limit: residual__bandwidth = (self._bandwidth - _total_bit_rate) if residual__bandwidth < 0: self._qb_upload_limit = 10 * 1024 self._tr_upload_limit = 10 else: _qb_upload_limit = residual__bandwidth / 8 / 1024 * self._qb_upload_ratio _tr_upload_limit = residual__bandwidth / 8 / 1024 * self._tr_upload_ratio self._qb_upload_limit = _qb_upload_limit * 1024 if _qb_upload_limit > 10 else 10 * 1024 self._tr_upload_limit = _tr_upload_limit if _tr_upload_limit > 10 else 10 return True if _mediaserver_type != self._mediaserver.get_type(): return # 当前播放的会话 playing_sessions = self._mediaserver.get_playing_sessions() # 本次是否限速 _limit_flag = False # 当前播放的总比特率 total_bit_rate = 0 if _mediaserver_type == MediaServerType.EMBY: for session in playing_sessions: if not SecurityHelper.allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) \ and session.get("NowPlayingItem", {}).get("MediaType") == "Video": total_bit_rate += int(session.get("NowPlayingItem", {}).get("Bitrate") or 0) elif _mediaserver_type == MediaServerType.JELLYFIN: for session in playing_sessions: if not SecurityHelper.allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) \ and session.get("NowPlayingItem", {}).get("MediaType") == "Video": media_streams = session.get("NowPlayingItem", {}).get("MediaStreams") or [] for media_stream in media_streams: total_bit_rate += int(media_stream.get("BitRate") or 0) elif _mediaserver_type == MediaServerType.PLEX: for session in playing_sessions: if not SecurityHelper.allow_access(self._unlimited_ips, session.get("address")) \ and session.get("type") == "Video": total_bit_rate += int(session.get("bitrate") or 0) else: return # 计算限速标志及速率 _limit_flag = __calc_limit(total_bit_rate) # 启动限速 if time_check or self._auto_limit: 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 def stop_service(self): """ 退出插件 """ try: if self._scheduler: self._scheduler.remove_all_jobs() if self._scheduler.running: self._scheduler.shutdown() self._scheduler = None except Exception as e: print(str(e))