mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-08 09:13:15 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcd235521e | ||
|
|
31a2eac302 | ||
|
|
7e6b7e5dd5 | ||
|
|
9ec9f48425 | ||
|
|
a3bec43eab | ||
|
|
f429b6397e | ||
|
|
9d6e7dc288 | ||
|
|
a27c09c1e8 | ||
|
|
ceb0697c73 | ||
|
|
6ad6a08bf1 | ||
|
|
fac6ad7116 | ||
|
|
7d8cda0457 | ||
|
|
33fc3fd63b | ||
|
|
8d39cc87f7 | ||
|
|
d0b1348c96 | ||
|
|
0afc38f6b8 | ||
|
|
264896ba17 | ||
|
|
08decf0b82 | ||
|
|
98381265e6 | ||
|
|
d323159719 | ||
|
|
7ef21e1d1c | ||
|
|
2d6b2ab7d7 |
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.4-slim-bookworm
|
||||
FROM python:3.12.8-slim-bookworm
|
||||
ENV LANG="C.UTF-8" \
|
||||
TZ="Asia/Shanghai" \
|
||||
HOME="/moviepilot" \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.4-slim-bookworm
|
||||
FROM python:3.12.8-slim-bookworm
|
||||
ENV LANG="C.UTF-8" \
|
||||
TZ="Asia/Shanghai" \
|
||||
HOME="/moviepilot" \
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
## 参与开发
|
||||
|
||||
需要 `Python 3.11`、`Node JS v20.12.1`
|
||||
需要 `Python 3.12`、`Node JS v20.12.1`
|
||||
|
||||
- 克隆主项目 [MoviePilot](https://github.com/jxxghp/MoviePilot)
|
||||
```shell
|
||||
|
||||
@@ -6,8 +6,8 @@ from app import schemas
|
||||
from app.core.event import eventmanager
|
||||
from app.core.security import verify_token
|
||||
from app.schemas.types import ChainEventType
|
||||
from chain.recommend import RecommendChain
|
||||
from schemas import RecommendSourceEventData
|
||||
from app.chain.recommend import RecommendChain
|
||||
from app.schemas import RecommendSourceEventData
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -75,23 +75,12 @@ def create_subscribe(
|
||||
title = subscribe_in.name
|
||||
else:
|
||||
title = None
|
||||
# 订阅用户
|
||||
subscribe_in.username = current_user.name
|
||||
sid, message = SubscribeChain().add(mtype=mtype,
|
||||
title=title,
|
||||
year=subscribe_in.year,
|
||||
tmdbid=subscribe_in.tmdbid,
|
||||
season=subscribe_in.season,
|
||||
doubanid=subscribe_in.doubanid,
|
||||
bangumiid=subscribe_in.bangumiid,
|
||||
mediaid=subscribe_in.mediaid,
|
||||
episode_group=subscribe_in.episode_group,
|
||||
username=current_user.name,
|
||||
best_version=subscribe_in.best_version,
|
||||
save_path=subscribe_in.save_path,
|
||||
search_imdbid=subscribe_in.search_imdbid,
|
||||
custom_words=subscribe_in.custom_words,
|
||||
media_category=subscribe_in.media_category,
|
||||
filter_groups=subscribe_in.filter_groups,
|
||||
exist_ok=True)
|
||||
exist_ok=True,
|
||||
**subscribe_in.dict())
|
||||
return schemas.Response(
|
||||
success=bool(sid), message=message, data={"id": sid}
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ from app.helper.message import MessageHelper, MessageQueueManager
|
||||
from app.helper.progress import ProgressHelper
|
||||
from app.helper.rule import RuleHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.helper.subscribe import SubscribeHelper
|
||||
from app.log import logger
|
||||
from app.monitor import Monitor
|
||||
from app.scheduler import Scheduler
|
||||
@@ -179,9 +180,10 @@ def get_global_setting():
|
||||
exclude={"SECRET_KEY", "RESOURCE_SECRET_KEY", "API_TOKEN", "TMDB_API_KEY", "TVDB_API_KEY", "FANART_API_KEY",
|
||||
"COOKIECLOUD_KEY", "COOKIECLOUD_PASSWORD", "GITHUB_TOKEN", "REPO_GITHUB_TOKEN"}
|
||||
)
|
||||
# 追加用户唯一ID
|
||||
# 追加用户唯一ID和订阅分享管理权限
|
||||
info.update({
|
||||
"USER_UNIQUE_ID": SystemUtils.generate_user_unique_id()
|
||||
"USER_UNIQUE_ID": SubscribeHelper().get_user_uuid(),
|
||||
"SUBSCRIBE_SHARE_MANAGE": SubscribeHelper().is_admin_user(),
|
||||
})
|
||||
return schemas.Response(success=True,
|
||||
data=info)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import base64
|
||||
import re
|
||||
from datetime import datetime
|
||||
from time import time
|
||||
from typing import Optional, Tuple, Union, Dict
|
||||
from urllib.parse import urljoin
|
||||
|
||||
@@ -178,12 +177,9 @@ class SiteChain(ChainBase):
|
||||
domain = StringUtils.get_url_domain(site.url)
|
||||
url = f"https://api.{domain}/api/member/profile"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": user_agent,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Authorization": site.token,
|
||||
"x-api-key": site.apikey,
|
||||
"ts": str(int(time()))
|
||||
}
|
||||
res = RequestUtils(
|
||||
headers=headers,
|
||||
@@ -193,27 +189,10 @@ class SiteChain(ChainBase):
|
||||
if res is None:
|
||||
return False, "无法打开网站!"
|
||||
if res.status_code == 200:
|
||||
state = False
|
||||
message = "鉴权已过期或无效"
|
||||
user_info = res.json() or {}
|
||||
if user_info.get("data"):
|
||||
# 更新最后访问时间
|
||||
del headers["x-api-key"]
|
||||
res = RequestUtils(headers=headers,
|
||||
timeout=site.timeout or 15,
|
||||
proxies=settings.PROXY if site.proxy else None,
|
||||
referer=f"{site.url}index"
|
||||
).post_res(url=f"https://api.{domain}/api/member/updateLastBrowse")
|
||||
state = True
|
||||
message = "连接成功,但更新状态失败"
|
||||
if res and res.status_code == 200:
|
||||
update_info = res.json() or {}
|
||||
if "code" in update_info and int(update_info["code"]) == 0:
|
||||
message = "连接成功"
|
||||
elif user_info.get("message"):
|
||||
# 使用馒头的错误提示
|
||||
message = user_info.get("message")
|
||||
return state, message
|
||||
return True, "连接成功"
|
||||
return False, user_info.get("message", "鉴权已过期或无效")
|
||||
else:
|
||||
return False, f"错误:{res.status_code} {res.reason}"
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
'filter_groups': self.__get_default_subscribe_config(mediainfo.type, "filter_groups") if not kwargs.get(
|
||||
"filter_groups") else kwargs.get("filter_groups")
|
||||
})
|
||||
# 操作数据库
|
||||
sid, err_msg = self.subscribeoper.add(mediainfo=mediainfo, season=season, username=username, **kwargs)
|
||||
if not sid:
|
||||
logger.error(f'{mediainfo.title_year} {err_msg}')
|
||||
|
||||
@@ -212,7 +212,8 @@ class ConfigModel(BaseModel):
|
||||
PLUGIN_MARKET: str = ("https://github.com/jxxghp/MoviePilot-Plugins,"
|
||||
"https://github.com/thsrite/MoviePilot-Plugins,"
|
||||
"https://github.com/honue/MoviePilot-Plugins,"
|
||||
"https://github.com/InfinityPacer/MoviePilot-Plugins")
|
||||
"https://github.com/InfinityPacer/MoviePilot-Plugins,"
|
||||
"https://github.com/DDS-Derek/MoviePilot-Plugins")
|
||||
# 插件安装数据共享
|
||||
PLUGIN_STATISTIC_SHARE: bool = True
|
||||
# 是否开启插件热加载
|
||||
|
||||
@@ -103,7 +103,7 @@ class ReleaseGroupsMatcher(metaclass=Singleton):
|
||||
else:
|
||||
groups = self.__release_groups
|
||||
title = f"{title} "
|
||||
groups_re = re.compile(r"(?<=[-@\[£【&])(?:%s)(?=[@.\s\]\[】&])" % groups, re.I)
|
||||
groups_re = re.compile(r"(?<=[-@\[£【&])(?:%s)(?=[@.\s\S\]\[】&])" % groups, re.I)
|
||||
# 处理一个制作组识别多次的情况,保留顺序
|
||||
unique_groups = []
|
||||
for item in re.findall(groups_re, title):
|
||||
|
||||
@@ -20,22 +20,24 @@ class SubscribeOper(DbOper):
|
||||
tmdbid=mediainfo.tmdb_id,
|
||||
doubanid=mediainfo.douban_id,
|
||||
season=kwargs.get('season'))
|
||||
kwargs.update({
|
||||
"name": mediainfo.title,
|
||||
"year": mediainfo.year,
|
||||
"type": mediainfo.type.value,
|
||||
"tmdbid": mediainfo.tmdb_id,
|
||||
"imdbid": mediainfo.imdb_id,
|
||||
"tvdbid": mediainfo.tvdb_id,
|
||||
"doubanid": mediainfo.douban_id,
|
||||
"bangumiid": mediainfo.bangumi_id,
|
||||
"episode_group": mediainfo.episode_group,
|
||||
"poster": mediainfo.get_poster_image(),
|
||||
"backdrop": mediainfo.get_backdrop_image(),
|
||||
"vote": mediainfo.vote_average,
|
||||
"description": mediainfo.overview,
|
||||
"date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
})
|
||||
if not subscribe:
|
||||
subscribe = Subscribe(name=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
type=mediainfo.type.value,
|
||||
tmdbid=mediainfo.tmdb_id,
|
||||
imdbid=mediainfo.imdb_id,
|
||||
tvdbid=mediainfo.tvdb_id,
|
||||
doubanid=mediainfo.douban_id,
|
||||
bangumiid=mediainfo.bangumi_id,
|
||||
episode_group=mediainfo.episode_group,
|
||||
poster=mediainfo.get_poster_image(),
|
||||
backdrop=mediainfo.get_backdrop_image(),
|
||||
vote=mediainfo.vote_average,
|
||||
description=mediainfo.overview,
|
||||
date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
|
||||
**kwargs)
|
||||
subscribe = Subscribe(**kwargs)
|
||||
subscribe.create(self._db)
|
||||
# 查询订阅
|
||||
subscribe = Subscribe.exists(self._db,
|
||||
|
||||
@@ -194,7 +194,6 @@ class TransferHistoryOper(DbOper):
|
||||
episodes=meta.episode,
|
||||
downloader=downloader,
|
||||
download_hash=download_hash,
|
||||
episode_group=mediainfo.episode_group,
|
||||
status=0,
|
||||
errmsg="未识别到媒体信息"
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ from app.core.cache import cached, cache_backend
|
||||
from app.core.config import settings
|
||||
from app.db.subscribe_oper import SubscribeOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.log import logger
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.singleton import Singleton
|
||||
@@ -32,13 +33,30 @@ class SubscribeHelper(metaclass=Singleton):
|
||||
|
||||
_shares_cache_region = "subscribe_share"
|
||||
|
||||
_github_user = None
|
||||
|
||||
_share_user_id = None
|
||||
|
||||
_admin_users = [
|
||||
"jxxghp",
|
||||
"thsrite",
|
||||
"InfinityPacer",
|
||||
"DDSRem",
|
||||
"Aqr-K",
|
||||
"Putarku",
|
||||
"4Nest",
|
||||
"xyswordzoro",
|
||||
"wikrin"
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.systemconfig = SystemConfigOper()
|
||||
self.share_user_id = SystemUtils.generate_user_unique_id()
|
||||
if settings.SUBSCRIBE_STATISTIC_SHARE:
|
||||
if not self.systemconfig.get(SystemConfigKey.SubscribeReport):
|
||||
if self.sub_report():
|
||||
self.systemconfig.set(SystemConfigKey.SubscribeReport, "1")
|
||||
self.get_user_uuid()
|
||||
self.get_github_user()
|
||||
|
||||
@cached(maxsize=20, ttl=1800)
|
||||
def get_statistic(self, stype: str, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||
@@ -135,7 +153,7 @@ class SubscribeHelper(metaclass=Singleton):
|
||||
"share_title": share_title,
|
||||
"share_comment": share_comment,
|
||||
"share_user": share_user,
|
||||
"share_uid": self.share_user_id,
|
||||
"share_uid": self._share_user_id,
|
||||
**subscribe_dict
|
||||
})
|
||||
if res is None:
|
||||
@@ -155,7 +173,7 @@ class SubscribeHelper(metaclass=Singleton):
|
||||
return False, "当前没有开启订阅数据共享功能"
|
||||
res = RequestUtils(proxies=settings.PROXY,
|
||||
timeout=5).delete_res(f"{self._sub_share}/{share_id}",
|
||||
params={"share_uid": self.share_user_id})
|
||||
params={"share_uid": self._share_user_id})
|
||||
if res is None:
|
||||
return False, "连接MoviePilot服务器失败"
|
||||
if res.ok:
|
||||
@@ -196,3 +214,35 @@ class SubscribeHelper(metaclass=Singleton):
|
||||
if res and res.status_code == 200:
|
||||
return res.json()
|
||||
return []
|
||||
|
||||
def get_user_uuid(self) -> str:
|
||||
"""
|
||||
获取用户uuid
|
||||
"""
|
||||
if not self._share_user_id:
|
||||
self._share_user_id = SystemUtils.generate_user_unique_id()
|
||||
logger.info(f"当前用户UUID: {self._share_user_id}")
|
||||
return self._share_user_id
|
||||
|
||||
def get_github_user(self) -> str:
|
||||
"""
|
||||
获取github用户
|
||||
"""
|
||||
if self._github_user is None and settings.GITHUB_HEADERS:
|
||||
res = RequestUtils(headers=settings.GITHUB_HEADERS,
|
||||
proxies=settings.PROXY,
|
||||
timeout=15).get_res(f"https://api.github.com/user")
|
||||
if res:
|
||||
self._github_user = res.json().get("login")
|
||||
logger.info(f"当前Github用户: {self._github_user}")
|
||||
return self._github_user
|
||||
|
||||
def is_admin_user(self) -> bool:
|
||||
"""
|
||||
判断是否是管理员
|
||||
"""
|
||||
if not self._github_user:
|
||||
return False
|
||||
if self._github_user in self._admin_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -695,7 +695,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
|
||||
return schemas.FileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=str(resp["file_id"]),
|
||||
path=str(path) + ("/" if resp["file_category"] == "1" else ""),
|
||||
path=str(path) + ("/" if resp["file_category"] == "0" else ""),
|
||||
type="file" if resp["file_category"] == "1" else "dir",
|
||||
name=resp["file_name"],
|
||||
basename=Path(resp["file_name"]).stem,
|
||||
|
||||
@@ -191,7 +191,6 @@ class MTorrentSpider:
|
||||
'id': torrent_id
|
||||
},
|
||||
'header': {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': f'{self._ua}',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'x-api-key': self._apikey
|
||||
|
||||
@@ -1358,6 +1358,9 @@ class TmdbApi:
|
||||
return {}
|
||||
for group_season in group_seasons:
|
||||
if group_season.get('order') == season:
|
||||
# 剧集组中每个季的episode_number从1开始
|
||||
for i, e in enumerate(group_season.get('episodes', []), start=1):
|
||||
e['episode_number'] = i
|
||||
return group_season
|
||||
return {}
|
||||
|
||||
|
||||
@@ -62,7 +62,9 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
|
||||
server.reconnect()
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
for server in self.get_instances().values():
|
||||
if server.is_authenticated():
|
||||
server.disconnect()
|
||||
|
||||
def test(self) -> Optional[Tuple[bool, str]]:
|
||||
"""
|
||||
@@ -73,7 +75,7 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
|
||||
for name, server in self.get_instances().items():
|
||||
if not server.is_configured():
|
||||
return False, f"飞牛影视配置不完整:{name}"
|
||||
if server.is_inactive() and server.reconnect() != True:
|
||||
if server.is_inactive() and not server.reconnect():
|
||||
return False, f"无法连接飞牛影视:{name}"
|
||||
return True, ""
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import random
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional, Union, List
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
@@ -19,27 +19,27 @@ class User:
|
||||
|
||||
|
||||
class Category(Enum):
|
||||
Movie = "Movie"
|
||||
MOVIE = "Movie"
|
||||
TV = "TV"
|
||||
Mix = "Mix"
|
||||
Others = "Others"
|
||||
MIX = "Mix"
|
||||
OTHERS = "Others"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.Others
|
||||
return cls.OTHERS
|
||||
|
||||
|
||||
class Type(Enum):
|
||||
Movie = "Movie"
|
||||
MOVIE = "Movie"
|
||||
TV = "TV"
|
||||
Season = "Season"
|
||||
Episode = "Episode"
|
||||
Video = "Video"
|
||||
Directory = "Directory"
|
||||
SEASON = "Season"
|
||||
EPISODE = "Episode"
|
||||
VIDEO = "Video"
|
||||
DIRECTORY = "Directory"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.Video
|
||||
return cls.VIDEO
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -60,6 +60,13 @@ class MediaDbSummary:
|
||||
total: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Version:
|
||||
# 飞牛影视版本
|
||||
frontend: Optional[str] = None
|
||||
backend: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Item:
|
||||
guid: str
|
||||
@@ -103,6 +110,7 @@ class Api:
|
||||
"_apikey",
|
||||
"_api_path",
|
||||
"_request_utils",
|
||||
"_version",
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -117,13 +125,34 @@ class Api:
|
||||
def apikey(self) -> str:
|
||||
return self._apikey
|
||||
|
||||
@property
|
||||
def version(self) -> Optional[Version]:
|
||||
return self._version
|
||||
|
||||
def __init__(self, host: str, apikey: str):
|
||||
self._api_path = "/v/api/v1"
|
||||
"""
|
||||
:param host: 飞牛服务端地址,如http://127.0.0.1:5666/v
|
||||
"""
|
||||
self._api_path = "/api/v1"
|
||||
self._host = host.rstrip("/")
|
||||
self._apikey = apikey
|
||||
self._token = None
|
||||
self._token: Optional[str] = None
|
||||
self._version: Optional[Version] = None
|
||||
self._request_utils = RequestUtils(session=requests.Session())
|
||||
|
||||
def sys_version(self) -> Optional[Version]:
|
||||
"""
|
||||
飞牛影视版本号
|
||||
"""
|
||||
if (res := self.__request_api("/sys/version")) and res.success:
|
||||
if res.data:
|
||||
self._version = Version(
|
||||
frontend=res.data.get("version"),
|
||||
backend=res.data.get("mediasrvVersion"),
|
||||
)
|
||||
return self._version
|
||||
return None
|
||||
|
||||
def login(self, username, password) -> Optional[str]:
|
||||
"""
|
||||
登录飞牛影视
|
||||
@@ -131,14 +160,14 @@ class Api:
|
||||
:return: 成功返回token 否则返回None
|
||||
"""
|
||||
if (
|
||||
res := self.__request_api(
|
||||
"/login",
|
||||
data={
|
||||
"username": username,
|
||||
"password": password,
|
||||
"app_name": "trimemedia-web",
|
||||
},
|
||||
)
|
||||
res := self.__request_api(
|
||||
"/login",
|
||||
data={
|
||||
"username": username,
|
||||
"password": password,
|
||||
"app_name": "trimemedia-web",
|
||||
},
|
||||
)
|
||||
) and res.success:
|
||||
self._token = res.data.get("token")
|
||||
return self._token
|
||||
@@ -250,7 +279,7 @@ class Api:
|
||||
扫描指定媒体库
|
||||
"""
|
||||
if (
|
||||
res := self.__request_api(f"/mdb/scan/{mdb.guid}", data={})
|
||||
res := self.__request_api(f"/mdb/scan/{mdb.guid}", data={})
|
||||
) and res.success:
|
||||
if res.data:
|
||||
return True
|
||||
@@ -272,22 +301,22 @@ class Api:
|
||||
return item
|
||||
|
||||
def item_list(
|
||||
self,
|
||||
guid: Optional[str] = None,
|
||||
type=None,
|
||||
exclude_grouped_video=True,
|
||||
page=1,
|
||||
page_size=22,
|
||||
sort_by="create_time",
|
||||
sort="DESC",
|
||||
self,
|
||||
guid: Optional[str] = None,
|
||||
types=None,
|
||||
exclude_grouped_video=True,
|
||||
page=1,
|
||||
page_size=22,
|
||||
sort_by="create_time",
|
||||
sort="DESC",
|
||||
) -> Optional[list[Item]]:
|
||||
"""
|
||||
媒体列表
|
||||
"""
|
||||
if type is None:
|
||||
type = [Type.Movie, Type.TV, Type.Directory, Type.Video]
|
||||
if types is None:
|
||||
types = [Type.MOVIE, Type.TV, Type.DIRECTORY, Type.VIDEO]
|
||||
post = {
|
||||
"tags": {"type": type} if type else {},
|
||||
"tags": {"type": types} if types else {},
|
||||
"sort_type": sort,
|
||||
"sort_column": sort_by,
|
||||
"page": page,
|
||||
@@ -307,25 +336,48 @@ class Api:
|
||||
搜索影片、演员
|
||||
"""
|
||||
if (
|
||||
res := self.__request_api("/search/list", params={"q": keywords})
|
||||
res := self.__request_api("/search/list", params={"q": keywords})
|
||||
) and res.success:
|
||||
return [self.__build_item(info) for info in res.data]
|
||||
return None
|
||||
|
||||
def item(self, guid: str) -> Optional[Item]:
|
||||
""" """
|
||||
"""
|
||||
查询媒体详情
|
||||
"""
|
||||
if (res := self.__request_api(f"/item/{guid}")) and res.success:
|
||||
return self.__build_item(res.data)
|
||||
return None
|
||||
|
||||
def del_item(self, guid: str, delete_file: bool) -> bool:
|
||||
"""
|
||||
删除媒体
|
||||
|
||||
:param delete_file: True删除媒体文件,False仅从媒体库移除
|
||||
"""
|
||||
if (
|
||||
res := self.__request_api(
|
||||
f"/item/{guid}",
|
||||
method="delete",
|
||||
data={"delete_file": 1 if delete_file else 0, "media_guids": []},
|
||||
)
|
||||
) and res.success:
|
||||
if res.data:
|
||||
return True
|
||||
return False
|
||||
|
||||
def season_list(self, tv_guid: str) -> Optional[list[Item]]:
|
||||
""" """
|
||||
"""
|
||||
查询季列表
|
||||
"""
|
||||
if (res := self.__request_api(f"/season/list/{tv_guid}")) and res.success:
|
||||
return [self.__build_item(info) for info in res.data]
|
||||
return None
|
||||
|
||||
def episode_list(self, season_guid: str) -> Optional[list[Item]]:
|
||||
""" """
|
||||
"""
|
||||
查询剧集列表
|
||||
"""
|
||||
if (res := self.__request_api(f"/episode/list/{season_guid}")) and res.success:
|
||||
return [self.__build_item(info) for info in res.data]
|
||||
return None
|
||||
@@ -338,12 +390,12 @@ class Api:
|
||||
return [self.__build_item(info) for info in res.data]
|
||||
return None
|
||||
|
||||
def __get_authx(self, api_path, body: Optional[str]):
|
||||
def __get_authx(self, api_path: str, body: Optional[str]):
|
||||
"""
|
||||
计算消息签名
|
||||
"""
|
||||
if api_path[0] != "/":
|
||||
api_path = "/" + api_path
|
||||
if not api_path.startswith("/v"):
|
||||
api_path = "/v" + api_path
|
||||
nonce = str(random.randint(100000, 999999))
|
||||
ts = str(int(time.time() * 1000))
|
||||
md5 = hashlib.md5()
|
||||
@@ -366,10 +418,17 @@ class Api:
|
||||
return f"nonce={nonce}×tamp={ts}&sign={sign}"
|
||||
|
||||
def __request_api(
|
||||
self, api: str, method: str = None, params: dict = None, data: dict = None
|
||||
self,
|
||||
api: str,
|
||||
method: Optional[str] = None,
|
||||
params: Optional[dict] = None,
|
||||
data: Optional[dict] = None,
|
||||
suppress_log=False,
|
||||
):
|
||||
"""
|
||||
请求飞牛影视API
|
||||
|
||||
:param suppress_log: 是否禁止日志
|
||||
"""
|
||||
|
||||
@dataclass
|
||||
@@ -397,7 +456,7 @@ class Api:
|
||||
url = self._host + api_path
|
||||
if method is None:
|
||||
method = "get" if data is None else "post"
|
||||
if method == "post":
|
||||
if method != "get":
|
||||
json_body = (
|
||||
json.dumps(data, allow_nan=False, cls=JsonEncoder) if data else ""
|
||||
)
|
||||
@@ -422,11 +481,13 @@ class Api:
|
||||
resp = res.json()
|
||||
msg = resp.get("msg")
|
||||
if code := int(resp.get("code", -1)):
|
||||
logger.error(f"请求接口 {api_path} 失败,错误码:{code} {msg}")
|
||||
if not suppress_log:
|
||||
logger.error(f"请求接口 {url} 失败,错误码:{code} {msg}")
|
||||
return Result(code, msg)
|
||||
return Result(0, msg, resp.get("data"))
|
||||
else:
|
||||
logger.error(f"请求接口 {api_path} 失败")
|
||||
elif not suppress_log:
|
||||
logger.error(f"请求接口 {url} 失败")
|
||||
except Exception as e:
|
||||
logger.error(f"请求接口 {api_path} 异常:" + str(e))
|
||||
if not suppress_log:
|
||||
logger.error(f"请求接口 {url} 异常:" + str(e))
|
||||
return None
|
||||
|
||||
@@ -26,21 +26,58 @@ class TrimeMedia:
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
play_host: Optional[str] = None,
|
||||
sync_libraries: list = None,
|
||||
sync_libraries: Optional[list] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if not host or not username or not password:
|
||||
logger.error("飞牛影视配置不完整!!")
|
||||
return
|
||||
host = UrlUtils.standardize_base_url(host).rstrip("/")
|
||||
if play_host:
|
||||
self._playhost = UrlUtils.standardize_base_url(play_host).rstrip("/")
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._sync_libraries = sync_libraries or []
|
||||
self._api = fnapi.Api(host, apikey="16CCEB3D-AB42-077D-36A1-F355324E4237")
|
||||
|
||||
if (api := self.__create_api(host)) is None:
|
||||
logger.error(f"请检查服务端地址 {host}")
|
||||
return
|
||||
self._api = api
|
||||
if play_api := self.__create_api(play_host):
|
||||
self._playhost = play_api.host
|
||||
elif play_host:
|
||||
logger.warning(f"请检查外网播放地址 {play_host}")
|
||||
|
||||
self.reconnect()
|
||||
|
||||
@property
|
||||
def api(self) -> Optional[fnapi.Api]:
|
||||
"""
|
||||
获得飞牛API
|
||||
"""
|
||||
return self._api
|
||||
|
||||
def __create_api(self, host: Optional[str]) -> Optional[fnapi.Api]:
|
||||
"""
|
||||
创建一个飞牛API
|
||||
|
||||
:param host: 服务端地址
|
||||
:return: 如果地址无效、不可访问则返回None
|
||||
"""
|
||||
if not host:
|
||||
return None
|
||||
api_key = "16CCEB3D-AB42-077D-36A1-F355324E4237"
|
||||
host = UrlUtils.standardize_base_url(host).rstrip("/")
|
||||
|
||||
if not host.endswith("/v"):
|
||||
# 尝试补上结尾的/v 测试能否正常访问
|
||||
api = fnapi.Api(host + "/v", api_key)
|
||||
if api.sys_version():
|
||||
return api
|
||||
# 测试用户配置的地址
|
||||
api = fnapi.Api(host, api_key)
|
||||
return api if api.sys_version() else None
|
||||
|
||||
def __del__(self):
|
||||
self.disconnect()
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
return self._api is not None
|
||||
|
||||
@@ -62,14 +99,27 @@ class TrimeMedia:
|
||||
"""
|
||||
if not self.is_configured():
|
||||
return False
|
||||
if (fnver := self._api.sys_version()) is None:
|
||||
return False
|
||||
# 版本号:0.8.36, 服务版本:0.8.19
|
||||
logger.debug(f"版本号:{fnver.frontend}, 服务版本:{fnver.backend}")
|
||||
if self._api.login(self._username, self._password) is None:
|
||||
return False
|
||||
self._userinfo = self._api.user_info()
|
||||
if self._userinfo is None:
|
||||
return False
|
||||
logger.debug(f"{self._userinfo.username} 成功登录飞牛影视")
|
||||
logger.debug(f"{self._username} 成功登录飞牛影视")
|
||||
return True
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
断开与飞牛的连接
|
||||
"""
|
||||
if self.is_authenticated():
|
||||
self._api.logout()
|
||||
self._userinfo = None
|
||||
logger.debug(f"{self._username} 已断开飞牛影视")
|
||||
|
||||
def get_librarys(
|
||||
self, hidden: Optional[bool] = False
|
||||
) -> List[schemas.MediaServerLibrary]:
|
||||
@@ -87,11 +137,11 @@ class TrimeMedia:
|
||||
for library in self._libraries.values():
|
||||
if hidden and self.__is_library_blocked(library.guid):
|
||||
continue
|
||||
if library.category == fnapi.Category.Movie:
|
||||
if library.category == fnapi.Category.MOVIE:
|
||||
library_type = MediaType.MOVIE.value
|
||||
elif library.category == fnapi.Category.TV:
|
||||
library_type = MediaType.TV.value
|
||||
elif library.category == fnapi.Category.Others:
|
||||
elif library.category == fnapi.Category.OTHERS:
|
||||
# 忽略这个库
|
||||
continue
|
||||
else:
|
||||
@@ -107,7 +157,7 @@ class TrimeMedia:
|
||||
f"{self._api.host}{img_path}?w=256"
|
||||
for img_path in library.posters or []
|
||||
],
|
||||
link=f"{self._playhost or self._api.host}/v/library/{library.guid}",
|
||||
link=f"{self._playhost or self._api.host}/library/{library.guid}",
|
||||
)
|
||||
)
|
||||
return libraries
|
||||
@@ -170,7 +220,7 @@ class TrimeMedia:
|
||||
movies = []
|
||||
items = self._api.search_list(keywords=title) or []
|
||||
for item in items:
|
||||
if item.type != fnapi.Type.Movie:
|
||||
if item.type != fnapi.Type.MOVIE:
|
||||
continue
|
||||
if (
|
||||
(not tmdb_id or tmdb_id == item.tmdb_id)
|
||||
@@ -280,7 +330,7 @@ class TrimeMedia:
|
||||
lib = self.__match_library_by_path(item.target_path)
|
||||
if lib is None:
|
||||
# 如果有匹配失败的,刷新整个库
|
||||
return self._api.mdb_scanall()
|
||||
return self.refresh_root_library()
|
||||
# 媒体库去重
|
||||
libraries.add(lib.guid)
|
||||
|
||||
@@ -290,7 +340,7 @@ class TrimeMedia:
|
||||
logger.info(f"刷新媒体库:{lib.name}")
|
||||
if not self._api.mdb_scan(lib):
|
||||
# 如果失败,刷新整个库
|
||||
return self._api.mdb_scanall()
|
||||
return self.refresh_root_library()
|
||||
return True
|
||||
|
||||
def __match_library_by_path(self, path: Path) -> Optional[fnapi.MediaDb]:
|
||||
@@ -336,7 +386,7 @@ class TrimeMedia:
|
||||
if item.watched:
|
||||
user_state.played = True
|
||||
if item.duration and item.ts is not None:
|
||||
user_state.percentage = item.ts / item.duration
|
||||
user_state.percentage = item.ts / item.duration * 100
|
||||
user_state.resume = True
|
||||
if item.type is None:
|
||||
item_type = None
|
||||
@@ -361,40 +411,37 @@ class TrimeMedia:
|
||||
"""
|
||||
拼装播放链接
|
||||
"""
|
||||
if item.type == fnapi.Type.Episode:
|
||||
return f"{host}/v/tv/episode/{item.guid}"
|
||||
elif item.type == fnapi.Type.Season:
|
||||
return f"{host}/v/tv/season/{item.guid}"
|
||||
elif item.type == fnapi.Type.Movie:
|
||||
return f"{host}/v/movie/{item.guid}"
|
||||
if item.type == fnapi.Type.EPISODE:
|
||||
return f"{host}/tv/episode/{item.guid}"
|
||||
elif item.type == fnapi.Type.SEASON:
|
||||
return f"{host}/tv/season/{item.guid}"
|
||||
elif item.type == fnapi.Type.MOVIE:
|
||||
return f"{host}/movie/{item.guid}"
|
||||
elif item.type == fnapi.Type.TV:
|
||||
return f"{host}/v/tv/{item.guid}"
|
||||
return f"{host}/tv/{item.guid}"
|
||||
else:
|
||||
# 其它类型走通用页面,由飞牛来判断
|
||||
return f"{host}/v/other/{item.guid}"
|
||||
return f"{host}/other/{item.guid}"
|
||||
|
||||
def __build_media_server_play_item(
|
||||
self, item: fnapi.Item
|
||||
) -> schemas.MediaServerPlayItem:
|
||||
"""
|
||||
:params use_backdrop: 是否优先使用Backdrop类型的图片
|
||||
"""
|
||||
if item.type == fnapi.Type.Episode:
|
||||
if item.type == fnapi.Type.EPISODE:
|
||||
title = item.tv_title
|
||||
subtitle = f"S{item.season_number}:{item.episode_number} - {item.title}"
|
||||
else:
|
||||
title = item.title
|
||||
subtitle = "电影" if item.type == fnapi.Type.Movie else "视频"
|
||||
type = (
|
||||
subtitle = "电影" if item.type == fnapi.Type.MOVIE else "视频"
|
||||
types = (
|
||||
MediaType.MOVIE.value
|
||||
if item.type in [fnapi.Type.Movie, fnapi.Type.Video]
|
||||
if item.type in [fnapi.Type.MOVIE, fnapi.Type.VIDEO]
|
||||
else MediaType.TV.value
|
||||
)
|
||||
return schemas.MediaServerPlayItem(
|
||||
id=item.guid,
|
||||
title=title,
|
||||
subtitle=subtitle,
|
||||
type=type,
|
||||
type=types,
|
||||
image=f"{self._api.host}{item.poster}",
|
||||
link=self.__build_play_url(self._playhost or self._api.host, item),
|
||||
percent=(
|
||||
@@ -421,22 +468,22 @@ class TrimeMedia:
|
||||
"""
|
||||
if not self.is_authenticated():
|
||||
return None
|
||||
if (SIZE := limit) is None:
|
||||
SIZE = -1
|
||||
if (page_size := limit) is None:
|
||||
page_size = -1
|
||||
items = (
|
||||
self._api.item_list(
|
||||
guid=parent,
|
||||
page=start_index + 1,
|
||||
page_size=SIZE,
|
||||
type=[fnapi.Type.Movie, fnapi.Type.TV, fnapi.Type.Directory],
|
||||
page_size=page_size,
|
||||
types=[fnapi.Type.MOVIE, fnapi.Type.TV, fnapi.Type.DIRECTORY],
|
||||
)
|
||||
or []
|
||||
)
|
||||
for item in items:
|
||||
if item.type == fnapi.Type.Directory:
|
||||
if item.type == fnapi.Type.DIRECTORY:
|
||||
for items in self.get_items(parent=item.guid):
|
||||
yield items
|
||||
elif item.type in [fnapi.Type.Movie, fnapi.Type.TV]:
|
||||
elif item.type in [fnapi.Type.MOVIE, fnapi.Type.TV]:
|
||||
yield self.__build_media_server_item(item)
|
||||
return None
|
||||
|
||||
@@ -482,7 +529,7 @@ class TrimeMedia:
|
||||
self._api.item_list(
|
||||
page=1,
|
||||
page_size=max(100, num * 5),
|
||||
type=[fnapi.Type.Movie, fnapi.Type.TV],
|
||||
types=[fnapi.Type.MOVIE, fnapi.Type.TV],
|
||||
)
|
||||
or []
|
||||
)
|
||||
@@ -505,7 +552,7 @@ class TrimeMedia:
|
||||
self._api.item_list(
|
||||
page=1,
|
||||
page_size=max(100, num * 5),
|
||||
type=[fnapi.Type.Movie, fnapi.Type.TV],
|
||||
types=[fnapi.Type.MOVIE, fnapi.Type.TV],
|
||||
)
|
||||
or []
|
||||
)
|
||||
@@ -534,7 +581,7 @@ class TrimeMedia:
|
||||
|
||||
def __is_library_blocked(self, library_guid: str):
|
||||
if library := self._libraries.get(library_guid):
|
||||
if library.category == fnapi.Category.Others:
|
||||
if library.category == fnapi.Category.OTHERS:
|
||||
# 忽略这个库
|
||||
return True
|
||||
return (
|
||||
|
||||
@@ -274,6 +274,7 @@ class RecommendMediaSource(BaseModel):
|
||||
"""
|
||||
name: str = Field(..., description="数据源名称")
|
||||
api_path: str = Field(..., description="媒体数据源API地址")
|
||||
type: str = Field(..., description="类型")
|
||||
|
||||
|
||||
class RecommendSourceEventData(ChainEventData):
|
||||
|
||||
@@ -74,7 +74,7 @@ class Subscribe(BaseModel):
|
||||
# 过滤规则组
|
||||
filter_groups: Optional[List[str]] = Field(default_factory=list)
|
||||
# 剧集组
|
||||
episode_group: str = None
|
||||
episode_group: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@@ -1,67 +1,25 @@
|
||||
#######################################################################
|
||||
# 【*】为必配项,其余为选配项,选配项可以删除整项配置项或者保留配置默认值 #
|
||||
#######################################################################
|
||||
#######################################################################################################
|
||||
# V2版本中大部分设置可通过后台设置界面进行配置,本文件仅展示界面无法配置的项, 这些项同样可以通过环境变量进行设置 #
|
||||
#######################################################################################################
|
||||
# 【*】API监听地址(注意不是前端访问地址)
|
||||
HOST=0.0.0.0
|
||||
# 是否调试模式,打开后将输出更多日志
|
||||
DEBUG=false
|
||||
# 是否开发模式,打开后后台服务将不会启动
|
||||
DEV=false
|
||||
# 日志级别(DEBUG、INFO、WARNING、ERROR等),当DEBUG=true时,此配置项将被忽略,日志级别始终为DEBUG
|
||||
LOG_LEVEL=INFO
|
||||
# 【*】超级管理员,设置后一但重启将固化到数据库中,修改将无效(初始化超级管理员密码仅会生成一次,请在日志中查看并自行登录系统修改)
|
||||
SUPERUSER=admin
|
||||
# 自动检查和更新站点资源包(索引、认证等)
|
||||
AUTO_UPDATE_RESOURCE=true
|
||||
# 媒体识别来源 themoviedb/douban,使用themoviedb时需要确保能正常连接api.themoviedb.org,使用douban时不支持二级分类
|
||||
RECOGNIZE_SOURCE=themoviedb
|
||||
# OCR服务器地址
|
||||
OCR_HOST=https://movie-pilot.org
|
||||
# 搜索多个名称,true/false,为true时搜索时会同时搜索中英文及原始名称,搜索结果会更全面,但会增加搜索时间;为false时其中一个名称搜索到结果或全部名称搜索完毕即停止
|
||||
SEARCH_MULTIPLE_NAME=false
|
||||
# 为指定字幕添加.default后缀设置为默认字幕,支持为'zh-cn','zh-tw','eng'添加默认字幕,未定义或设置为None则不添加
|
||||
DEFAULT_SUB=zh-cn
|
||||
# 数据库连接池的大小,可适当降低如20-50以减少I/O压力
|
||||
DB_POOL_SIZE=100
|
||||
# 数据库连接池最大溢出连接数,可适当降低如0以减少I/O压力
|
||||
DB_MAX_OVERFLOW=500
|
||||
# SQLite 的 busy_timeout 参数,可适当增加如180以减少锁定错误
|
||||
DB_TIMEOUT=60
|
||||
# SQLite 是否启用 WAL 模式,启用可提升读写并发性能,但可能在异常情况下增加数据丢失的风险
|
||||
DB_WAL_ENABLE=false
|
||||
# 【*】超级管理员,设置后一但重启将固化到数据库中,修改将无效(初始化超级管理员密码仅会生成一次,请在日志中查看并自行登录系统修改)
|
||||
SUPERUSER=admin
|
||||
# 辅助认证,允许通过外部服务进行认证、单点登录以及自动创建用户
|
||||
AUXILIARY_AUTH_ENABLE=false
|
||||
# 大内存模式,开启后会增加缓存数量,但会占用更多内存
|
||||
BIG_MEMORY_MODE=false
|
||||
# 是否启用DOH域名解析,启用后对于api.themovie.org等域名通过DOH解析,避免域名DNS被污染
|
||||
DOH_ENABLE=true
|
||||
# 使用 DOH 解析的域名列表,多个域名使用`,`分隔
|
||||
DOH_DOMAINS=api.themoviedb.org,api.tmdb.org,webservice.fanart.tv,api.github.com,github.com,raw.githubusercontent.com,api.telegram.org
|
||||
# DOH 解析服务器列表,多个服务器使用`,`分隔
|
||||
DOH_RESOLVERS=1.0.0.1,1.1.1.1,9.9.9.9,149.112.112.112
|
||||
# 元数据识别缓存过期时间,数字型,单位小时,0为系统默认(大内存模式为7天,滞则为3天),调大该值可减少themoviedb的访问次数
|
||||
META_CACHE_EXPIRE=0
|
||||
# 自动检查和更新站点资源包(索引、认证等)
|
||||
AUTO_UPDATE_RESOURCE=true
|
||||
# 【*】API密钥,未设置时系统将随机生成,建议使用复杂字符串,用于Jellyseerr/Overseerr、媒体服务器Webhook等配置以及部分支持API_TOKEN的API请求
|
||||
API_TOKEN=''
|
||||
# 登录页面电影海报,tmdb/bing/mediaserver,tmdb要求能正常连接api.themoviedb.org
|
||||
WALLPAPER=tmdb
|
||||
# TMDB图片地址,无需修改需保留默认值,如果默认地址连通性不好可以尝试修改为:`static-mdb.v.geilijiasu.com`
|
||||
TMDB_IMAGE_DOMAIN=image.tmdb.org
|
||||
# TMDB API地址,无需修改需保留默认值,也可配置为`api.tmdb.org`或其它中转代理服务地址,能连通即可
|
||||
TMDB_API_DOMAIN=api.themoviedb.org
|
||||
# 媒体识别来源 themoviedb/douban,使用themoviedb时需要确保能正常连接api.themoviedb.org,使用douban时不支持二级分类
|
||||
RECOGNIZE_SOURCE=themoviedb
|
||||
# Fanart开关
|
||||
FANART_ENABLE=true
|
||||
# 新增已入库媒体是否跟随TMDB信息变化,true/false,为false时即使TMDB信息变化时也会仍然按历史记录中已入库的信息进行刮削
|
||||
SCRAP_FOLLOW_TMDB=true
|
||||
# 刮削来源 themoviedb/douban,使用themoviedb时需要确保能正常连接api.themoviedb.org,使用douban时会缺失部分信息
|
||||
SCRAP_SOURCE=themoviedb
|
||||
# 电影重命名格式,Jinja2语法,参考:https://jinja.palletsprojects.com/en/3.0.x/templates/
|
||||
MOVIE_RENAME_FORMAT={{title}}{% if year %} ({{year}}){% endif %}/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}}
|
||||
# 电视剧重命名格式,Jinja2语法,参考:https://jinja.palletsprojects.com/en/3.0.x/templates/
|
||||
TV_RENAME_FORMAT={{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}{{fileExt}}
|
||||
# 交互搜索自动下载用户ID(消息通知渠道的用户ID),使用,分割,设置为 all 代表所有用户自动择优下载,未设置需要用户手动选择资源或者回复`0`才自动择优下载
|
||||
AUTO_DOWNLOAD_USER=
|
||||
# 自动下载站点字幕(如有)
|
||||
DOWNLOAD_SUBTITLE=true
|
||||
# OCR服务器地址
|
||||
OCR_HOST=https://movie-pilot.org
|
||||
# 插件市场仓库地址,多个地址使用`,`分隔,保留最后的/
|
||||
PLUGIN_MARKET=https://github.com/jxxghp/MoviePilot-Plugins,https://github.com/thsrite/MoviePilot-Plugins,https://github.com/InfinityPacer/MoviePilot-Plugins,https://github.com/honue/MoviePilot-Plugins
|
||||
# 搜索多个名称,true/false,为true时搜索时会同时搜索中英文及原始名称,搜索结果会更全面,但会增加搜索时间;为false时其中一个名称搜索到结果或全部名称搜索完毕即停止
|
||||
SEARCH_MULTIPLE_NAME=true
|
||||
# 为指定字幕添加.default后缀设置为默认字幕,支持为'zh-cn','zh-tw','eng'添加默认字幕,未定义或设置为None则不添加
|
||||
DEFAULT_SUB=None
|
||||
# 是否开发调试模式,仅开发人员使用,打开后将停止后台服务
|
||||
DEV=false
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
在开始之前,请确保您的系统已安装以下软件:
|
||||
|
||||
- **Python 3.11 或更高版本**
|
||||
- **Python 3.12 或更高版本** (暂时兼容 3.11 ,推荐使用 3.12+)
|
||||
- **pip** (Python 包管理器)
|
||||
- **Git** (用于版本控制)
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ APScheduler~=3.10.1
|
||||
cryptography~=43.0.0
|
||||
pytz~=2023.3
|
||||
pycryptodome~=3.20.0
|
||||
qbittorrent-api==2024.11.69
|
||||
plexapi~=4.15.16
|
||||
qbittorrent-api==2024.11.70
|
||||
plexapi~=4.16.0
|
||||
transmission-rpc~=4.3.0
|
||||
Jinja2~=3.1.4
|
||||
pyparsing~=3.0.9
|
||||
@@ -34,7 +34,7 @@ beautifulsoup4~=4.12.2
|
||||
pillow~=10.4.0
|
||||
pillow-avif-plugin~=1.4.6
|
||||
pyTelegramBotAPI~=4.12.0
|
||||
playwright~=1.37.0
|
||||
playwright~=1.49.1
|
||||
cf-clearance~=0.31.0
|
||||
torrentool~=1.2.0
|
||||
slack-bolt~=1.18.0
|
||||
@@ -69,4 +69,4 @@ packaging~=24.2
|
||||
cf_clearance~=0.31.0
|
||||
oss2~=2.19.1
|
||||
tqdm~=4.67.1
|
||||
setuptools~=65.5.0
|
||||
setuptools~=78.1.0
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = 'v2.3.8'
|
||||
FRONTEND_VERSION = 'v2.3.8'
|
||||
APP_VERSION = 'v2.3.9'
|
||||
FRONTEND_VERSION = 'v2.3.9'
|
||||
|
||||
Reference in New Issue
Block a user