2023-10-01 21:36:33 +08:00

630 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import ipaddress
from typing import List, Tuple, Dict, Any
from apscheduler.schedulers.background import BackgroundScheduler
from app.core.config import settings
from app.core.event import eventmanager, Event
from app.log import logger
from app.modules.emby import Emby
from app.modules.jellyfin import Jellyfin
from app.modules.plex import Plex
from app.modules.qbittorrent import Qbittorrent
from app.modules.transmission import Transmission
from app.plugins import _PluginBase
from app.schemas import NotificationType, WebhookEventInfo
from app.schemas.types import EventType
from app.utils.ip import IpUtils
class SpeedLimiter(_PluginBase):
# 插件名称
plugin_name = "播放限速"
# 插件描述
plugin_desc = "外网播放媒体库视频时,自动对下载器进行限速。"
# 插件图标
plugin_icon = "SpeedLimiter.jpg"
# 主题色
plugin_color = "#183883"
# 插件版本
plugin_version = "1.0"
# 插件作者
plugin_author = "Shurelol"
# 作者主页
author_url = "https://github.com/Shurelol"
# 插件配置项ID前缀
plugin_config_prefix = "speedlimit_"
# 加载顺序
plugin_order = 11
# 可使用的用户级别
auth_level = 1
# 私有属性
_scheduler = None
_qb = None
_tr = None
_enabled: bool = False
_notify: bool = False
_interval: int = 60
_downloader: list = []
_play_up_speed: float = 0
_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 = {}
# 当前限速状态
_current_state = ""
def init_plugin(self, config: dict = None):
# 读取配置
if config:
self._enabled = config.get("enabled")
self._notify = config.get("notify")
self._play_up_speed = float(config.get("play_up_speed")) if config.get("play_up_speed") else 0
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
self._current_state = f"U:{self._noplay_up_speed},D:{self._noplay_down_speed}"
try:
# 总带宽
self._bandwidth = int(float(config.get("bandwidth") or 0)) * 1000000
# 自动限速开关
if self._bandwidth > 0:
self._auto_limit = True
else:
self._auto_limit = False
except Exception as e:
logger.error(f"智能限速上行带宽设置错误:{str(e)}")
self._bandwidth = 0
# 限速服务开关
self._limit_enabled = True if (self._play_up_speed
or self._play_down_speed
or self._auto_limit) else False
self._allocation_ratio = config.get("allocation_ratio") or ""
# 不限速地址
self._unlimited_ips["ipv4"] = config.get("ipv4") or ""
self._unlimited_ips["ipv6"] = config.get("ipv6") or ""
self._downloader = config.get("downloader") or []
if self._downloader:
if 'qbittorrent' in self._downloader:
self._qb = Qbittorrent()
if 'transmission' in self._downloader:
self._tr = Transmission()
# 移出现有任务
self.stop_service()
# 启动限速任务
if self._enabled and self._limit_enabled:
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
self._scheduler.add_job(func=self.check_playing_sessions,
trigger='interval',
seconds=self._interval,
name="播放限速检查")
self._scheduler.print_jobs()
self._scheduler.start()
logger.info("播放限速检查服务启动")
def get_state(self) -> bool:
return self._enabled
@staticmethod
def get_command() -> List[Dict[str, Any]]:
pass
def get_api(self) -> List[Dict[str, Any]]:
pass
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
return [
{
'component': 'VForm',
'content': [
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'enabled',
'label': '启用插件',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'notify',
'label': '发送通知',
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'content': [
{
'component': 'VSelect',
'props': {
'chips': True,
'multiple': True,
'model': 'downloader',
'label': '下载器',
'items': [
{'title': 'Qbittorrent', 'value': 'qbittorrent'},
{'title': 'Transmission', 'value': 'transmission'},
]
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'play_up_speed',
'label': '播放限速(上传)',
'placeholder': 'KB/s'
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'play_down_speed',
'label': '播放限速(下载)',
'placeholder': 'KB/s'
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'noplay_up_speed',
'label': '未播放限速(上传)',
'placeholder': 'KB/s'
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'noplay_down_speed',
'label': '未播放限速(下载)',
'placeholder': 'KB/s'
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'bandwidth',
'label': '智能限速上行带宽',
'placeholder': 'Mbps'
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VSelect',
'props': {
'model': 'allocation_ratio',
'label': '智能限速分配比例',
'items': [
{'title': '平均', 'value': ''},
{'title': '19', 'value': '1:9'},
{'title': '28', 'value': '2:8'},
{'title': '37', 'value': '3:7'},
{'title': '46', 'value': '4:6'},
{'title': '64', 'value': '6:4'},
{'title': '73', 'value': '7:3'},
{'title': '82', 'value': '8:2'},
{'title': '91', 'value': '9:1'},
]
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'ipv4',
'label': '不限速地址范围ipv4',
'placeholder': '留空默认不限速内网ipv4'
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'ipv6',
'label': '不限速地址范围ipv6',
'placeholder': '留空默认不限速内网ipv6'
}
}
]
}
]
}
]
}
], {
"enabled": False,
"notify": True,
"downloader": [],
"play_up_speed": None,
"play_down_speed": None,
"noplay_up_speed": None,
"noplay_down_speed": None,
"bandwidth": None,
"allocation_ratio": "",
"ipv4": "",
"ipv6": ""
}
def get_page(self) -> List[dict]:
pass
@eventmanager.register(EventType.WebhookMessage)
def check_playing_sessions(self, event: Event = None):
"""
检查播放会话
"""
if not self._qb and not self._tr:
return
if not self._enabled:
return
if event:
event_data: WebhookEventInfo = event.event_data
if event_data.event not in [
"playback.start",
"PlaybackStart",
"media.play",
"media.stop",
"PlaybackStop",
"playback.stop"
]:
return
# 当前播放的总比特率
total_bit_rate = 0
# 媒体服务器类型,多个以,分隔
if not settings.MEDIASERVER:
return
media_servers = settings.MEDIASERVER.split(',')
# 查询所有媒体服务器状态
for media_server in media_servers:
# 查询播放中会话
playing_sessions = []
if media_server == "emby":
req_url = "[HOST]emby/Sessions?api_key=[APIKEY]"
try:
res = Emby().get_data(req_url)
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"):
playing_sessions.append(session)
except Exception as e:
logger.error(f"获取Emby播放会话失败{str(e)}")
continue
# 计算有效比特率
for session in playing_sessions:
# 设置了不限速范围则判断session ip是否在不限速范围内
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
if not self.__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)
# 未设置不限速范围则默认不限速内网ip
elif not IpUtils.is_private_ip(session.get("RemoteEndPoint")) \
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
total_bit_rate += int(session.get("NowPlayingItem", {}).get("Bitrate") or 0)
elif media_server == "jellyfin":
req_url = "[HOST]Sessions?api_key=[APIKEY]"
try:
res = Jellyfin().get_data(req_url)
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"):
playing_sessions.append(session)
except Exception as e:
logger.error(f"获取Jellyfin播放会话失败{str(e)}")
continue
# 计算有效比特率
for session in playing_sessions:
# 设置了不限速范围则判断session ip是否在不限速范围内
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
if not self.__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)
# 未设置不限速范围则默认不限速内网ip
elif not IpUtils.is_private_ip(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 media_server == "plex":
_plex = Plex().get_plex()
if _plex:
sessions = _plex.sessions()
for session in sessions:
bitrate = sum([m.bitrate or 0 for m in session.media])
playing_sessions.append({
"type": session.TAG,
"bitrate": bitrate,
"address": session.player.address
})
# 计算有效比特率
for session in playing_sessions:
# 设置了不限速范围则判断session ip是否在不限速范围内
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
if not self.__allow_access(self._unlimited_ips, session.get("address")) \
and session.get("type") == "Video":
total_bit_rate += int(session.get("bitrate") or 0)
# 未设置不限速范围则默认不限速内网ip
elif not 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:
play_up_speed = self.__calc_limit(total_bit_rate)
else:
play_up_speed = self._play_up_speed
# 当前正在播放,开始限速
self.__set_limiter(limit_type="播放", upload_limit=play_up_speed,
download_limit=self._play_down_speed)
else:
# 当前没有播放,取消限速
self.__set_limiter(limit_type="未播放", upload_limit=self._noplay_up_speed,
download_limit=self._noplay_down_speed)
def __calc_limit(self, total_bit_rate: float) -> float:
"""
计算智能上传限速
"""
if not self._bandwidth:
return 10
return round((self._bandwidth - total_bit_rate) / 8 / 1024, 2)
def __set_limiter(self, limit_type: str, upload_limit: float, download_limit: float):
"""
设置限速
"""
if not self._qb and not self._tr:
return
state = f"U:{upload_limit},D:{download_limit}"
if self._current_state == state:
# 限速状态没有改变
return
else:
self._current_state = state
try:
cnt = 0
for download in self._downloader:
if self._auto_limit and limit_type == "播放":
# 开启了播放智能限速
if len(self._downloader) == 1:
# 只有一个下载器
upload_limit = int(upload_limit)
else:
# 多个下载器
if not self._allocation_ratio:
# 平均
upload_limit = int(upload_limit / len(self._downloader))
else:
# 按比例
allocation_count = sum([int(i) for i in self._allocation_ratio.split(":")])
upload_limit = int(upload_limit * int(self._allocation_ratio.split(":")[cnt]) / allocation_count)
cnt += 1
if upload_limit:
text = f"上传:{upload_limit} KB/s"
else:
text = f"上传:未限速"
if download_limit:
text = f"{text}\n下载:{download_limit} KB/s"
else:
text = f"{text}\n下载:未限速"
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:
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: dict, ip: str) -> bool:
"""
判断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, strict=False):
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, strict=False):
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, strict=False):
return True
except Exception as err:
print(str(err))
return False
return False
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))