mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-11 18:10:15 +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)
|
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)
|
@router.get("/{subscribe_id}", summary="订阅详情", response_model=schemas.Subscribe)
|
||||||
def read_subscribe(
|
def read_subscribe(
|
||||||
subscribe_id: int,
|
subscribe_id: int,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import sys
|
import sys
|
||||||
@@ -12,9 +13,10 @@ from dotenv import set_key
|
|||||||
from pydantic import BaseModel, BaseSettings, validator, Field
|
from pydantic import BaseModel, BaseSettings, validator, Field
|
||||||
|
|
||||||
from app.log import logger, log_settings, LogConfigModel
|
from app.log import logger, log_settings, LogConfigModel
|
||||||
|
from app.schemas import MediaType
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
from app.utils.url import UrlUtils
|
from app.utils.url import UrlUtils
|
||||||
from app.schemas import MediaType
|
from version import APP_VERSION
|
||||||
|
|
||||||
|
|
||||||
class SystemConfModel(BaseModel):
|
class SystemConfModel(BaseModel):
|
||||||
@@ -233,8 +235,6 @@ class ConfigModel(BaseModel):
|
|||||||
COOKIECLOUD_INTERVAL: Optional[int] = 60 * 24
|
COOKIECLOUD_INTERVAL: Optional[int] = 60 * 24
|
||||||
# CookieCloud同步黑名单,多个域名,分割
|
# CookieCloud同步黑名单,多个域名,分割
|
||||||
COOKIECLOUD_BLACKLIST: Optional[str] = None
|
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 %}" \
|
MOVIE_RENAME_FORMAT: str = "{{title}}{% if year %} ({{year}}){% endif %}" \
|
||||||
"/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% 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"
|
return "v2"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def USER_AGENT(self) -> str:
|
||||||
|
"""
|
||||||
|
全局用户代理字符串
|
||||||
|
"""
|
||||||
|
return f"{self.PROJECT_NAME}/{APP_VERSION[1:]} ({platform.system()} {platform.release()}; {SystemUtils.cpu_arch()})"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def INNER_CONFIG_PATH(self):
|
def INNER_CONFIG_PATH(self):
|
||||||
return self.ROOT_PATH / "config"
|
return self.ROOT_PATH / "config"
|
||||||
|
|||||||
@@ -474,7 +474,16 @@ class MediaInfo:
|
|||||||
self.names = info.get('names') or []
|
self.names = info.get('names') or []
|
||||||
# 剩余属性赋值
|
# 剩余属性赋值
|
||||||
for key, value in info.items():
|
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)
|
setattr(self, key, value)
|
||||||
|
|
||||||
def set_douban_info(self, info: dict):
|
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 []]
|
self.production_countries = [{"id": country, "name": country} for country in info.get("countries") or []]
|
||||||
# 剩余属性赋值
|
# 剩余属性赋值
|
||||||
for key, value in info.items():
|
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)
|
setattr(self, key, value)
|
||||||
|
|
||||||
def set_bangumi_info(self, info: dict):
|
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_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"
|
_sub_fork = f"{settings.MP_SERVER_HOST}/subscribe/fork/%s"
|
||||||
|
|
||||||
_shares_cache_region = "subscribe_share"
|
_shares_cache_region = "subscribe_share"
|
||||||
@@ -215,6 +217,18 @@ class SubscribeHelper(metaclass=WeakSingleton):
|
|||||||
return res.json()
|
return res.json()
|
||||||
return []
|
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:
|
def get_user_uuid(self) -> str:
|
||||||
"""
|
"""
|
||||||
获取用户uuid
|
获取用户uuid
|
||||||
|
|||||||
@@ -138,6 +138,15 @@ class SubscribeShare(BaseModel):
|
|||||||
count: Optional[int] = 0
|
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):
|
class SubscribeDownloadFileInfo(BaseModel):
|
||||||
# 种子名称
|
# 种子名称
|
||||||
torrent_title: Optional[str] = None
|
torrent_title: Optional[str] = None
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import sys
|
||||||
import re
|
import re
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
import chardet
|
import chardet
|
||||||
@@ -8,6 +10,7 @@ import urllib3
|
|||||||
from requests import Response, Session
|
from requests import Response, Session
|
||||||
from urllib3.exceptions import InsecureRequestWarning
|
from urllib3.exceptions import InsecureRequestWarning
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
|
|
||||||
urllib3.disable_warnings(InsecureRequestWarning)
|
urllib3.disable_warnings(InsecureRequestWarning)
|
||||||
@@ -86,6 +89,7 @@ class AutoCloseResponse:
|
|||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
class RequestUtils:
|
class RequestUtils:
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
@@ -106,6 +110,10 @@ class RequestUtils:
|
|||||||
if headers:
|
if headers:
|
||||||
self._headers = headers
|
self._headers = headers
|
||||||
else:
|
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 = {
|
self._headers = {
|
||||||
"User-Agent": ua,
|
"User-Agent": ua,
|
||||||
"Content-Type": content_type,
|
"Content-Type": content_type,
|
||||||
@@ -120,6 +128,43 @@ class RequestUtils:
|
|||||||
else:
|
else:
|
||||||
self._cookies = None
|
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]:
|
def request(self, method: str, url: str, raise_exception: bool = False, **kwargs) -> Optional[Response]:
|
||||||
"""
|
"""
|
||||||
发起HTTP请求
|
发起HTTP请求
|
||||||
|
|||||||
@@ -68,35 +68,57 @@ class SystemUtils:
|
|||||||
"""
|
"""
|
||||||
if SystemUtils.is_windows():
|
if SystemUtils.is_windows():
|
||||||
return False
|
return False
|
||||||
return True if "synology" in SystemUtils.execute('uname -a') else False
|
return "synology" in SystemUtils.execute('uname -a')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_windows() -> bool:
|
def is_windows() -> bool:
|
||||||
"""
|
"""
|
||||||
判断是否为Windows系统
|
判断是否为Windows系统
|
||||||
"""
|
"""
|
||||||
return True if os.name == "nt" else False
|
return os.name == "nt"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_frozen() -> bool:
|
def is_frozen() -> bool:
|
||||||
"""
|
"""
|
||||||
判断是否为冻结的二进制文件
|
判断是否为冻结的二进制文件
|
||||||
"""
|
"""
|
||||||
return True if getattr(sys, 'frozen', False) else False
|
return getattr(sys, 'frozen', False)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_macos() -> bool:
|
def is_macos() -> bool:
|
||||||
"""
|
"""
|
||||||
判断是否为MacOS系统
|
判断是否为MacOS系统
|
||||||
"""
|
"""
|
||||||
return True if platform.system() == 'Darwin' else False
|
return platform.system() == 'Darwin'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_aarch64() -> bool:
|
def is_aarch64() -> bool:
|
||||||
"""
|
"""
|
||||||
判断是否为ARM64架构
|
判断是否为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
|
@staticmethod
|
||||||
def platform() -> str:
|
def platform() -> str:
|
||||||
@@ -112,6 +134,22 @@ class SystemUtils:
|
|||||||
else:
|
else:
|
||||||
return "Linux"
|
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
|
@staticmethod
|
||||||
def copy(src: Path, dest: Path) -> Tuple[int, str]:
|
def copy(src: Path, dest: Path) -> Tuple[int, str]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
APP_VERSION = 'v2.6.5'
|
APP_VERSION = 'v2.6.6'
|
||||||
FRONTEND_VERSION = 'v2.6.5'
|
FRONTEND_VERSION = 'v2.6.6'
|
||||||
|
|||||||
Reference in New Issue
Block a user