diff --git a/app/plugins/speedlimiter/__init__.py b/app/plugins/speedlimiter/__init__.py index a21f5a38..1b4d1016 100644 --- a/app/plugins/speedlimiter/__init__.py +++ b/app/plugins/speedlimiter/__init__.py @@ -1,3 +1,4 @@ +import ipaddress from typing import List, Tuple, Dict, Any from apscheduler.schedulers.background import BackgroundScheduler @@ -50,6 +51,12 @@ class SpeedLimiter(_PluginBase): _play_down_speed: float = 0 _noplay_up_speed: float = 0 _noplay_down_speed: float = 0 + _bandwidth: float = 0 + _allocation_ratio: str = "" + _auto_limit: bool = False + _limit_enabled: bool = False + # 不限速地址 + _unlimited_ips = {"ipv4": "0.0.0.0/0", "ipv6": "::/0"} # 当前限速状态 _current_state = "" @@ -62,6 +69,19 @@ class SpeedLimiter(_PluginBase): self._play_down_speed = float(config.get("play_down_speed")) if config.get("play_down_speed") else 0 self._noplay_up_speed = float(config.get("noplay_up_speed")) if config.get("noplay_up_speed") else 0 self._noplay_down_speed = float(config.get("noplay_down_speed")) if config.get("noplay_down_speed") else 0 + try: + # 总带宽 + self._bandwidth = int(float(config.get("bandwidth") or 0)) * 1000000 + except Exception: + self._bandwidth = 0 + # 自动限速开关 + self._auto_limit = True if self._bandwidth else False + self._allocation_ratio = config.get("allocation_ratio") or "" + # 不限速地址 + self._unlimited_ips["ipv4"] = config.get("ipv4") or "0.0.0.0/0" + self._unlimited_ips["ipv6"] = config.get("ipv6") or "::/0" + if "0.0.0.0/0" in self._unlimited_ips["ipv4"] and "::/0" in self._unlimited_ips["ipv6"]: + self._limit_enabled = False self._downloader = config.get("downloader") or [] if self._downloader: if 'qbittorrent' in self._downloader: @@ -73,7 +93,7 @@ class SpeedLimiter(_PluginBase): self.stop_service() # 启动限速任务 - if self._enabled: + if self._enabled and self._limit_enabled: self._scheduler = BackgroundScheduler(timezone=settings.TZ) self._scheduler.add_job(func=self.check_playing_sessions, trigger='interval', @@ -235,7 +255,85 @@ class SpeedLimiter(_PluginBase): ] } ] - } + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'bandwidth', + 'label': '智能限速上行带宽', + 'placeholder': '设置上行带宽后,媒体服务器有媒体播放时根据上行带宽和媒体播放占用带宽计算上传限速数值。' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'allocation_ratio', + 'label': '智能限速分配比例', + 'placeholder': '多个下载器设置分配比例,如两个下载器设置1:2,留空均分' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'ipv4', + 'label': '不限速地址范围(ipv4)', + 'placeholder': '192.168.1.0/24' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'ipv6', + 'label': '不限速地址范围(ipv6)', + 'placeholder': 'FE80::/10' + } + } + ] + } + ] + }, ] } ], { @@ -246,6 +344,10 @@ class SpeedLimiter(_PluginBase): "play_down_speed": 0, "noplay_up_speed": 0, "noplay_down_speed": 0, + "bandwidth": 0, + "allocation_ratio": "", + "ipv4": "", + "ipv6": "", } def get_page(self) -> List[dict]: @@ -273,7 +375,8 @@ class SpeedLimiter(_PluginBase): if res and res.status_code == 200: sessions = res.json() for session in sessions: - if session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"): + if not self.__allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) and \ + session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"): playing_sessions.append(session) except Exception as e: logger.error(f"获取Emby播放会话失败:{str(e)}") @@ -289,7 +392,8 @@ class SpeedLimiter(_PluginBase): if res and res.status_code == 200: sessions = res.json() for session in sessions: - if session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"): + if not self.__allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) and \ + session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"): playing_sessions.append(session) except Exception as e: logger.error(f"获取Jellyfin播放会话失败:{str(e)}") @@ -313,11 +417,16 @@ class SpeedLimiter(_PluginBase): }) # 计算有效比特率 for session in playing_sessions: - if not IpUtils.is_private_ip(session.get("address")) \ + if not self.__allow_access(self._unlimited_ips, session.get("address")) and \ + IpUtils.is_private_ip(session.get("address")) \ and session.get("type") == "Video": total_bit_rate += int(session.get("bitrate") or 0) if total_bit_rate: + # 开启智能限速计算上传限速 + if self._auto_limit: + self.__calc_limit(total_bit_rate) + # 当前正在播放,开始限速 self.__set_limiter(limit_type="播放", upload_limit=self._play_up_speed, download_limit=self._play_down_speed) @@ -326,6 +435,16 @@ class SpeedLimiter(_PluginBase): self.__set_limiter(limit_type="未播放", upload_limit=self._noplay_up_speed, download_limit=self._noplay_down_speed) + def __calc_limit(self, total_bit_rate): + """ + 计算智能上传限速 + """ + residual_bandwidth = (self._bandwidth - total_bit_rate) + if residual_bandwidth < 0: + self._play_up_speed = 10 + else: + self._play_up_speed = residual_bandwidth / 8 / 1024 + def __set_limiter(self, limit_type: str, upload_limit: float, download_limit: float): """ 设置限速 @@ -348,45 +467,94 @@ class SpeedLimiter(_PluginBase): else: text = f"{text}\n下载:未限速" try: - if self._qb: - self._qb.set_speed_limit(download_limit=download_limit, upload_limit=upload_limit) - # 发送通知 - if self._notify: - title = "【播放限速】" - if upload_limit or download_limit: - subtitle = f"Qbittorrent 开始{limit_type}限速" - self.post_message( - mtype=NotificationType.MediaServer, - title=title, - text=f"{subtitle}\n{text}" - ) + cnt = 0 + for download in self._downloader: + if self._auto_limit and limit_type == "播放": + # 开启了播放智能限速 + allocation_count = sum(self._allocation_ratio) if self._allocation_ratio else len(self._downloader) + if not self._allocation_ratio: + upload_limit = int(upload_limit / allocation_count) else: - self.post_message( - mtype=NotificationType.MediaServer, - title=title, - text=f"Qbittorrent 已取消限速" - ) - if self._tr: - self._tr.set_speed_limit(download_limit=download_limit, upload_limit=upload_limit) - # 发送通知 - if self._notify: - title = "【播放限速】" - if upload_limit or download_limit: - subtitle = f"Transmission 开始{limit_type}限速" - self.post_message( - mtype=NotificationType.MediaServer, - title=title, - text=f"{subtitle}\n{text}" - ) + upload_limit = int(upload_limit * float(self._allocation_ratio[cnt]) / allocation_count) + cnt += 1 + + if str(download) == 'qbittorrent': + if self._qb: + self._qb.set_speed_limit(download_limit=download_limit, upload_limit=upload_limit) + # 发送通知 + if self._notify: + title = "【播放限速】" + if upload_limit or download_limit: + subtitle = f"Qbittorrent 开始{limit_type}限速" + self.post_message( + mtype=NotificationType.MediaServer, + title=title, + text=f"{subtitle}\n{text}" + ) + else: + self.post_message( + mtype=NotificationType.MediaServer, + title=title, + text=f"Qbittorrent 已取消限速" + ) else: - self.post_message( - mtype=NotificationType.MediaServer, - title=title, - text=f"Transmission 已取消限速" - ) + if self._tr: + self._tr.set_speed_limit(download_limit=download_limit, upload_limit=upload_limit) + # 发送通知 + if self._notify: + title = "【播放限速】" + if upload_limit or download_limit: + subtitle = f"Transmission 开始{limit_type}限速" + self.post_message( + mtype=NotificationType.MediaServer, + title=title, + text=f"{subtitle}\n{text}" + ) + else: + self.post_message( + mtype=NotificationType.MediaServer, + title=title, + text=f"Transmission 已取消限速" + ) except Exception as e: logger.error(f"设置限速失败:{str(e)}") + @staticmethod + def __allow_access(allow_ips, ip): + """ + 判断IP是否合法 + :param allow_ips: 充许的IP范围 {"ipv4":, "ipv6":} + :param ip: 需要检查的ip + """ + if not allow_ips: + return True + try: + ipaddr = ipaddress.ip_address(ip) + 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 in ipaddress.ip_network(allow_ipv4): + return True + 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'): + return True + allow_ipv6s = allow_ips.get('ipv6').split(",") + for allow_ipv6 in allow_ipv6s: + if ipaddr in ipaddress.ip_network(allow_ipv6): + return True + except Exception: + return False + return False + def stop_service(self): """ 退出插件