mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-10 15:42:39 +08:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8902fb50d6 | ||
|
|
b6aa013eb3 | ||
|
|
034b43bf70 | ||
|
|
59e9032286 | ||
|
|
52a98efd0a | ||
|
|
90cc91aa7f | ||
|
|
1973a26e83 | ||
|
|
6519ad25ca | ||
|
|
cacfde8166 | ||
|
|
df85873726 | ||
|
|
dfea294cc9 | ||
|
|
d35b855404 | ||
|
|
7a1cbf70e3 |
@@ -560,6 +560,15 @@ def popular_subscribes(
|
||||
return SubscribeHelper().get_shares(name=name, page=page, count=count)
|
||||
|
||||
|
||||
@router.get("/share/statistics", summary="查询订阅分享统计", response_model=List[schemas.SubscribeShareStatistics])
|
||||
def subscribe_share_statistics(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询订阅分享统计
|
||||
返回每个分享人分享的媒体数量以及总的复用人次
|
||||
"""
|
||||
return SubscribeHelper().get_share_statistics()
|
||||
|
||||
|
||||
@router.get("/{subscribe_id}", summary="订阅详情", response_model=schemas.Subscribe)
|
||||
def read_subscribe(
|
||||
subscribe_id: int,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import secrets
|
||||
import sys
|
||||
@@ -12,9 +13,10 @@ from dotenv import set_key
|
||||
from pydantic import BaseModel, BaseSettings, validator, Field
|
||||
|
||||
from app.log import logger, log_settings, LogConfigModel
|
||||
from app.schemas import MediaType
|
||||
from app.utils.system import SystemUtils
|
||||
from app.utils.url import UrlUtils
|
||||
from app.schemas import MediaType
|
||||
from version import APP_VERSION
|
||||
|
||||
|
||||
class SystemConfModel(BaseModel):
|
||||
@@ -233,8 +235,6 @@ class ConfigModel(BaseModel):
|
||||
COOKIECLOUD_INTERVAL: Optional[int] = 60 * 24
|
||||
# CookieCloud同步黑名单,多个域名,分割
|
||||
COOKIECLOUD_BLACKLIST: Optional[str] = None
|
||||
# CookieCloud对应的浏览器UA
|
||||
USER_AGENT: str = "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"
|
||||
# 电影重命名格式
|
||||
MOVIE_RENAME_FORMAT: str = "{{title}}{% if year %} ({{year}}){% endif %}" \
|
||||
"/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}" \
|
||||
@@ -510,6 +510,13 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
||||
"""
|
||||
return "v2"
|
||||
|
||||
@property
|
||||
def USER_AGENT(self) -> str:
|
||||
"""
|
||||
全局用户代理字符串
|
||||
"""
|
||||
return f"{self.PROJECT_NAME}/{APP_VERSION[1:]} ({platform.system()} {platform.release()}; {SystemUtils.cpu_arch()})"
|
||||
|
||||
@property
|
||||
def INNER_CONFIG_PATH(self):
|
||||
return self.ROOT_PATH / "config"
|
||||
|
||||
@@ -474,7 +474,16 @@ class MediaInfo:
|
||||
self.names = info.get('names') or []
|
||||
# 剩余属性赋值
|
||||
for key, value in info.items():
|
||||
if hasattr(self, key) and getattr(self, key) is None:
|
||||
if not value:
|
||||
continue
|
||||
if not hasattr(self, key):
|
||||
continue
|
||||
current_value = getattr(self, key)
|
||||
if current_value:
|
||||
continue
|
||||
if current_value is None:
|
||||
setattr(self, key, value)
|
||||
elif type(current_value) == type(value):
|
||||
setattr(self, key, value)
|
||||
|
||||
def set_douban_info(self, info: dict):
|
||||
@@ -606,7 +615,16 @@ class MediaInfo:
|
||||
self.production_countries = [{"id": country, "name": country} for country in info.get("countries") or []]
|
||||
# 剩余属性赋值
|
||||
for key, value in info.items():
|
||||
if hasattr(self, key) and getattr(self, key) is None:
|
||||
if not value:
|
||||
continue
|
||||
if not hasattr(self, key):
|
||||
continue
|
||||
current_value = getattr(self, key)
|
||||
if current_value:
|
||||
continue
|
||||
if current_value is None:
|
||||
setattr(self, key, value)
|
||||
elif type(current_value) == type(value):
|
||||
setattr(self, key, value)
|
||||
|
||||
def set_bangumi_info(self, info: dict):
|
||||
|
||||
@@ -29,6 +29,8 @@ class SubscribeHelper(metaclass=WeakSingleton):
|
||||
|
||||
_sub_shares = f"{settings.MP_SERVER_HOST}/subscribe/shares"
|
||||
|
||||
_sub_share_statistic = f"{settings.MP_SERVER_HOST}/subscribe/share/statistics"
|
||||
|
||||
_sub_fork = f"{settings.MP_SERVER_HOST}/subscribe/fork/%s"
|
||||
|
||||
_shares_cache_region = "subscribe_share"
|
||||
@@ -215,6 +217,18 @@ class SubscribeHelper(metaclass=WeakSingleton):
|
||||
return res.json()
|
||||
return []
|
||||
|
||||
@cached(maxsize=1, ttl=1800)
|
||||
def get_share_statistics(self) -> List[dict]:
|
||||
"""
|
||||
获取订阅分享统计数据
|
||||
"""
|
||||
if not settings.SUBSCRIBE_STATISTIC_SHARE:
|
||||
return []
|
||||
res = RequestUtils(proxies=settings.PROXY, timeout=15).get_res(self._sub_share_statistic)
|
||||
if res and res.status_code == 200:
|
||||
return res.json()
|
||||
return []
|
||||
|
||||
def get_user_uuid(self) -> str:
|
||||
"""
|
||||
获取用户uuid
|
||||
|
||||
@@ -138,6 +138,15 @@ class SubscribeShare(BaseModel):
|
||||
count: Optional[int] = 0
|
||||
|
||||
|
||||
class SubscribeShareStatistics(BaseModel):
|
||||
# 分享人
|
||||
share_user: Optional[str] = None
|
||||
# 分享数量
|
||||
share_count: Optional[int] = 0
|
||||
# 总复用人次
|
||||
total_reuse_count: Optional[int] = 0
|
||||
|
||||
|
||||
class SubscribeDownloadFileInfo(BaseModel):
|
||||
# 种子名称
|
||||
torrent_title: Optional[str] = None
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import sys
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
import chardet
|
||||
@@ -8,6 +10,7 @@ import urllib3
|
||||
from requests import Response, Session
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
|
||||
urllib3.disable_warnings(InsecureRequestWarning)
|
||||
@@ -86,6 +89,7 @@ class AutoCloseResponse:
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
|
||||
class RequestUtils:
|
||||
|
||||
def __init__(self,
|
||||
@@ -106,6 +110,10 @@ class RequestUtils:
|
||||
if headers:
|
||||
self._headers = headers
|
||||
else:
|
||||
if ua and ua == settings.USER_AGENT:
|
||||
caller_name = self.__get_caller()
|
||||
if caller_name:
|
||||
ua = f"{settings.USER_AGENT} Plugin/{caller_name}"
|
||||
self._headers = {
|
||||
"User-Agent": ua,
|
||||
"Content-Type": content_type,
|
||||
@@ -120,6 +128,43 @@ class RequestUtils:
|
||||
else:
|
||||
self._cookies = None
|
||||
|
||||
@staticmethod
|
||||
def __get_caller():
|
||||
"""
|
||||
获取调用者的名称,识别是否为插件调用
|
||||
"""
|
||||
# 调用者名称
|
||||
caller_name = None
|
||||
|
||||
try:
|
||||
frame = sys._getframe(3) # noqa
|
||||
except (AttributeError, ValueError):
|
||||
return None
|
||||
|
||||
while frame:
|
||||
filepath = Path(frame.f_code.co_filename)
|
||||
parts = filepath.parts
|
||||
if "app" in parts:
|
||||
if not caller_name and "plugins" in parts:
|
||||
try:
|
||||
plugins_index = parts.index("plugins")
|
||||
if plugins_index + 1 < len(parts):
|
||||
plugin_candidate = parts[plugins_index + 1]
|
||||
if plugin_candidate != "__init__.py":
|
||||
caller_name = plugin_candidate
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
if "main.py" in parts:
|
||||
break
|
||||
elif len(parts) != 1:
|
||||
break
|
||||
try:
|
||||
frame = frame.f_back
|
||||
except AttributeError:
|
||||
break
|
||||
return caller_name
|
||||
|
||||
def request(self, method: str, url: str, raise_exception: bool = False, **kwargs) -> Optional[Response]:
|
||||
"""
|
||||
发起HTTP请求
|
||||
|
||||
@@ -68,35 +68,57 @@ class SystemUtils:
|
||||
"""
|
||||
if SystemUtils.is_windows():
|
||||
return False
|
||||
return True if "synology" in SystemUtils.execute('uname -a') else False
|
||||
return "synology" in SystemUtils.execute('uname -a')
|
||||
|
||||
@staticmethod
|
||||
def is_windows() -> bool:
|
||||
"""
|
||||
判断是否为Windows系统
|
||||
"""
|
||||
return True if os.name == "nt" else False
|
||||
return os.name == "nt"
|
||||
|
||||
@staticmethod
|
||||
def is_frozen() -> bool:
|
||||
"""
|
||||
判断是否为冻结的二进制文件
|
||||
"""
|
||||
return True if getattr(sys, 'frozen', False) else False
|
||||
return getattr(sys, 'frozen', False)
|
||||
|
||||
@staticmethod
|
||||
def is_macos() -> bool:
|
||||
"""
|
||||
判断是否为MacOS系统
|
||||
"""
|
||||
return True if platform.system() == 'Darwin' else False
|
||||
return platform.system() == 'Darwin'
|
||||
|
||||
@staticmethod
|
||||
def is_aarch64() -> bool:
|
||||
"""
|
||||
判断是否为ARM64架构
|
||||
"""
|
||||
return True if platform.machine() == 'aarch64' else False
|
||||
return platform.machine().lower() in ('aarch64', 'arm64')
|
||||
|
||||
@staticmethod
|
||||
def is_aarch() -> bool:
|
||||
"""
|
||||
判断是否为ARM32架构
|
||||
"""
|
||||
arch_name = platform.machine().lower()
|
||||
return arch_name.startswith(('arm', 'aarch')) and arch_name not in ('aarch64', 'arm64')
|
||||
|
||||
@staticmethod
|
||||
def is_x86_64() -> bool:
|
||||
"""
|
||||
判断是否为AMD64架构
|
||||
"""
|
||||
return platform.machine().lower() in ('amd64', 'x86_64')
|
||||
|
||||
@staticmethod
|
||||
def is_x86_32() -> bool:
|
||||
"""
|
||||
判断是否为AMD32架构
|
||||
"""
|
||||
return platform.machine().lower() in ('i386', 'i686', 'x86', '386', 'x86_32')
|
||||
|
||||
@staticmethod
|
||||
def platform() -> str:
|
||||
@@ -112,6 +134,22 @@ class SystemUtils:
|
||||
else:
|
||||
return "Linux"
|
||||
|
||||
@staticmethod
|
||||
def cpu_arch() -> str:
|
||||
"""
|
||||
获取CPU架构
|
||||
"""
|
||||
if SystemUtils.is_x86_64():
|
||||
return "x86_64"
|
||||
elif SystemUtils.is_x86_32():
|
||||
return "x86_32"
|
||||
elif SystemUtils.is_aarch64():
|
||||
return "Arm64"
|
||||
elif SystemUtils.is_aarch():
|
||||
return "Arm32"
|
||||
else:
|
||||
return platform.machine()
|
||||
|
||||
@staticmethod
|
||||
def copy(src: Path, dest: Path) -> Tuple[int, str]:
|
||||
"""
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = 'v2.6.5'
|
||||
FRONTEND_VERSION = 'v2.6.5'
|
||||
APP_VERSION = 'v2.6.6'
|
||||
FRONTEND_VERSION = 'v2.6.6'
|
||||
|
||||
Reference in New Issue
Block a user