Compare commits

...

13 Commits

Author SHA1 Message Date
jxxghp
8902fb50d6 更新 context.py 2025-07-16 22:22:45 +08:00
jxxghp
b6aa013eb3 v2.6.6 2025-07-16 20:25:43 +08:00
jxxghp
034b43bf70 fix context 2025-07-16 19:59:06 +08:00
jxxghp
59e9032286 add subscribe share statistic api 2025-07-16 08:47:54 +08:00
jxxghp
52a98efd0a add subscribe share statistic api 2025-07-16 08:31:28 +08:00
jxxghp
90cc91aa7f Merge pull request #4614 from Aqr-K/feature-ua 2025-07-15 06:47:34 +08:00
Aqr-K
1973a26e83 fix: 去除冗余代码,简化写法 2025-07-14 22:19:48 +08:00
Aqr-K
6519ad25ca fix is_aarch 2025-07-14 22:17:04 +08:00
Aqr-K
cacfde8166 fix 2025-07-14 22:14:52 +08:00
Aqr-K
df85873726 feat(ua): add cup_arch , USER_AGENT value add cup_arch 2025-07-14 22:04:09 +08:00
jxxghp
dfea294cc9 fix ua 2025-07-14 13:42:49 +08:00
jxxghp
d35b855404 fix ua 2025-07-14 13:30:18 +08:00
jxxghp
7a1cbf70e3 feat:特定默认UA 2025-07-14 12:35:08 +08:00
8 changed files with 152 additions and 12 deletions

View File

@@ -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,

View File

@@ -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"

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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请求

View File

@@ -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]:
""" """

View File

@@ -1,2 +1,2 @@
APP_VERSION = 'v2.6.5' APP_VERSION = 'v2.6.6'
FRONTEND_VERSION = 'v2.6.5' FRONTEND_VERSION = 'v2.6.6'