Files
archived-MoviePilot-Plugins/plugins/subscribestatistic/__init__.py
2025-01-15 09:14:48 +08:00

755 lines
32 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 json
from datetime import datetime, timedelta
from app.db.downloadhistory_oper import DownloadHistoryOper
from app.db.site_oper import SiteOper
from app.plugins import _PluginBase
from app.db.subscribe_oper import SubscribeOper
from typing import Any, List, Dict, Tuple, Optional
import pytz
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from app.log import logger
from app.core.config import settings
from app.schemas import NotificationType
from app.schemas.types import SystemConfigKey
class SubscribeStatistic(_PluginBase):
# 插件名称
plugin_name = "订阅下载统计"
# 插件描述
plugin_desc = "统计指定时间内各站点订阅及下载情况。"
# 插件图标
plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/subscribestatistic.png"
# 插件版本
plugin_version = "1.6"
# 插件作者
plugin_author = "thsrite"
# 作者主页
author_url = "https://github.com/thsrite"
# 插件配置项ID前缀
plugin_config_prefix = "subscribestatistic_"
# 加载顺序
plugin_order = 28
# 可使用的用户级别
auth_level = 1
# 任务执行间隔
_enabled = False
_notify = False
_onlyonce = False
_movie_subscribe_days = None
_tv_subscribe_days = None
_movie_download_days = None
_tv_download_days = None
_notify_type = None
_msgtype = None
subscribe = None
downloadhis = None
siteoper = None
_cron: str = ""
_scheduler: Optional[BackgroundScheduler] = None
def init_plugin(self, config: dict = None):
self.subscribe = SubscribeOper()
self.downloadhis = DownloadHistoryOper()
self.siteoper = SiteOper()
# 停止现有任务
self.stop_service()
if config:
self._enabled = config.get("enabled")
self._notify = config.get("notify")
self._onlyonce = config.get("onlyonce")
self._cron = config.get("cron")
self._movie_subscribe_days = config.get("movie_subscribe_days")
self._tv_subscribe_days = config.get("tv_subscribe_days")
self._movie_download_days = config.get("movie_download_days")
self._tv_download_days = config.get("tv_download_days")
self._notify_type = config.get("notify_type")
self._msgtype = config.get("msgtype")
if self._enabled and (
self._cron or self._onlyonce) and self._notify and self._msgtype and self._notify_type:
# 定时服务
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
# 立即运行一次
if self._onlyonce:
logger.info(f"订阅下载统计服务启动,立即运行一次")
self._scheduler.add_job(self.notify, 'date',
run_date=datetime.now(
tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3),
name="订阅下载统计")
# 关闭一次性开关
self._onlyonce = False
# 保存配置
self.__update_config()
# 周期运行
if self._cron:
try:
self._scheduler.add_job(func=self.notify,
trigger=CronTrigger.from_crontab(self._cron),
name="订阅下载统计")
except Exception as err:
logger.error(f"定时任务配置错误:{err}")
# 推送实时消息
self.systemmessage.put(f"执行周期配置错误:{err}")
# 启动任务
if self._scheduler.get_jobs():
self._scheduler.print_jobs()
self._scheduler.start()
def __update_config(self):
self.update_config({
"enabled": self._enabled,
"onlyonce": self._onlyonce,
"cron": self._cron,
"notify": self._notify,
"movie_subscribe_days": self._movie_subscribe_days,
"tv_subscribe_days": self._tv_subscribe_days,
"movie_download_days": self._movie_download_days,
"tv_download_days": self._tv_download_days,
"notify_type": self._notify_type,
"msgtype": self._msgtype,
})
def notify(self):
"""
发送统计消息
"""
text = ""
if 'movie_subscribes' in self._notify_type:
text += f"【电影{self._movie_subscribe_days}天内订阅 共STATIC】\n"
_, movie_subscribe_sites, movie_subscribe_datas = self.__get_movie_subscribes()
movie_subscribe_dict = dict(zip(movie_subscribe_sites, movie_subscribe_datas))
movie_subscribe_dict = dict(sorted(movie_subscribe_dict.items(), key=lambda x: x[1], reverse=True))
total = 0
for movie_subscribe_site in movie_subscribe_dict.keys():
text += f"{movie_subscribe_site}\n"
total += movie_subscribe_dict[movie_subscribe_site]
text += "\n"
text = text.replace("STATIC", str(total))
if 'tv_subscribes' in self._notify_type:
text += f"【电视剧{self._tv_subscribe_days}天内订阅 共STATIC】\n"
_, tv_subscribe_sites, tv_subscribe_datas = self.__get_tv_subscribes()
tv_subscribe_dict = dict(zip(tv_subscribe_sites, tv_subscribe_datas))
tv_subscribe_dict = dict(sorted(tv_subscribe_dict.items(), key=lambda x: x[1], reverse=True))
total = 0
for tv_subscribe_site in tv_subscribe_dict.keys():
text += f"{tv_subscribe_site}\n"
total += tv_subscribe_dict[tv_subscribe_site]
text += "\n"
text = text.replace("STATIC", str(total))
if 'movie_downloads' in self._notify_type:
text += f"【电影{self._movie_download_days}天内下载 共STATIC】\n"
_, movie_download_sites, movie_download_datas = self.__get_movie_downloads()
movie_download_dict = dict(zip(movie_download_sites, movie_download_datas))
movie_download_dict = dict(sorted(movie_download_dict.items(), key=lambda x: x[1], reverse=True))
total = 0
for movie_download_site in movie_download_dict.keys():
text += f"{movie_download_site}\n"
total += movie_download_dict[movie_download_site]
text += "\n"
text = text.replace("STATIC", str(total))
if 'tv_downloads' in self._notify_type:
text += f"【电视剧{self._tv_download_days}天内下载 共STATIC】\n"
_, tv_download_sites, tv_download_datas = self.__get_tv_downloads()
tv_download_dict = dict(zip(tv_download_sites, tv_download_datas))
tv_download_dict = dict(sorted(tv_download_dict.items(), key=lambda x: x[1], reverse=True))
total = 0
for tv_download_site in tv_download_dict.keys():
text += f"{tv_download_site}\n"
total += tv_download_dict[tv_download_site]
text = text.replace("STATIC", str(total))
# 发送通知
mtype = NotificationType.Manual
if self._msgtype:
mtype = NotificationType.__getitem__(str(self._msgtype)) or NotificationType.Manual
self.post_message(title="【订阅下载统计】",
mtype=mtype,
text=text)
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_movie_subscribes(self):
"""
获取电影订阅统计数据
"""
# 电影订阅
movie_subscribes = self.subscribe.list_by_type(mtype='电影', days=self._movie_subscribe_days)
movie_subscribe_sites = []
movie_subscribe_datas = []
if movie_subscribes:
movie_subscribe_site_ids = []
for movie_subscribe in movie_subscribes:
if movie_subscribe.sites:
movie_subscribe_site_ids += [site for site in json.loads(movie_subscribe.sites)]
else:
movie_subscribe_site_ids += self.systemconfig.get(SystemConfigKey.RssSites) or []
for movie_subscribe_site_id in movie_subscribe_site_ids:
site = self.siteoper.get(movie_subscribe_site_id)
if site:
if site.name not in [movie_subscribe_site.split("")[0] for movie_subscribe_site in
movie_subscribe_sites]:
movie_subscribe_sites.append(
f"{site.name}{movie_subscribe_site_ids.count(movie_subscribe_site_id)}")
movie_subscribe_datas.append(movie_subscribe_site_ids.count(movie_subscribe_site_id))
return movie_subscribes, movie_subscribe_sites, movie_subscribe_datas
def __get_tv_subscribes(self):
"""
获取电视剧订阅统计数据
"""
tv_subscribes = self.subscribe.list_by_type(mtype='电视剧', days=self._tv_subscribe_days)
tv_subscribe_sites = []
tv_subscribe_datas = []
if tv_subscribes:
tv_subscribe_site_ids = []
for tv_subscribe in tv_subscribes:
if tv_subscribe.sites:
tv_subscribe_site_ids += [site for site in json.loads(tv_subscribe.sites)]
else:
tv_subscribe_site_ids += self.systemconfig.get(SystemConfigKey.RssSites) or []
for tv_subscribe_site_id in tv_subscribe_site_ids:
site = self.siteoper.get(tv_subscribe_site_id)
if site:
if site.name not in [tv_subscribe_site.split("")[0] for tv_subscribe_site in tv_subscribe_sites]:
tv_subscribe_sites.append(f"{site.name}{tv_subscribe_site_ids.count(tv_subscribe_site_id)}")
tv_subscribe_datas.append(tv_subscribe_site_ids.count(tv_subscribe_site_id))
return tv_subscribes, tv_subscribe_sites, tv_subscribe_datas
def __get_movie_downloads(self):
"""
获取电影下载统计数据
"""
movie_downloads = self.downloadhis.list_by_type(mtype="电影", days=self._movie_download_days)
movie_download_sites = []
movie_download_datas = []
if movie_downloads:
movie_download_sites2 = []
for movie_download in movie_downloads:
if movie_download.torrent_site:
movie_download_sites2.append(movie_download.torrent_site)
for movie_download_site in movie_download_sites2:
if movie_download_site not in [movie_download_site2.split("")[0] for movie_download_site2 in
movie_download_sites]:
if movie_download_site not in movie_download_datas:
movie_download_datas.append(movie_download_sites2.count(movie_download_site))
movie_download_sites.append(
f"{movie_download_site}{movie_download_sites2.count(movie_download_site)}")
return movie_downloads, movie_download_sites, movie_download_datas
def __get_tv_downloads(self):
"""
获取电视剧下载统计数据
"""
tv_downloads = self.downloadhis.list_by_type(mtype="电视剧", days=self._tv_download_days)
tv_download_sites = []
tv_download_datas = []
if tv_downloads:
tv_download_sites2 = []
for tv_download in tv_downloads:
if tv_download.torrent_site:
tv_download_sites2.append(tv_download.torrent_site)
for tv_download_site in tv_download_sites2:
if tv_download_site not in [tv_download_site2.split("")[0] for tv_download_site2 in tv_download_sites]:
if tv_download_site not in tv_download_datas:
tv_download_datas.append(tv_download_sites2.count(tv_download_site))
tv_download_sites.append(f"{tv_download_site}{tv_download_sites2.count(tv_download_site)}")
return tv_downloads, tv_download_sites, tv_download_datas
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
"""
拼装插件配置页面需要返回两块数据1、页面配置2、数据结构
"""
# 编历 NotificationType 枚举,生成消息类型选项
MsgTypeOptions = []
for item in NotificationType:
MsgTypeOptions.append({
"title": item.value,
"value": item.name
})
return [
{
'component': 'VForm',
'content': [
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'enabled',
'label': '启用插件',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'notify',
'label': '发送通知',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'onlyonce',
'label': '立即运行一次',
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 3
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'movie_subscribe_days',
'label': '电影订阅天数',
'placeholder': '30'
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 3
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'tv_subscribe_days',
'label': '电视剧订阅天数',
'placeholder': '30'
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 3
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'movie_download_days',
'label': '电影下载天数',
'placeholder': '7'
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 3
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'tv_download_days',
'label': '电视剧下载天数',
'placeholder': '7'
}
}
]
},
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 3
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'cron',
'label': '执行周期',
'placeholder': '5位cron表达式留空自动'
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 3
},
'content': [
{
'component': 'VSelect',
'props': {
'multiple': False,
'chips': True,
'model': 'msgtype',
'label': '消息类型',
'items': MsgTypeOptions
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 3
},
'content': [
{
'component': 'VSelect',
'props': {
'multiple': True,
'chips': True,
'model': 'notify_type',
'label': '推送类型',
'items': [
{'title': '电影订阅', 'value': 'movie_subscribes'},
{'title': '电视剧订阅', 'value': 'tv_subscribes'},
{'title': '电影下载', 'value': 'movie_downloads'},
{'title': '电视剧下载', 'value': 'tv_downloads'},
]
}
}
]
},
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '订阅数量MoviePilot指定天数内正在订阅的数量。'
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '下载数量通过MoviePilot下载的数量包括订阅下载、手动下载以及其他下载等场景。'
}
}
]
}
]
}
]
}
], {
"enabled": False,
"notify": False,
"onlyonce": False,
"cron": "5 1 * * *",
"movie_subscribe_days": 30,
"tv_subscribe_days": 30,
"movie_download_days": 7,
"tv_download_days": 7,
"notify_type": "movie_downloads",
"msgtype": []
}
def get_page(self) -> List[dict]:
if not self._enabled:
return [
{
'component': 'div',
'text': '暂未开启插件',
'props': {
'class': 'text-center',
}
}
]
form_page = []
if "movie_subscribes" in self._notify_type:
# 电影订阅
movie_subscribes, movie_subscribe_sites, movie_subscribe_datas = self.__get_movie_subscribes()
form_page.append(
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VApexChart',
'props': {
'height': 300,
'options': {
'chart': {
'type': 'pie',
},
'labels': movie_subscribe_sites,
'title': {
'text': f'电影近 {self._movie_subscribe_days} 天订阅 {len(movie_subscribes)}'
},
'legend': {
'show': True
},
'plotOptions': {
'pie': {
'expandOnClick': False
}
},
'noData': {
'text': '订阅未选择站点或站点已删除'
}
},
'series': movie_subscribe_datas
}
}
]
}
)
if "tv_subscribes" in self._notify_type:
# 电视剧订阅
tv_subscribes, tv_subscribe_sites, tv_subscribe_datas = self.__get_tv_subscribes()
form_page.append(
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VApexChart',
'props': {
'height': 300,
'options': {
'chart': {
'type': 'pie',
},
'labels': tv_subscribe_sites,
'title': {
'text': f'电视剧近 {self._tv_subscribe_days} 天订阅 {len(tv_subscribes)}'
},
'legend': {
'show': True
},
'plotOptions': {
'pie': {
'expandOnClick': False
}
},
'noData': {
'text': '订阅未选择站点或站点已删除'
}
},
'series': tv_subscribe_datas
}
}
]
}
)
if "movie_downloads" in self._notify_type:
# 电影下载
movie_downloads, movie_download_sites, movie_download_datas = self.__get_movie_downloads()
form_page.append(
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VApexChart',
'props': {
'height': 300,
'options': {
'chart': {
'type': 'pie',
},
'labels': movie_download_sites,
'title': {
'text': f'电影近 {self._movie_download_days} 天下载 {len(movie_downloads)} 个种子'
},
'legend': {
'show': True
},
'plotOptions': {
'pie': {
'expandOnClick': False
}
},
'noData': {
'text': '暂无数据'
}
},
'series': movie_download_datas
}
}
]
}
)
if "tv_downloads" in self._notify_type:
# 电视剧下载
tv_downloads, tv_download_sites, tv_download_datas = self.__get_tv_downloads()
form_page.append(
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VApexChart',
'props': {
'height': 300,
'options': {
'chart': {
'type': 'pie',
},
'labels': tv_download_sites,
'title': {
'text': f'电视剧近 {self._tv_download_days} 天下载 {len(tv_downloads)} 个种子'
},
'legend': {
'show': True
},
'plotOptions': {
'pie': {
'expandOnClick': False
}
},
'noData': {
'text': '暂无数据'
}
},
'series': tv_download_datas
}
}
]
}
)
# 拼装页面
return [
{
'component': 'VRow',
'content': form_page
}
]
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:
logger.error("退出插件失败:%s" % str(e))