init
This commit is contained in:
6
app/core/__init__.py
Normal file
6
app/core/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from .config import settings
|
||||
from .event_manager import eventmanager, EventManager
|
||||
from .meta_info import MetaInfo
|
||||
from .module_manager import ModuleManager
|
||||
from .plugin_manager import PluginManager
|
||||
from .context import Context, MediaInfo, TorrentInfo
|
BIN
app/core/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
app/core/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/config.cpython-310.pyc
Normal file
BIN
app/core/__pycache__/config.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/context.cpython-310.pyc
Normal file
BIN
app/core/__pycache__/context.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/event_manager.cpython-310.pyc
Normal file
BIN
app/core/__pycache__/event_manager.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/meta_info.cpython-310.pyc
Normal file
BIN
app/core/__pycache__/meta_info.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/module_manager.cpython-310.pyc
Normal file
BIN
app/core/__pycache__/module_manager.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/plugin_manager.cpython-310.pyc
Normal file
BIN
app/core/__pycache__/plugin_manager.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/core/__pycache__/security.cpython-310.pyc
Normal file
BIN
app/core/__pycache__/security.cpython-310.pyc
Normal file
Binary file not shown.
182
app/core/config.py
Normal file
182
app/core/config.py
Normal file
@ -0,0 +1,182 @@
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# 项目名称
|
||||
PROJECT_NAME = "NASbot"
|
||||
# API路径
|
||||
API_V1_STR: str = "/api/v1"
|
||||
# 密钥
|
||||
SECRET_KEY: str = secrets.token_urlsafe(32)
|
||||
# TOKEN过期时间
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
|
||||
# 时区
|
||||
TZ: str = "Asia/Shanghai"
|
||||
# 监听地址,ipv6改为::
|
||||
HOST: str = "0.0.0.0"
|
||||
# 监听端口
|
||||
PORT: int = 3001
|
||||
# 配置文件目录
|
||||
CONFIG_DIR: str = None
|
||||
# 超级管理员
|
||||
SUPERUSER: str = "admin"
|
||||
# 超级管理员密码
|
||||
SUPERUSER_PASSWORD: str = "password"
|
||||
# API密钥,需要更换
|
||||
API_TOKEN: str = "nasbot"
|
||||
# 网络代理
|
||||
PROXY_HOST: str = None
|
||||
# 媒体信息搜索来源
|
||||
SEARCH_SOURCE: str = "themoviedb"
|
||||
# 刮削来源
|
||||
SCRAP_SOURCE: str = "themoviedb"
|
||||
# TMDB图片地址
|
||||
TMDB_IMAGE_DOMAIN: str = "image.tmdb.org"
|
||||
# TMDB API地址
|
||||
TMDB_API_DOMAIN: str = "api.themoviedb.org"
|
||||
# TMDB API Key
|
||||
TMDB_API_KEY: str = "db55323b8d3e4154498498a75642b381"
|
||||
# Fanart API Key
|
||||
FANART_API_KEY: str = "d2d31f9ecabea050fc7d68aa3146015f"
|
||||
# 支持的后缀格式
|
||||
RMT_MEDIAEXT: list = ['.mp4', '.mkv', '.ts', '.iso',
|
||||
'.rmvb', '.avi', '.mov', '.mpeg',
|
||||
'.mpg', '.wmv', '.3gp', '.asf',
|
||||
'.m4v', '.flv', '.m2ts', '.strm',
|
||||
'.tp']
|
||||
# 支持的字幕文件后缀格式
|
||||
RMT_SUBEXT: list = ['.srt', '.ass', '.ssa']
|
||||
# 支持的音轨文件后缀格式
|
||||
RMT_AUDIO_TRACK_EXT: list = ['.mka']
|
||||
# 索引器
|
||||
INDEXER: str = "builtin"
|
||||
# 消息通知渠道 telegram/wechat
|
||||
MESSAGER: str = "telegram"
|
||||
# WeChat企业ID
|
||||
WECHAT_CORPID: str = None
|
||||
# WeChat应用Secret
|
||||
WECHAT_APP_SECRET: str = None
|
||||
# WeChat应用ID
|
||||
WECHAT_APP_ID: str = None
|
||||
# WeChat代理服务器
|
||||
WECHAT_PROXY: str = None
|
||||
# WeChat Token
|
||||
WECHAT_TOKEN: str = None
|
||||
# WeChat EncodingAESKey
|
||||
WECHAT_ENCODING_AESKEY: str = None
|
||||
# WeChat 管理员
|
||||
WECHAT_ADMINS: str = None
|
||||
# Telegram Bot Token
|
||||
TELEGRAM_TOKEN: str = None
|
||||
# Telegram Chat ID
|
||||
TELEGRAM_CHAT_ID: str = None
|
||||
# Telegram 用户ID,使用,分隔
|
||||
TELEGRAM_USERS: str = ""
|
||||
# Telegram 管理员ID,使用,分隔
|
||||
TELEGRAM_ADMINS: str = ""
|
||||
# 下载器 qbittorrent/transmission
|
||||
DOWNLOADER: str = "qbittorrent"
|
||||
# Qbittorrent地址
|
||||
QB_HOST: str = None
|
||||
# Qbittorrent用户名
|
||||
QB_USER: str = None
|
||||
# Qbittorrent密码
|
||||
QB_PASSWORD: str = None
|
||||
# Transmission地址
|
||||
TR_HOST: str = None
|
||||
# Transmission用户名
|
||||
TR_USER: str = None
|
||||
# Transmission密码
|
||||
TR_PASSWORD: str = None
|
||||
# 下载保存目录,容器内映射路径需要一致
|
||||
DOWNLOAD_PATH: str = "/downloads"
|
||||
# 媒体服务器 emby/jellyfin/plex
|
||||
MEDIASERVER: str = "emby"
|
||||
# EMBY服务器地址
|
||||
EMBY_HOST: str = None
|
||||
# EMBY Api Key
|
||||
EMBY_API_KEY: str = None
|
||||
# Jellyfin服务器地址
|
||||
JELLYFIN_HOST: str = None
|
||||
# Jellyfin Api Key
|
||||
JELLYFIN_API_KEY: str = None
|
||||
# Plex服务器地址
|
||||
PLEX_HOST: str = None
|
||||
# Plex Token
|
||||
PLEX_TOKEN: str = None
|
||||
# 过滤规则
|
||||
FILTER_RULE: str = ""
|
||||
# 转移方式 link/copy/move/softlink
|
||||
TRANSFER_TYPE: str = "copy"
|
||||
# CookieCloud服务器地址
|
||||
COOKIECLOUD_HOST: str = "https://nastool.org/cookiecloud"
|
||||
# CookieCloud用户KEY
|
||||
COOKIECLOUD_KEY: str = None
|
||||
# CookieCloud端对端加密密码
|
||||
COOKIECLOUD_PASSWORD: str = None
|
||||
# CookieCloud同步间隔(分钟)
|
||||
COOKIECLOUD_INTERVAL: int = 3600
|
||||
# CookieCloud对应的浏览器UA
|
||||
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57"
|
||||
# 媒体库目录
|
||||
LIBRARY_PATH: str = None
|
||||
# 二级分类
|
||||
LIBRARY_CATEGORY: bool = True
|
||||
# 豆瓣用户ID,用于同步豆瓣数据,使用,分隔
|
||||
DOUBAN_USER_IDS: str = ""
|
||||
|
||||
@property
|
||||
def INNER_CONFIG_PATH(self):
|
||||
return self.ROOT_PATH / "config"
|
||||
|
||||
@property
|
||||
def CONFIG_PATH(self):
|
||||
if self.CONFIG_DIR:
|
||||
return Path(self.CONFIG_DIR)
|
||||
return self.INNER_CONFIG_PATH
|
||||
|
||||
@property
|
||||
def TEMP_PATH(self):
|
||||
return self.CONFIG_PATH / "temp"
|
||||
|
||||
@property
|
||||
def ROOT_PATH(self):
|
||||
return Path(__file__).parents[2]
|
||||
|
||||
@property
|
||||
def PLUGIN_DATA_PATH(self):
|
||||
return self.CONFIG_PATH / "plugins"
|
||||
|
||||
@property
|
||||
def LOG_PATH(self):
|
||||
return self.CONFIG_PATH / "logs"
|
||||
|
||||
@property
|
||||
def PROXY(self):
|
||||
if self.PROXY_HOST:
|
||||
return {
|
||||
"http": self.PROXY_HOST,
|
||||
"https": self.PROXY_HOST
|
||||
}
|
||||
return None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
with self.CONFIG_PATH as p:
|
||||
if not p.exists():
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
with self.TEMP_PATH as p:
|
||||
if not p.exists():
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
with self.LOG_PATH as p:
|
||||
if not p.exists():
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
class Config:
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
settings = Settings()
|
421
app/core/context.py
Normal file
421
app/core/context.py
Normal file
@ -0,0 +1,421 @@
|
||||
from typing import Optional, Any
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.meta import MetaBase
|
||||
from app.core.meta_info import MetaInfo
|
||||
from app.utils.types import MediaType
|
||||
|
||||
|
||||
class TorrentInfo(object):
|
||||
# 站点ID
|
||||
site: int = None
|
||||
# 站点名称
|
||||
site_name: Optional[str] = None
|
||||
# 站点Cookie
|
||||
site_cookie: Optional[str] = None
|
||||
# 站点UA
|
||||
site_ua: Optional[str] = None
|
||||
# 站点是否使用代理
|
||||
site_proxy: bool = False
|
||||
# 站点优先级
|
||||
site_order: int = 0
|
||||
# 种子名称
|
||||
title: Optional[str] = None
|
||||
# 种子副标题
|
||||
description: Optional[str] = None
|
||||
# IMDB ID
|
||||
imdbid: str = None
|
||||
# 种子链接
|
||||
enclosure: Optional[str] = None
|
||||
# 详情页面
|
||||
page_url: Optional[str] = None
|
||||
# 种子大小
|
||||
size: float = 0
|
||||
# 做种者
|
||||
seeders: int = 0
|
||||
# 下载者
|
||||
peers: int = 0
|
||||
# 完成者
|
||||
grabs: int = 0
|
||||
# 发布时间
|
||||
pubdate: Optional[str] = None
|
||||
# 已过时间
|
||||
date_elapsed: Optional[str] = None
|
||||
# 上传因子
|
||||
uploadvolumefactor: Optional[float] = None
|
||||
# 下载因子
|
||||
downloadvolumefactor: Optional[float] = None
|
||||
# HR
|
||||
hit_and_run: bool = False
|
||||
# 种子标签
|
||||
labels: Optional[list] = []
|
||||
# 种子优先级
|
||||
pri_order: int = 0
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(self, key) and value is not None:
|
||||
setattr(self, key, value)
|
||||
|
||||
def __getattr__(self, attribute):
|
||||
return None
|
||||
|
||||
def __setattr__(self, name: str, value: Any):
|
||||
self.__dict__[name] = value
|
||||
|
||||
@staticmethod
|
||||
def get_free_string(upload_volume_factor, download_volume_factor):
|
||||
"""
|
||||
计算促销类型
|
||||
"""
|
||||
if upload_volume_factor is None or download_volume_factor is None:
|
||||
return "未知"
|
||||
free_strs = {
|
||||
"1.0 1.0": "普通",
|
||||
"1.0 0.0": "免费",
|
||||
"2.0 1.0": "2X",
|
||||
"2.0 0.0": "2X免费",
|
||||
"1.0 0.5": "50%",
|
||||
"2.0 0.5": "2X 50%",
|
||||
"1.0 0.7": "70%",
|
||||
"1.0 0.3": "30%"
|
||||
}
|
||||
return free_strs.get('%.1f %.1f' % (upload_volume_factor, download_volume_factor), "未知")
|
||||
|
||||
def get_volume_factor_string(self):
|
||||
"""
|
||||
返回促销信息
|
||||
"""
|
||||
return self.get_free_string(self.uploadvolumefactor, self.downloadvolumefactor)
|
||||
|
||||
|
||||
class MediaInfo(object):
|
||||
# 类型 电影、电视剧
|
||||
type: MediaType = None
|
||||
# 媒体标题
|
||||
title: Optional[str] = None
|
||||
# 年份
|
||||
year: Optional[str] = None
|
||||
# TMDB ID
|
||||
tmdb_id: Optional[str] = None
|
||||
# IMDB ID
|
||||
imdb_id: Optional[str] = None
|
||||
# TVDB ID
|
||||
tvdb_id: Optional[str] = None
|
||||
# 豆瓣ID
|
||||
douban_id: Optional[str] = None
|
||||
# 媒体原语种
|
||||
original_language: Optional[str] = None
|
||||
# 媒体原发行标题
|
||||
original_title: Optional[str] = None
|
||||
# 媒体发行日期
|
||||
release_date: Optional[str] = None
|
||||
# 背景图片
|
||||
backdrop_path: Optional[str] = None
|
||||
# 海报图片
|
||||
poster_path: Optional[str] = None
|
||||
# 评分
|
||||
vote_average: int = 0
|
||||
# 描述
|
||||
overview: Optional[str] = None
|
||||
# 各季的剧集清单信息
|
||||
seasons: Optional[dict] = {}
|
||||
# 二级分类
|
||||
category: str = ""
|
||||
# TMDB INFO
|
||||
tmdb_info: Optional[dict] = {}
|
||||
# 豆瓣 INFO
|
||||
douban_info: Optional[dict] = {}
|
||||
|
||||
def __init__(self, tmdb_info: dict = None, douban_info: dict = None):
|
||||
if tmdb_info:
|
||||
self.set_tmdb_info(tmdb_info)
|
||||
if douban_info:
|
||||
self.set_douban_info(douban_info)
|
||||
|
||||
def __getattr__(self, attribute):
|
||||
return None
|
||||
|
||||
def __setattr__(self, name: str, value: Any):
|
||||
self.__dict__[name] = value
|
||||
|
||||
def set_image(self, name: str, image: str):
|
||||
"""
|
||||
设置图片地址
|
||||
"""
|
||||
setattr(self, f"{name}_path", image)
|
||||
|
||||
def set_category(self, cat: str):
|
||||
"""
|
||||
设置二级分类
|
||||
"""
|
||||
self.category = cat
|
||||
|
||||
def set_tmdb_info(self, info: dict):
|
||||
"""
|
||||
初始化媒信息
|
||||
"""
|
||||
if not info:
|
||||
return
|
||||
# 本体
|
||||
self.tmdb_info = info
|
||||
# 类型
|
||||
self.type = info.get('media_type')
|
||||
# TMDBID
|
||||
self.tmdb_id = info.get('id')
|
||||
if not self.tmdb_id:
|
||||
return
|
||||
# 额外ID
|
||||
if info.get("external_ids"):
|
||||
self.tvdb_id = info.get("external_ids", {}).get("tvdb_id", 0)
|
||||
self.imdb_id = info.get("external_ids", {}).get("imdb_id", "")
|
||||
# 评分
|
||||
self.vote_average = round(float(info.get('vote_average')), 1) if info.get('vote_average') else 0
|
||||
# 描述
|
||||
self.overview = info.get('overview')
|
||||
# 原语种
|
||||
self.original_language = info.get('original_language')
|
||||
if self.type == MediaType.MOVIE:
|
||||
# 标题
|
||||
self.title = info.get('title')
|
||||
# 原标题
|
||||
self.original_title = info.get('original_title')
|
||||
# 发行日期
|
||||
self.release_date = info.get('release_date')
|
||||
if self.release_date:
|
||||
# 年份
|
||||
self.year = self.release_date[:4]
|
||||
else:
|
||||
# 电视剧
|
||||
self.title = info.get('name')
|
||||
# 原标题
|
||||
self.original_title = info.get('original_name')
|
||||
# 发行日期
|
||||
self.release_date = info.get('first_air_date')
|
||||
if self.release_date:
|
||||
# 年份
|
||||
self.year = self.release_date[:4]
|
||||
# 季集信息
|
||||
if info.get('seasons'):
|
||||
for season_info in info.get('seasons'):
|
||||
if not season_info.get("season_number"):
|
||||
continue
|
||||
episode_count = season_info.get("episode_count")
|
||||
self.seasons[season_info.get("season_number")] = list(range(1, episode_count + 1))
|
||||
# 海报
|
||||
if info.get('poster_path'):
|
||||
self.poster_path = f"https://{settings.TMDB_IMAGE_DOMAIN}{info.get('poster_path')}"
|
||||
# 背景
|
||||
if info.get('backdrop_path'):
|
||||
self.backdrop_path = f"https://{settings.TMDB_IMAGE_DOMAIN}{info.get('backdrop_path')}"
|
||||
|
||||
def set_douban_info(self, info: dict):
|
||||
"""
|
||||
初始化豆瓣信息
|
||||
"""
|
||||
if not info:
|
||||
return
|
||||
# 本体
|
||||
self.douban_info = info
|
||||
# 豆瓣ID
|
||||
self.douban_id = info.get("id")
|
||||
# 评分
|
||||
if not self.vote_average:
|
||||
rating = info.get('rating')
|
||||
if rating:
|
||||
vote_average = float(rating.get("value"))
|
||||
else:
|
||||
vote_average = 0
|
||||
self.vote_average = vote_average
|
||||
# 标题
|
||||
if not self.title:
|
||||
self.title = info.get('title')
|
||||
# 年份
|
||||
if not self.year:
|
||||
self.year = info.get('year')[:4] if info.get('year') else None
|
||||
# 原语种标题
|
||||
if not self.original_title:
|
||||
self.original_title = info.get("original_title")
|
||||
# 类型
|
||||
if not self.type:
|
||||
self.type = MediaType.MOVIE if info.get("type") == "movie" else MediaType.TV
|
||||
if not self.poster_path:
|
||||
if self.type == MediaType.MOVIE:
|
||||
# 海报
|
||||
poster_path = info.get('cover', {}).get("url")
|
||||
if not poster_path:
|
||||
poster_path = info.get('cover_url')
|
||||
if not poster_path:
|
||||
poster_path = info.get('pic', {}).get("large")
|
||||
else:
|
||||
# 海报
|
||||
poster_path = info.get('pic', {}).get("normal")
|
||||
self.poster_path = poster_path
|
||||
# 简介
|
||||
if not self.overview:
|
||||
overview = info.get("card_subtitle") or ""
|
||||
if not self.year and overview:
|
||||
if overview.split("/")[0].strip().isdigit():
|
||||
self.year = overview.split("/")[0].strip()
|
||||
|
||||
def get_detail_url(self):
|
||||
"""
|
||||
TMDB媒体详情页地址
|
||||
"""
|
||||
if self.tmdb_id:
|
||||
if self.type == MediaType.MOVIE:
|
||||
return "https://www.themoviedb.org/movie/%s" % self.tmdb_id
|
||||
else:
|
||||
return "https://www.themoviedb.org/tv/%s" % self.tmdb_id
|
||||
elif self.douban_id:
|
||||
return "https://movie.douban.com/subject/%s" % self.douban_id
|
||||
return ""
|
||||
|
||||
def get_stars(self):
|
||||
"""
|
||||
返回评分星星个数
|
||||
"""
|
||||
if not self.vote_average:
|
||||
return ""
|
||||
return "".rjust(int(self.vote_average), "★")
|
||||
|
||||
def get_star_string(self):
|
||||
if self.vote_average:
|
||||
return "评分:%s" % self.get_stars()
|
||||
return ""
|
||||
|
||||
def get_backdrop_image(self, default: bool = False):
|
||||
"""
|
||||
返回背景图片地址
|
||||
"""
|
||||
if self.backdrop_path:
|
||||
return self.backdrop_path
|
||||
return default or ""
|
||||
|
||||
def get_message_image(self, default: bool = None):
|
||||
"""
|
||||
返回消息图片地址
|
||||
"""
|
||||
if self.backdrop_path:
|
||||
return self.backdrop_path
|
||||
return self.get_poster_image(default=default)
|
||||
|
||||
def get_poster_image(self, default: bool = None):
|
||||
"""
|
||||
返回海报图片地址
|
||||
"""
|
||||
if self.poster_path:
|
||||
return self.poster_path
|
||||
return default or ""
|
||||
|
||||
def get_title_string(self):
|
||||
if self.title:
|
||||
return "%s (%s)" % (self.title, self.year) if self.year else self.title
|
||||
return ""
|
||||
|
||||
def get_overview_string(self, max_len: int = 140):
|
||||
"""
|
||||
返回带限定长度的简介信息
|
||||
:param max_len: 内容长度
|
||||
:return:
|
||||
"""
|
||||
overview = str(self.overview).strip()
|
||||
placeholder = ' ...'
|
||||
max_len = max(len(placeholder), max_len - len(placeholder))
|
||||
overview = (overview[:max_len] + placeholder) if len(overview) > max_len else overview
|
||||
return overview
|
||||
|
||||
def get_season_episodes(self, sea: int) -> list:
|
||||
"""
|
||||
返回指定季度的剧集信息
|
||||
"""
|
||||
if not self.seasons:
|
||||
return []
|
||||
return self.seasons.get(sea) or []
|
||||
|
||||
|
||||
class Context(object):
|
||||
"""
|
||||
上下文对象
|
||||
"""
|
||||
# 识别前的信息
|
||||
title: Optional[str] = None
|
||||
subtitle: Optional[str] = None
|
||||
|
||||
# 用户信息
|
||||
userid: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
|
||||
# 操作类型
|
||||
action: Optional[str] = None
|
||||
|
||||
# 识别信息
|
||||
_meta_info: Optional[MetaBase] = None
|
||||
# 种子信息
|
||||
_torrent_info: Optional[TorrentInfo] = None
|
||||
# 媒体信息
|
||||
_media_info: Optional[MediaInfo] = None
|
||||
|
||||
def __init__(self,
|
||||
meta: MetaBase = None,
|
||||
mediainfo: MediaInfo = None,
|
||||
torrentinfo: TorrentInfo = None,
|
||||
**kwargs):
|
||||
if meta:
|
||||
self._meta_info = meta
|
||||
if mediainfo:
|
||||
self._media_info = mediainfo
|
||||
if torrentinfo:
|
||||
self._torrent_info = torrentinfo
|
||||
if kwargs:
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
@property
|
||||
def meta_info(self):
|
||||
return self._meta_info
|
||||
|
||||
def set_meta_info(self, title: str, subtitle: str = None):
|
||||
self._meta_info = MetaInfo(title, subtitle)
|
||||
|
||||
@property
|
||||
def media_info(self):
|
||||
return self._media_info
|
||||
|
||||
def set_media_info(self,
|
||||
tmdb_info: dict = None,
|
||||
douban_info: dict = None):
|
||||
self._media_info = MediaInfo(tmdb_info, douban_info)
|
||||
|
||||
@property
|
||||
def torrent_info(self):
|
||||
return self._torrent_info
|
||||
|
||||
def set_torrent_info(self, info: dict):
|
||||
self._torrent_info = TorrentInfo(**info)
|
||||
|
||||
def __getattr__(self, attribute):
|
||||
return None
|
||||
|
||||
def __setattr__(self, name: str, value: Any):
|
||||
self.__dict__[name] = value
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
转换为字典
|
||||
"""
|
||||
def object_to_dict(obj):
|
||||
attributes = [
|
||||
attr for attr in dir(obj)
|
||||
if not callable(getattr(obj, attr)) and not attr.startswith("_")
|
||||
]
|
||||
return {
|
||||
attr: getattr(obj, attr).value
|
||||
if isinstance(getattr(obj, attr), MediaType)
|
||||
else getattr(obj, attr) for attr in attributes
|
||||
}
|
||||
|
||||
return {
|
||||
"meta_info": object_to_dict(self.meta_info),
|
||||
"media_info": object_to_dict(self.media_info)
|
||||
}
|
105
app/core/event_manager.py
Normal file
105
app/core/event_manager.py
Normal file
@ -0,0 +1,105 @@
|
||||
from queue import Queue, Empty
|
||||
|
||||
from app.log import logger
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.types import EventType
|
||||
|
||||
|
||||
class EventManager(metaclass=Singleton):
|
||||
"""
|
||||
事件管理器
|
||||
"""
|
||||
|
||||
# 事件队列
|
||||
_eventQueue: Queue = None
|
||||
# 事件响应函数字典
|
||||
_handlers: dict = {}
|
||||
|
||||
def __init__(self):
|
||||
# 事件队列
|
||||
self._eventQueue = Queue()
|
||||
# 事件响应函数字典
|
||||
self._handlers = {}
|
||||
|
||||
def get_event(self):
|
||||
"""
|
||||
获取事件
|
||||
"""
|
||||
try:
|
||||
event = self._eventQueue.get(block=True, timeout=1)
|
||||
handlerList = self._handlers.get(event.event_type)
|
||||
return event, handlerList or []
|
||||
except Empty:
|
||||
return None, []
|
||||
|
||||
def add_event_listener(self, etype: EventType, handler: type):
|
||||
"""
|
||||
注册事件处理
|
||||
"""
|
||||
try:
|
||||
handlerList = self._handlers[etype.value]
|
||||
except KeyError:
|
||||
handlerList = []
|
||||
self._handlers[etype.value] = handlerList
|
||||
if handler not in handlerList:
|
||||
handlerList.append(handler)
|
||||
logger.debug(f"Event Registed:{etype.value} - {handler}")
|
||||
|
||||
def remove_event_listener(self, etype: EventType, handler: type):
|
||||
"""
|
||||
移除监听器的处理函数
|
||||
"""
|
||||
try:
|
||||
handlerList = self._handlers[etype.value]
|
||||
if handler in handlerList[:]:
|
||||
handlerList.remove(handler)
|
||||
if not handlerList:
|
||||
del self._handlers[etype.value]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def send_event(self, etype: EventType, data: dict = None):
|
||||
"""
|
||||
发送事件
|
||||
"""
|
||||
if etype not in EventType:
|
||||
return
|
||||
event = Event(etype.value)
|
||||
event.event_data = data or {}
|
||||
logger.debug(f"发送事件:{etype.value} - {event.event_data}")
|
||||
self._eventQueue.put(event)
|
||||
|
||||
def register(self, etype: [EventType, list]):
|
||||
"""
|
||||
事件注册
|
||||
:param etype: 事件类型
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
if isinstance(etype, list):
|
||||
for et in etype:
|
||||
self.add_event_listener(et, f)
|
||||
elif type(etype) == type(EventType):
|
||||
for et in etype.__members__.values():
|
||||
self.add_event_listener(et, f)
|
||||
else:
|
||||
self.add_event_listener(etype, f)
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class Event(object):
|
||||
"""
|
||||
事件对象
|
||||
"""
|
||||
|
||||
def __init__(self, event_type=None):
|
||||
# 事件类型
|
||||
self.event_type = event_type
|
||||
# 字典用于保存具体的事件数据
|
||||
self.event_data = {}
|
||||
|
||||
|
||||
# 实例引用,用于注册事件
|
||||
eventmanager = EventManager()
|
3
app/core/meta/__init__.py
Normal file
3
app/core/meta/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .metabase import MetaBase
|
||||
from .metavideo import MetaVideo
|
||||
from .metaanime import MetaAnime
|
BIN
app/core/meta/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
app/core/meta/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/core/meta/__pycache__/metaanime.cpython-310.pyc
Normal file
BIN
app/core/meta/__pycache__/metaanime.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/core/meta/__pycache__/metabase.cpython-310.pyc
Normal file
BIN
app/core/meta/__pycache__/metabase.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/core/meta/__pycache__/metavideo.cpython-310.pyc
Normal file
BIN
app/core/meta/__pycache__/metavideo.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/core/meta/__pycache__/release_groups.cpython-310.pyc
Normal file
BIN
app/core/meta/__pycache__/release_groups.cpython-310.pyc
Normal file
Binary file not shown.
218
app/core/meta/metaanime.py
Normal file
218
app/core/meta/metaanime.py
Normal file
@ -0,0 +1,218 @@
|
||||
import re
|
||||
import zhconv
|
||||
import anitopy
|
||||
from app.core.meta.metabase import MetaBase
|
||||
from app.core.meta.release_groups import ReleaseGroupsMatcher
|
||||
from app.utils.string import StringUtils
|
||||
from app.utils.types import MediaType
|
||||
|
||||
|
||||
class MetaAnime(MetaBase):
|
||||
"""
|
||||
识别动漫
|
||||
"""
|
||||
_anime_no_words = ['CHS&CHT', 'MP4', 'GB MP4', 'WEB-DL']
|
||||
_name_nostring_re = r"S\d{2}\s*-\s*S\d{2}|S\d{2}|\s+S\d{1,2}|EP?\d{2,4}\s*-\s*EP?\d{2,4}|EP?\d{2,4}|\s+EP?\d{1,4}"
|
||||
|
||||
def __init__(self, title: str, subtitle: str = None, isfile: bool = False):
|
||||
super().__init__(title, subtitle, isfile)
|
||||
if not title:
|
||||
return
|
||||
# 调用第三方模块识别动漫
|
||||
try:
|
||||
original_title = title
|
||||
# 字幕组信息会被预处理掉
|
||||
anitopy_info_origin = anitopy.parse(title)
|
||||
title = self.__prepare_title(title)
|
||||
anitopy_info = anitopy.parse(title)
|
||||
if anitopy_info:
|
||||
# 名称
|
||||
name = anitopy_info.get("anime_title")
|
||||
if name and name.find("/") != -1:
|
||||
name = name.split("/")[-1].strip()
|
||||
if not name or name in self._anime_no_words or (len(name) < 5 and not StringUtils.is_chinese(name)):
|
||||
anitopy_info = anitopy.parse("[ANIME]" + title)
|
||||
if anitopy_info:
|
||||
name = anitopy_info.get("anime_title")
|
||||
if not name or name in self._anime_no_words or (len(name) < 5 and not StringUtils.is_chinese(name)):
|
||||
name_match = re.search(r'\[(.+?)]', title)
|
||||
if name_match and name_match.group(1):
|
||||
name = name_match.group(1).strip()
|
||||
# 拆份中英文名称
|
||||
if name:
|
||||
lastword_type = ""
|
||||
for word in name.split():
|
||||
if not word:
|
||||
continue
|
||||
if word.endswith(']'):
|
||||
word = word[:-1]
|
||||
if word.isdigit():
|
||||
if lastword_type == "cn":
|
||||
self.cn_name = "%s %s" % (self.cn_name or "", word)
|
||||
elif lastword_type == "en":
|
||||
self.en_name = "%s %s" % (self.en_name or "", word)
|
||||
elif StringUtils.is_chinese(word):
|
||||
self.cn_name = "%s %s" % (self.cn_name or "", word)
|
||||
lastword_type = "cn"
|
||||
else:
|
||||
self.en_name = "%s %s" % (self.en_name or "", word)
|
||||
lastword_type = "en"
|
||||
if self.cn_name:
|
||||
_, self.cn_name, _, _, _, _ = StringUtils.get_keyword(self.cn_name)
|
||||
if self.cn_name:
|
||||
self.cn_name = re.sub(r'%s' % self._name_nostring_re, '', self.cn_name, flags=re.IGNORECASE).strip()
|
||||
self.cn_name = zhconv.convert(self.cn_name, "zh-hans")
|
||||
if self.en_name:
|
||||
self.en_name = re.sub(r'%s' % self._name_nostring_re, '', self.en_name, flags=re.IGNORECASE).strip().title()
|
||||
self._name = StringUtils.str_title(self.en_name)
|
||||
# 年份
|
||||
year = anitopy_info.get("anime_year")
|
||||
if str(year).isdigit():
|
||||
self.year = str(year)
|
||||
# 季号
|
||||
anime_season = anitopy_info.get("anime_season")
|
||||
if isinstance(anime_season, list):
|
||||
if len(anime_season) == 1:
|
||||
begin_season = anime_season[0]
|
||||
end_season = None
|
||||
else:
|
||||
begin_season = anime_season[0]
|
||||
end_season = anime_season[-1]
|
||||
elif anime_season:
|
||||
begin_season = anime_season
|
||||
end_season = None
|
||||
else:
|
||||
begin_season = None
|
||||
end_season = None
|
||||
if begin_season:
|
||||
self.begin_season = int(begin_season)
|
||||
if end_season and int(end_season) != self.begin_season:
|
||||
self.end_season = int(end_season)
|
||||
self.total_seasons = (self.end_season - self.begin_season) + 1
|
||||
else:
|
||||
self.total_seasons = 1
|
||||
self.type = MediaType.TV
|
||||
# 集号
|
||||
episode_number = anitopy_info.get("episode_number")
|
||||
if isinstance(episode_number, list):
|
||||
if len(episode_number) == 1:
|
||||
begin_episode = episode_number[0]
|
||||
end_episode = None
|
||||
else:
|
||||
begin_episode = episode_number[0]
|
||||
end_episode = episode_number[-1]
|
||||
elif episode_number:
|
||||
begin_episode = episode_number
|
||||
end_episode = None
|
||||
else:
|
||||
begin_episode = None
|
||||
end_episode = None
|
||||
if begin_episode:
|
||||
try:
|
||||
self.begin_episode = int(begin_episode)
|
||||
if end_episode and int(end_episode) != self.begin_episode:
|
||||
self.end_episode = int(end_episode)
|
||||
self.total_episodes = (self.end_episode - self.begin_episode) + 1
|
||||
else:
|
||||
self.total_episodes = 1
|
||||
except Exception as err:
|
||||
print(str(err))
|
||||
self.begin_episode = None
|
||||
self.end_episode = None
|
||||
self.type = MediaType.TV
|
||||
# 类型
|
||||
if not self.type:
|
||||
anime_type = anitopy_info.get('anime_type')
|
||||
if isinstance(anime_type, list):
|
||||
anime_type = anime_type[0]
|
||||
if anime_type and anime_type.upper() == "TV":
|
||||
self.type = MediaType.TV
|
||||
else:
|
||||
self.type = MediaType.MOVIE
|
||||
# 分辨率
|
||||
self.resource_pix = anitopy_info.get("video_resolution")
|
||||
if isinstance(self.resource_pix, list):
|
||||
self.resource_pix = self.resource_pix[0]
|
||||
if self.resource_pix:
|
||||
if re.search(r'x', self.resource_pix, re.IGNORECASE):
|
||||
self.resource_pix = re.split(r'[Xx]', self.resource_pix)[-1] + "p"
|
||||
else:
|
||||
self.resource_pix = self.resource_pix.lower()
|
||||
if str(self.resource_pix).isdigit():
|
||||
self.resource_pix = str(self.resource_pix) + "p"
|
||||
# 制作组/字幕组
|
||||
self.resource_team = \
|
||||
ReleaseGroupsMatcher().match(title=original_title) or \
|
||||
anitopy_info_origin.get("release_group") or None
|
||||
# 视频编码
|
||||
self.video_encode = anitopy_info.get("video_term")
|
||||
if isinstance(self.video_encode, list):
|
||||
self.video_encode = self.video_encode[0]
|
||||
# 音频编码
|
||||
self.audio_encode = anitopy_info.get("audio_term")
|
||||
if isinstance(self.audio_encode, list):
|
||||
self.audio_encode = self.audio_encode[0]
|
||||
# 解析副标题,只要季和集
|
||||
self.init_subtitle(self.org_string)
|
||||
if not self._subtitle_flag and self.subtitle:
|
||||
self.init_subtitle(self.subtitle)
|
||||
if not self.type:
|
||||
self.type = MediaType.TV
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
@staticmethod
|
||||
def __prepare_title(title: str):
|
||||
"""
|
||||
对命名进行预处理
|
||||
"""
|
||||
if not title:
|
||||
return title
|
||||
# 所有【】换成[]
|
||||
title = title.replace("【", "[").replace("】", "]").strip()
|
||||
# 截掉xx番剧漫
|
||||
match = re.search(r"新番|月?番|[日美国][漫剧]", title)
|
||||
if match and match.span()[1] < len(title) - 1:
|
||||
title = re.sub(".*番.|.*[日美国][漫剧].", "", title)
|
||||
elif match:
|
||||
title = title[:title.rfind('[')]
|
||||
# 截掉分类
|
||||
first_item = title.split(']')[0]
|
||||
if first_item and re.search(r"[动漫画纪录片电影视连续剧集日美韩中港台海外亚洲华语大陆综艺原盘高清]{2,}|TV|Animation|Movie|Documentar|Anime",
|
||||
zhconv.convert(first_item, "zh-hans"),
|
||||
re.IGNORECASE):
|
||||
title = re.sub(r"^[^]]*]", "", title).strip()
|
||||
# 去掉大小
|
||||
title = re.sub(r'[0-9.]+\s*[MGT]i?B(?![A-Z]+)', "", title, flags=re.IGNORECASE)
|
||||
# 将TVxx改为xx
|
||||
title = re.sub(r"\[TV\s+(\d{1,4})", r"[\1", title, flags=re.IGNORECASE)
|
||||
# 将4K转为2160p
|
||||
title = re.sub(r'\[4k]', '2160p', title, flags=re.IGNORECASE)
|
||||
# 处理/分隔的中英文标题
|
||||
names = title.split("]")
|
||||
if len(names) > 1 and title.find("- ") == -1:
|
||||
titles = []
|
||||
for name in names:
|
||||
if not name:
|
||||
continue
|
||||
left_char = ''
|
||||
if name.startswith('['):
|
||||
left_char = '['
|
||||
name = name[1:]
|
||||
if name and name.find("/") != -1:
|
||||
if name.split("/")[-1].strip():
|
||||
titles.append("%s%s" % (left_char, name.split("/")[-1].strip()))
|
||||
else:
|
||||
titles.append("%s%s" % (left_char, name.split("/")[0].strip()))
|
||||
elif name:
|
||||
if StringUtils.is_chinese(name) and not StringUtils.is_all_chinese(name):
|
||||
if not re.search(r"\[\d+", name, re.IGNORECASE):
|
||||
name = re.sub(r'[\d|#::\-()()\u4e00-\u9fff]', '', name).strip()
|
||||
if not name or name.strip().isdigit():
|
||||
continue
|
||||
if name == '[':
|
||||
titles.append("")
|
||||
else:
|
||||
titles.append("%s%s" % (left_char, name.strip()))
|
||||
return "]".join(titles)
|
||||
return title
|
427
app/core/meta/metabase.py
Normal file
427
app/core/meta/metabase.py
Normal file
@ -0,0 +1,427 @@
|
||||
from typing import Union, Optional
|
||||
|
||||
import cn2an
|
||||
import regex as re
|
||||
|
||||
from app.utils.string import StringUtils
|
||||
from app.utils.types import MediaType
|
||||
|
||||
|
||||
class MetaBase(object):
|
||||
"""
|
||||
媒体信息基类
|
||||
"""
|
||||
# 是否处理的文件
|
||||
isfile: bool = False
|
||||
# 原字符串
|
||||
org_string: Optional[str] = None
|
||||
# 副标题
|
||||
subtitle: Optional[str] = None
|
||||
# 类型 电影、电视剧
|
||||
type: Optional[MediaType] = None
|
||||
# 识别的中文名
|
||||
cn_name: Optional[str] = None
|
||||
# 识别的英文名
|
||||
en_name: Optional[str] = None
|
||||
# 年份
|
||||
year: Optional[str] = None
|
||||
# 总季数
|
||||
total_seasons: int = 0
|
||||
# 识别的开始季 数字
|
||||
begin_season: Optional[int] = None
|
||||
# 识别的结束季 数字
|
||||
end_season: Optional[int] = None
|
||||
# 总集数
|
||||
total_episodes: int = 0
|
||||
# 识别的开始集
|
||||
begin_episode: Optional[int] = None
|
||||
# 识别的结束集
|
||||
end_episode: Optional[int] = None
|
||||
# Partx Cd Dvd Disk Disc
|
||||
part: Optional[str] = None
|
||||
# 识别的资源类型
|
||||
resource_type: Optional[str] = None
|
||||
# 识别的效果
|
||||
resource_effect: Optional[str] = None
|
||||
# 识别的分辨率
|
||||
resource_pix: Optional[str] = None
|
||||
# 识别的制作组/字幕组
|
||||
resource_team: Optional[str] = None
|
||||
# 视频编码
|
||||
video_encode: Optional[str] = None
|
||||
# 音频编码
|
||||
audio_encode: Optional[str] = None
|
||||
|
||||
# 副标题解析
|
||||
_subtitle_flag = False
|
||||
_subtitle_season_re = r"(?<![全共]\s*)[第\s]+([0-9一二三四五六七八九十S\-]+)\s*季(?!\s*[全共])"
|
||||
_subtitle_season_all_re = r"[全共]\s*([0-9一二三四五六七八九十]+)\s*季|([0-9一二三四五六七八九十]+)\s*季\s*全"
|
||||
_subtitle_episode_re = r"(?<![全共]\s*)[第\s]+([0-9一二三四五六七八九十百零EP\-]+)\s*[集话話期](?!\s*[全共])"
|
||||
_subtitle_episode_all_re = r"([0-9一二三四五六七八九十百零]+)\s*集\s*全|[全共]\s*([0-9一二三四五六七八九十百零]+)\s*[集话話期]"
|
||||
|
||||
def __init__(self, title: str, subtitle: str = None, isfile: bool = False):
|
||||
if not title:
|
||||
return
|
||||
self.org_string = title
|
||||
self.subtitle = subtitle
|
||||
self.isfile = isfile
|
||||
|
||||
def get_name(self):
|
||||
"""
|
||||
返回名称
|
||||
"""
|
||||
if self.cn_name and StringUtils.is_all_chinese(self.cn_name):
|
||||
return self.cn_name
|
||||
elif self.en_name:
|
||||
return self.en_name
|
||||
elif self.cn_name:
|
||||
return self.cn_name
|
||||
return ""
|
||||
|
||||
def init_subtitle(self, title_text: str):
|
||||
"""
|
||||
副标题识别
|
||||
"""
|
||||
if not title_text:
|
||||
return
|
||||
title_text = f" {title_text} "
|
||||
if re.search(r'[全第季集话話期]', title_text, re.IGNORECASE):
|
||||
# 第x季
|
||||
season_str = re.search(r'%s' % self._subtitle_season_re, title_text, re.IGNORECASE)
|
||||
if season_str:
|
||||
seasons = season_str.group(1)
|
||||
if seasons:
|
||||
seasons = seasons.upper().replace("S", "").strip()
|
||||
else:
|
||||
return
|
||||
try:
|
||||
end_season = None
|
||||
if seasons.find('-') != -1:
|
||||
seasons = seasons.split('-')
|
||||
begin_season = int(cn2an.cn2an(seasons[0].strip(), mode='smart'))
|
||||
if len(seasons) > 1:
|
||||
end_season = int(cn2an.cn2an(seasons[1].strip(), mode='smart'))
|
||||
else:
|
||||
begin_season = int(cn2an.cn2an(seasons, mode='smart'))
|
||||
except Exception as err:
|
||||
print(str(err))
|
||||
return
|
||||
if self.begin_season is None and isinstance(begin_season, int):
|
||||
self.begin_season = begin_season
|
||||
self.total_seasons = 1
|
||||
if self.begin_season is not None \
|
||||
and self.end_season is None \
|
||||
and isinstance(end_season, int) \
|
||||
and end_season != self.begin_season:
|
||||
self.end_season = end_season
|
||||
self.total_seasons = (self.end_season - self.begin_season) + 1
|
||||
self.type = MediaType.TV
|
||||
self._subtitle_flag = True
|
||||
# 第x集
|
||||
episode_str = re.search(r'%s' % self._subtitle_episode_re, title_text, re.IGNORECASE)
|
||||
if episode_str:
|
||||
episodes = episode_str.group(1)
|
||||
if episodes:
|
||||
episodes = episodes.upper().replace("E", "").replace("P", "").strip()
|
||||
else:
|
||||
return
|
||||
try:
|
||||
end_episode = None
|
||||
if episodes.find('-') != -1:
|
||||
episodes = episodes.split('-')
|
||||
begin_episode = int(cn2an.cn2an(episodes[0].strip(), mode='smart'))
|
||||
if len(episodes) > 1:
|
||||
end_episode = int(cn2an.cn2an(episodes[1].strip(), mode='smart'))
|
||||
else:
|
||||
begin_episode = int(cn2an.cn2an(episodes, mode='smart'))
|
||||
except Exception as err:
|
||||
print(str(err))
|
||||
return
|
||||
if self.begin_episode is None and isinstance(begin_episode, int):
|
||||
self.begin_episode = begin_episode
|
||||
self.total_episodes = 1
|
||||
if self.begin_episode is not None \
|
||||
and self.end_episode is None \
|
||||
and isinstance(end_episode, int) \
|
||||
and end_episode != self.begin_episode:
|
||||
self.end_episode = end_episode
|
||||
self.total_episodes = (self.end_episode - self.begin_episode) + 1
|
||||
self.type = MediaType.TV
|
||||
self._subtitle_flag = True
|
||||
# x集全
|
||||
episode_all_str = re.search(r'%s' % self._subtitle_episode_all_re, title_text, re.IGNORECASE)
|
||||
if episode_all_str:
|
||||
episode_all = episode_all_str.group(1)
|
||||
if not episode_all:
|
||||
episode_all = episode_all_str.group(2)
|
||||
if episode_all and self.begin_episode is None:
|
||||
try:
|
||||
self.total_episodes = int(cn2an.cn2an(episode_all.strip(), mode='smart'))
|
||||
except Exception as err:
|
||||
print(str(err))
|
||||
return
|
||||
self.begin_episode = None
|
||||
self.end_episode = None
|
||||
self.type = MediaType.TV
|
||||
self._subtitle_flag = True
|
||||
# 全x季 x季全
|
||||
season_all_str = re.search(r"%s" % self._subtitle_season_all_re, title_text, re.IGNORECASE)
|
||||
if season_all_str:
|
||||
season_all = season_all_str.group(1)
|
||||
if not season_all:
|
||||
season_all = season_all_str.group(2)
|
||||
if season_all and self.begin_season is None and self.begin_episode is None:
|
||||
try:
|
||||
self.total_seasons = int(cn2an.cn2an(season_all.strip(), mode='smart'))
|
||||
except Exception as err:
|
||||
print(str(err))
|
||||
return
|
||||
self.begin_season = 1
|
||||
self.end_season = self.total_seasons
|
||||
self.type = MediaType.TV
|
||||
self._subtitle_flag = True
|
||||
|
||||
def is_in_season(self, season: Union[list, int, str]):
|
||||
"""
|
||||
是否包含季
|
||||
"""
|
||||
if isinstance(season, list):
|
||||
if self.end_season is not None:
|
||||
meta_season = list(range(self.begin_season, self.end_season + 1))
|
||||
else:
|
||||
if self.begin_season is not None:
|
||||
meta_season = [self.begin_season]
|
||||
else:
|
||||
meta_season = [1]
|
||||
|
||||
return set(meta_season).issuperset(set(season))
|
||||
else:
|
||||
if self.end_season is not None:
|
||||
return self.begin_season <= int(season) <= self.end_season
|
||||
else:
|
||||
if self.begin_season is not None:
|
||||
return int(season) == self.begin_season
|
||||
else:
|
||||
return int(season) == 1
|
||||
|
||||
def is_in_episode(self, episode: Union[list, int, str]):
|
||||
"""
|
||||
是否包含集
|
||||
"""
|
||||
if isinstance(episode, list):
|
||||
if self.end_episode is not None:
|
||||
meta_episode = list(range(self.begin_episode, self.end_episode + 1))
|
||||
else:
|
||||
meta_episode = [self.begin_episode]
|
||||
return set(meta_episode).issuperset(set(episode))
|
||||
else:
|
||||
if self.end_episode is not None:
|
||||
return self.begin_episode <= int(episode) <= self.end_episode
|
||||
else:
|
||||
return int(episode) == self.begin_episode
|
||||
|
||||
def get_season_string(self):
|
||||
"""
|
||||
返回季字符串
|
||||
"""
|
||||
if self.begin_season is not None:
|
||||
return "S%s" % str(self.begin_season).rjust(2, "0") \
|
||||
if self.end_season is None \
|
||||
else "S%s-S%s" % \
|
||||
(str(self.begin_season).rjust(2, "0"),
|
||||
str(self.end_season).rjust(2, "0"))
|
||||
else:
|
||||
if self.type == MediaType.MOVIE:
|
||||
return ""
|
||||
else:
|
||||
return "S01"
|
||||
|
||||
def get_season_item(self):
|
||||
"""
|
||||
返回begin_season 的Sxx
|
||||
"""
|
||||
if self.begin_season is not None:
|
||||
return "S%s" % str(self.begin_season).rjust(2, "0")
|
||||
else:
|
||||
if self.type == MediaType.MOVIE:
|
||||
return ""
|
||||
else:
|
||||
return "S01"
|
||||
|
||||
def get_season_seq(self):
|
||||
"""
|
||||
返回begin_season 的数字
|
||||
"""
|
||||
if self.begin_season is not None:
|
||||
return str(self.begin_season)
|
||||
else:
|
||||
if self.type == MediaType.MOVIE:
|
||||
return ""
|
||||
else:
|
||||
return "1"
|
||||
|
||||
def get_season_list(self):
|
||||
"""
|
||||
返回季的数组
|
||||
"""
|
||||
if self.begin_season is None:
|
||||
if self.type == MediaType.MOVIE:
|
||||
return []
|
||||
else:
|
||||
return [1]
|
||||
elif self.end_season is not None:
|
||||
return [season for season in range(self.begin_season, self.end_season + 1)]
|
||||
else:
|
||||
return [self.begin_season]
|
||||
|
||||
def set_season(self, sea: Union[list, int, str]):
|
||||
"""
|
||||
更新季
|
||||
"""
|
||||
if not sea:
|
||||
return
|
||||
if isinstance(sea, list):
|
||||
if len(sea) == 1 and str(sea[0]).isdigit():
|
||||
self.begin_season = int(sea[0])
|
||||
self.end_season = None
|
||||
elif len(sea) > 1 and str(sea[0]).isdigit() and str(sea[-1]).isdigit():
|
||||
self.begin_season = int(sea[0])
|
||||
self.end_season = int(sea[-1])
|
||||
elif str(sea).isdigit():
|
||||
self.begin_season = int(sea)
|
||||
self.end_season = None
|
||||
|
||||
def set_episode(self, ep: Union[list, int, str]):
|
||||
"""
|
||||
更新集
|
||||
"""
|
||||
if not ep:
|
||||
return
|
||||
if isinstance(ep, list):
|
||||
if len(ep) == 1 and str(ep[0]).isdigit():
|
||||
self.begin_episode = int(ep[0])
|
||||
self.end_episode = None
|
||||
elif len(ep) > 1 and str(ep[0]).isdigit() and str(ep[-1]).isdigit():
|
||||
self.begin_episode = int(ep[0])
|
||||
self.end_episode = int(ep[-1])
|
||||
elif str(ep).isdigit():
|
||||
self.begin_episode = int(ep)
|
||||
self.end_episode = None
|
||||
|
||||
#
|
||||
def get_episode_string(self):
|
||||
"""
|
||||
返回集字符串
|
||||
"""
|
||||
if self.begin_episode is not None:
|
||||
return "E%s" % str(self.begin_episode).rjust(2, "0") \
|
||||
if self.end_episode is None \
|
||||
else "E%s-E%s" % \
|
||||
(
|
||||
str(self.begin_episode).rjust(2, "0"),
|
||||
str(self.end_episode).rjust(2, "0"))
|
||||
else:
|
||||
return ""
|
||||
|
||||
def get_episode_list(self):
|
||||
"""
|
||||
返回集的数组
|
||||
"""
|
||||
if self.begin_episode is None:
|
||||
return []
|
||||
elif self.end_episode is not None:
|
||||
return [episode for episode in range(self.begin_episode, self.end_episode + 1)]
|
||||
else:
|
||||
return [self.begin_episode]
|
||||
|
||||
def get_episode_items(self):
|
||||
"""
|
||||
返回集的并列表达方式,用于支持单文件多集
|
||||
"""
|
||||
return "E%s" % "E".join(str(episode).rjust(2, '0') for episode in self.get_episode_list())
|
||||
|
||||
def get_episode_seqs(self):
|
||||
"""
|
||||
返回单文件多集的集数表达方式,用于支持单文件多集
|
||||
"""
|
||||
episodes = self.get_episode_list()
|
||||
if episodes:
|
||||
# 集 xx
|
||||
if len(episodes) == 1:
|
||||
return str(episodes[0])
|
||||
else:
|
||||
return "%s-%s" % (episodes[0], episodes[-1])
|
||||
else:
|
||||
return ""
|
||||
|
||||
def get_episode_seq(self):
|
||||
"""
|
||||
返回begin_episode 的数字
|
||||
"""
|
||||
episodes = self.get_episode_list()
|
||||
if episodes:
|
||||
return str(episodes[0])
|
||||
else:
|
||||
return ""
|
||||
|
||||
def get_season_episode_string(self):
|
||||
"""
|
||||
返回季集字符串
|
||||
"""
|
||||
if self.type == MediaType.MOVIE:
|
||||
return ""
|
||||
else:
|
||||
seaion = self.get_season_string()
|
||||
episode = self.get_episode_string()
|
||||
if seaion and episode:
|
||||
return "%s %s" % (seaion, episode)
|
||||
elif seaion:
|
||||
return "%s" % seaion
|
||||
elif episode:
|
||||
return "%s" % episode
|
||||
return ""
|
||||
|
||||
def get_resource_type_string(self):
|
||||
"""
|
||||
返回资源类型字符串,含分辨率
|
||||
"""
|
||||
ret_string = ""
|
||||
if self.resource_type:
|
||||
ret_string = f"{ret_string} {self.resource_type}"
|
||||
if self.resource_effect:
|
||||
ret_string = f"{ret_string} {self.resource_effect}"
|
||||
if self.resource_pix:
|
||||
ret_string = f"{ret_string} {self.resource_pix}"
|
||||
return ret_string
|
||||
|
||||
def get_edtion_string(self):
|
||||
"""
|
||||
返回资源类型字符串,不含分辨率
|
||||
"""
|
||||
ret_string = ""
|
||||
if self.resource_type:
|
||||
ret_string = f"{ret_string} {self.resource_type}"
|
||||
if self.resource_effect:
|
||||
ret_string = f"{ret_string} {self.resource_effect}"
|
||||
return ret_string.strip()
|
||||
|
||||
def get_resource_team_string(self):
|
||||
"""
|
||||
返回发布组/字幕组字符串
|
||||
"""
|
||||
if self.resource_team:
|
||||
return self.resource_team
|
||||
else:
|
||||
return ""
|
||||
|
||||
def get_video_encode_string(self):
|
||||
"""
|
||||
返回视频编码
|
||||
"""
|
||||
return self.video_encode or ""
|
||||
|
||||
def get_audio_encode_string(self):
|
||||
"""
|
||||
返回音频编码
|
||||
"""
|
||||
return self.audio_encode or ""
|
557
app/core/meta/metavideo.py
Normal file
557
app/core/meta/metavideo.py
Normal file
@ -0,0 +1,557 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.meta.metabase import MetaBase
|
||||
from app.core.meta.release_groups import ReleaseGroupsMatcher
|
||||
from app.utils.string import StringUtils
|
||||
from app.utils.tokens import Tokens
|
||||
from app.utils.types import MediaType
|
||||
|
||||
|
||||
class MetaVideo(MetaBase):
|
||||
"""
|
||||
识别电影、电视剧
|
||||
"""
|
||||
# 控制标位区
|
||||
_stop_name_flag = False
|
||||
_stop_cnname_flag = False
|
||||
_last_token = ""
|
||||
_last_token_type = ""
|
||||
_continue_flag = True
|
||||
_unknown_name_str = ""
|
||||
_source = ""
|
||||
_effect = []
|
||||
# 正则式区
|
||||
_season_re = r"S(\d{2})|^S(\d{1,2})$|S(\d{1,2})E"
|
||||
_episode_re = r"EP?(\d{2,4})$|^EP?(\d{1,4})$|^S\d{1,2}EP?(\d{1,4})$|S\d{2}EP?(\d{2,4})"
|
||||
_part_re = r"(^PART[0-9ABI]{0,2}$|^CD[0-9]{0,2}$|^DVD[0-9]{0,2}$|^DISK[0-9]{0,2}$|^DISC[0-9]{0,2}$)"
|
||||
_roman_numerals = r"^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"
|
||||
_source_re = r"^BLURAY$|^HDTV$|^UHDTV$|^HDDVD$|^WEBRIP$|^DVDRIP$|^BDRIP$|^BLU$|^WEB$|^BD$|^HDRip$"
|
||||
_effect_re = r"^REMUX$|^UHD$|^SDR$|^HDR\d*$|^DOLBY$|^DOVI$|^DV$|^3D$|^REPACK$"
|
||||
_resources_type_re = r"%s|%s" % (_source_re, _effect_re)
|
||||
_name_no_begin_re = r"^\[.+?]"
|
||||
_name_no_chinese_re = r".*版|.*字幕"
|
||||
_name_se_words = ['共', '第', '季', '集', '话', '話', '期']
|
||||
_name_nostring_re = r"^PTS|^JADE|^AOD|^CHC|^[A-Z]{1,4}TV[\-0-9UVHDK]*" \
|
||||
r"|HBO$|\s+HBO|\d{1,2}th|\d{1,2}bit|NETFLIX|AMAZON|IMAX|^3D|\s+3D|^BBC\s+|\s+BBC|BBC$|DISNEY\+?|XXX|\s+DC$" \
|
||||
r"|[第\s共]+[0-9一二三四五六七八九十\-\s]+季" \
|
||||
r"|[第\s共]+[0-9一二三四五六七八九十百零\-\s]+[集话話]" \
|
||||
r"|连载|日剧|美剧|电视剧|动画片|动漫|欧美|西德|日韩|超高清|高清|蓝光|翡翠台|梦幻天堂·龙网|★?\d*月?新番" \
|
||||
r"|最终季|合集|[多中国英葡法俄日韩德意西印泰台港粤双文语简繁体特效内封官译外挂]+字幕|版本|出品|台版|港版|\w+字幕组" \
|
||||
r"|未删减版|UNCUT$|UNRATE$|WITH EXTRAS$|RERIP$|SUBBED$|PROPER$|REPACK$|SEASON$|EPISODE$|Complete$|Extended$|Extended Version$" \
|
||||
r"|S\d{2}\s*-\s*S\d{2}|S\d{2}|\s+S\d{1,2}|EP?\d{2,4}\s*-\s*EP?\d{2,4}|EP?\d{2,4}|\s+EP?\d{1,4}" \
|
||||
r"|CD[\s.]*[1-9]|DVD[\s.]*[1-9]|DISK[\s.]*[1-9]|DISC[\s.]*[1-9]" \
|
||||
r"|[248]K|\d{3,4}[PIX]+" \
|
||||
r"|CD[\s.]*[1-9]|DVD[\s.]*[1-9]|DISK[\s.]*[1-9]|DISC[\s.]*[1-9]"
|
||||
_resources_pix_re = r"^[SBUHD]*(\d{3,4}[PI]+)|\d{3,4}X(\d{3,4})"
|
||||
_resources_pix_re2 = r"(^[248]+K)"
|
||||
_video_encode_re = r"^[HX]26[45]$|^AVC$|^HEVC$|^VC\d?$|^MPEG\d?$|^Xvid$|^DivX$|^HDR\d*$"
|
||||
_audio_encode_re = r"^DTS\d?$|^DTSHD$|^DTSHDMA$|^Atmos$|^TrueHD\d?$|^AC3$|^\dAudios?$|^DDP\d?$|^DD\d?$|^LPCM\d?$|^AAC\d?$|^FLAC\d?$|^HD\d?$|^MA\d?$"
|
||||
|
||||
def __init__(self, title: str, subtitle: str = None, isfile: bool = False):
|
||||
super().__init__(title, subtitle, isfile)
|
||||
if not title:
|
||||
return
|
||||
original_title = title
|
||||
self._source = ""
|
||||
self._effect = []
|
||||
# 判断是否纯数字命名
|
||||
title_path = Path(title)
|
||||
if title_path.suffix.lower() in settings.RMT_MEDIAEXT \
|
||||
and title_path.stem.isdigit() \
|
||||
and len(title_path.stem) < 5:
|
||||
self.begin_episode = int(title_path.stem)
|
||||
self.type = MediaType.TV
|
||||
return
|
||||
# 去掉名称中第1个[]的内容
|
||||
title = re.sub(r'%s' % self._name_no_begin_re, "", title, count=1)
|
||||
# 把xxxx-xxxx年份换成前一个年份,常出现在季集上
|
||||
title = re.sub(r'([\s.]+)(\d{4})-(\d{4})', r'\1\2', title)
|
||||
# 把大小去掉
|
||||
title = re.sub(r'[0-9.]+\s*[MGT]i?B(?![A-Z]+)', "", title, flags=re.IGNORECASE)
|
||||
# 把年月日去掉
|
||||
title = re.sub(r'\d{4}[\s._-]\d{1,2}[\s._-]\d{1,2}', "", title)
|
||||
# 拆分tokens
|
||||
tokens = Tokens(title)
|
||||
self.tokens = tokens
|
||||
# 解析名称、年份、季、集、资源类型、分辨率等
|
||||
token = tokens.get_next()
|
||||
while token:
|
||||
# Part
|
||||
self.__init_part(token)
|
||||
# 标题
|
||||
if self._continue_flag:
|
||||
self.__init_name(token)
|
||||
# 年份
|
||||
if self._continue_flag:
|
||||
self.__init_year(token)
|
||||
# 分辨率
|
||||
if self._continue_flag:
|
||||
self.__init_resource_pix(token)
|
||||
# 季
|
||||
if self._continue_flag:
|
||||
self.__init_season(token)
|
||||
# 集
|
||||
if self._continue_flag:
|
||||
self.__init_episode(token)
|
||||
# 资源类型
|
||||
if self._continue_flag:
|
||||
self.__init_resource_type(token)
|
||||
# 视频编码
|
||||
if self._continue_flag:
|
||||
self.__init_video_encode(token)
|
||||
# 音频编码
|
||||
if self._continue_flag:
|
||||
self.__init_audio_encode(token)
|
||||
# 取下一个,直到没有为卡
|
||||
token = tokens.get_next()
|
||||
self._continue_flag = True
|
||||
# 合成质量
|
||||
if self._effect:
|
||||
self._effect.reverse()
|
||||
self.resource_effect = " ".join(self._effect)
|
||||
if self._source:
|
||||
self.resource_type = self._source.strip()
|
||||
# 提取原盘DIY
|
||||
if self.resource_type and "BluRay" in self.resource_type:
|
||||
if (self.subtitle and re.findall(r'D[Ii]Y', self.subtitle)) \
|
||||
or re.findall(r'-D[Ii]Y@', original_title):
|
||||
self.resource_type = f"{self.resource_type} DIY"
|
||||
# 解析副标题,只要季和集
|
||||
self.init_subtitle(self.org_string)
|
||||
if not self._subtitle_flag and self.subtitle:
|
||||
self.init_subtitle(self.subtitle)
|
||||
# 没有识别出类型时默认为电影
|
||||
if not self.type:
|
||||
self.type = MediaType.MOVIE
|
||||
# 去掉名字中不需要的干扰字符,过短的纯数字不要
|
||||
self.cn_name = self.__fix_name(self.cn_name)
|
||||
self.en_name = StringUtils.str_title(self.__fix_name(self.en_name))
|
||||
# 处理part
|
||||
if self.part and self.part.upper() == "PART":
|
||||
self.part = None
|
||||
# 制作组/字幕组
|
||||
self.resource_team = ReleaseGroupsMatcher().match(title=original_title) or None
|
||||
|
||||
def __fix_name(self, name: str):
|
||||
if not name:
|
||||
return name
|
||||
name = re.sub(r'%s' % self._name_nostring_re, '', name,
|
||||
flags=re.IGNORECASE).strip()
|
||||
name = re.sub(r'\s+', ' ', name)
|
||||
if name.isdigit() \
|
||||
and int(name) < 1800 \
|
||||
and not self.year \
|
||||
and not self.begin_season \
|
||||
and not self.resource_pix \
|
||||
and not self.resource_type \
|
||||
and not self.audio_encode \
|
||||
and not self.video_encode:
|
||||
if self.begin_episode is None:
|
||||
self.begin_episode = int(name)
|
||||
name = None
|
||||
elif self.is_in_episode(int(name)) and not self.begin_season:
|
||||
name = None
|
||||
return name
|
||||
|
||||
def __init_name(self, token: str):
|
||||
if not token:
|
||||
return
|
||||
# 回收标题
|
||||
if self._unknown_name_str:
|
||||
if not self.cn_name:
|
||||
if not self.en_name:
|
||||
self.en_name = self._unknown_name_str
|
||||
elif self._unknown_name_str != self.year:
|
||||
self.en_name = "%s %s" % (self.en_name, self._unknown_name_str)
|
||||
self._last_token_type = "enname"
|
||||
self._unknown_name_str = ""
|
||||
if self._stop_name_flag:
|
||||
return
|
||||
if token.upper() == "AKA":
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
return
|
||||
if token in self._name_se_words:
|
||||
self._last_token_type = 'name_se_words'
|
||||
return
|
||||
if StringUtils.is_chinese(token):
|
||||
# 含有中文,直接做为标题(连着的数字或者英文会保留),且不再取用后面出现的中文
|
||||
self._last_token_type = "cnname"
|
||||
if not self.cn_name:
|
||||
self.cn_name = token
|
||||
elif not self._stop_cnname_flag:
|
||||
if not re.search("%s" % self._name_no_chinese_re, token, flags=re.IGNORECASE) \
|
||||
and not re.search("%s" % self._name_se_words, token, flags=re.IGNORECASE):
|
||||
self.cn_name = "%s %s" % (self.cn_name, token)
|
||||
self._stop_cnname_flag = True
|
||||
else:
|
||||
is_roman_digit = re.search(self._roman_numerals, token)
|
||||
# 阿拉伯数字或者罗马数字
|
||||
if token.isdigit() or is_roman_digit:
|
||||
# 第季集后面的不要
|
||||
if self._last_token_type == 'name_se_words':
|
||||
return
|
||||
if self.get_name():
|
||||
# 名字后面以 0 开头的不要,极有可能是集
|
||||
if token.startswith('0'):
|
||||
return
|
||||
# 检查是否真正的数字
|
||||
if token.isdigit():
|
||||
try:
|
||||
int(token)
|
||||
except ValueError:
|
||||
return
|
||||
# 中文名后面跟的数字不是年份的极有可能是集
|
||||
if not is_roman_digit \
|
||||
and self._last_token_type == "cnname" \
|
||||
and int(token) < 1900:
|
||||
return
|
||||
if (token.isdigit() and len(token) < 4) or is_roman_digit:
|
||||
# 4位以下的数字或者罗马数字,拼装到已有标题中
|
||||
if self._last_token_type == "cnname":
|
||||
self.cn_name = "%s %s" % (self.cn_name, token)
|
||||
elif self._last_token_type == "enname":
|
||||
self.en_name = "%s %s" % (self.en_name, token)
|
||||
self._continue_flag = False
|
||||
elif token.isdigit() and len(token) == 4:
|
||||
# 4位数字,可能是年份,也可能真的是标题的一部分,也有可能是集
|
||||
if not self._unknown_name_str:
|
||||
self._unknown_name_str = token
|
||||
else:
|
||||
# 名字未出现前的第一个数字,记下来
|
||||
if not self._unknown_name_str:
|
||||
self._unknown_name_str = token
|
||||
elif re.search(r"%s" % self._season_re, token, re.IGNORECASE):
|
||||
# 季的处理
|
||||
if self.en_name and re.search(r"SEASON$", self.en_name, re.IGNORECASE):
|
||||
# 如果匹配到季,英文名结尾为Season,说明Season属于标题,不应在后续作为干扰词去除
|
||||
self.en_name += ' '
|
||||
self._stop_name_flag = True
|
||||
return
|
||||
elif re.search(r"%s" % self._episode_re, token, re.IGNORECASE) \
|
||||
or re.search(r"(%s)" % self._resources_type_re, token, re.IGNORECASE) \
|
||||
or re.search(r"%s" % self._resources_pix_re, token, re.IGNORECASE):
|
||||
# 集、来源、版本等不要
|
||||
self._stop_name_flag = True
|
||||
return
|
||||
else:
|
||||
# 后缀名不要
|
||||
if ".%s".lower() % token in settings.RMT_MEDIAEXT:
|
||||
return
|
||||
# 英文或者英文+数字,拼装起来
|
||||
if self.en_name:
|
||||
self.en_name = "%s %s" % (self.en_name, token)
|
||||
else:
|
||||
self.en_name = token
|
||||
self._last_token_type = "enname"
|
||||
|
||||
def __init_part(self, token: str):
|
||||
if not self.get_name():
|
||||
return
|
||||
if not self.year \
|
||||
and not self.begin_season \
|
||||
and not self.begin_episode \
|
||||
and not self.resource_pix \
|
||||
and not self.resource_type:
|
||||
return
|
||||
re_res = re.search(r"%s" % self._part_re, token, re.IGNORECASE)
|
||||
if re_res:
|
||||
if not self.part:
|
||||
self.part = re_res.group(1)
|
||||
nextv = self.tokens.cur()
|
||||
if nextv \
|
||||
and ((nextv.isdigit() and (len(nextv) == 1 or len(nextv) == 2 and nextv.startswith('0')))
|
||||
or nextv.upper() in ['A', 'B', 'C', 'I', 'II', 'III']):
|
||||
self.part = "%s%s" % (self.part, nextv)
|
||||
self.tokens.get_next()
|
||||
self._last_token_type = "part"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = False
|
||||
|
||||
def __init_year(self, token: str):
|
||||
if not self.get_name():
|
||||
return
|
||||
if not token.isdigit():
|
||||
return
|
||||
if len(token) != 4:
|
||||
return
|
||||
if not 1900 < int(token) < 2050:
|
||||
return
|
||||
if self.year:
|
||||
if self.en_name:
|
||||
self.en_name = "%s %s" % (self.en_name.strip(), self.year)
|
||||
elif self.cn_name:
|
||||
self.cn_name = "%s %s" % (self.cn_name, self.year)
|
||||
elif self.en_name and re.search(r"SEASON$", self.en_name, re.IGNORECASE):
|
||||
# 如果匹配到年,且英文名结尾为Season,说明Season属于标题,不应在后续作为干扰词去除
|
||||
self.en_name += ' '
|
||||
self.year = token
|
||||
self._last_token_type = "year"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
|
||||
def __init_resource_pix(self, token: str):
|
||||
if not self.get_name():
|
||||
return
|
||||
re_res = re.findall(r"%s" % self._resources_pix_re, token, re.IGNORECASE)
|
||||
if re_res:
|
||||
self._last_token_type = "pix"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
resource_pix = None
|
||||
for pixs in re_res:
|
||||
if isinstance(pixs, tuple):
|
||||
pix_t = None
|
||||
for pix_i in pixs:
|
||||
if pix_i:
|
||||
pix_t = pix_i
|
||||
break
|
||||
if pix_t:
|
||||
resource_pix = pix_t
|
||||
else:
|
||||
resource_pix = pixs
|
||||
if resource_pix and not self.resource_pix:
|
||||
self.resource_pix = resource_pix.lower()
|
||||
break
|
||||
if self.resource_pix \
|
||||
and self.resource_pix.isdigit() \
|
||||
and self.resource_pix[-1] not in 'kpi':
|
||||
self.resource_pix = "%sp" % self.resource_pix
|
||||
else:
|
||||
re_res = re.search(r"%s" % self._resources_pix_re2, token, re.IGNORECASE)
|
||||
if re_res:
|
||||
self._last_token_type = "pix"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
if not self.resource_pix:
|
||||
self.resource_pix = re_res.group(1).lower()
|
||||
|
||||
def __init_season(self, token: str):
|
||||
re_res = re.findall(r"%s" % self._season_re, token, re.IGNORECASE)
|
||||
if re_res:
|
||||
self._last_token_type = "season"
|
||||
self.type = MediaType.TV
|
||||
self._stop_name_flag = True
|
||||
self._continue_flag = True
|
||||
for se in re_res:
|
||||
if isinstance(se, tuple):
|
||||
se_t = None
|
||||
for se_i in se:
|
||||
if se_i and str(se_i).isdigit():
|
||||
se_t = se_i
|
||||
break
|
||||
if se_t:
|
||||
se = int(se_t)
|
||||
else:
|
||||
break
|
||||
else:
|
||||
se = int(se)
|
||||
if self.begin_season is None:
|
||||
self.begin_season = se
|
||||
self.total_seasons = 1
|
||||
else:
|
||||
if se > self.begin_season:
|
||||
self.end_season = se
|
||||
self.total_seasons = (self.end_season - self.begin_season) + 1
|
||||
if self.isfile and self.total_seasons > 1:
|
||||
self.end_season = None
|
||||
self.total_seasons = 1
|
||||
elif token.isdigit():
|
||||
try:
|
||||
int(token)
|
||||
except ValueError:
|
||||
return
|
||||
if self._last_token_type == "SEASON" \
|
||||
and self.begin_season is None \
|
||||
and len(token) < 3:
|
||||
self.begin_season = int(token)
|
||||
self.total_seasons = 1
|
||||
self._last_token_type = "season"
|
||||
self._stop_name_flag = True
|
||||
self._continue_flag = False
|
||||
self.type = MediaType.TV
|
||||
elif token.upper() == "SEASON" and self.begin_season is None:
|
||||
self._last_token_type = "SEASON"
|
||||
|
||||
def __init_episode(self, token: str):
|
||||
re_res = re.findall(r"%s" % self._episode_re, token, re.IGNORECASE)
|
||||
if re_res:
|
||||
self._last_token_type = "episode"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
self.type = MediaType.TV
|
||||
for se in re_res:
|
||||
if isinstance(se, tuple):
|
||||
se_t = None
|
||||
for se_i in se:
|
||||
if se_i and str(se_i).isdigit():
|
||||
se_t = se_i
|
||||
break
|
||||
if se_t:
|
||||
se = int(se_t)
|
||||
else:
|
||||
break
|
||||
else:
|
||||
se = int(se)
|
||||
if self.begin_episode is None:
|
||||
self.begin_episode = se
|
||||
self.total_episodes = 1
|
||||
else:
|
||||
if se > self.begin_episode:
|
||||
self.end_episode = se
|
||||
self.total_episodes = (self.end_episode - self.begin_episode) + 1
|
||||
if self.isfile and self.total_episodes > 2:
|
||||
self.end_episode = None
|
||||
self.total_episodes = 1
|
||||
elif token.isdigit():
|
||||
try:
|
||||
int(token)
|
||||
except ValueError:
|
||||
return
|
||||
if self.begin_episode is not None \
|
||||
and self.end_episode is None \
|
||||
and len(token) < 5 \
|
||||
and int(token) > self.begin_episode \
|
||||
and self._last_token_type == "episode":
|
||||
self.end_episode = int(token)
|
||||
self.total_episodes = (self.end_episode - self.begin_episode) + 1
|
||||
if self.isfile and self.total_episodes > 2:
|
||||
self.end_episode = None
|
||||
self.total_episodes = 1
|
||||
self._continue_flag = False
|
||||
self.type = MediaType.TV
|
||||
elif self.begin_episode is None \
|
||||
and 1 < len(token) < 4 \
|
||||
and self._last_token_type != "year" \
|
||||
and self._last_token_type != "videoencode" \
|
||||
and token != self._unknown_name_str:
|
||||
self.begin_episode = int(token)
|
||||
self.total_episodes = 1
|
||||
self._last_token_type = "episode"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
self.type = MediaType.TV
|
||||
elif self._last_token_type == "EPISODE" \
|
||||
and self.begin_episode is None \
|
||||
and len(token) < 5:
|
||||
self.begin_episode = int(token)
|
||||
self.total_episodes = 1
|
||||
self._last_token_type = "episode"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
self.type = MediaType.TV
|
||||
elif token.upper() == "EPISODE":
|
||||
self._last_token_type = "EPISODE"
|
||||
|
||||
def __init_resource_type(self, token):
|
||||
if not self.get_name():
|
||||
return
|
||||
source_res = re.search(r"(%s)" % self._source_re, token, re.IGNORECASE)
|
||||
if source_res:
|
||||
self._last_token_type = "source"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
if not self._source:
|
||||
self._source = source_res.group(1)
|
||||
self._last_token = self._source.upper()
|
||||
return
|
||||
elif token.upper() == "DL" \
|
||||
and self._last_token_type == "source" \
|
||||
and self._last_token == "WEB":
|
||||
self._source = "WEB-DL"
|
||||
self._continue_flag = False
|
||||
return
|
||||
elif token.upper() == "RAY" \
|
||||
and self._last_token_type == "source" \
|
||||
and self._last_token == "BLU":
|
||||
self._source = "BluRay"
|
||||
self._continue_flag = False
|
||||
return
|
||||
elif token.upper() == "WEBDL":
|
||||
self._source = "WEB-DL"
|
||||
self._continue_flag = False
|
||||
return
|
||||
effect_res = re.search(r"(%s)" % self._effect_re, token, re.IGNORECASE)
|
||||
if effect_res:
|
||||
self._last_token_type = "effect"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
effect = effect_res.group(1)
|
||||
if effect not in self._effect:
|
||||
self._effect.append(effect)
|
||||
self._last_token = effect.upper()
|
||||
|
||||
def __init_video_encode(self, token: str):
|
||||
if not self.get_name():
|
||||
return
|
||||
if not self.year \
|
||||
and not self.resource_pix \
|
||||
and not self.resource_type \
|
||||
and not self.begin_season \
|
||||
and not self.begin_episode:
|
||||
return
|
||||
re_res = re.search(r"(%s)" % self._video_encode_re, token, re.IGNORECASE)
|
||||
if re_res:
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
self._last_token_type = "videoencode"
|
||||
if not self.video_encode:
|
||||
self.video_encode = re_res.group(1).upper()
|
||||
self._last_token = self.video_encode
|
||||
elif self.video_encode == "10bit":
|
||||
self.video_encode = f"{re_res.group(1).upper()} 10bit"
|
||||
self._last_token = re_res.group(1).upper()
|
||||
elif token.upper() in ['H', 'X']:
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
self._last_token_type = "videoencode"
|
||||
self._last_token = token.upper() if token.upper() == "H" else token.lower()
|
||||
elif token in ["264", "265"] \
|
||||
and self._last_token_type == "videoencode" \
|
||||
and self._last_token in ['H', 'X']:
|
||||
self.video_encode = "%s%s" % (self._last_token, token)
|
||||
elif token.isdigit() \
|
||||
and self._last_token_type == "videoencode" \
|
||||
and self._last_token in ['VC', 'MPEG']:
|
||||
self.video_encode = "%s%s" % (self._last_token, token)
|
||||
elif token.upper() == "10BIT":
|
||||
self._last_token_type = "videoencode"
|
||||
if not self.video_encode:
|
||||
self.video_encode = "10bit"
|
||||
else:
|
||||
self.video_encode = f"{self.video_encode} 10bit"
|
||||
|
||||
def __init_audio_encode(self, token: str):
|
||||
if not self.get_name():
|
||||
return
|
||||
if not self.year \
|
||||
and not self.resource_pix \
|
||||
and not self.resource_type \
|
||||
and not self.begin_season \
|
||||
and not self.begin_episode:
|
||||
return
|
||||
re_res = re.search(r"(%s)" % self._audio_encode_re, token, re.IGNORECASE)
|
||||
if re_res:
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = True
|
||||
self._last_token_type = "audioencode"
|
||||
self._last_token = re_res.group(1).upper()
|
||||
if not self.audio_encode:
|
||||
self.audio_encode = re_res.group(1)
|
||||
else:
|
||||
if self.audio_encode.upper() == "DTS":
|
||||
self.audio_encode = "%s-%s" % (self.audio_encode, re_res.group(1))
|
||||
else:
|
||||
self.audio_encode = "%s %s" % (self.audio_encode, re_res.group(1))
|
||||
elif token.isdigit() \
|
||||
and self._last_token_type == "audioencode":
|
||||
if self.audio_encode:
|
||||
if self._last_token.isdigit():
|
||||
self.audio_encode = "%s.%s" % (self.audio_encode, token)
|
||||
elif self.audio_encode[-1].isdigit():
|
||||
self.audio_encode = "%s %s.%s" % (self.audio_encode[:-1], self.audio_encode[-1], token)
|
||||
else:
|
||||
self.audio_encode = "%s %s" % (self.audio_encode, token)
|
||||
self._last_token = token
|
111
app/core/meta/release_groups.py
Normal file
111
app/core/meta/release_groups.py
Normal file
@ -0,0 +1,111 @@
|
||||
import regex as re
|
||||
|
||||
from app.utils.singleton import Singleton
|
||||
|
||||
|
||||
class ReleaseGroupsMatcher(metaclass=Singleton):
|
||||
"""
|
||||
识别制作组、字幕组
|
||||
"""
|
||||
__release_groups: str = None
|
||||
custom_release_groups: str = None
|
||||
custom_separator: str = None
|
||||
RELEASE_GROUPS: dict = {
|
||||
"0ff": ['FF(?:(?:A|WE)B|CD|E(?:DU|B)|TV)'],
|
||||
"1pt": [],
|
||||
"52pt": [],
|
||||
"audiences": ['Audies', 'AD(?:Audio|E(?:|book)|Music|Web)'],
|
||||
"azusa": [],
|
||||
"beitai": ['BeiTai'],
|
||||
"btschool": ['Bts(?:CHOOL|HD|PAD|TV)', 'Zone'],
|
||||
"carpt": ['CarPT'],
|
||||
"chdbits": ['CHD(?:|Bits|PAD|(?:|HK)TV|WEB)', 'StBOX', 'OneHD', 'Lee', 'xiaopie'],
|
||||
"discfan": [],
|
||||
"dragonhd": [],
|
||||
"eastgame": ['(?:(?:iNT|(?:HALFC|Mini(?:S|H|FH)D))-|)TLF'],
|
||||
"filelist": [],
|
||||
"gainbound": ['(?:DG|GBWE)B'],
|
||||
"hares": ['Hares(?:|(?:M|T)V|Web)'],
|
||||
"hd4fans": [],
|
||||
"hdarea": ['HDA(?:pad|rea|TV)', 'EPiC'],
|
||||
"hdatmos": [],
|
||||
"hdbd": [],
|
||||
"hdchina": ['HDC(?:|hina|TV)', 'k9611', 'tudou', 'iHD'],
|
||||
"hddolby": ['D(?:ream|BTV)', '(?:HD|QHstudI)o'],
|
||||
"hdfans": ['beAst(?:|TV)'],
|
||||
"hdhome": ['HDH(?:|ome|Pad|TV|WEB)'],
|
||||
"hdpt": ['HDPT(?:|Web)'],
|
||||
"hdsky": ['HDS(?:|ky|TV|Pad|WEB)', 'AQLJ'],
|
||||
"hdtime": [],
|
||||
"HDU": [],
|
||||
"hdvideo": [],
|
||||
"hdzone": ['HDZ(?:|one)'],
|
||||
"hhanclub": ['HHWEB'],
|
||||
"hitpt": [],
|
||||
"htpt": ['HTPT'],
|
||||
"iptorrents": [],
|
||||
"joyhd": [],
|
||||
"keepfrds": ['FRDS', 'Yumi', 'cXcY'],
|
||||
"lemonhd": ['L(?:eague(?:(?:C|H)D|(?:M|T)V|NF|WEB)|HD)', 'i18n', 'CiNT'],
|
||||
"mteam": ['MTeam(?:|TV)', 'MPAD'],
|
||||
"nanyangpt": [],
|
||||
"nicept": [],
|
||||
"oshen": [],
|
||||
"ourbits": ['Our(?:Bits|TV)', 'FLTTH', 'Ao', 'PbK', 'MGs', 'iLove(?:HD|TV)'],
|
||||
"piggo": ['PiGo(?:NF|(?:H|WE)B)'],
|
||||
"ptchina": [],
|
||||
"pterclub": ['PTer(?:|DIY|Game|(?:M|T)V|WEB)'],
|
||||
"pthome": ['PTH(?:|Audio|eBook|music|ome|tv|WEB)'],
|
||||
"ptmsg": [],
|
||||
"ptsbao": ['PTsbao', 'OPS', 'F(?:Fans(?:AIeNcE|BD|D(?:VD|IY)|TV|WEB)|HDMv)', 'SGXT'],
|
||||
"pttime": [],
|
||||
"putao": ['PuTao'],
|
||||
"soulvoice": [],
|
||||
"springsunday": ['CMCT(?:|V)'],
|
||||
"sharkpt": ['Shark(?:|WEB|DIY|TV|MV)'],
|
||||
"tccf": [],
|
||||
"tjupt": ['TJUPT'],
|
||||
"totheglory": ['TTG', 'WiKi', 'NGB', 'DoA', '(?:ARi|ExRE)N'],
|
||||
"U2": [],
|
||||
"ultrahd": [],
|
||||
"others": ['B(?:MDru|eyondHD|TN)', 'C(?:fandora|trlhd|MRG)', 'DON', 'EVO', 'FLUX', 'HONE(?:|yG)',
|
||||
'N(?:oGroup|T(?:b|G))', 'PandaMoon', 'SMURF', 'T(?:EPES|aengoo|rollHD )'],
|
||||
"anime": ['ANi', 'HYSUB', 'KTXP', 'LoliHouse', 'MCE', 'Nekomoe kissaten', '(?:Lilith|NC)-Raws', '织梦字幕组']
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
release_groups = []
|
||||
for site_groups in self.RELEASE_GROUPS.values():
|
||||
for release_group in site_groups:
|
||||
release_groups.append(release_group)
|
||||
self.__release_groups = '|'.join(release_groups)
|
||||
|
||||
def match(self, title: str = None, groups: str = None):
|
||||
"""
|
||||
:param title: 资源标题或文件名
|
||||
:param groups: 制作组/字幕组
|
||||
:return: 匹配结果
|
||||
"""
|
||||
if not title:
|
||||
return ""
|
||||
if not groups:
|
||||
if self.custom_release_groups:
|
||||
groups = f"{self.__release_groups}|{self.custom_release_groups}"
|
||||
else:
|
||||
groups = self.__release_groups
|
||||
title = f"{title} "
|
||||
groups_re = re.compile(r"(?<=[-@\[£【&])(?:%s)(?=[@.\s\]\[】&])" % groups, re.I)
|
||||
# 处理一个制作组识别多次的情况,保留顺序
|
||||
unique_groups = []
|
||||
for item in re.findall(groups_re, title):
|
||||
if item not in unique_groups:
|
||||
unique_groups.append(item)
|
||||
separator = self.custom_separator or "@"
|
||||
return separator.join(unique_groups)
|
||||
|
||||
def update_custom(self, release_groups: str = None, separator: str = None):
|
||||
"""
|
||||
更新自定义制作组/字幕组,自定义分隔符
|
||||
"""
|
||||
self.custom_release_groups = release_groups
|
||||
self.custom_separator = separator
|
43
app/core/meta_info.py
Normal file
43
app/core/meta_info.py
Normal file
@ -0,0 +1,43 @@
|
||||
from pathlib import Path
|
||||
|
||||
import regex as re
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.meta import MetaAnime, MetaVideo
|
||||
|
||||
|
||||
def MetaInfo(title: str, subtitle: str = None):
|
||||
"""
|
||||
媒体整理入口,根据名称和副标题,判断是哪种类型的识别,返回对应对象
|
||||
:param title: 标题、种子名、文件名
|
||||
:param subtitle: 副标题、描述
|
||||
:return: MetaAnime、MetaVideo
|
||||
"""
|
||||
|
||||
# 判断是否处理文件
|
||||
if title and Path(title).suffix.lower() in settings.RMT_MEDIAEXT:
|
||||
isfile = True
|
||||
else:
|
||||
isfile = False
|
||||
|
||||
return MetaAnime(title, subtitle, isfile) if is_anime(title) else MetaVideo(title, subtitle, isfile)
|
||||
|
||||
|
||||
def is_anime(name: str):
|
||||
"""
|
||||
判断是否为动漫
|
||||
:param name: 名称
|
||||
:return: 是否动漫
|
||||
"""
|
||||
if not name:
|
||||
return False
|
||||
if re.search(r'【[+0-9XVPI-]+】\s*【', name, re.IGNORECASE):
|
||||
return True
|
||||
if re.search(r'\s+-\s+[\dv]{1,4}\s+', name, re.IGNORECASE):
|
||||
return True
|
||||
if re.search(r"S\d{2}\s*-\s*S\d{2}|S\d{2}|\s+S\d{1,2}|EP?\d{2,4}\s*-\s*EP?\d{2,4}|EP?\d{2,4}|\s+EP?\d{1,4}", name,
|
||||
re.IGNORECASE):
|
||||
return False
|
||||
if re.search(r'\[[+0-9XVPI-]+]\s*\[', name, re.IGNORECASE):
|
||||
return True
|
||||
return False
|
72
app/core/module_manager.py
Normal file
72
app/core/module_manager.py
Normal file
@ -0,0 +1,72 @@
|
||||
from types import FunctionType
|
||||
from typing import Generator, Optional
|
||||
|
||||
from app.core import settings
|
||||
from app.helper import ModuleHelper
|
||||
from app.log import logger
|
||||
from app.utils.singleton import Singleton
|
||||
|
||||
|
||||
class ModuleManager(metaclass=Singleton):
|
||||
"""
|
||||
模块管理器
|
||||
"""
|
||||
|
||||
# 模块列表
|
||||
_modules: dict = {}
|
||||
# 运行态模块列表
|
||||
_running_modules: dict = {}
|
||||
|
||||
def __init__(self):
|
||||
self.load_modules()
|
||||
|
||||
def load_modules(self):
|
||||
"""
|
||||
加载所有模块
|
||||
"""
|
||||
# 扫描模块目录
|
||||
modules = ModuleHelper.load(
|
||||
"app.modules",
|
||||
filter_func=lambda _, obj: hasattr(obj, 'init_module') and hasattr(obj, 'init_setting')
|
||||
)
|
||||
self._running_modules = {}
|
||||
self._modules = {}
|
||||
for module in modules:
|
||||
module_id = module.__name__
|
||||
self._modules[module_id] = module
|
||||
# 生成实例
|
||||
self._running_modules[module_id] = module()
|
||||
self._running_modules[module_id].init_module()
|
||||
logger.info(f"Moudle Loaded:{module_id}")
|
||||
|
||||
def get_modules(self, method: str) -> Generator:
|
||||
"""
|
||||
获取模块列表
|
||||
"""
|
||||
|
||||
def check_method(func: FunctionType) -> bool:
|
||||
"""
|
||||
检查函数是否已实现
|
||||
"""
|
||||
return func.__code__.co_code != b'd\x01S\x00'
|
||||
|
||||
def check_setting(setting: Optional[tuple]) -> bool:
|
||||
"""
|
||||
检查开关是否己打开
|
||||
"""
|
||||
if not setting:
|
||||
return True
|
||||
switch, value = setting
|
||||
if getattr(settings, switch) and value is True:
|
||||
return True
|
||||
if getattr(settings, switch) == value:
|
||||
return True
|
||||
return False
|
||||
|
||||
if not self._running_modules:
|
||||
return []
|
||||
for _, module in self._running_modules.items():
|
||||
if hasattr(module, method) \
|
||||
and check_method(getattr(module, method)) \
|
||||
and check_setting(module.init_setting()):
|
||||
yield module
|
302
app/core/plugin_manager.py
Normal file
302
app/core/plugin_manager.py
Normal file
@ -0,0 +1,302 @@
|
||||
import traceback
|
||||
from threading import Thread
|
||||
from typing import Tuple, Optional, List, Any
|
||||
|
||||
from app.helper import ModuleHelper
|
||||
|
||||
from app.core import EventManager
|
||||
from app.db.systemconfigs import SystemConfigs
|
||||
from app.log import logger
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.types import SystemConfigKey
|
||||
|
||||
|
||||
class PluginManager(metaclass=Singleton):
|
||||
"""
|
||||
插件管理器
|
||||
"""
|
||||
systemconfigs: SystemConfigs = None
|
||||
eventmanager: EventManager = None
|
||||
|
||||
# 插件列表
|
||||
_plugins: dict = {}
|
||||
# 运行态插件列表
|
||||
_running_plugins: dict = {}
|
||||
# 配置Key
|
||||
_config_key: str = "plugin.%s"
|
||||
# 事件处理线程
|
||||
_thread: Thread = None
|
||||
# 开关
|
||||
_active: bool = False
|
||||
|
||||
def __init__(self):
|
||||
self.init_config()
|
||||
|
||||
def init_config(self):
|
||||
self.systemconfigs = SystemConfigs()
|
||||
self.eventmanager = EventManager()
|
||||
# 停止已有插件
|
||||
self.stop_service()
|
||||
# 启动插件
|
||||
self.start_service()
|
||||
|
||||
def __run(self):
|
||||
"""
|
||||
事件处理线程
|
||||
"""
|
||||
while self._active:
|
||||
event, handlers = self.eventmanager.get_event()
|
||||
if event:
|
||||
logger.info(f"处理事件:{event.event_type} - {handlers}")
|
||||
for handler in handlers:
|
||||
try:
|
||||
names = handler.__qualname__.split(".")
|
||||
self.run_plugin_method(names[0], names[1], event)
|
||||
except Exception as e:
|
||||
logger.error(f"事件处理出错:{str(e)} - {traceback.format_exc()}")
|
||||
|
||||
def start_service(self):
|
||||
"""
|
||||
启动
|
||||
"""
|
||||
# 加载插件
|
||||
self.__load_plugins()
|
||||
|
||||
# 将事件管理器设为启动
|
||||
self._active = True
|
||||
self._thread = Thread(target=self.__run)
|
||||
# 启动事件处理线程
|
||||
self._thread.start()
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
停止
|
||||
"""
|
||||
# 将事件管理器设为停止
|
||||
self._active = False
|
||||
# 等待事件处理线程退出
|
||||
if self._thread:
|
||||
self._thread.join()
|
||||
# 停止所有插件
|
||||
self.__stop_plugins()
|
||||
|
||||
def __load_plugins(self):
|
||||
"""
|
||||
加载所有插件
|
||||
"""
|
||||
# 扫描插件目录
|
||||
plugins = ModuleHelper.load(
|
||||
"app.plugins",
|
||||
filter_func=lambda _, obj: hasattr(obj, 'init_plugin')
|
||||
)
|
||||
# 排序
|
||||
plugins.sort(key=lambda x: x.plugin_order if hasattr(x, "plugin_order") else 0)
|
||||
# 用户已安装插件列表
|
||||
user_plugins = self.systemconfigs.get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
self._running_plugins = {}
|
||||
self._plugins = {}
|
||||
for plugin in plugins:
|
||||
plugin_id = plugin.__name__
|
||||
self._plugins[plugin_id] = plugin
|
||||
# 未安装的跳过加载
|
||||
if plugin_id not in user_plugins:
|
||||
continue
|
||||
# 生成实例
|
||||
self._running_plugins[plugin_id] = plugin()
|
||||
# 初始化配置
|
||||
self.reload_plugin(plugin_id)
|
||||
logger.info(f"加载插件:{plugin}")
|
||||
|
||||
def reload_plugin(self, pid: str):
|
||||
"""
|
||||
生效插件配置
|
||||
"""
|
||||
if not pid:
|
||||
return
|
||||
if not self._running_plugins.get(pid):
|
||||
return
|
||||
if hasattr(self._running_plugins[pid], "init_plugin"):
|
||||
try:
|
||||
self._running_plugins[pid].init_plugin(self.get_plugin_config(pid))
|
||||
logger.debug(f"生效插件配置:{pid}")
|
||||
except Exception as err:
|
||||
logger.error(f"加载插件 {pid} 出错:{err} - {traceback.format_exc()}")
|
||||
|
||||
def __stop_plugins(self):
|
||||
"""
|
||||
停止所有插件
|
||||
"""
|
||||
for plugin in self._running_plugins.values():
|
||||
if hasattr(plugin, "stop_service"):
|
||||
plugin.stop_service()
|
||||
|
||||
def get_plugin_config(self, pid: str) -> dict:
|
||||
"""
|
||||
获取插件配置
|
||||
"""
|
||||
if not self._plugins.get(pid):
|
||||
return {}
|
||||
return self.systemconfigs.get(self._config_key % pid) or {}
|
||||
|
||||
def get_plugin_page(self, pid: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
||||
"""
|
||||
获取插件额外页面数据
|
||||
:return: 标题,页面内容,确定按钮响应函数
|
||||
"""
|
||||
if not self._running_plugins.get(pid):
|
||||
return None, None, None
|
||||
if not hasattr(self._running_plugins[pid], "get_page"):
|
||||
return None, None, None
|
||||
return self._running_plugins[pid].get_page()
|
||||
|
||||
def get_plugin_script(self, pid: str) -> Optional[str]:
|
||||
"""
|
||||
获取插件额外脚本
|
||||
"""
|
||||
if not self._running_plugins.get(pid):
|
||||
return None
|
||||
if not hasattr(self._running_plugins[pid], "get_script"):
|
||||
return None
|
||||
return self._running_plugins[pid].get_script()
|
||||
|
||||
def get_plugin_state(self, pid: str) -> Optional[bool]:
|
||||
"""
|
||||
获取插件状态
|
||||
"""
|
||||
if not self._running_plugins.get(pid):
|
||||
return None
|
||||
if not hasattr(self._running_plugins[pid], "get_state"):
|
||||
return None
|
||||
return self._running_plugins[pid].get_state()
|
||||
|
||||
def save_plugin_config(self, pid: str, conf: dict) -> bool:
|
||||
"""
|
||||
保存插件配置
|
||||
"""
|
||||
if not self._plugins.get(pid):
|
||||
return False
|
||||
return self.systemconfigs.set(self._config_key % pid, conf)
|
||||
|
||||
@staticmethod
|
||||
def __get_plugin_color(plugin: str) -> str:
|
||||
"""
|
||||
获取插件的主题色
|
||||
"""
|
||||
if hasattr(plugin, "plugin_color") and plugin.plugin_color:
|
||||
return plugin.plugin_color
|
||||
return ""
|
||||
|
||||
def get_plugins_conf(self, auth_level: int) -> dict:
|
||||
"""
|
||||
获取所有插件配置
|
||||
"""
|
||||
all_confs = {}
|
||||
for pid, plugin in self._running_plugins.items():
|
||||
# 基本属性
|
||||
conf = {}
|
||||
# 权限
|
||||
if hasattr(plugin, "auth_level") \
|
||||
and plugin.auth_level > auth_level:
|
||||
continue
|
||||
# 名称
|
||||
if hasattr(plugin, "plugin_name"):
|
||||
conf.update({"name": plugin.plugin_name})
|
||||
# 描述
|
||||
if hasattr(plugin, "plugin_desc"):
|
||||
conf.update({"desc": plugin.plugin_desc})
|
||||
# 版本号
|
||||
if hasattr(plugin, "plugin_version"):
|
||||
conf.update({"version": plugin.plugin_version})
|
||||
# 图标
|
||||
if hasattr(plugin, "plugin_icon"):
|
||||
conf.update({"icon": plugin.plugin_icon})
|
||||
# ID前缀
|
||||
if hasattr(plugin, "plugin_config_prefix"):
|
||||
conf.update({"prefix": plugin.plugin_config_prefix})
|
||||
# 插件额外的页面
|
||||
if hasattr(plugin, "get_page"):
|
||||
title, _, _ = plugin.get_page()
|
||||
conf.update({"page": title})
|
||||
# 插件额外的脚本
|
||||
if hasattr(plugin, "get_script"):
|
||||
conf.update({"script": plugin.get_script()})
|
||||
# 主题色
|
||||
conf.update({"color": self.__get_plugin_color(plugin)})
|
||||
# 配置项
|
||||
conf.update({"fields": plugin.get_fields() or {}})
|
||||
# 配置值
|
||||
conf.update({"config": self.get_plugin_config(pid)})
|
||||
# 状态
|
||||
conf.update({"state": plugin.get_state()})
|
||||
# 汇总
|
||||
all_confs[pid] = conf
|
||||
return all_confs
|
||||
|
||||
def get_plugin_apps(self, auth_level: int) -> dict:
|
||||
"""
|
||||
获取所有插件
|
||||
"""
|
||||
all_confs = {}
|
||||
installed_apps = self.systemconfigs.get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
for pid, plugin in self._plugins.items():
|
||||
# 基本属性
|
||||
conf = {}
|
||||
# 权限
|
||||
if hasattr(plugin, "auth_level") \
|
||||
and plugin.auth_level > auth_level:
|
||||
continue
|
||||
# ID
|
||||
conf.update({"id": pid})
|
||||
# 安装状态
|
||||
if pid in installed_apps:
|
||||
conf.update({"installed": True})
|
||||
else:
|
||||
conf.update({"installed": False})
|
||||
# 名称
|
||||
if hasattr(plugin, "plugin_name"):
|
||||
conf.update({"name": plugin.plugin_name})
|
||||
# 描述
|
||||
if hasattr(plugin, "plugin_desc"):
|
||||
conf.update({"desc": plugin.plugin_desc})
|
||||
# 版本
|
||||
if hasattr(plugin, "plugin_version"):
|
||||
conf.update({"version": plugin.plugin_version})
|
||||
# 图标
|
||||
if hasattr(plugin, "plugin_icon"):
|
||||
conf.update({"icon": plugin.plugin_icon})
|
||||
# 主题色
|
||||
conf.update({"color": self.__get_plugin_color(plugin)})
|
||||
if hasattr(plugin, "plugin_author"):
|
||||
conf.update({"author": plugin.plugin_author})
|
||||
# 作者链接
|
||||
if hasattr(plugin, "author_url"):
|
||||
conf.update({"author_url": plugin.author_url})
|
||||
# 汇总
|
||||
all_confs[pid] = conf
|
||||
return all_confs
|
||||
|
||||
def get_plugin_commands(self) -> List[dict]:
|
||||
"""
|
||||
获取插件命令
|
||||
[{
|
||||
"cmd": "/xx",
|
||||
"event": EventType.xx,
|
||||
"desc": "xxxx",
|
||||
"data": {}
|
||||
}]
|
||||
"""
|
||||
ret_commands = []
|
||||
for _, plugin in self._running_plugins.items():
|
||||
if hasattr(plugin, "get_command"):
|
||||
ret_commands.append(plugin.get_command())
|
||||
return ret_commands
|
||||
|
||||
def run_plugin_method(self, pid: str, method: str, *args, **kwargs) -> Any:
|
||||
"""
|
||||
运行插件方法
|
||||
"""
|
||||
if not self._running_plugins.get(pid):
|
||||
return None
|
||||
if not hasattr(self._running_plugins[pid], method):
|
||||
return None
|
||||
return getattr(self._running_plugins[pid], method)(*args, **kwargs)
|
49
app/core/security.py
Normal file
49
app/core/security.py
Normal file
@ -0,0 +1,49 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Union, Optional
|
||||
import jwt
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from passlib.context import CryptContext
|
||||
from app.core.config import settings
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
# Token认证
|
||||
reusable_oauth2 = OAuth2PasswordBearer(
|
||||
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
|
||||
)
|
||||
|
||||
|
||||
def create_access_token(
|
||||
subject: Union[str, Any], expires_delta: timedelta = None
|
||||
) -> str:
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(
|
||||
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode = {"exp": expire, "sub": str(subject)}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def decrypt(data, key) -> Optional[bytes]:
|
||||
"""
|
||||
解密二进制数据
|
||||
"""
|
||||
fernet = Fernet(key)
|
||||
try:
|
||||
return fernet.decrypt(data)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return None
|
Reference in New Issue
Block a user