mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-10 17:42:45 +08:00
Compare commits
170 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cacee7abfe | ||
|
|
61694f4c2b | ||
|
|
9c328e3d1c | ||
|
|
b2fe86c744 | ||
|
|
600e32d3e4 | ||
|
|
3ad733bab4 | ||
|
|
1799b63abb | ||
|
|
d71dc13e32 | ||
|
|
f4633788e9 | ||
|
|
2250e7db39 | ||
|
|
b1bb0ced7a | ||
|
|
28aecd79c6 | ||
|
|
d097ef45eb | ||
|
|
dac718edc8 | ||
|
|
598ab23a2c | ||
|
|
8be6e28933 | ||
|
|
bd6805be58 | ||
|
|
c147d36cb2 | ||
|
|
7a5d210167 | ||
|
|
ef335f2b8e | ||
|
|
19eca11d17 | ||
|
|
ab99bd356a | ||
|
|
70f2d72532 | ||
|
|
0ca995da0f | ||
|
|
2a67abe62d | ||
|
|
03a07ac7bf | ||
|
|
f104c903ec | ||
|
|
6b74a8e266 | ||
|
|
cadd885dbf | ||
|
|
7e0cad8491 | ||
|
|
4c05e9fb2b | ||
|
|
42311f0118 | ||
|
|
951be74a21 | ||
|
|
c86a21d11d | ||
|
|
3fb02f6490 | ||
|
|
ca2c0392bb | ||
|
|
b8663ee735 | ||
|
|
4ab60423c1 | ||
|
|
1ea80e6870 | ||
|
|
6f1d4754be | ||
|
|
52288d98c0 | ||
|
|
d1368c4f84 | ||
|
|
4367c53bb0 | ||
|
|
d87f69da35 | ||
|
|
5ece44090e | ||
|
|
01be4f9549 | ||
|
|
94077917f3 | ||
|
|
8af981738c | ||
|
|
4d7982803e | ||
|
|
a1bba6da4a | ||
|
|
4eb3e16b37 | ||
|
|
1f0b40fe05 | ||
|
|
29e92a17e7 | ||
|
|
8cc4469282 | ||
|
|
a5e66071ba | ||
|
|
fb4e817993 | ||
|
|
8f26110e65 | ||
|
|
9f65a088c0 | ||
|
|
15c15388b6 | ||
|
|
950a43e001 | ||
|
|
9a28f8c365 | ||
|
|
32cb96fc44 | ||
|
|
f7982e3e43 | ||
|
|
d13602827c | ||
|
|
182adc77b6 | ||
|
|
ef4cdb41c8 | ||
|
|
9a60121914 | ||
|
|
6fb0c92183 | ||
|
|
96c4e0ba2f | ||
|
|
7afe82480c | ||
|
|
c37c8e7318 | ||
|
|
3d10ca4c8b | ||
|
|
4e515ec442 | ||
|
|
5eb37b5d28 | ||
|
|
7f95bab0d5 | ||
|
|
3fc267bcfa | ||
|
|
648f0b6ec1 | ||
|
|
be3c3ef37f | ||
|
|
a47f382c21 | ||
|
|
61c59b4405 | ||
|
|
8ee391688d | ||
|
|
68c7bf0a96 | ||
|
|
6dd517a490 | ||
|
|
9baa5e1d35 | ||
|
|
e675e4358a | ||
|
|
c9a6081a57 | ||
|
|
2de20f601b | ||
|
|
79c708c30e | ||
|
|
f38defb515 | ||
|
|
ac11d4eb30 | ||
|
|
221c31f481 | ||
|
|
7c3c6ee999 | ||
|
|
08560fc7c3 | ||
|
|
4659e7367f | ||
|
|
2fa11a4796 | ||
|
|
01a153902e | ||
|
|
5eb65046f0 | ||
|
|
bb64e57f7c | ||
|
|
0cb75d689c | ||
|
|
d7310ade86 | ||
|
|
dd7803c90a | ||
|
|
d8afa339de | ||
|
|
1b2f09b95f | ||
|
|
0414854832 | ||
|
|
9e6a7be5b1 | ||
|
|
e3c1407b62 | ||
|
|
7a9ee954c5 | ||
|
|
99a06dcba0 | ||
|
|
bb8fc14bc6 | ||
|
|
50d9dcf17b | ||
|
|
141b99d134 | ||
|
|
18457a4de7 | ||
|
|
a343d736ae | ||
|
|
df5c364185 | ||
|
|
edcec114ae | ||
|
|
605a7486b3 | ||
|
|
efe89f59b9 | ||
|
|
fdd4aef3d3 | ||
|
|
08aef1f47f | ||
|
|
c45f5e6ac4 | ||
|
|
f239cede07 | ||
|
|
b2eb952cd0 | ||
|
|
3a2fba0422 | ||
|
|
1034caa9fd | ||
|
|
8b243e23ab | ||
|
|
1f76dc1e2a | ||
|
|
ea5c2fb4cf | ||
|
|
e50b56d542 | ||
|
|
2206fafda9 | ||
|
|
345b74d881 | ||
|
|
d231d75446 | ||
|
|
afb5874350 | ||
|
|
1bd7b5c77e | ||
|
|
ba41de61cb | ||
|
|
ae40d32115 | ||
|
|
3fe4c9467e | ||
|
|
b89512cc33 | ||
|
|
f3b12bed20 | ||
|
|
08c7fff5ab | ||
|
|
9c20d1a270 | ||
|
|
b7b1aee878 | ||
|
|
f998b39152 | ||
|
|
ca01db31a9 | ||
|
|
a0b8cc6719 | ||
|
|
66b91abe90 | ||
|
|
9b17d55ac0 | ||
|
|
a7a0889867 | ||
|
|
af6cf306c8 | ||
|
|
20f35854f9 | ||
|
|
e5165c8fea | ||
|
|
0e36d003c0 | ||
|
|
ccc249f29d | ||
|
|
f4edb32886 | ||
|
|
475a84bfa6 | ||
|
|
3914ff4dd6 | ||
|
|
5bcbacf3a5 | ||
|
|
27238ac467 | ||
|
|
019d40c17a | ||
|
|
fa5b92214f | ||
|
|
32a5f67e72 | ||
|
|
d6e9c14183 | ||
|
|
87325d5bbd | ||
|
|
67ead871c1 | ||
|
|
691beb1186 | ||
|
|
b30d3c7dac | ||
|
|
5e048f0150 | ||
|
|
cb2cfe9d85 | ||
|
|
482fca9b8c | ||
|
|
42511b95d8 | ||
|
|
b18e901fbd |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_USERNAME }}/moviepilot
|
||||
tags: |
|
||||
@@ -35,22 +35,22 @@ jobs:
|
||||
|
||||
-
|
||||
name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
-
|
||||
name: Set Up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
-
|
||||
name: Login DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
-
|
||||
name: Build Image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
|
||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -29,8 +29,7 @@ jobs:
|
||||
with:
|
||||
tag_name: v${{ env.app_version }}
|
||||
release_name: v${{ env.app_version }}
|
||||
body: |
|
||||
${{ github.event.commits[0].message }}
|
||||
body: ${{ github.event.commits[0].message }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
|
||||
@@ -65,6 +65,7 @@ RUN apt-get update \
|
||||
&& useradd -r moviepilot -g moviepilot -d ${HOME} -s /bin/bash -u 911 \
|
||||
&& apt-get install -y build-essential \
|
||||
&& pip install --upgrade pip \
|
||||
&& pip install Cython \
|
||||
&& pip install -r requirements.txt \
|
||||
&& playwright install-deps chromium \
|
||||
&& python_ver=$(python3 -V | awk '{print $2}') \
|
||||
|
||||
35
README.md
35
README.md
@@ -59,9 +59,9 @@ docker pull jxxghp/moviepilot:latest
|
||||
- **PROXY_HOST:** 网络代理(可选),访问themoviedb或者重启更新需要使用代理访问,格式为`http(s)://ip:port`
|
||||
- **TMDB_API_DOMAIN:** TMDB API地址,默认`api.themoviedb.org`,也可配置为`api.tmdb.org`或其它中转代理服务地址,能连通即可
|
||||
- **DOWNLOAD_PATH:** 下载保存目录,**注意:需要将`moviepilot`及`下载器`的映射路径保持一致**,否则会导致下载文件无法转移
|
||||
- **DOWNLOAD_MOVIE_PATH:** 电影下载保存目录,**必须是`DOWNLOAD_PATH`的下级路径**,不设置则下载到`DOWNLOAD_PATH`
|
||||
- **DOWNLOAD_TV_PATH:** 电视剧下载保存目录,**必须是`DOWNLOAD_PATH`的下级路径**,不设置则下载到`DOWNLOAD_PATH`
|
||||
- **DOWNLOAD_ANIME_PATH:** 动漫下载保存目录,**必须是`DOWNLOAD_PATH`的下级路径**,不设置则下载到`DOWNLOAD_PATH`
|
||||
- **DOWNLOAD_MOVIE_PATH:** 电影下载保存目录,不设置则下载到`DOWNLOAD_PATH`
|
||||
- **DOWNLOAD_TV_PATH:** 电视剧下载保存目录,不设置则下载到`DOWNLOAD_PATH`
|
||||
- **DOWNLOAD_ANIME_PATH:** 动漫下载保存目录,不设置则下载到`DOWNLOAD_PATH`
|
||||
- **DOWNLOAD_CATEGORY:** 下载二级分类开关,`true`/`false`,默认`false`,开启后会根据配置`category.yaml`自动在下载目录下建立二级目录分类
|
||||
- **DOWNLOAD_SUBTITLE:** 下载站点字幕,`true`/`false`,默认`true`
|
||||
- **REFRESH_MEDIASERVER:** 入库刷新媒体库,`true`/`false`,默认`true`
|
||||
@@ -81,6 +81,8 @@ docker pull jxxghp/moviepilot:latest
|
||||
- **OCR_HOST:** OCR识别服务器地址,格式:`http(s)://ip:port`,用于识别站点二维码实现自动登录获取Cookie等,不配置默认使用内建服务器`https://movie-pilot.org`,可使用 [这个镜像](https://hub.docker.com/r/jxxghp/moviepilot-ocr) 自行搭建。
|
||||
- **USER_AGENT:** CookieCloud对应的浏览器UA,可选,设置后可增加连接站点的成功率,同步站点后可以在管理界面中修改
|
||||
- **AUTO_DOWNLOAD_USER:** 交互搜索自动下载用户ID,使用,分割
|
||||
- **SUBSCRIBE_MODE:** 订阅模式,`rss`/`spider`,默认`spider`,`rss`模式通过定时刷新RSS来匹配订阅(RSS地址会自动获取,也可手动维护),对站点压力小,同时可设置订阅刷新周期,24小时运行,但订阅和下载通知不能过滤和显示免费,推荐使用rss模式。
|
||||
- **SUBSCRIBE_RSS_INTERVAL:** RSS订阅模式刷新时间间隔(分钟),默认`30`分钟,不能小于5分钟。
|
||||
- **SUBSCRIBE_SEARCH:** 订阅搜索,`true`/`false`,默认`false`,开启后会每隔24小时对所有订阅进行全量搜索,以补齐缺失剧集(一般情况下正常订阅即可,订阅搜索只做为兜底,会增加站点压力,不建议开启)。
|
||||
- **MESSAGER:** 消息通知渠道,支持 `telegram`/`wechat`/`slack`,开启多个渠道时使用`,`分隔。同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`telegram`
|
||||
|
||||
@@ -147,23 +149,24 @@ docker pull jxxghp/moviepilot:latest
|
||||
|
||||
### 2. **用户认证**
|
||||
|
||||
- **AUTH_SITE:** 认证站点,支持`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`iyuu`
|
||||
- **AUTH_SITE:** 认证站点,支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`
|
||||
|
||||
`MoviePilot`需要认证后才能使用,配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数。
|
||||
|
||||
| 站点 | 参数 |
|
||||
|:--:|:-----------------------------------------------------:|
|
||||
| iyuu | `IYUU_SIGN`:IYUU登录令牌 |
|
||||
| hhclub | `HHCLUB_USERNAME`:用户名<br/>`HHCLUB_PASSKEY`:密钥 |
|
||||
| audiences | `AUDIENCES_UID`:用户ID<br/>`AUDIENCES_PASSKEY`:密钥 |
|
||||
| hddolby | `HDDOLBY_ID`:用户ID<br/>`HDDOLBY_PASSKEY`:密钥 |
|
||||
| zmpt | `ZMPT_UID`:用户ID<br/>`ZMPT_PASSKEY`:密钥 |
|
||||
| freefarm | `FREEFARM_UID`:用户ID<br/>`FREEFARM_PASSKEY`:密钥 |
|
||||
| hdfans | `HDFANS_UID`:用户ID<br/>`HDFANS_PASSKEY`:密钥 |
|
||||
| 站点 | 参数 |
|
||||
|:------------:|:-----------------------------------------------------:|
|
||||
| iyuu | `IYUU_SIGN`:IYUU登录令牌 |
|
||||
| hhclub | `HHCLUB_USERNAME`:用户名<br/>`HHCLUB_PASSKEY`:密钥 |
|
||||
| audiences | `AUDIENCES_UID`:用户ID<br/>`AUDIENCES_PASSKEY`:密钥 |
|
||||
| hddolby | `HDDOLBY_ID`:用户ID<br/>`HDDOLBY_PASSKEY`:密钥 |
|
||||
| zmpt | `ZMPT_UID`:用户ID<br/>`ZMPT_PASSKEY`:密钥 |
|
||||
| freefarm | `FREEFARM_UID`:用户ID<br/>`FREEFARM_PASSKEY`:密钥 |
|
||||
| hdfans | `HDFANS_UID`:用户ID<br/>`HDFANS_PASSKEY`:密钥 |
|
||||
| wintersakura | `WINTERSAKURA_UID`:用户ID<br/>`WINTERSAKURA_PASSKEY`:密钥 |
|
||||
| leaves | `LEAVES_UID`:用户ID<br/>`LEAVES_PASSKEY`:密钥 |
|
||||
| 1ptba | `1PTBA_UID`:用户ID<br/>`1PTBA_PASSKEY`:密钥 |
|
||||
| icc2022 | `ICC2022_UID`:用户ID<br/>`ICC2022_PASSKEY`:密钥 |
|
||||
| leaves | `LEAVES_UID`:用户ID<br/>`LEAVES_PASSKEY`:密钥 |
|
||||
| 1ptba | `1PTBA_UID`:用户ID<br/>`1PTBA_PASSKEY`:密钥 |
|
||||
| icc2022 | `ICC2022_UID`:用户ID<br/>`ICC2022_PASSKEY`:密钥 |
|
||||
| ptlsp | `PTLSP_UID`:用户ID<br/>`PTLSP_PASSKEY`:密钥 |
|
||||
|
||||
|
||||
### 2. **进阶配置**
|
||||
|
||||
40
alembic/versions/232dfa044617_1_0_6.py
Normal file
40
alembic/versions/232dfa044617_1_0_6.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""1.0.6
|
||||
|
||||
Revision ID: 232dfa044617
|
||||
Revises: e734c7fe6056
|
||||
Create Date: 2023-09-19 21:34:41.994617
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '232dfa044617'
|
||||
down_revision = 'e734c7fe6056'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# 搜索优先级
|
||||
op.execute("delete from systemconfig where key = 'SearchFilterRules';")
|
||||
op.execute(
|
||||
"insert into systemconfig(key, value) VALUES('SearchFilterRules', (select value from systemconfig where key= 'FilterRules'));")
|
||||
# 订阅优先级
|
||||
op.execute("delete from systemconfig where key = 'SubscribeFilterRules';")
|
||||
op.execute(
|
||||
"insert into systemconfig(key, value) VALUES('SubscribeFilterRules', (select value from systemconfig where key= 'FilterRules'));")
|
||||
# 洗版优先级
|
||||
op.execute("delete from systemconfig where key = 'BestVersionFilterRules';")
|
||||
op.execute(
|
||||
"insert into systemconfig(key, value) VALUES('BestVersionFilterRules', (select value from systemconfig where key= 'FilterRules2'));")
|
||||
# 删除旧的优先级规则
|
||||
op.execute("delete from systemconfig where key = 'FilterRules';")
|
||||
op.execute("delete from systemconfig where key = 'FilterRules2';")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -6,8 +6,6 @@ Create Date: 2023-08-28 13:21:45.152012
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '52ab4930be04'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.endpoints import login, user, site, message, webhook, subscribe, \
|
||||
media, douban, search, plugin, tmdb, history, system, download, dashboard, rss, filebrowser, transfer
|
||||
media, douban, search, plugin, tmdb, history, system, download, dashboard, filebrowser, transfer
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(login.router, prefix="/login", tags=["login"])
|
||||
@@ -19,6 +19,5 @@ api_router.include_router(system.router, prefix="/system", tags=["system"])
|
||||
api_router.include_router(plugin.router, prefix="/plugin", tags=["plugin"])
|
||||
api_router.include_router(download.router, prefix="/download", tags=["download"])
|
||||
api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"])
|
||||
api_router.include_router(rss.router, prefix="/rss", tags=["rss"])
|
||||
api_router.include_router(filebrowser.router, prefix="/filebrowser", tags=["filebrowser"])
|
||||
api_router.include_router(transfer.router, prefix="/transfer", tags=["transfer"])
|
||||
|
||||
@@ -41,12 +41,7 @@ def storage(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询存储空间信息
|
||||
"""
|
||||
if settings.LIBRARY_PATH:
|
||||
total_storage, free_storage = SystemUtils.space_usage(
|
||||
[Path(path) for path in settings.LIBRARY_PATH.split(",")]
|
||||
)
|
||||
else:
|
||||
total_storage, free_storage = 0, 0
|
||||
total_storage, free_storage = SystemUtils.space_usage(settings.LIBRARY_PATHS)
|
||||
return schemas.Storage(
|
||||
total_storage=total_storage,
|
||||
used_storage=total_storage - free_storage
|
||||
|
||||
@@ -64,14 +64,13 @@ def wechat_verify(echostr: str, msg_signature: str,
|
||||
|
||||
|
||||
@router.get("/switchs", summary="查询通知消息渠道开关", response_model=List[NotificationSwitch])
|
||||
def read_switchs(db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
def read_switchs(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询通知消息渠道开关
|
||||
"""
|
||||
return_list = []
|
||||
# 读取数据库
|
||||
switchs = SystemConfigOper(db).get(SystemConfigKey.NotificationChannels)
|
||||
switchs = SystemConfigOper().get(SystemConfigKey.NotificationChannels)
|
||||
if not switchs:
|
||||
for noti in NotificationType:
|
||||
return_list.append(NotificationSwitch(mtype=noti.value, wechat=True, telegram=True, slack=True))
|
||||
@@ -83,7 +82,6 @@ def read_switchs(db: Session = Depends(get_db),
|
||||
|
||||
@router.post("/switchs", summary="设置通知消息渠道开关", response_model=schemas.Response)
|
||||
def set_switchs(switchs: List[NotificationSwitch],
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询通知消息渠道开关
|
||||
@@ -92,6 +90,6 @@ def set_switchs(switchs: List[NotificationSwitch],
|
||||
for switch in switchs:
|
||||
switch_list.append(switch.dict())
|
||||
# 存入数据库
|
||||
SystemConfigOper(db).set(SystemConfigKey.NotificationChannels, switch_list)
|
||||
SystemConfigOper().set(SystemConfigKey.NotificationChannels, switch_list)
|
||||
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@@ -22,28 +22,26 @@ def all_plugins(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
|
||||
|
||||
@router.get("/installed", summary="已安装插件", response_model=List[str])
|
||||
def installed_plugins(db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
def installed_plugins(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询用户已安装插件清单
|
||||
"""
|
||||
return SystemConfigOper(db).get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
return SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
|
||||
|
||||
@router.get("/install/{plugin_id}", summary="安装插件", response_model=schemas.Response)
|
||||
def install_plugin(plugin_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
安装插件
|
||||
"""
|
||||
# 已安装插件
|
||||
install_plugins = SystemConfigOper(db).get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
install_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
# 安装插件
|
||||
if plugin_id not in install_plugins:
|
||||
install_plugins.append(plugin_id)
|
||||
# 保存设置
|
||||
SystemConfigOper(db).set(SystemConfigKey.UserInstalledPlugins, install_plugins)
|
||||
SystemConfigOper().set(SystemConfigKey.UserInstalledPlugins, install_plugins)
|
||||
# 重载插件管理器
|
||||
PluginManager().init_config()
|
||||
return schemas.Response(success=True)
|
||||
@@ -93,19 +91,18 @@ def set_plugin_config(plugin_id: str, conf: dict,
|
||||
|
||||
@router.delete("/{plugin_id}", summary="卸载插件", response_model=schemas.Response)
|
||||
def uninstall_plugin(plugin_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
卸载插件
|
||||
"""
|
||||
# 删除已安装信息
|
||||
install_plugins = SystemConfigOper(db).get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
install_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
for plugin in install_plugins:
|
||||
if plugin == plugin_id:
|
||||
install_plugins.remove(plugin)
|
||||
break
|
||||
# 保存
|
||||
SystemConfigOper(db).set(SystemConfigKey.UserInstalledPlugins, install_plugins)
|
||||
SystemConfigOper().set(SystemConfigKey.UserInstalledPlugins, install_plugins)
|
||||
# 重载插件管理器
|
||||
PluginManager().init_config()
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
from typing import List, Any
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
from app import schemas
|
||||
from app.chain.rss import RssChain
|
||||
from app.core.security import verify_token
|
||||
from app.db import get_db
|
||||
from app.db.models.rss import Rss
|
||||
from app.helper.rss import RssHelper
|
||||
from app.schemas import MediaType
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def start_rss_refresh(db: Session, rssid: int = None):
|
||||
"""
|
||||
启动自定义订阅刷新
|
||||
"""
|
||||
RssChain(db).refresh(rssid=rssid, manual=True)
|
||||
|
||||
|
||||
@router.get("/", summary="所有自定义订阅", response_model=List[schemas.Rss])
|
||||
def read_rsses(
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询所有自定义订阅
|
||||
"""
|
||||
return Rss.list(db)
|
||||
|
||||
|
||||
@router.post("/", summary="新增自定义订阅", response_model=schemas.Response)
|
||||
def create_rss(
|
||||
*,
|
||||
rss_in: schemas.Rss,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)
|
||||
) -> Any:
|
||||
"""
|
||||
新增自定义订阅
|
||||
"""
|
||||
if rss_in.type:
|
||||
mtype = MediaType(rss_in.type)
|
||||
else:
|
||||
mtype = None
|
||||
rssid, errormsg = RssChain(db).add(
|
||||
mtype=mtype,
|
||||
**rss_in.dict()
|
||||
)
|
||||
if not rssid:
|
||||
return schemas.Response(success=False, message=errormsg)
|
||||
return schemas.Response(success=True, data={
|
||||
"id": rssid
|
||||
})
|
||||
|
||||
|
||||
@router.put("/", summary="更新自定义订阅", response_model=schemas.Response)
|
||||
def update_rss(
|
||||
*,
|
||||
rss_in: schemas.Rss,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)
|
||||
) -> Any:
|
||||
"""
|
||||
更新自定义订阅信息
|
||||
"""
|
||||
rss = Rss.get(db, rss_in.id)
|
||||
if not rss:
|
||||
return schemas.Response(success=False, message="自定义订阅不存在")
|
||||
|
||||
rss.update(db, rss_in.dict())
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/preview/{rssid}", summary="预览自定义订阅", response_model=List[schemas.TorrentInfo])
|
||||
def preview_rss(
|
||||
rssid: int,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
根据ID查询自定义订阅RSS报文
|
||||
"""
|
||||
rssinfo: Rss = Rss.get(db, rssid)
|
||||
if not rssinfo:
|
||||
return []
|
||||
torrents = RssHelper.parse(rssinfo.url, proxy=True if rssinfo.proxy else False) or []
|
||||
return [schemas.TorrentInfo(
|
||||
title=t.get("title"),
|
||||
description=t.get("description"),
|
||||
enclosure=t.get("enclosure"),
|
||||
size=t.get("size"),
|
||||
page_url=t.get("link"),
|
||||
pubdate=t["pubdate"].strftime("%Y-%m-%d %H:%M:%S") if t.get("pubdate") else None,
|
||||
) for t in torrents]
|
||||
|
||||
|
||||
@router.get("/refresh/{rssid}", summary="刷新自定义订阅", response_model=schemas.Response)
|
||||
def refresh_rss(
|
||||
rssid: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
根据ID刷新自定义订阅
|
||||
"""
|
||||
background_tasks.add_task(start_rss_refresh,
|
||||
db=db,
|
||||
rssid=rssid)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/{rssid}", summary="查询自定义订阅详情", response_model=schemas.Rss)
|
||||
def read_rss(
|
||||
rssid: int,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
根据ID查询自定义订阅详情
|
||||
"""
|
||||
return Rss.get(db, rssid)
|
||||
|
||||
|
||||
@router.delete("/{rssid}", summary="删除自定义订阅", response_model=schemas.Response)
|
||||
def read_rss(
|
||||
rssid: int,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
根据ID删除自定义订阅
|
||||
"""
|
||||
Rss.delete(db, rssid)
|
||||
return schemas.Response(success=True)
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import List, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import schemas
|
||||
|
||||
@@ -6,8 +6,8 @@ from starlette.background import BackgroundTasks
|
||||
|
||||
from app import schemas
|
||||
from app.chain.cookiecloud import CookieCloudChain
|
||||
from app.chain.search import SearchChain
|
||||
from app.chain.site import SiteChain
|
||||
from app.chain.torrents import TorrentsChain
|
||||
from app.core.event import EventManager
|
||||
from app.core.security import verify_token
|
||||
from app.db import get_db
|
||||
@@ -117,9 +117,9 @@ def cookie_cloud_sync(db: Session = Depends(get_db),
|
||||
清空所有站点数据并重新同步CookieCloud站点信息
|
||||
"""
|
||||
Site.reset(db)
|
||||
SystemConfigOper(db).set(SystemConfigKey.IndexerSites, [])
|
||||
SystemConfigOper(db).set(SystemConfigKey.RssSites, [])
|
||||
CookieCloudChain(db).process(manual=True)
|
||||
SystemConfigOper().set(SystemConfigKey.IndexerSites, [])
|
||||
SystemConfigOper().set(SystemConfigKey.RssSites, [])
|
||||
CookieCloudChain().process(manual=True)
|
||||
# 插件站点删除
|
||||
EventManager().send_event(EventType.SiteDeleted,
|
||||
{
|
||||
@@ -191,7 +191,7 @@ def site_icon(site_id: int,
|
||||
|
||||
|
||||
@router.get("/resource/{site_id}", summary="站点资源", response_model=List[schemas.TorrentInfo])
|
||||
def site_resource(site_id: int, keyword: str = None,
|
||||
def site_resource(site_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
@@ -203,7 +203,7 @@ def site_resource(site_id: int, keyword: str = None,
|
||||
status_code=404,
|
||||
detail=f"站点 {site_id} 不存在",
|
||||
)
|
||||
torrents = SearchChain(db).browse(site.domain, keyword)
|
||||
torrents = TorrentsChain().browse(domain=site.domain)
|
||||
if not torrents:
|
||||
return []
|
||||
return [torrent.to_dict() for torrent in torrents]
|
||||
@@ -234,14 +234,14 @@ def read_rss_sites(db: Session = Depends(get_db)) -> List[dict]:
|
||||
获取站点列表
|
||||
"""
|
||||
# 选中的rss站点
|
||||
rss_sites = SystemConfigOper(db).get(SystemConfigKey.RssSites)
|
||||
selected_sites = SystemConfigOper().get(SystemConfigKey.RssSites) or []
|
||||
# 所有站点
|
||||
all_site = Site.list_order_by_pri(db)
|
||||
if not rss_sites or not all_site:
|
||||
if not selected_sites or not all_site:
|
||||
return []
|
||||
|
||||
# 选中的rss站点
|
||||
rss_sites = [site for site in all_site if site and site.id in rss_sites]
|
||||
rss_sites = [site for site in all_site if site and site.id in selected_sites]
|
||||
return rss_sites
|
||||
|
||||
|
||||
|
||||
@@ -138,6 +138,53 @@ def subscribe_mediaid(
|
||||
return result if result else Subscribe()
|
||||
|
||||
|
||||
@router.get("/refresh", summary="刷新订阅", response_model=schemas.Response)
|
||||
def refresh_subscribes(
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
刷新所有订阅
|
||||
"""
|
||||
SubscribeChain(db).refresh()
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/check", summary="刷新订阅 TMDB 信息", response_model=schemas.Response)
|
||||
def check_subscribes(
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
刷新所有订阅
|
||||
"""
|
||||
SubscribeChain(db).check()
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/search", summary="搜索所有订阅", response_model=schemas.Response)
|
||||
def search_subscribes(
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
搜索所有订阅
|
||||
"""
|
||||
background_tasks.add_task(start_subscribe_search, db=db, sid=None, state='R')
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/search/{subscribe_id}", summary="搜索订阅", response_model=schemas.Response)
|
||||
def search_subscribe(
|
||||
subscribe_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
根据订阅编号搜索订阅
|
||||
"""
|
||||
background_tasks.add_task(start_subscribe_search, db=db, sid=subscribe_id, state=None)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/{subscribe_id}", summary="订阅详情", response_model=schemas.Subscribe)
|
||||
def read_subscribe(
|
||||
subscribe_id: int,
|
||||
@@ -243,39 +290,3 @@ async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
|
||||
username=user_name)
|
||||
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/refresh", summary="刷新订阅", response_model=schemas.Response)
|
||||
def refresh_subscribes(
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
刷新所有订阅
|
||||
"""
|
||||
SubscribeChain(db).refresh()
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/search/{subscribe_id}", summary="搜索订阅", response_model=schemas.Response)
|
||||
def search_subscribe(
|
||||
subscribe_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
搜索所有订阅
|
||||
"""
|
||||
background_tasks.add_task(start_subscribe_search, db=db, sid=subscribe_id, state=None)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/search", summary="搜索所有订阅", response_model=schemas.Response)
|
||||
def search_subscribes(
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
搜索所有订阅
|
||||
"""
|
||||
background_tasks.add_task(start_subscribe_search, db=db, sid=None, state='R')
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@@ -25,7 +25,7 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/env", summary="查询系统环境变量", response_model=schemas.Response)
|
||||
def get_setting(_: schemas.TokenPayload = Depends(verify_token)):
|
||||
def get_env_setting(_: schemas.TokenPayload = Depends(verify_token)):
|
||||
"""
|
||||
查询系统环境变量,包括当前版本号
|
||||
"""
|
||||
@@ -63,29 +63,27 @@ def get_progress(process_type: str, token: str):
|
||||
|
||||
@router.get("/setting/{key}", summary="查询系统设置", response_model=schemas.Response)
|
||||
def get_setting(key: str,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)):
|
||||
"""
|
||||
查询系统设置
|
||||
"""
|
||||
return schemas.Response(success=True, data={
|
||||
"value": SystemConfigOper(db).get(key)
|
||||
"value": SystemConfigOper().get(key)
|
||||
})
|
||||
|
||||
|
||||
@router.post("/setting/{key}", summary="更新系统设置", response_model=schemas.Response)
|
||||
def set_setting(key: str, value: Union[list, dict, str, int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)):
|
||||
"""
|
||||
更新系统设置
|
||||
"""
|
||||
SystemConfigOper(db).set(key, value)
|
||||
SystemConfigOper().set(key, value)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/message", summary="实时消息")
|
||||
def get_progress(token: str):
|
||||
def get_message(token: str):
|
||||
"""
|
||||
实时获取系统消息,返回格式为SSE
|
||||
"""
|
||||
@@ -171,31 +169,33 @@ def latest_version(_: schemas.TokenPayload = Depends(verify_token)):
|
||||
return schemas.Response(success=False)
|
||||
|
||||
|
||||
@router.get("/ruletest", summary="过滤规则测试", response_model=schemas.Response)
|
||||
@router.get("/ruletest", summary="优先级规则测试", response_model=schemas.Response)
|
||||
def ruletest(title: str,
|
||||
subtitle: str = None,
|
||||
ruletype: str = None,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)):
|
||||
"""
|
||||
过滤规则测试,规则类型 1-订阅,2-洗版
|
||||
过滤规则测试,规则类型 1-订阅,2-洗版,3-搜索
|
||||
"""
|
||||
torrent = schemas.TorrentInfo(
|
||||
title=title,
|
||||
description=subtitle,
|
||||
)
|
||||
if ruletype == "2":
|
||||
rule_string = SystemConfigOper(db).get(SystemConfigKey.FilterRules2)
|
||||
rule_string = SystemConfigOper().get(SystemConfigKey.BestVersionFilterRules)
|
||||
elif ruletype == "3":
|
||||
rule_string = SystemConfigOper().get(SystemConfigKey.SearchFilterRules)
|
||||
else:
|
||||
rule_string = SystemConfigOper(db).get(SystemConfigKey.FilterRules)
|
||||
rule_string = SystemConfigOper().get(SystemConfigKey.SubscribeFilterRules)
|
||||
if not rule_string:
|
||||
return schemas.Response(success=False, message="过滤规则未设置!")
|
||||
return schemas.Response(success=False, message="优先级规则未设置!")
|
||||
|
||||
# 过滤
|
||||
result = SearchChain(db).filter_torrents(rule_string=rule_string,
|
||||
torrent_list=[torrent])
|
||||
if not result:
|
||||
return schemas.Response(success=False, message="不符合过滤规则!")
|
||||
return schemas.Response(success=False, message="不符合优先级规则!")
|
||||
return schemas.Response(success=True, data={
|
||||
"priority": 100 - result[0].pri_order + 1
|
||||
})
|
||||
|
||||
@@ -132,13 +132,10 @@ def arr_rootfolder(apikey: str) -> Any:
|
||||
status_code=403,
|
||||
detail="认证失败!",
|
||||
)
|
||||
library_path = "/"
|
||||
if settings.LIBRARY_PATH:
|
||||
library_path = settings.LIBRARY_PATH.split(",")[0]
|
||||
return [
|
||||
{
|
||||
"id": 1,
|
||||
"path": library_path,
|
||||
"path": "/" if not settings.LIBRARY_PATHS else str(settings.LIBRARY_PATHS[0]),
|
||||
"accessible": True,
|
||||
"freeSpace": 0,
|
||||
"unmappedFolders": []
|
||||
|
||||
@@ -197,7 +197,7 @@ class ChainBase(metaclass=ABCMeta):
|
||||
return self.run_module("search_medias", meta=meta)
|
||||
|
||||
def search_torrents(self, site: CommentedMap,
|
||||
mediainfo: Optional[MediaInfo] = None,
|
||||
mediainfo: MediaInfo,
|
||||
keyword: str = None,
|
||||
page: int = 0,
|
||||
area: str = "title") -> List[TorrentInfo]:
|
||||
@@ -223,44 +223,45 @@ class ChainBase(metaclass=ABCMeta):
|
||||
|
||||
def filter_torrents(self, rule_string: str,
|
||||
torrent_list: List[TorrentInfo],
|
||||
season_episodes: Dict[int, list] = None) -> List[TorrentInfo]:
|
||||
season_episodes: Dict[int, list] = None,
|
||||
mediainfo: MediaInfo = None) -> List[TorrentInfo]:
|
||||
"""
|
||||
过滤种子资源
|
||||
:param rule_string: 过滤规则
|
||||
:param torrent_list: 资源列表
|
||||
:param season_episodes: 季集数过滤 {season:[episodes]}
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:return: 过滤后的资源列表,添加资源优先级
|
||||
"""
|
||||
return self.run_module("filter_torrents", rule_string=rule_string,
|
||||
torrent_list=torrent_list, season_episodes=season_episodes)
|
||||
torrent_list=torrent_list, season_episodes=season_episodes,
|
||||
mediainfo=mediainfo)
|
||||
|
||||
def download(self, torrent_path: Path, download_dir: Path, cookie: str,
|
||||
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
|
||||
episodes: Set[int] = None, category: str = None
|
||||
) -> Optional[Tuple[Optional[str], str]]:
|
||||
"""
|
||||
根据种子文件,选择并添加下载任务
|
||||
:param torrent_path: 种子文件地址
|
||||
:param content: 种子文件地址或者磁力链接
|
||||
:param download_dir: 下载目录
|
||||
:param cookie: cookie
|
||||
:param episodes: 需要下载的集数
|
||||
:param category: 种子分类
|
||||
:return: 种子Hash,错误信息
|
||||
"""
|
||||
return self.run_module("download", torrent_path=torrent_path, download_dir=download_dir,
|
||||
return self.run_module("download", content=content, download_dir=download_dir,
|
||||
cookie=cookie, episodes=episodes, category=category)
|
||||
|
||||
def download_added(self, context: Context, torrent_path: Path, download_dir: Path) -> None:
|
||||
def download_added(self, context: Context, download_dir: Path, torrent_path: Path = None) -> None:
|
||||
"""
|
||||
添加下载任务成功后,从站点下载字幕,保存到下载目录
|
||||
:param context: 上下文,包括识别信息、媒体信息、种子信息
|
||||
:param torrent_path: 种子文件地址
|
||||
:param download_dir: 下载目录
|
||||
:param torrent_path: 种子文件地址
|
||||
:return: None,该方法可被多个模块同时处理
|
||||
"""
|
||||
if settings.DOWNLOAD_SUBTITLE:
|
||||
return self.run_module("download_added", context=context, torrent_path=torrent_path,
|
||||
download_dir=download_dir)
|
||||
return None
|
||||
return self.run_module("download_added", context=context, torrent_path=torrent_path,
|
||||
download_dir=download_dir)
|
||||
|
||||
def list_torrents(self, status: TorrentStatus = None,
|
||||
hashs: Union[list, str] = None) -> Optional[List[Union[TransferTorrent, DownloadingTorrent]]]:
|
||||
@@ -342,9 +343,7 @@ class ChainBase(metaclass=ABCMeta):
|
||||
:param file_path: 文件路径
|
||||
:return: 成功或失败
|
||||
"""
|
||||
if settings.REFRESH_MEDIASERVER:
|
||||
return self.run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path)
|
||||
return None
|
||||
return self.run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path)
|
||||
|
||||
def post_message(self, message: Notification) -> None:
|
||||
"""
|
||||
@@ -392,9 +391,7 @@ class ChainBase(metaclass=ABCMeta):
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:return: 成功或失败
|
||||
"""
|
||||
if settings.SCRAP_METADATA:
|
||||
return self.run_module("scrape_metadata", path=path, mediainfo=mediainfo)
|
||||
return None
|
||||
return self.run_module("scrape_metadata", path=path, mediainfo=mediainfo)
|
||||
|
||||
def register_commands(self, commands: Dict[str, dict]) -> None:
|
||||
"""
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.db.siteicon_oper import SiteIconOper
|
||||
from app.helper.cloudflare import under_challenge
|
||||
from app.helper.cookiecloud import CookieCloudHelper
|
||||
from app.helper.message import MessageHelper
|
||||
from app.helper.rss import RssHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.log import logger
|
||||
from app.schemas import Notification, NotificationType, MessageChannel
|
||||
@@ -30,6 +31,7 @@ class CookieCloudChain(ChainBase):
|
||||
self.siteoper = SiteOper(self._db)
|
||||
self.siteiconoper = SiteIconOper(self._db)
|
||||
self.siteshelper = SitesHelper()
|
||||
self.rsshelper = RssHelper()
|
||||
self.sitechain = SiteChain(self._db)
|
||||
self.message = MessageHelper()
|
||||
self.cookiecloud = CookieCloudHelper(
|
||||
@@ -78,8 +80,23 @@ class CookieCloudChain(ChainBase):
|
||||
# 更新站点Cookie
|
||||
if status:
|
||||
logger.info(f"站点【{site_info.name}】连通性正常,不同步CookieCloud数据")
|
||||
# 更新站点rss地址
|
||||
if not site_info.public and not site_info.rss:
|
||||
# 自动生成rss地址
|
||||
rss_url, errmsg = self.rsshelper.get_rss_link(
|
||||
url=site_info.url,
|
||||
cookie=cookie,
|
||||
ua=settings.USER_AGENT,
|
||||
proxy=True if site_info.proxy else False
|
||||
)
|
||||
if rss_url:
|
||||
logger.info(f"更新站点 {domain} RSS地址 ...")
|
||||
self.siteoper.update_rss(domain=domain, rss=rss_url)
|
||||
else:
|
||||
logger.warn(errmsg)
|
||||
continue
|
||||
# 更新站点Cookie
|
||||
logger.info(f"更新站点 {domain} Cookie ...")
|
||||
self.siteoper.update_cookie(domain=domain, cookies=cookie)
|
||||
_update_count += 1
|
||||
elif indexer:
|
||||
@@ -104,12 +121,25 @@ class CookieCloudChain(ChainBase):
|
||||
_fail_count += 1
|
||||
logger.warn(f"站点 {indexer.get('name')} 连接失败,无法添加站点")
|
||||
continue
|
||||
# 获取rss地址
|
||||
rss_url = None
|
||||
if not indexer.get("public") and indexer.get("domain"):
|
||||
# 自动生成rss地址
|
||||
rss_url, errmsg = self.rsshelper.get_rss_link(url=indexer.get("domain"),
|
||||
cookie=cookie,
|
||||
ua=settings.USER_AGENT)
|
||||
if errmsg:
|
||||
logger.warn(errmsg)
|
||||
# 插入数据库
|
||||
logger.info(f"新增站点 {indexer.get('name')} ...")
|
||||
self.siteoper.add(name=indexer.get("name"),
|
||||
url=indexer.get("domain"),
|
||||
domain=domain,
|
||||
cookie=cookie,
|
||||
rss=rss_url,
|
||||
public=1 if indexer.get("public") else 0)
|
||||
_add_count += 1
|
||||
|
||||
# 保存站点图标
|
||||
if indexer:
|
||||
site_icon = self.siteiconoper.get_by_domain(domain)
|
||||
|
||||
@@ -48,9 +48,12 @@ class DownloadChain(ChainBase):
|
||||
msg_text = f"{msg_text}\n大小:{size}"
|
||||
if torrent.title:
|
||||
msg_text = f"{msg_text}\n种子:{torrent.title}"
|
||||
if torrent.pubdate:
|
||||
msg_text = f"{msg_text}\n发布时间:{torrent.pubdate}"
|
||||
if torrent.seeders:
|
||||
msg_text = f"{msg_text}\n做种数:{torrent.seeders}"
|
||||
msg_text = f"{msg_text}\n促销:{torrent.volume_factor}"
|
||||
if torrent.uploadvolumefactor and torrent.downloadvolumefactor:
|
||||
msg_text = f"{msg_text}\n促销:{torrent.volume_factor}"
|
||||
if torrent.hit_and_run:
|
||||
msg_text = f"{msg_text}\nHit&Run:是"
|
||||
if torrent.description:
|
||||
@@ -70,25 +73,33 @@ class DownloadChain(ChainBase):
|
||||
|
||||
def download_torrent(self, torrent: TorrentInfo,
|
||||
channel: MessageChannel = None,
|
||||
userid: Union[str, int] = None) -> Tuple[Optional[Path], str, list]:
|
||||
userid: Union[str, int] = None
|
||||
) -> Tuple[Optional[Union[Path, str]], str, list]:
|
||||
"""
|
||||
下载种子文件
|
||||
下载种子文件,如果是磁力链,会返回磁力链接本身
|
||||
:return: 种子路径,种子目录名,种子文件清单
|
||||
"""
|
||||
torrent_file, _, download_folder, files, error_msg = self.torrent.download_torrent(
|
||||
torrent_file, content, download_folder, files, error_msg = self.torrent.download_torrent(
|
||||
url=torrent.enclosure,
|
||||
cookie=torrent.site_cookie,
|
||||
ua=torrent.site_ua,
|
||||
proxy=torrent.site_proxy)
|
||||
|
||||
if isinstance(content, str):
|
||||
# 磁力链
|
||||
return content, "", []
|
||||
|
||||
if not torrent_file:
|
||||
logger.error(f"下载种子文件失败:{torrent.title} - {torrent.enclosure}")
|
||||
self.post_message(Notification(
|
||||
channel=channel,
|
||||
mtype=NotificationType.Manual,
|
||||
title=f"{torrent.title} 种子下载失败!",
|
||||
text=f"错误信息:{error_msg}\n种子链接:{torrent.enclosure}",
|
||||
text=f"错误信息:{error_msg}\n站点:{torrent.site_name}",
|
||||
userid=userid))
|
||||
return None, "", []
|
||||
|
||||
# 返回 种子文件路径,种子目录名,种子文件清单
|
||||
return torrent_file, download_folder, files
|
||||
|
||||
def download_single(self, context: Context, torrent_file: Path = None,
|
||||
@@ -98,19 +109,27 @@ class DownloadChain(ChainBase):
|
||||
userid: Union[str, int] = None) -> Optional[str]:
|
||||
"""
|
||||
下载及发送通知
|
||||
:param context: 资源上下文
|
||||
:param torrent_file: 种子文件路径
|
||||
:param episodes: 需要下载的集数
|
||||
:param channel: 通知渠道
|
||||
:param save_path: 保存路径
|
||||
:param userid: 用户ID
|
||||
"""
|
||||
_torrent = context.torrent_info
|
||||
_media = context.media_info
|
||||
_meta = context.meta_info
|
||||
_folder_name = ""
|
||||
if not torrent_file:
|
||||
# 下载种子文件
|
||||
torrent_file, _folder_name, _file_list = self.download_torrent(_torrent, userid=userid)
|
||||
if not torrent_file:
|
||||
# 下载种子文件,得到的可能是文件也可能是磁力链
|
||||
content, _folder_name, _file_list = self.download_torrent(_torrent, userid=userid)
|
||||
if not content:
|
||||
return
|
||||
else:
|
||||
content = torrent_file
|
||||
# 获取种子文件的文件夹名和文件清单
|
||||
_folder_name, _file_list = self.torrent.get_torrent_info(torrent_file)
|
||||
|
||||
# 下载目录
|
||||
if not save_path:
|
||||
if settings.DOWNLOAD_CATEGORY and _media and _media.category:
|
||||
@@ -149,7 +168,7 @@ class DownloadChain(ChainBase):
|
||||
download_dir = Path(save_path)
|
||||
|
||||
# 添加下载
|
||||
result: Optional[tuple] = self.download(torrent_path=torrent_file,
|
||||
result: Optional[tuple] = self.download(content=content,
|
||||
cookie=_torrent.site_cookie,
|
||||
episodes=episodes,
|
||||
download_dir=download_dir,
|
||||
@@ -206,13 +225,12 @@ class DownloadChain(ChainBase):
|
||||
self.downloadhis.add_files(files_to_add)
|
||||
|
||||
# 发送消息
|
||||
self.post_download_message(meta=_meta, mediainfo=_media, torrent=_torrent, channel=channel)
|
||||
self.post_download_message(meta=_meta, mediainfo=_media, torrent=_torrent, channel=channel, userid=userid)
|
||||
# 下载成功后处理
|
||||
self.download_added(context=context, torrent_path=torrent_file, download_dir=download_dir)
|
||||
self.download_added(context=context, download_dir=download_dir, torrent_path=torrent_file)
|
||||
# 广播事件
|
||||
self.eventmanager.send_event(EventType.DownloadAdded, {
|
||||
"hash": _hash,
|
||||
"torrent_file": torrent_file,
|
||||
"context": context
|
||||
})
|
||||
else:
|
||||
@@ -226,7 +244,6 @@ class DownloadChain(ChainBase):
|
||||
% (_media.title_year, _meta.season_episode),
|
||||
text=f"站点:{_torrent.site_name}\n"
|
||||
f"种子名称:{_meta.org_string}\n"
|
||||
f"种子链接:{_torrent.enclosure}\n"
|
||||
f"错误信息:{error_msg}",
|
||||
image=_media.get_message_image(),
|
||||
userid=userid))
|
||||
@@ -347,8 +364,11 @@ class DownloadChain(ChainBase):
|
||||
if set(torrent_season).issubset(set(need_season)):
|
||||
if len(torrent_season) == 1:
|
||||
# 只有一季的可能是命名错误,需要打开种子鉴别,只有实际集数大于等于总集数才下载
|
||||
torrent_path, _, torrent_files = self.download_torrent(torrent)
|
||||
if not torrent_path:
|
||||
content, _, torrent_files = self.download_torrent(torrent)
|
||||
if not content:
|
||||
continue
|
||||
if isinstance(content, str):
|
||||
logger.warn(f"{meta.org_string} 下载地址是磁力链,无法确定种子文件集数")
|
||||
continue
|
||||
torrent_episodes = self.torrent.get_torrent_episodes(torrent_files)
|
||||
logger.info(f"{meta.org_string} 解析文件集数为 {torrent_episodes}")
|
||||
@@ -366,10 +386,12 @@ class DownloadChain(ChainBase):
|
||||
continue
|
||||
else:
|
||||
# 下载
|
||||
download_id = self.download_single(context=context,
|
||||
torrent_file=torrent_path,
|
||||
save_path=save_path,
|
||||
userid=userid)
|
||||
download_id = self.download_single(
|
||||
context=context,
|
||||
torrent_file=content if isinstance(content, Path) else None,
|
||||
save_path=save_path,
|
||||
userid=userid
|
||||
)
|
||||
else:
|
||||
# 下载
|
||||
download_id = self.download_single(context, save_path=save_path, userid=userid)
|
||||
@@ -486,8 +508,11 @@ class DownloadChain(ChainBase):
|
||||
and len(meta.season_list) == 1 \
|
||||
and meta.season_list[0] == need_season:
|
||||
# 检查种子看是否有需要的集
|
||||
torrent_path, _, torrent_files = self.download_torrent(torrent, userid=userid)
|
||||
if not torrent_path:
|
||||
content, _, torrent_files = self.download_torrent(torrent, userid=userid)
|
||||
if not content:
|
||||
continue
|
||||
if isinstance(content, str):
|
||||
logger.warn(f"{meta.org_string} 下载地址是磁力链,无法解析种子文件集数")
|
||||
continue
|
||||
# 种子全部集
|
||||
torrent_episodes = self.torrent.get_torrent_episodes(torrent_files)
|
||||
@@ -499,11 +524,13 @@ class DownloadChain(ChainBase):
|
||||
continue
|
||||
logger.info(f"{torrent.site_name} - {torrent.title} 选中集数:{selected_episodes}")
|
||||
# 添加下载
|
||||
download_id = self.download_single(context=context,
|
||||
torrent_file=torrent_path,
|
||||
episodes=selected_episodes,
|
||||
save_path=save_path,
|
||||
userid=userid)
|
||||
download_id = self.download_single(
|
||||
context=context,
|
||||
torrent_file=content if isinstance(content, Path) else None,
|
||||
episodes=selected_episodes,
|
||||
save_path=save_path,
|
||||
userid=userid
|
||||
)
|
||||
if not download_id:
|
||||
continue
|
||||
# 把识别的集更新到上下文
|
||||
|
||||
@@ -214,6 +214,11 @@ class MessageChain(ChainBase):
|
||||
start = _current_page * self._page_size
|
||||
end = start + self._page_size
|
||||
if cache_type == "Torrent":
|
||||
# 更新缓存
|
||||
user_cache[userid] = {
|
||||
"type": "Torrent",
|
||||
"items": cache_list[start:end]
|
||||
}
|
||||
# 发送种子数据
|
||||
self.__post_torrents_message(channel=channel,
|
||||
title=_current_media.title,
|
||||
@@ -251,6 +256,11 @@ class MessageChain(ChainBase):
|
||||
# 加一页
|
||||
_current_page += 1
|
||||
if cache_type == "Torrent":
|
||||
# 更新缓存
|
||||
user_cache[userid] = {
|
||||
"type": "Torrent",
|
||||
"items": cache_list
|
||||
}
|
||||
# 发送种子数据
|
||||
self.__post_torrents_message(channel=channel,
|
||||
title=_current_media.title,
|
||||
@@ -270,7 +280,7 @@ class MessageChain(ChainBase):
|
||||
elif text.startswith("#") \
|
||||
or re.search(r"^请[问帮你]", text) \
|
||||
or re.search(r"[??]$", text) \
|
||||
or StringUtils.count_words(text) > 15 \
|
||||
or StringUtils.count_words(text) > 10 \
|
||||
or text.find("继续") != -1:
|
||||
# 聊天
|
||||
content = text
|
||||
|
||||
320
app/chain/rss.py
320
app/chain/rss.py
@@ -1,320 +0,0 @@
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.chain.download import DownloadChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import Context, TorrentInfo, MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.db.rss_oper import RssOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.message import MessageHelper
|
||||
from app.helper.rss import RssHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.log import logger
|
||||
from app.schemas import Notification, NotExistMediaInfo
|
||||
from app.schemas.types import SystemConfigKey, MediaType, NotificationType
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
class RssChain(ChainBase):
|
||||
"""
|
||||
RSS处理链
|
||||
"""
|
||||
|
||||
def __init__(self, db: Session = None):
|
||||
super().__init__(db)
|
||||
self.rssoper = RssOper(self._db)
|
||||
self.sites = SitesHelper()
|
||||
self.systemconfig = SystemConfigOper(self._db)
|
||||
self.downloadchain = DownloadChain(self._db)
|
||||
self.message = MessageHelper()
|
||||
|
||||
def add(self, title: str, year: str,
|
||||
mtype: MediaType = None,
|
||||
season: int = None,
|
||||
**kwargs) -> Tuple[Optional[int], str]:
|
||||
"""
|
||||
识别媒体信息并添加订阅
|
||||
"""
|
||||
logger.info(f'开始添加自定义订阅,标题:{title} ...')
|
||||
|
||||
# 识别元数据
|
||||
metainfo = MetaInfo(title)
|
||||
if year:
|
||||
metainfo.year = year
|
||||
if mtype:
|
||||
metainfo.type = mtype
|
||||
if season:
|
||||
metainfo.type = MediaType.TV
|
||||
metainfo.begin_season = season
|
||||
|
||||
# 识别媒体信息
|
||||
mediainfo: MediaInfo = self.recognize_media(meta=metainfo)
|
||||
if not mediainfo:
|
||||
logger.warn(f'{title} 未识别到媒体信息')
|
||||
return None, "未识别到媒体信息"
|
||||
|
||||
# 更新媒体图片
|
||||
self.obtain_images(mediainfo=mediainfo)
|
||||
|
||||
# 总集数
|
||||
if mediainfo.type == MediaType.TV:
|
||||
if not season:
|
||||
season = 1
|
||||
# 总集数
|
||||
if not kwargs.get('total_episode'):
|
||||
if not mediainfo.seasons:
|
||||
# 补充媒体信息
|
||||
mediainfo: MediaInfo = self.recognize_media(mtype=mediainfo.type,
|
||||
tmdbid=mediainfo.tmdb_id)
|
||||
if not mediainfo:
|
||||
logger.error(f"媒体信息识别失败!")
|
||||
return None, "媒体信息识别失败"
|
||||
if not mediainfo.seasons:
|
||||
logger.error(f"{title} 媒体信息中没有季集信息")
|
||||
return None, "媒体信息中没有季集信息"
|
||||
total_episode = len(mediainfo.seasons.get(season) or [])
|
||||
if not total_episode:
|
||||
logger.error(f'{title} 未获取到总集数')
|
||||
return None, "未获取到总集数"
|
||||
kwargs.update({
|
||||
'total_episode': total_episode
|
||||
})
|
||||
|
||||
# 检查是否存在
|
||||
if self.rssoper.exists(tmdbid=mediainfo.tmdb_id, season=season):
|
||||
logger.warn(f'{mediainfo.title} 已存在')
|
||||
return None, f'{mediainfo.title} 自定义订阅已存在'
|
||||
if not kwargs.get("name"):
|
||||
kwargs.update({
|
||||
"name": mediainfo.title
|
||||
})
|
||||
kwargs.update({
|
||||
"tmdbid": mediainfo.tmdb_id,
|
||||
"poster": mediainfo.get_poster_image(),
|
||||
"backdrop": mediainfo.get_backdrop_image(),
|
||||
"vote": mediainfo.vote_average,
|
||||
"description": mediainfo.overview,
|
||||
})
|
||||
|
||||
# 添加订阅
|
||||
sid = self.rssoper.add(title=title, year=year, season=season, **kwargs)
|
||||
if not sid:
|
||||
logger.error(f'{mediainfo.title_year} 添加自定义订阅失败')
|
||||
return None, "添加自定义订阅失败"
|
||||
else:
|
||||
logger.info(f'{mediainfo.title_year} {metainfo.season} 添加订阅成功')
|
||||
|
||||
# 返回结果
|
||||
return sid, ""
|
||||
|
||||
def refresh(self, rssid: int = None, manual: bool = False):
|
||||
"""
|
||||
刷新RSS订阅数据
|
||||
"""
|
||||
# 所有RSS订阅
|
||||
logger.info("开始刷新RSS订阅数据 ...")
|
||||
rss_tasks = self.rssoper.list(rssid) or []
|
||||
for rss_task in rss_tasks:
|
||||
if not rss_task:
|
||||
continue
|
||||
if not rss_task.url:
|
||||
continue
|
||||
|
||||
# 下载Rss报文
|
||||
items = RssHelper.parse(rss_task.url, True if rss_task.proxy else False)
|
||||
if not items:
|
||||
logger.error(f"RSS未下载到数据:{rss_task.url}")
|
||||
logger.info(f"{rss_task.name} RSS下载到数据:{len(items)}")
|
||||
|
||||
# 检查站点
|
||||
domain = StringUtils.get_url_domain(rss_task.url)
|
||||
site_info = self.sites.get_indexer(domain) or {}
|
||||
|
||||
# 过滤规则
|
||||
if rss_task.best_version:
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.FilterRules2)
|
||||
else:
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.FilterRules)
|
||||
|
||||
# 处理RSS条目
|
||||
matched_contexts = []
|
||||
|
||||
# 处理过的title
|
||||
processed_data = json.loads(rss_task.note) if rss_task.note else {
|
||||
"titles": [],
|
||||
"season_episodes": []
|
||||
}
|
||||
|
||||
for item in items:
|
||||
if not item.get("title"):
|
||||
continue
|
||||
|
||||
# 标题是否已处理过
|
||||
if item.get("title") in processed_data.get('titles'):
|
||||
logger.info(f"{item.get('title')} 已处理过")
|
||||
continue
|
||||
|
||||
# 基本要素匹配
|
||||
if rss_task.include \
|
||||
and not re.search(r"%s" % rss_task.include, item.get("title")):
|
||||
logger.info(f"{item.get('title')} 未包含 {rss_task.include}")
|
||||
continue
|
||||
if rss_task.exclude \
|
||||
and re.search(r"%s" % rss_task.exclude, item.get("title")):
|
||||
logger.info(f"{item.get('title')} 包含 {rss_task.exclude}")
|
||||
continue
|
||||
|
||||
# 识别媒体信息
|
||||
meta = MetaInfo(title=item.get("title"), subtitle=item.get("description"))
|
||||
if not meta.name:
|
||||
logger.error(f"{item.get('title')} 未识别到有效信息")
|
||||
continue
|
||||
mediainfo = self.recognize_media(meta=meta)
|
||||
if not mediainfo:
|
||||
logger.error(f"{item.get('title')} 未识别到TMDB媒体信息")
|
||||
continue
|
||||
if mediainfo.tmdb_id != rss_task.tmdbid:
|
||||
logger.error(f"{item.get('title')} 不匹配")
|
||||
continue
|
||||
|
||||
# 季集是否已处理过
|
||||
if meta.season_episode in processed_data.get('season_episodes'):
|
||||
logger.info(f"{meta.org_string} {meta.season_episode} 已处理过")
|
||||
continue
|
||||
|
||||
# 种子
|
||||
torrentinfo = TorrentInfo(
|
||||
site=site_info.get("id"),
|
||||
site_name=site_info.get("name"),
|
||||
site_cookie=site_info.get("cookie"),
|
||||
site_ua=site_info.get("cookie") or settings.USER_AGENT,
|
||||
site_proxy=site_info.get("proxy") or rss_task.proxy,
|
||||
site_order=site_info.get("pri"),
|
||||
title=item.get("title"),
|
||||
description=item.get("description"),
|
||||
enclosure=item.get("enclosure"),
|
||||
page_url=item.get("link"),
|
||||
size=item.get("size"),
|
||||
pubdate=item["pubdate"].strftime("%Y-%m-%d %H:%M:%S") if item.get("pubdate") else None,
|
||||
)
|
||||
|
||||
# 过滤种子
|
||||
if rss_task.filter:
|
||||
result = self.filter_torrents(
|
||||
rule_string=filter_rule,
|
||||
torrent_list=[torrentinfo]
|
||||
)
|
||||
if not result:
|
||||
logger.info(f"{rss_task.name} 不匹配过滤规则")
|
||||
continue
|
||||
|
||||
# 清除多余数据
|
||||
mediainfo.clear()
|
||||
|
||||
# 匹配到的数据
|
||||
matched_contexts.append(Context(
|
||||
meta_info=meta,
|
||||
media_info=mediainfo,
|
||||
torrent_info=torrentinfo
|
||||
))
|
||||
|
||||
# 匹配结果
|
||||
if not matched_contexts:
|
||||
logger.info(f"{rss_task.name} 未匹配到数据")
|
||||
continue
|
||||
|
||||
logger.info(f"{rss_task.name} 匹配到 {len(matched_contexts)} 条数据")
|
||||
|
||||
# 查询本地存在情况
|
||||
if not rss_task.best_version:
|
||||
# 查询缺失的媒体信息
|
||||
rss_meta = MetaInfo(title=rss_task.title)
|
||||
rss_meta.year = rss_task.year
|
||||
rss_meta.begin_season = rss_task.season
|
||||
rss_meta.type = MediaType(rss_task.type)
|
||||
|
||||
# 每季总集数
|
||||
totals = {}
|
||||
if rss_task.season and rss_task.total_episode:
|
||||
totals = {
|
||||
rss_task.season: rss_task.total_episode
|
||||
}
|
||||
|
||||
# 检查缺失
|
||||
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
|
||||
meta=rss_meta,
|
||||
mediainfo=MediaInfo(
|
||||
title=rss_task.title,
|
||||
year=rss_task.year,
|
||||
tmdb_id=rss_task.tmdbid,
|
||||
season=rss_task.season
|
||||
),
|
||||
totals=totals
|
||||
)
|
||||
if exist_flag:
|
||||
logger.info(f'{rss_task.name} 媒体库中已存在,完成订阅')
|
||||
self.rssoper.delete(rss_task.id)
|
||||
# 发送通知
|
||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
||||
title=f'自定义订阅 {rss_task.name} 已完成',
|
||||
image=rss_task.backdrop))
|
||||
continue
|
||||
elif rss_meta.type == MediaType.TV.value:
|
||||
# 打印缺失集信息
|
||||
if no_exists and no_exists.get(rss_task.tmdbid):
|
||||
no_exists_info = no_exists.get(rss_task.tmdbid).get(rss_task.season)
|
||||
if no_exists_info:
|
||||
logger.info(f'订阅 {rss_task.name} 缺失集:{no_exists_info.episodes}')
|
||||
else:
|
||||
if rss_task.type == MediaType.TV.value:
|
||||
no_exists = {
|
||||
rss_task.season: NotExistMediaInfo(
|
||||
season=rss_task.season,
|
||||
episodes=[],
|
||||
total_episode=rss_task.total_episode,
|
||||
start_episode=1)
|
||||
}
|
||||
else:
|
||||
no_exists = {}
|
||||
|
||||
# 开始下载
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=matched_contexts,
|
||||
no_exists=no_exists,
|
||||
save_path=rss_task.save_path)
|
||||
if downloads and not lefts:
|
||||
if not rss_task.best_version:
|
||||
# 非洗版结束订阅
|
||||
self.rssoper.delete(rss_task.id)
|
||||
# 发送通知
|
||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
||||
title=f'自定义订阅 {rss_task.name} 已完成',
|
||||
image=rss_task.backdrop))
|
||||
else:
|
||||
# 未完成下载
|
||||
logger.info(f'{rss_task.name} 未下载未完整,继续订阅 ...')
|
||||
|
||||
if downloads:
|
||||
for download in downloads:
|
||||
meta = download.meta_info
|
||||
# 更新已处理数据
|
||||
processed_data['titles'].append(meta.org_string)
|
||||
processed_data['season_episodes'].append(meta.season_episode)
|
||||
# 更新已处理过的数据
|
||||
self.rssoper.update(rssid=rss_task.id, note=json.dumps(processed_data))
|
||||
# 更新最后更新时间和已处理数量
|
||||
self.rssoper.update(rssid=rss_task.id,
|
||||
processed=(rss_task.processed or 0) + len(downloads),
|
||||
last_update=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
logger.info("刷新RSS订阅数据完成")
|
||||
if manual:
|
||||
if len(rss_tasks) == 1:
|
||||
self.message.put(f"{rss_tasks[0].name} 自定义订阅刷新完成")
|
||||
else:
|
||||
self.message.put(f"自定义订阅刷新完成")
|
||||
@@ -29,7 +29,7 @@ class SearchChain(ChainBase):
|
||||
super().__init__(db)
|
||||
self.siteshelper = SitesHelper()
|
||||
self.progress = ProgressHelper()
|
||||
self.systemconfig = SystemConfigOper(self._db)
|
||||
self.systemconfig = SystemConfigOper()
|
||||
self.torrenthelper = TorrentHelper()
|
||||
|
||||
def search_by_tmdbid(self, tmdbid: int, mtype: MediaType = None, area: str = "title") -> List[Context]:
|
||||
@@ -76,22 +76,6 @@ class SearchChain(ChainBase):
|
||||
print(str(e))
|
||||
return []
|
||||
|
||||
def browse(self, domain: str, keyword: str = None) -> List[TorrentInfo]:
|
||||
"""
|
||||
浏览站点首页内容
|
||||
:param domain: 站点域名
|
||||
:param keyword: 关键词,有值时为搜索
|
||||
"""
|
||||
if not keyword:
|
||||
logger.info(f'开始浏览站点首页内容,站点:{domain} ...')
|
||||
else:
|
||||
logger.info(f'开始搜索资源,关键词:{keyword},站点:{domain} ...')
|
||||
site = self.siteshelper.get_indexer(domain)
|
||||
if not site:
|
||||
logger.error(f'站点 {domain} 不存在!')
|
||||
return []
|
||||
return self.search_torrents(site=site, keyword=keyword)
|
||||
|
||||
def process(self, mediainfo: MediaInfo,
|
||||
keyword: str = None,
|
||||
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
||||
@@ -104,7 +88,7 @@ class SearchChain(ChainBase):
|
||||
:param keyword: 搜索关键词
|
||||
:param no_exists: 缺失的媒体信息
|
||||
:param sites: 站点ID列表,为空时搜索所有站点
|
||||
:param filter_rule: 过滤规则,为空是使用默认过滤规则
|
||||
:param filter_rule: 过滤规则,为空是使用默认搜索过滤规则
|
||||
:param area: 搜索范围,title or imdbid
|
||||
"""
|
||||
logger.info(f'开始搜索资源,关键词:{keyword or mediainfo.title} ...')
|
||||
@@ -146,12 +130,13 @@ class SearchChain(ChainBase):
|
||||
# 过滤种子
|
||||
if filter_rule is None:
|
||||
# 取默认过滤规则
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.FilterRules)
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.SearchFilterRules)
|
||||
if filter_rule:
|
||||
logger.info(f'开始过滤资源,当前规则:{filter_rule} ...')
|
||||
result: List[TorrentInfo] = self.filter_torrents(rule_string=filter_rule,
|
||||
torrent_list=torrents,
|
||||
season_episodes=season_episodes)
|
||||
season_episodes=season_episodes,
|
||||
mediainfo=mediainfo)
|
||||
if result is not None:
|
||||
torrents = result
|
||||
if not torrents:
|
||||
@@ -201,19 +186,30 @@ class SearchChain(ChainBase):
|
||||
str(int(mediainfo.year) + 1)]:
|
||||
logger.warn(f'{torrent.site_name} - {torrent.title} 年份不匹配')
|
||||
continue
|
||||
# 比对标题
|
||||
# 比对标题和原语种标题
|
||||
meta_name = StringUtils.clear_upper(torrent_meta.name)
|
||||
if meta_name in [
|
||||
StringUtils.clear_upper(mediainfo.title),
|
||||
StringUtils.clear_upper(mediainfo.original_title)
|
||||
]:
|
||||
logger.info(f'{mediainfo.title} 匹配到资源:{torrent.site_name} - {torrent.title}')
|
||||
logger.info(f'{mediainfo.title} 通过标题匹配到资源:{torrent.site_name} - {torrent.title}')
|
||||
_match_torrents.append(torrent)
|
||||
continue
|
||||
# 在副标题中判断是否存在标题与原语种标题
|
||||
if torrent.description:
|
||||
subtitle = torrent.description.split()
|
||||
if (StringUtils.is_chinese(mediainfo.title)
|
||||
and str(mediainfo.title) in subtitle) \
|
||||
or (StringUtils.is_chinese(mediainfo.original_title)
|
||||
and str(mediainfo.original_title) in subtitle):
|
||||
logger.info(f'{mediainfo.title} 通过副标题匹配到资源:{torrent.site_name} - {torrent.title},'
|
||||
f'副标题:{torrent.description}')
|
||||
_match_torrents.append(torrent)
|
||||
continue
|
||||
# 比对别名和译名
|
||||
for name in mediainfo.names:
|
||||
if StringUtils.clear_upper(name) == meta_name:
|
||||
logger.info(f'{mediainfo.title} 匹配到资源:{torrent.site_name} - {torrent.title}')
|
||||
logger.info(f'{mediainfo.title} 通过别名或译名匹配到资源:{torrent.site_name} - {torrent.title}')
|
||||
_match_torrents.append(torrent)
|
||||
break
|
||||
else:
|
||||
@@ -252,14 +248,14 @@ class SearchChain(ChainBase):
|
||||
"""
|
||||
# 未开启的站点不搜索
|
||||
indexer_sites = []
|
||||
|
||||
# 配置的索引站点
|
||||
if sites:
|
||||
config_indexers = [str(sid) for sid in sites]
|
||||
else:
|
||||
config_indexers = [str(sid) for sid in self.systemconfig.get(SystemConfigKey.IndexerSites) or []]
|
||||
if not sites:
|
||||
sites = self.systemconfig.get(SystemConfigKey.IndexerSites) or []
|
||||
|
||||
for indexer in self.siteshelper.get_indexers():
|
||||
# 检查站点索引开关
|
||||
if not config_indexers or str(indexer.get("id")) in config_indexers:
|
||||
if not sites or indexer.get("id") in sites:
|
||||
# 站点流控
|
||||
state, msg = self.siteshelper.check(indexer.get("domain"))
|
||||
if state:
|
||||
@@ -269,6 +265,7 @@ class SearchChain(ChainBase):
|
||||
if not indexer_sites:
|
||||
logger.warn('未开启任何有效站点,无法搜索资源')
|
||||
return []
|
||||
|
||||
# 开始进度
|
||||
self.progress.start(ProgressKey.Search)
|
||||
# 开始计时
|
||||
|
||||
@@ -33,7 +33,7 @@ class SubscribeChain(ChainBase):
|
||||
self.subscribeoper = SubscribeOper(self._db)
|
||||
self.torrentschain = TorrentsChain()
|
||||
self.message = MessageHelper()
|
||||
self.systemconfig = SystemConfigOper(self._db)
|
||||
self.systemconfig = SystemConfigOper()
|
||||
|
||||
def add(self, title: str, year: str,
|
||||
mtype: MediaType = None,
|
||||
@@ -264,9 +264,9 @@ class SubscribeChain(ChainBase):
|
||||
sites = None
|
||||
# 过滤规则
|
||||
if subscribe.best_version:
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.FilterRules2)
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
|
||||
else:
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.FilterRules)
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
|
||||
# 搜索,同时电视剧会过滤掉不需要的剧集
|
||||
contexts = self.searchchain.process(mediainfo=mediainfo,
|
||||
keyword=subscribe.keyword,
|
||||
@@ -277,7 +277,7 @@ class SubscribeChain(ChainBase):
|
||||
logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源')
|
||||
if meta.type == MediaType.TV:
|
||||
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
||||
self.__upate_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
|
||||
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
|
||||
continue
|
||||
# 过滤
|
||||
matched_contexts = []
|
||||
@@ -285,15 +285,21 @@ class SubscribeChain(ChainBase):
|
||||
torrent_meta = context.meta_info
|
||||
torrent_info = context.torrent_info
|
||||
torrent_mediainfo = context.media_info
|
||||
# 包含与排除规则
|
||||
default_include_exclude = self.systemconfig.get(SystemConfigKey.DefaultIncludeExcludeFilter) or {}
|
||||
include = subscribe.include or default_include_exclude.get("include")
|
||||
exclude = subscribe.exclude or default_include_exclude.get("exclude")
|
||||
# 包含
|
||||
if subscribe.include:
|
||||
if not re.search(r"%s" % subscribe.include,
|
||||
if include:
|
||||
if not re.search(r"%s" % include,
|
||||
f"{torrent_info.title} {torrent_info.description}", re.I):
|
||||
logger.info(f"{torrent_info.title} 不匹配包含规则 {include}")
|
||||
continue
|
||||
# 排除
|
||||
if subscribe.exclude:
|
||||
if re.search(r"%s" % subscribe.exclude,
|
||||
if exclude:
|
||||
if re.search(r"%s" % exclude,
|
||||
f"{torrent_info.title} {torrent_info.description}", re.I):
|
||||
logger.info(f"{torrent_info.title} 匹配排除规则 {exclude}")
|
||||
continue
|
||||
# 非洗版
|
||||
if not subscribe.best_version:
|
||||
@@ -308,12 +314,17 @@ class SubscribeChain(ChainBase):
|
||||
if torrent_meta.episode_list:
|
||||
logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 不是整季')
|
||||
continue
|
||||
# 优先级小于已下载优先级的不要
|
||||
if subscribe.current_priority \
|
||||
and torrent_info.pri_order < subscribe.current_priority:
|
||||
logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于已下载优先级')
|
||||
continue
|
||||
matched_contexts.append(context)
|
||||
if not matched_contexts:
|
||||
logger.warn(f'订阅 {subscribe.name} 没有符合过滤条件的资源')
|
||||
# 非洗版未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
||||
if meta.type == MediaType.TV and not subscribe.best_version:
|
||||
self.__upate_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
|
||||
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
|
||||
continue
|
||||
# 自动下载
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=matched_contexts,
|
||||
@@ -330,18 +341,18 @@ class SubscribeChain(ChainBase):
|
||||
mediainfo=mediainfo, downloads=downloads)
|
||||
else:
|
||||
# 未完成下载
|
||||
logger.info(f'{mediainfo.title_year} 未下载未完整,继续订阅 ...')
|
||||
logger.info(f'{mediainfo.title_year} 未下载完整,继续订阅 ...')
|
||||
if meta.type == MediaType.TV and not subscribe.best_version:
|
||||
# 更新订阅剩余集数和时间
|
||||
update_date = True if downloads else False
|
||||
self.__upate_lack_episodes(lefts=lefts, subscribe=subscribe,
|
||||
mediainfo=mediainfo, update_date=update_date)
|
||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
|
||||
mediainfo=mediainfo, update_date=update_date)
|
||||
# 手动触发时发送系统消息
|
||||
if manual:
|
||||
if sid:
|
||||
self.message.put(f'订阅 {subscribes[0].name} 搜索完成!')
|
||||
else:
|
||||
self.message.put(f'所有订阅搜索完成!')
|
||||
self.message.put('所有订阅搜索完成!')
|
||||
|
||||
def finish_subscribe_or_not(self, subscribe: Subscribe, meta: MetaInfo,
|
||||
mediainfo: MediaInfo, downloads: List[Context]):
|
||||
@@ -375,17 +386,40 @@ class SubscribeChain(ChainBase):
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
刷新订阅
|
||||
订阅刷新
|
||||
"""
|
||||
# 触发刷新站点资源,从缓存中匹配订阅
|
||||
sites = self.get_subscribed_sites()
|
||||
if sites is None:
|
||||
return
|
||||
self.match(
|
||||
self.torrentschain.refresh(sites=sites)
|
||||
)
|
||||
|
||||
def get_subscribed_sites(self) -> Optional[List[int]]:
|
||||
"""
|
||||
获取订阅中涉及的所有站点清单(节约资源)
|
||||
:return: 返回[]代表所有站点命中,返回None代表没有订阅
|
||||
"""
|
||||
# 查询所有订阅
|
||||
subscribes = self.subscribeoper.list('R')
|
||||
if not subscribes:
|
||||
# 没有订阅不运行
|
||||
return
|
||||
# 刷新站点资源,从缓存中匹配订阅
|
||||
self.match(
|
||||
self.torrentschain.refresh()
|
||||
)
|
||||
return None
|
||||
ret_sites = []
|
||||
# 刷新订阅选中的Rss站点
|
||||
for subscribe in subscribes:
|
||||
# 如果有一个订阅没有选择站点,则刷新所有订阅站点
|
||||
if not subscribe.sites:
|
||||
return []
|
||||
# 刷新选中的站点
|
||||
sub_sites = json.loads(subscribe.sites)
|
||||
if sub_sites:
|
||||
ret_sites.extend(sub_sites)
|
||||
# 去重
|
||||
if ret_sites:
|
||||
ret_sites = list(set(ret_sites))
|
||||
|
||||
return ret_sites
|
||||
|
||||
def match(self, torrents: Dict[str, List[Context]]):
|
||||
"""
|
||||
@@ -473,19 +507,22 @@ class SubscribeChain(ChainBase):
|
||||
continue
|
||||
# 过滤规则
|
||||
if subscribe.best_version:
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.FilterRules2)
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
|
||||
else:
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.FilterRules)
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
|
||||
result: List[TorrentInfo] = self.filter_torrents(
|
||||
rule_string=filter_rule,
|
||||
torrent_list=[torrent_info])
|
||||
torrent_list=[torrent_info],
|
||||
mediainfo=torrent_mediainfo)
|
||||
if result is not None and not result:
|
||||
# 不符合过滤规则
|
||||
logger.info(f"{torrent_info.title} 不匹配当前过滤规则")
|
||||
continue
|
||||
# 不在订阅站点范围的不处理
|
||||
if subscribe.sites:
|
||||
sub_sites = json.loads(subscribe.sites)
|
||||
if sub_sites and torrent_info.site not in sub_sites:
|
||||
logger.info(f"{torrent_info.title} 不符合 {torrent_mediainfo.title_year} 订阅站点要求")
|
||||
continue
|
||||
# 如果是电视剧
|
||||
if torrent_mediainfo.type == MediaType.TV:
|
||||
@@ -527,15 +564,21 @@ class SubscribeChain(ChainBase):
|
||||
if torrent_meta.episode_list:
|
||||
logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 不是整季')
|
||||
continue
|
||||
# 包含与排除规则
|
||||
default_include_exclude = self.systemconfig.get(SystemConfigKey.DefaultIncludeExcludeFilter) or {}
|
||||
include = subscribe.include or default_include_exclude.get("include")
|
||||
exclude = subscribe.exclude or default_include_exclude.get("exclude")
|
||||
# 包含
|
||||
if subscribe.include:
|
||||
if not re.search(r"%s" % subscribe.include,
|
||||
if include:
|
||||
if not re.search(r"%s" % include,
|
||||
f"{torrent_info.title} {torrent_info.description}", re.I):
|
||||
logger.info(f"{torrent_info.title} 不匹配包含规则 {include}")
|
||||
continue
|
||||
# 排除
|
||||
if subscribe.exclude:
|
||||
if re.search(r"%s" % subscribe.exclude,
|
||||
if exclude:
|
||||
if re.search(r"%s" % exclude,
|
||||
f"{torrent_info.title} {torrent_info.description}", re.I):
|
||||
logger.info(f"{torrent_info.title} 匹配排除规则 {exclude}")
|
||||
continue
|
||||
# 匹配成功
|
||||
logger.info(f'{mediainfo.title_year} 匹配成功:{torrent_info.title}')
|
||||
@@ -557,12 +600,12 @@ class SubscribeChain(ChainBase):
|
||||
if meta.type == MediaType.TV and not subscribe.best_version:
|
||||
update_date = True if downloads else False
|
||||
# 未完成下载,计算剩余集数
|
||||
self.__upate_lack_episodes(lefts=lefts, subscribe=subscribe,
|
||||
mediainfo=mediainfo, update_date=update_date)
|
||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
|
||||
mediainfo=mediainfo, update_date=update_date)
|
||||
else:
|
||||
if meta.type == MediaType.TV:
|
||||
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
||||
self.__upate_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
|
||||
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
|
||||
|
||||
def check(self):
|
||||
"""
|
||||
@@ -586,18 +629,16 @@ class SubscribeChain(ChainBase):
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid}')
|
||||
continue
|
||||
if not mediainfo.seasons:
|
||||
continue
|
||||
# 获取当前季的总集数
|
||||
# 对于电视剧,获取当前季的总集数
|
||||
episodes = mediainfo.seasons.get(subscribe.season) or []
|
||||
if len(episodes) > subscribe.total_episode or 0:
|
||||
if len(episodes) > (subscribe.total_episode or 0):
|
||||
total_episode = len(episodes)
|
||||
lack_episode = subscribe.lack_episode + (total_episode - subscribe.total_episode)
|
||||
logger.info(f'订阅 {subscribe.name} 总集数变化,更新总集数为{total_episode},缺失集数为{lack_episode} ...')
|
||||
logger.info(
|
||||
f'订阅 {subscribe.name} 总集数变化,更新总集数为{total_episode},缺失集数为{lack_episode} ...')
|
||||
else:
|
||||
total_episode = subscribe.total_episode
|
||||
lack_episode = subscribe.lack_episode
|
||||
logger.info(f'订阅 {subscribe.name} 总集数未变化')
|
||||
# 更新TMDB信息
|
||||
self.subscribeoper.update(subscribe.id, {
|
||||
"name": mediainfo.title,
|
||||
@@ -654,10 +695,10 @@ class SubscribeChain(ChainBase):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __upate_lack_episodes(self, lefts: Dict[int, Dict[int, NotExistMediaInfo]],
|
||||
subscribe: Subscribe,
|
||||
mediainfo: MediaInfo,
|
||||
update_date: bool = False):
|
||||
def __update_lack_episodes(self, lefts: Dict[int, Dict[int, NotExistMediaInfo]],
|
||||
subscribe: Subscribe,
|
||||
mediainfo: MediaInfo,
|
||||
update_date: bool = False):
|
||||
"""
|
||||
更新订阅剩余集数
|
||||
"""
|
||||
@@ -742,7 +783,7 @@ class SubscribeChain(ChainBase):
|
||||
total_episode: int,
|
||||
start_episode: int):
|
||||
"""
|
||||
根据订阅开始集数和总结数,结合TMDB信息计算当前订阅的缺失集数
|
||||
根据订阅开始集数和总集数,结合TMDB信息计算当前订阅的缺失集数
|
||||
:param no_exists: 缺失季集列表
|
||||
:param tmdb_id: TMDB ID
|
||||
:param begin_season: 开始季
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
from datetime import datetime
|
||||
import re
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from requests import Session
|
||||
from cachetools import cached, TTLCache
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.core.config import settings
|
||||
from app.core.context import TorrentInfo, Context, MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.db import SessionFactory
|
||||
from app.db.site_oper import SiteOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.rss import RssHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.log import logger
|
||||
from app.schemas import Notification
|
||||
from app.schemas.types import SystemConfigKey, MessageChannel
|
||||
from app.schemas.types import SystemConfigKey, MessageChannel, NotificationType
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.string import StringUtils
|
||||
from app.utils.timer import TimerUtils
|
||||
|
||||
|
||||
class TorrentsChain(ChainBase):
|
||||
class TorrentsChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
种子刷新处理链
|
||||
站点首页或RSS种子处理链,服务于订阅、刷流等
|
||||
"""
|
||||
|
||||
_cache_file = "__torrents_cache__"
|
||||
_last_refresh_time = None
|
||||
_spider_file = "__torrents_cache__"
|
||||
_rss_file = "__rss_cache__"
|
||||
|
||||
def __init__(self, db: Session = None):
|
||||
super().__init__(db)
|
||||
def __init__(self):
|
||||
self._db = SessionFactory()
|
||||
super().__init__(self._db)
|
||||
self.siteshelper = SitesHelper()
|
||||
self.systemconfig = SystemConfigOper(self._db)
|
||||
self.siteoper = SiteOper(self._db)
|
||||
self.rsshelper = RssHelper()
|
||||
self.systemconfig = SystemConfigOper()
|
||||
|
||||
def remote_refresh(self, channel: MessageChannel, userid: Union[str, int] = None):
|
||||
"""
|
||||
@@ -39,40 +45,109 @@ class TorrentsChain(ChainBase):
|
||||
self.post_message(Notification(channel=channel,
|
||||
title=f"种子刷新完成!", userid=userid))
|
||||
|
||||
def get_torrents(self) -> Dict[str, List[Context]]:
|
||||
def get_torrents(self, stype: str = None) -> Dict[str, List[Context]]:
|
||||
"""
|
||||
获取当前缓存的种子
|
||||
:param stype: 强制指定缓存类型,spider:爬虫缓存,rss:rss缓存
|
||||
"""
|
||||
|
||||
if not stype:
|
||||
stype = settings.SUBSCRIBE_MODE
|
||||
|
||||
# 读取缓存
|
||||
return self.load_cache(self._cache_file) or {}
|
||||
if stype == 'spider':
|
||||
return self.load_cache(self._spider_file) or {}
|
||||
else:
|
||||
return self.load_cache(self._rss_file) or {}
|
||||
|
||||
def refresh(self) -> Dict[str, List[Context]]:
|
||||
@cached(cache=TTLCache(maxsize=128, ttl=600))
|
||||
def browse(self, domain: str) -> List[TorrentInfo]:
|
||||
"""
|
||||
刷新站点最新资源
|
||||
浏览站点首页内容,返回种子清单,TTL缓存10分钟
|
||||
:param domain: 站点域名
|
||||
"""
|
||||
# 控制刷新频率不能小于10分钟
|
||||
if self._last_refresh_time and TimerUtils.diff_minutes(self._last_refresh_time) < 10:
|
||||
logger.warn(f'种子刷新频率过快,跳过本次刷新')
|
||||
return self.get_torrents()
|
||||
logger.info(f'开始获取站点 {domain} 最新种子 ...')
|
||||
site = self.siteshelper.get_indexer(domain)
|
||||
if not site:
|
||||
logger.error(f'站点 {domain} 不存在!')
|
||||
return []
|
||||
return self.refresh_torrents(site=site)
|
||||
|
||||
# 记录刷新时间
|
||||
self._last_refresh_time = datetime.now()
|
||||
@cached(cache=TTLCache(maxsize=128, ttl=300))
|
||||
def rss(self, domain: str) -> List[TorrentInfo]:
|
||||
"""
|
||||
获取站点RSS内容,返回种子清单,TTL缓存5分钟
|
||||
:param domain: 站点域名
|
||||
"""
|
||||
logger.info(f'开始获取站点 {domain} RSS ...')
|
||||
site = self.siteshelper.get_indexer(domain)
|
||||
if not site:
|
||||
logger.error(f'站点 {domain} 不存在!')
|
||||
return []
|
||||
if not site.get("rss"):
|
||||
logger.error(f'站点 {domain} 未配置RSS地址!')
|
||||
return []
|
||||
rss_items = self.rsshelper.parse(site.get("rss"), True if site.get("proxy") else False)
|
||||
if rss_items is None:
|
||||
# rss过期,尝试保留原配置生成新的rss
|
||||
self.__renew_rss_url(domain=domain, site=site)
|
||||
return []
|
||||
if not rss_items:
|
||||
logger.error(f'站点 {domain} 未获取到RSS数据!')
|
||||
return []
|
||||
# 组装种子
|
||||
ret_torrents: List[TorrentInfo] = []
|
||||
for item in rss_items:
|
||||
if not item.get("title"):
|
||||
continue
|
||||
torrentinfo = TorrentInfo(
|
||||
site=site.get("id"),
|
||||
site_name=site.get("name"),
|
||||
site_cookie=site.get("cookie"),
|
||||
site_ua=site.get("ua") or settings.USER_AGENT,
|
||||
site_proxy=site.get("proxy"),
|
||||
site_order=site.get("pri"),
|
||||
title=item.get("title"),
|
||||
enclosure=item.get("enclosure"),
|
||||
page_url=item.get("link"),
|
||||
size=item.get("size"),
|
||||
pubdate=item["pubdate"].strftime("%Y-%m-%d %H:%M:%S") if item.get("pubdate") else None,
|
||||
)
|
||||
ret_torrents.append(torrentinfo)
|
||||
|
||||
return ret_torrents
|
||||
|
||||
def refresh(self, stype: str = None, sites: List[int] = None) -> Dict[str, List[Context]]:
|
||||
"""
|
||||
刷新站点最新资源,识别并缓存起来
|
||||
:param stype: 强制指定缓存类型,spider:爬虫缓存,rss:rss缓存
|
||||
:param sites: 强制指定站点ID列表,为空则读取设置的订阅站点
|
||||
"""
|
||||
# 刷新类型
|
||||
if not stype:
|
||||
stype = settings.SUBSCRIBE_MODE
|
||||
|
||||
# 刷新站点
|
||||
if not sites:
|
||||
sites = self.systemconfig.get(SystemConfigKey.RssSites) or []
|
||||
|
||||
# 读取缓存
|
||||
torrents_cache = self.get_torrents()
|
||||
|
||||
# 所有站点索引
|
||||
indexers = self.siteshelper.get_indexers()
|
||||
# 配置的Rss站点
|
||||
config_indexers = [str(sid) for sid in self.systemconfig.get(SystemConfigKey.RssSites) or []]
|
||||
# 遍历站点缓存资源
|
||||
for indexer in indexers:
|
||||
# 未开启的站点不搜索
|
||||
if config_indexers and str(indexer.get("id")) not in config_indexers:
|
||||
# 未开启的站点不刷新
|
||||
if sites and indexer.get("id") not in sites:
|
||||
continue
|
||||
logger.info(f'开始刷新 {indexer.get("name")} 最新种子 ...')
|
||||
domain = StringUtils.get_url_domain(indexer.get("domain"))
|
||||
torrents: List[TorrentInfo] = self.refresh_torrents(site=indexer)
|
||||
if stype == "spider":
|
||||
# 刷新首页种子
|
||||
torrents: List[TorrentInfo] = self.browse(domain=domain)
|
||||
else:
|
||||
# 刷新RSS种子
|
||||
torrents: List[TorrentInfo] = self.rss(domain=domain)
|
||||
# 按pubdate降序排列
|
||||
torrents.sort(key=lambda x: x.pubdate or '', reverse=True)
|
||||
# 取前N条
|
||||
@@ -114,7 +189,46 @@ class TorrentsChain(ChainBase):
|
||||
del torrents
|
||||
else:
|
||||
logger.info(f'{indexer.get("name")} 没有获取到种子')
|
||||
|
||||
# 保存缓存到本地
|
||||
self.save_cache(torrents_cache, self._cache_file)
|
||||
if stype == "spider":
|
||||
self.save_cache(torrents_cache, self._spider_file)
|
||||
else:
|
||||
self.save_cache(torrents_cache, self._rss_file)
|
||||
|
||||
# 返回
|
||||
return torrents_cache
|
||||
|
||||
def __renew_rss_url(self, domain: str, site: dict):
|
||||
"""
|
||||
保留原配置生成新的rss地址
|
||||
"""
|
||||
try:
|
||||
# RSS链接过期
|
||||
logger.error(f"站点 {domain} RSS链接已过期,正在尝试自动获取!")
|
||||
# 自动生成rss地址
|
||||
rss_url, errmsg = self.rsshelper.get_rss_link(
|
||||
url=site.get("url"),
|
||||
cookie=site.get("cookie"),
|
||||
ua=site.get("ua") or settings.USER_AGENT,
|
||||
proxy=True if site.get("proxy") else False
|
||||
)
|
||||
if rss_url:
|
||||
# 获取新的日期的passkey
|
||||
match = re.search(r'passkey=([a-zA-Z0-9]+)', rss_url)
|
||||
if match:
|
||||
new_passkey = match.group(1)
|
||||
# 获取过期rss除去passkey部分
|
||||
new_rss = re.sub(r'&passkey=([a-zA-Z0-9]+)', f'&passkey={new_passkey}', site.get("rss"))
|
||||
logger.info(f"更新站点 {domain} RSS地址 ...")
|
||||
self.siteoper.update_rss(domain=domain, rss=new_rss)
|
||||
else:
|
||||
# 发送消息
|
||||
self.post_message(
|
||||
Notification(mtype=NotificationType.SiteMessage, title=f"站点 {domain} RSS链接已过期"))
|
||||
else:
|
||||
self.post_message(
|
||||
Notification(mtype=NotificationType.SiteMessage, title=f"站点 {domain} RSS链接已过期"))
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
self.post_message(Notification(mtype=NotificationType.SiteMessage, title=f"站点 {domain} RSS链接已过期"))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import glob
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
@@ -85,7 +86,7 @@ class TransferChain(ChainBase):
|
||||
mediainfo: MediaInfo = None, download_hash: str = None,
|
||||
target: Path = None, transfer_type: str = None,
|
||||
season: int = None, epformat: EpisodeFormat = None,
|
||||
min_filesize: int = 0) -> Tuple[bool, str]:
|
||||
min_filesize: int = 0, force: bool = False) -> Tuple[bool, str]:
|
||||
"""
|
||||
执行一个复杂目录的转移操作
|
||||
:param path: 待转移目录或文件
|
||||
@@ -97,6 +98,7 @@ class TransferChain(ChainBase):
|
||||
:param season: 季
|
||||
:param epformat: 剧集格式
|
||||
:param min_filesize: 最小文件大小(MB)
|
||||
:param force: 是否强制转移
|
||||
返回:成功标识,错误信息
|
||||
"""
|
||||
if not transfer_type:
|
||||
@@ -174,19 +176,25 @@ class TransferChain(ChainBase):
|
||||
continue
|
||||
|
||||
# 整理屏蔽词不处理
|
||||
is_blocked = False
|
||||
if transfer_exclude_words:
|
||||
for keyword in transfer_exclude_words:
|
||||
if not keyword:
|
||||
continue
|
||||
if keyword and re.findall(keyword, file_path_str):
|
||||
if keyword and re.search(r"%s" % keyword, file_path_str, re.IGNORECASE):
|
||||
logger.info(f"{file_path} 命中整理屏蔽词 {keyword},不处理")
|
||||
continue
|
||||
is_blocked = True
|
||||
break
|
||||
if is_blocked:
|
||||
err_msgs.append(f"{file_path.name} 命中整理屏蔽词")
|
||||
continue
|
||||
|
||||
# 转移成功的不再处理
|
||||
transferd = self.transferhis.get_by_src(file_path_str)
|
||||
if transferd and transferd.status:
|
||||
logger.info(f"{file_path} 已成功转移过,如需重新处理,请删除历史记录。")
|
||||
continue
|
||||
if not force:
|
||||
transferd = self.transferhis.get_by_src(file_path_str)
|
||||
if transferd and transferd.status:
|
||||
logger.info(f"{file_path} 已成功转移过,如需重新处理,请删除历史记录。")
|
||||
continue
|
||||
|
||||
# 更新进度
|
||||
self.progress.update(value=processed_num / total_num * 100,
|
||||
@@ -246,10 +254,10 @@ class TransferChain(ChainBase):
|
||||
|
||||
# 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title
|
||||
if not settings.SCRAP_FOLLOW_TMDB:
|
||||
transfer_historys = self.transferhis.get_by(tmdbid=file_mediainfo.tmdb_id,
|
||||
mtype=file_mediainfo.type.value)
|
||||
if transfer_historys:
|
||||
file_mediainfo.title = transfer_historys[0].title
|
||||
transfer_history = self.transferhis.get_by_type_tmdbid(tmdbid=file_mediainfo.tmdb_id,
|
||||
mtype=file_mediainfo.type.value)
|
||||
if transfer_history:
|
||||
file_mediainfo.title = transfer_history.title
|
||||
|
||||
logger.info(f"{file_path.name} 识别为:{file_mediainfo.type.value} {file_mediainfo.title_year}")
|
||||
|
||||
@@ -342,7 +350,8 @@ class TransferChain(ChainBase):
|
||||
transferinfo=transferinfo
|
||||
)
|
||||
# 刮削单个文件
|
||||
self.scrape_metadata(path=transferinfo.target_path, mediainfo=file_mediainfo)
|
||||
if settings.SCRAP_METADATA:
|
||||
self.scrape_metadata(path=transferinfo.target_path, mediainfo=file_mediainfo)
|
||||
# 更新进度
|
||||
processed_num += 1
|
||||
self.progress.update(value=processed_num / total_num * 100,
|
||||
@@ -362,7 +371,8 @@ class TransferChain(ChainBase):
|
||||
if transfer_info.target_path.is_file():
|
||||
transfer_info.target_path = transfer_info.target_path.parent
|
||||
# 刷新媒体库,根目录或季目录
|
||||
self.refresh_mediaserver(mediainfo=media, file_path=transfer_info.target_path)
|
||||
if settings.REFRESH_MEDIASERVER:
|
||||
self.refresh_mediaserver(mediainfo=media, file_path=transfer_info.target_path)
|
||||
# 发送通知
|
||||
se_str = None
|
||||
if media.type == MediaType.TV:
|
||||
@@ -493,10 +503,11 @@ class TransferChain(ChainBase):
|
||||
if history.dest:
|
||||
self.delete_files(Path(history.dest))
|
||||
|
||||
# 转移
|
||||
# 强制转移
|
||||
state, errmsg = self.do_transfer(path=src_path,
|
||||
mediainfo=mediainfo,
|
||||
download_hash=history.download_hash)
|
||||
download_hash=history.download_hash,
|
||||
force=True)
|
||||
if not state:
|
||||
return False, errmsg
|
||||
|
||||
@@ -591,8 +602,10 @@ class TransferChain(ChainBase):
|
||||
if not path.exists():
|
||||
return
|
||||
if path.is_file():
|
||||
# 删除文件
|
||||
path.unlink()
|
||||
# 删除文件、nfo、jpg
|
||||
files = glob.glob(f"{Path(path.parent).joinpath(path.stem)}*")
|
||||
for file in files:
|
||||
Path(file).unlink()
|
||||
logger.warn(f"文件 {path} 已删除")
|
||||
# 需要删除父目录
|
||||
elif str(path.parent) == str(path.root):
|
||||
@@ -605,11 +618,24 @@ class TransferChain(ChainBase):
|
||||
# 删除目录
|
||||
logger.warn(f"目录 {path} 已删除")
|
||||
# 需要删除父目录
|
||||
# 判断父目录是否为空, 为空则删除
|
||||
for parent_path in path.parents:
|
||||
if str(parent_path.parent) != str(path.root):
|
||||
# 父目录非根目录,才删除父目录
|
||||
files = SystemUtils.list_files(parent_path, settings.RMT_MEDIAEXT)
|
||||
if not files:
|
||||
shutil.rmtree(parent_path)
|
||||
logger.warn(f"目录 {parent_path} 已删除")
|
||||
|
||||
# 判断当前媒体父路径下是否有媒体文件,如有则无需遍历父级
|
||||
if not SystemUtils.exits_files(path.parent, settings.RMT_MEDIAEXT):
|
||||
# 媒体库二级分类根路径
|
||||
library_root_names = [
|
||||
settings.LIBRARY_MOVIE_NAME or '电影',
|
||||
settings.LIBRARY_TV_NAME or '电视剧',
|
||||
settings.LIBRARY_ANIME_NAME or '动漫',
|
||||
]
|
||||
|
||||
# 判断父目录是否为空, 为空则删除
|
||||
for parent_path in path.parents:
|
||||
# 遍历父目录到媒体库二级分类根路径
|
||||
if str(parent_path.name) in library_root_names:
|
||||
break
|
||||
if str(parent_path.parent) != str(path.root):
|
||||
# 父目录非根目录,才删除父目录
|
||||
if not SystemUtils.exits_files(parent_path, settings.RMT_MEDIAEXT):
|
||||
# 当前路径下没有媒体文件则删除
|
||||
shutil.rmtree(parent_path)
|
||||
logger.warn(f"目录 {parent_path} 已删除")
|
||||
|
||||
@@ -13,11 +13,12 @@ from app.chain.transfer import TransferChain
|
||||
from app.core.event import Event as ManagerEvent
|
||||
from app.core.event import eventmanager, EventManager
|
||||
from app.core.plugin import PluginManager
|
||||
from app.db import ScopedSession
|
||||
from app.db import SessionFactory
|
||||
from app.log import logger
|
||||
from app.schemas.types import EventType, MessageChannel
|
||||
from app.utils.object import ObjectUtils
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.system import SystemUtils
|
||||
|
||||
|
||||
class CommandChian(ChainBase):
|
||||
@@ -41,7 +42,7 @@ class Command(metaclass=Singleton):
|
||||
|
||||
def __init__(self):
|
||||
# 数据库连接
|
||||
self._db = ScopedSession()
|
||||
self._db = SessionFactory()
|
||||
# 事件管理器
|
||||
self.eventmanager = EventManager()
|
||||
# 插件管理器
|
||||
@@ -128,6 +129,12 @@ class Command(metaclass=Singleton):
|
||||
"description": "清理缓存",
|
||||
"category": "管理",
|
||||
"data": {}
|
||||
},
|
||||
"/restart": {
|
||||
"func": SystemUtils.restart,
|
||||
"description": "重启系统",
|
||||
"category": "管理",
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
# 汇总插件命令
|
||||
@@ -174,6 +181,8 @@ class Command(metaclass=Singleton):
|
||||
"""
|
||||
self._event.set()
|
||||
self._thread.join()
|
||||
if self._db:
|
||||
self._db.close()
|
||||
|
||||
def get_commands(self):
|
||||
"""
|
||||
@@ -213,6 +222,10 @@ class Command(metaclass=Singleton):
|
||||
if args_num > 0:
|
||||
if cmd_data:
|
||||
# 有内置参数直接使用内置参数
|
||||
data = cmd_data.get("data") or {}
|
||||
data['channel'] = channel
|
||||
data['user'] = userid
|
||||
cmd_data['data'] = data
|
||||
command['func'](**cmd_data)
|
||||
elif args_num == 2:
|
||||
# 没有输入参数,只输入渠道和用户ID
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseSettings
|
||||
|
||||
@@ -65,9 +66,13 @@ class Settings(BaseSettings):
|
||||
RMT_AUDIO_TRACK_EXT: list = ['.mka']
|
||||
# 索引器
|
||||
INDEXER: str = "builtin"
|
||||
# 订阅模式
|
||||
SUBSCRIBE_MODE: str = "spider"
|
||||
# RSS订阅模式刷新时间间隔(分钟)
|
||||
SUBSCRIBE_RSS_INTERVAL: int = 30
|
||||
# 订阅搜索开关
|
||||
SUBSCRIBE_SEARCH: bool = False
|
||||
# 用户认证站点 hhclub/audiences/hddolby/zmpt/freefarm/hdfans/wintersakura/leaves/1ptba/icc2022/iyuu
|
||||
# 用户认证站点
|
||||
AUTH_SITE: str = ""
|
||||
# 交互搜索自动下载用户ID,使用,分割
|
||||
AUTO_DOWNLOAD_USER: str = None
|
||||
@@ -165,7 +170,7 @@ class Settings(BaseSettings):
|
||||
OCR_HOST: str = "https://movie-pilot.org"
|
||||
# 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_MOVIE_NAME: str = None
|
||||
@@ -251,6 +256,12 @@ class Settings(BaseSettings):
|
||||
"server": self.PROXY_HOST
|
||||
}
|
||||
|
||||
@property
|
||||
def LIBRARY_PATHS(self) -> List[Path]:
|
||||
if self.LIBRARY_PATH:
|
||||
return [Path(path) for path in self.LIBRARY_PATH.split(",")]
|
||||
return []
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
with self.CONFIG_PATH as p:
|
||||
|
||||
@@ -28,7 +28,23 @@ class WordsMatcher(metaclass=Singleton):
|
||||
if not word:
|
||||
continue
|
||||
try:
|
||||
if word.count(" => "):
|
||||
if word.count(" => ") and word.count(" && ") and word.count(" >> ") and word.count(" <> "):
|
||||
# 替换词
|
||||
thc = str(re.findall(r'(.*?)\s*=>', word)[0]).strip()
|
||||
# 被替换词
|
||||
bthc = str(re.findall(r'=>\s*(.*?)\s*&&', word)[0]).strip()
|
||||
# 集偏移前字段
|
||||
pyq = str(re.findall(r'&&\s*(.*?)\s*<>', word)[0]).strip()
|
||||
# 集偏移后字段
|
||||
pyh = str(re.findall(r'<>(.*?)\s*>>', word)[0]).strip()
|
||||
# 集偏移
|
||||
offsets = str(re.findall(r'>>\s*(.*?)$', word)[0]).strip()
|
||||
# 替换词
|
||||
title, message, state = self.__replace_regex(title, thc, bthc)
|
||||
if state:
|
||||
# 替换词成功再进行集偏移
|
||||
title, message, state = self.__episode_offset(title, pyq, pyh, offsets)
|
||||
elif word.count(" => "):
|
||||
# 替换词
|
||||
strings = word.split(" => ")
|
||||
title, message, state = self.__replace_regex(title, strings[0], strings[1])
|
||||
|
||||
@@ -83,6 +83,9 @@ class PluginManager(metaclass=Singleton):
|
||||
"""
|
||||
# 停止所有插件
|
||||
for plugin in self._running_plugins.values():
|
||||
# 关闭数据库
|
||||
if hasattr(plugin, "close"):
|
||||
plugin.close()
|
||||
# 关闭插件
|
||||
if hasattr(plugin, "stop_service"):
|
||||
plugin.stop_service()
|
||||
|
||||
@@ -6,7 +6,7 @@ from alembic.config import Config
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import get_password_hash
|
||||
from app.db import Engine, ScopedSession
|
||||
from app.db import Engine, SessionFactory
|
||||
from app.db.models import Base
|
||||
from app.db.models.user import User
|
||||
from app.log import logger
|
||||
@@ -22,7 +22,7 @@ def init_db():
|
||||
# 全量建表
|
||||
Base.metadata.create_all(bind=Engine)
|
||||
# 初始化超级管理员
|
||||
db = ScopedSession()
|
||||
db = SessionFactory()
|
||||
user = User.get_by_name(db=db, name=settings.SUPERUSER)
|
||||
if not user:
|
||||
user = User(
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.models import Base
|
||||
|
||||
|
||||
class Rss(Base):
|
||||
"""
|
||||
RSS订阅
|
||||
"""
|
||||
id = Column(Integer, Sequence('id'), primary_key=True, index=True)
|
||||
# 名称
|
||||
name = Column(String, nullable=False)
|
||||
# RSS地址
|
||||
url = Column(String, nullable=False)
|
||||
# 类型
|
||||
type = Column(String)
|
||||
# 标题
|
||||
title = Column(String)
|
||||
# 年份
|
||||
year = Column(String)
|
||||
# TMDBID
|
||||
tmdbid = Column(Integer, index=True)
|
||||
# 季号
|
||||
season = Column(Integer)
|
||||
# 海报
|
||||
poster = Column(String)
|
||||
# 背景图
|
||||
backdrop = Column(String)
|
||||
# 评分
|
||||
vote = Column(Integer)
|
||||
# 简介
|
||||
description = Column(String)
|
||||
# 总集数
|
||||
total_episode = Column(Integer)
|
||||
# 包含
|
||||
include = Column(String)
|
||||
# 排除
|
||||
exclude = Column(String)
|
||||
# 洗版
|
||||
best_version = Column(Integer)
|
||||
# 是否使用代理服务器
|
||||
proxy = Column(Integer)
|
||||
# 是否使用过滤规则
|
||||
filter = Column(Integer)
|
||||
# 保存路径
|
||||
save_path = Column(String)
|
||||
# 已处理数量
|
||||
processed = Column(Integer)
|
||||
# 附加信息,已处理数据
|
||||
note = Column(String)
|
||||
# 最后更新时间
|
||||
last_update = Column(String)
|
||||
# 状态 0-停用,1-启用
|
||||
state = Column(Integer, default=1)
|
||||
|
||||
@staticmethod
|
||||
def get_by_tmdbid(db: Session, tmdbid: int, season: int = None):
|
||||
if season:
|
||||
return db.query(Rss).filter(Rss.tmdbid == tmdbid,
|
||||
Rss.season == season).all()
|
||||
return db.query(Rss).filter(Rss.tmdbid == tmdbid).all()
|
||||
|
||||
@staticmethod
|
||||
def get_by_title(db: Session, title: str):
|
||||
return db.query(Rss).filter(Rss.title == title).first()
|
||||
@@ -86,28 +86,35 @@ class TransferHistory(Base):
|
||||
|
||||
@staticmethod
|
||||
def list_by(db: Session, mtype: str = None, title: str = None, year: str = None, season: str = None,
|
||||
episode: str = None, tmdbid: int = None):
|
||||
episode: str = None, tmdbid: int = None, dest: str = None):
|
||||
"""
|
||||
据tmdbid、season、season_episode查询转移记录
|
||||
tmdbid + mtype 或 title + year 必输
|
||||
"""
|
||||
# TMDBID + 类型
|
||||
if tmdbid and mtype:
|
||||
# 电视剧某季某集
|
||||
if season and episode:
|
||||
# 查询一集
|
||||
return db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
|
||||
TransferHistory.type == mtype,
|
||||
TransferHistory.seasons == season,
|
||||
TransferHistory.episodes == episode).all()
|
||||
TransferHistory.episodes == episode,
|
||||
TransferHistory.dest == dest).all()
|
||||
# 电视剧某季
|
||||
elif season:
|
||||
# 查询一季
|
||||
return db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
|
||||
TransferHistory.type == mtype,
|
||||
TransferHistory.seasons == season).all()
|
||||
else:
|
||||
# 查询所有
|
||||
return db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
|
||||
TransferHistory.type == mtype).all()
|
||||
if dest:
|
||||
# 电影
|
||||
return db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
|
||||
TransferHistory.type == mtype,
|
||||
TransferHistory.dest == dest).all()
|
||||
else:
|
||||
# 电视剧所有季集
|
||||
return db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
|
||||
TransferHistory.type == mtype).all()
|
||||
# 标题 + 年份
|
||||
elif title and year:
|
||||
# 电视剧某季某集
|
||||
@@ -115,19 +122,33 @@ class TransferHistory(Base):
|
||||
return db.query(TransferHistory).filter(TransferHistory.title == title,
|
||||
TransferHistory.year == year,
|
||||
TransferHistory.seasons == season,
|
||||
TransferHistory.episodes == episode).all()
|
||||
TransferHistory.episodes == episode,
|
||||
TransferHistory.dest == dest).all()
|
||||
# 电视剧某季
|
||||
elif season:
|
||||
return db.query(TransferHistory).filter(TransferHistory.title == title,
|
||||
TransferHistory.year == year,
|
||||
TransferHistory.seasons == season).all()
|
||||
# 电视剧所有季集|电影
|
||||
else:
|
||||
return db.query(TransferHistory).filter(TransferHistory.title == title,
|
||||
TransferHistory.year == year).all()
|
||||
|
||||
if dest:
|
||||
# 电影
|
||||
return db.query(TransferHistory).filter(TransferHistory.title == title,
|
||||
TransferHistory.year == year,
|
||||
TransferHistory.dest == dest).all()
|
||||
else:
|
||||
# 电视剧所有季集
|
||||
return db.query(TransferHistory).filter(TransferHistory.title == title,
|
||||
TransferHistory.year == year).all()
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_by_type_tmdbid(db: Session, mtype: str = None, tmdbid: int = None):
|
||||
"""
|
||||
据tmdbid、type查询转移记录
|
||||
"""
|
||||
return db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
|
||||
TransferHistory.type == mtype).first()
|
||||
|
||||
@staticmethod
|
||||
def update_download_hash(db: Session, historyid: int = None, download_hash: str = None):
|
||||
db.query(TransferHistory).filter(TransferHistory.id == historyid).update(
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.rss import Rss
|
||||
|
||||
|
||||
class RssOper(DbOper):
|
||||
"""
|
||||
RSS订阅数据管理
|
||||
"""
|
||||
|
||||
def __init__(self, db: Session = None):
|
||||
super().__init__(db)
|
||||
|
||||
def add(self, **kwargs) -> bool:
|
||||
"""
|
||||
新增RSS订阅
|
||||
"""
|
||||
item = Rss(**kwargs)
|
||||
item.create(self._db)
|
||||
return True
|
||||
|
||||
def exists(self, tmdbid: int, season: int = None):
|
||||
"""
|
||||
判断是否存在
|
||||
"""
|
||||
return Rss.get_by_tmdbid(self._db, tmdbid, season)
|
||||
|
||||
def list(self, rssid: int = None) -> List[Rss]:
|
||||
"""
|
||||
查询所有RSS订阅
|
||||
"""
|
||||
if rssid:
|
||||
return [Rss.get(self._db, rssid)]
|
||||
return Rss.list(self._db)
|
||||
|
||||
def delete(self, rssid: int) -> bool:
|
||||
"""
|
||||
删除RSS订阅
|
||||
"""
|
||||
item = Rss.get(self._db, rssid)
|
||||
if item:
|
||||
item.delete(self._db)
|
||||
return True
|
||||
return False
|
||||
|
||||
def update(self, rssid: int, **kwargs) -> bool:
|
||||
"""
|
||||
更新RSS订阅
|
||||
"""
|
||||
item = Rss.get(self._db, rssid)
|
||||
if item:
|
||||
item.update(self._db, kwargs)
|
||||
return True
|
||||
return False
|
||||
@@ -74,3 +74,15 @@ class SiteOper(DbOper):
|
||||
"cookie": cookies
|
||||
})
|
||||
return True, "更新站点Cookie成功"
|
||||
|
||||
def update_rss(self, domain: str, rss: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
更新站点rss
|
||||
"""
|
||||
site = Site.get_by_domain(self._db, domain)
|
||||
if not site:
|
||||
return False, "站点不存在"
|
||||
site.update(self._db, {
|
||||
"rss": rss
|
||||
})
|
||||
return True, "更新站点RSS地址成功"
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import json
|
||||
from typing import Any, Union
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db import DbOper, SessionFactory
|
||||
from app.db.models.systemconfig import SystemConfig
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.utils.object import ObjectUtils
|
||||
@@ -14,11 +12,12 @@ class SystemConfigOper(DbOper, metaclass=Singleton):
|
||||
# 配置对象
|
||||
__SYSTEMCONF: dict = {}
|
||||
|
||||
def __init__(self, db: Session = None):
|
||||
def __init__(self):
|
||||
"""
|
||||
加载配置到内存
|
||||
"""
|
||||
super().__init__(db)
|
||||
self._db = SessionFactory()
|
||||
super().__init__(self._db)
|
||||
for item in SystemConfig.list(self._db):
|
||||
if ObjectUtils.is_obj(item.value):
|
||||
self.__SYSTEMCONF[item.key] = json.loads(item.value)
|
||||
@@ -57,3 +56,7 @@ class SystemConfigOper(DbOper, metaclass=Singleton):
|
||||
if not key:
|
||||
return self.__SYSTEMCONF
|
||||
return self.__SYSTEMCONF.get(key)
|
||||
|
||||
def __del__(self):
|
||||
if self._db:
|
||||
self._db.close()
|
||||
|
||||
@@ -52,18 +52,27 @@ class TransferHistoryOper(DbOper):
|
||||
return TransferHistory.statistic(self._db, days)
|
||||
|
||||
def get_by(self, title: str = None, year: str = None, mtype: str = None,
|
||||
season: str = None, episode: str = None, tmdbid: int = None) -> List[TransferHistory]:
|
||||
season: str = None, episode: str = None, tmdbid: int = None, dest: str = None) -> List[TransferHistory]:
|
||||
"""
|
||||
按类型、标题、年份、季集查询转移记录
|
||||
"""
|
||||
return TransferHistory.list_by(db=self._db,
|
||||
mtype=mtype,
|
||||
title=title,
|
||||
dest=dest,
|
||||
year=year,
|
||||
season=season,
|
||||
episode=episode,
|
||||
tmdbid=tmdbid)
|
||||
|
||||
def get_by_type_tmdbid(self, mtype: str = None, tmdbid: int = None) -> TransferHistory:
|
||||
"""
|
||||
按类型、tmdb查询转移记录
|
||||
"""
|
||||
return TransferHistory.get_by_type_tmdbid(db=self._db,
|
||||
mtype=mtype,
|
||||
tmdbid=tmdbid)
|
||||
|
||||
def delete(self, historyid):
|
||||
"""
|
||||
删除转移记录
|
||||
|
||||
@@ -1,16 +1,227 @@
|
||||
import xml.dom.minidom
|
||||
from typing import List
|
||||
from typing import List, Tuple, Union
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from app.core.config import settings
|
||||
from app.helper.browser import PlaywrightHelper
|
||||
from app.utils.dom import DomUtils
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
class RssHelper:
|
||||
|
||||
"""
|
||||
RSS帮助类,解析RSS报文、获取RSS地址等
|
||||
"""
|
||||
# 各站点RSS链接获取配置
|
||||
rss_link_conf = {
|
||||
"default": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
}
|
||||
},
|
||||
"hares.top": {
|
||||
"xpath": "//*[@id='layui-layer100001']/div[2]/div/p[4]/a/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
}
|
||||
},
|
||||
"et8.org": {
|
||||
"xpath": "//*[@id='outer']/table/tbody/tr/td/table/tbody/tr/td/a[2]/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
}
|
||||
},
|
||||
"pttime.org": {
|
||||
"xpath": "//*[@id='outer']/table/tbody/tr/td/table/tbody/tr/td/text()[5]",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"showrows": 10,
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1
|
||||
}
|
||||
},
|
||||
"ourbits.club": {
|
||||
"xpath": "//a[@class='gen_rsslink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
}
|
||||
},
|
||||
"totheglory.im": {
|
||||
"xpath": "//textarea/text()",
|
||||
"url": "rsstools.php?c51=51&c52=52&c53=53&c54=54&c108=108&c109=109&c62=62&c63=63&c67=67&c69=69&c70=70&c73=73&c76=76&c75=75&c74=74&c87=87&c88=88&c99=99&c90=90&c58=58&c103=103&c101=101&c60=60",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
}
|
||||
},
|
||||
"monikadesign.uk": {
|
||||
"xpath": "//a/@href",
|
||||
"url": "rss",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
}
|
||||
},
|
||||
"zhuque.in": {
|
||||
"xpath": "//a/@href",
|
||||
"url": "user/rss",
|
||||
"render": True,
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
}
|
||||
},
|
||||
"hdchina.org": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
"rsscart": 0
|
||||
}
|
||||
},
|
||||
"audiences.me": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
"torrent_type": 1,
|
||||
"exp": 180
|
||||
}
|
||||
},
|
||||
"shadowflow.org": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"paid": 0,
|
||||
"search_mode": 0,
|
||||
"showrows": 30
|
||||
}
|
||||
},
|
||||
"hddolby.com": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
"exp": 180
|
||||
}
|
||||
},
|
||||
"hdhome.org": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
"exp": 180
|
||||
}
|
||||
},
|
||||
"pthome.net": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
"exp": 180
|
||||
}
|
||||
},
|
||||
"ptsbao.club": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
"size": 0
|
||||
}
|
||||
},
|
||||
"leaves.red": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 0,
|
||||
"paid": 2
|
||||
}
|
||||
},
|
||||
"hdtime.org": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 0,
|
||||
}
|
||||
},
|
||||
"m-team.io": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"showrows": 50,
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"https": 1
|
||||
}
|
||||
},
|
||||
"u2.dmhy.org": {
|
||||
"xpath": "//a[@class='faqlink']/@href",
|
||||
"url": "getrss.php",
|
||||
"params": {
|
||||
"inclbookmarked": 0,
|
||||
"itemsmalldescr": 1,
|
||||
"showrows": 50,
|
||||
"search_mode": 1,
|
||||
"inclautochecked": 1,
|
||||
"trackerssl": 1
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def parse(url, proxy: bool = False) -> List[dict]:
|
||||
def parse(url, proxy: bool = False) -> Union[List[dict], None]:
|
||||
"""
|
||||
解析RSS订阅URL,获取RSS中的种子信息
|
||||
:param url: RSS地址
|
||||
@@ -77,4 +288,61 @@ class RssHelper:
|
||||
continue
|
||||
except Exception as e2:
|
||||
print(str(e2))
|
||||
# RSS过期 观众RSS 链接已过期,您需要获得一个新的! pthome RSS Link has expired, You need to get a new one!
|
||||
_rss_expired_msg = [
|
||||
"RSS 链接已过期, 您需要获得一个新的!",
|
||||
"RSS Link has expired, You need to get a new one!",
|
||||
"RSS Link has expired, You need to get new!"
|
||||
]
|
||||
if ret_xml in _rss_expired_msg:
|
||||
return None
|
||||
return ret_array
|
||||
|
||||
def get_rss_link(self, url: str, cookie: str, ua: str, proxy: bool = False) -> Tuple[str, str]:
|
||||
"""
|
||||
获取站点rss地址
|
||||
:param url: 站点地址
|
||||
:param cookie: 站点cookie
|
||||
:param ua: 站点ua
|
||||
:param proxy: 是否使用代理
|
||||
:return: rss地址、错误信息
|
||||
"""
|
||||
try:
|
||||
# 获取站点域名
|
||||
domain = StringUtils.get_url_domain(url)
|
||||
# 获取配置
|
||||
site_conf = self.rss_link_conf.get(domain) or self.rss_link_conf.get("default")
|
||||
# RSS地址
|
||||
rss_url = urljoin(url, site_conf.get("url"))
|
||||
# RSS请求参数
|
||||
rss_params = site_conf.get("params")
|
||||
# 请求RSS页面
|
||||
if site_conf.get("render"):
|
||||
html_text = PlaywrightHelper().get_page_source(
|
||||
url=rss_url,
|
||||
cookies=cookie,
|
||||
ua=ua,
|
||||
proxies=settings.PROXY if proxy else None
|
||||
)
|
||||
else:
|
||||
res = RequestUtils(
|
||||
cookies=cookie,
|
||||
timeout=60,
|
||||
ua=ua,
|
||||
proxies=settings.PROXY if proxy else None
|
||||
).post_res(url=rss_url, data=rss_params)
|
||||
if res:
|
||||
html_text = res.text
|
||||
elif res is not None:
|
||||
return "", f"获取 {url} RSS链接失败,错误码:{res.status_code},错误原因:{res.reason}"
|
||||
else:
|
||||
return "", f"获取RSS链接失败:无法连接 {url} "
|
||||
# 解析HTML
|
||||
html = etree.HTML(html_text)
|
||||
if html:
|
||||
rss_link = html.xpath(site_conf.get("xpath"))
|
||||
if rss_link:
|
||||
return str(rss_link[-1]), ""
|
||||
return "", f"获取RSS链接失败:{url}"
|
||||
except Exception as e:
|
||||
return "", f"获取 {url} RSS链接失败:{str(e)}"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -68,7 +68,7 @@ class EmbyModule(_ModuleBase):
|
||||
if movie:
|
||||
logger.info(f"媒体库中已存在:{movie}")
|
||||
return ExistMediaInfo(type=MediaType.MOVIE)
|
||||
movies = self.emby.get_movies(title=mediainfo.title, year=mediainfo.year)
|
||||
movies = self.emby.get_movies(title=mediainfo.title, year=mediainfo.year, tmdb_id=mediainfo.tmdb_id)
|
||||
if not movies:
|
||||
logger.info(f"{mediainfo.title_year} 在媒体库中不存在")
|
||||
return None
|
||||
|
||||
@@ -272,11 +272,15 @@ class Emby(metaclass=Singleton):
|
||||
return None
|
||||
return ""
|
||||
|
||||
def get_movies(self, title: str, year: str = None) -> Optional[List[dict]]:
|
||||
def get_movies(self,
|
||||
title: str,
|
||||
year: str = None,
|
||||
tmdb_id: int = None) -> Optional[List[dict]]:
|
||||
"""
|
||||
根据标题和年份,检查电影是否在Emby中存在,存在则返回列表
|
||||
:param title: 标题
|
||||
:param year: 年份,可以为空,为空时不按年份过滤
|
||||
:param tmdb_id: TMDB ID
|
||||
:return: 含title、year属性的字典列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
@@ -291,11 +295,19 @@ class Emby(metaclass=Singleton):
|
||||
if res_items:
|
||||
ret_movies = []
|
||||
for res_item in res_items:
|
||||
item_tmdbid = res_item.get("ProviderIds", {}).get("Tmdb")
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(item_tmdbid) != str(tmdb_id):
|
||||
continue
|
||||
else:
|
||||
ret_movies.append(
|
||||
{'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))})
|
||||
continue
|
||||
if res_item.get('Name') == title and (
|
||||
not year or str(res_item.get('ProductionYear')) == str(year)):
|
||||
ret_movies.append(
|
||||
{'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))})
|
||||
return ret_movies
|
||||
return ret_movies
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items出错:" + str(e))
|
||||
return None
|
||||
@@ -326,10 +338,12 @@ class Emby(metaclass=Singleton):
|
||||
if not item_id:
|
||||
return {}
|
||||
# 验证tmdbid是否相同
|
||||
item_tmdbid = (self.get_iteminfo(item_id).get("ProviderIds") or {}).get("Tmdb")
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(tmdb_id) != str(item_tmdbid):
|
||||
return {}
|
||||
item_info = self.get_iteminfo(item_id)
|
||||
if item_info:
|
||||
item_tmdbid = (item_info.get("ProviderIds") or {}).get("Tmdb")
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(tmdb_id) != str(item_tmdbid):
|
||||
return {}
|
||||
# /Shows/Id/Episodes 查集的信息
|
||||
if not season:
|
||||
season = ""
|
||||
|
||||
@@ -520,13 +520,13 @@ class FileTransferModule(_ModuleBase):
|
||||
计算一个最好的目的目录,有in_path时找与in_path同路径的,没有in_path时,顺序查找1个符合大小要求的,没有in_path和size时,返回第1个
|
||||
:param in_path: 源目录
|
||||
"""
|
||||
if not settings.LIBRARY_PATH:
|
||||
if not settings.LIBRARY_PATHS:
|
||||
return None
|
||||
# 目的路径,多路径以,分隔
|
||||
dest_paths = str(settings.LIBRARY_PATH).split(",")
|
||||
dest_paths = settings.LIBRARY_PATHS
|
||||
# 只有一个路径,直接返回
|
||||
if len(dest_paths) == 1:
|
||||
return Path(dest_paths[0])
|
||||
return dest_paths[0]
|
||||
# 匹配有最长共同上级路径的目录
|
||||
max_length = 0
|
||||
target_path = None
|
||||
@@ -541,15 +541,15 @@ class FileTransferModule(_ModuleBase):
|
||||
logger.debug(f"计算目标路径时出错:{e}")
|
||||
continue
|
||||
if target_path:
|
||||
return Path(target_path)
|
||||
return target_path
|
||||
# 顺序匹配第1个满足空间存储要求的目录
|
||||
if in_path.exists():
|
||||
file_size = in_path.stat().st_size
|
||||
for path in dest_paths:
|
||||
if SystemUtils.free_space(Path(path)) > file_size:
|
||||
return Path(path)
|
||||
if SystemUtils.free_space(path) > file_size:
|
||||
return path
|
||||
# 默认返回第1个
|
||||
return Path(dest_paths[0])
|
||||
return dest_paths[0]
|
||||
|
||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
||||
"""
|
||||
@@ -558,14 +558,14 @@ class FileTransferModule(_ModuleBase):
|
||||
:param itemid: 媒体服务器ItemID
|
||||
:return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}}
|
||||
"""
|
||||
if not settings.LIBRARY_PATH:
|
||||
if not settings.LIBRARY_PATHS:
|
||||
return None
|
||||
# 目的路径,多路径以,分隔
|
||||
dest_paths = str(settings.LIBRARY_PATH).split(",")
|
||||
# 目的路径
|
||||
dest_paths = settings.LIBRARY_PATHS
|
||||
# 检查每一个媒体库目录
|
||||
for dest_path in dest_paths:
|
||||
# 媒体库路径
|
||||
target_dir = self.get_target_path(Path(dest_path))
|
||||
target_dir = self.get_target_path(dest_path)
|
||||
if not target_dir:
|
||||
continue
|
||||
# 媒体分类路径
|
||||
@@ -573,20 +573,19 @@ class FileTransferModule(_ModuleBase):
|
||||
# 重命名格式
|
||||
rename_format = settings.TV_RENAME_FORMAT \
|
||||
if mediainfo.type == MediaType.TV else settings.MOVIE_RENAME_FORMAT
|
||||
# 媒体完整路径
|
||||
media_path = self.get_rename_path(
|
||||
path=target_dir,
|
||||
# 相对路径
|
||||
rel_path = self.get_rename_path(
|
||||
template_string=rename_format,
|
||||
rename_dict=self.__get_naming_dict(meta=MetaInfo(mediainfo.title),
|
||||
mediainfo=mediainfo)
|
||||
)
|
||||
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
# 电影取父目录
|
||||
media_path = media_path.parent
|
||||
# 取相对路径的第1层目录
|
||||
if rel_path.parts:
|
||||
media_path = target_dir / rel_path.parts[0]
|
||||
else:
|
||||
# 电视剧取上两级目录
|
||||
media_path = media_path.parent.parent
|
||||
continue
|
||||
|
||||
# 检查媒体文件夹是否存在
|
||||
if not media_path.exists():
|
||||
continue
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import re
|
||||
from typing import List, Tuple, Union, Dict, Optional
|
||||
|
||||
from app.core.context import TorrentInfo
|
||||
from app.core.context import TorrentInfo, MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
@@ -9,9 +9,10 @@ from app.modules.filter.RuleParser import RuleParser
|
||||
|
||||
|
||||
class FilterModule(_ModuleBase):
|
||||
|
||||
# 规则解析器
|
||||
parser: RuleParser = None
|
||||
# 媒体信息
|
||||
media: MediaInfo = None
|
||||
|
||||
# 内置规则集
|
||||
rule_set: Dict[str, dict] = {
|
||||
@@ -37,8 +38,12 @@ class FilterModule(_ModuleBase):
|
||||
},
|
||||
# 中字
|
||||
"CNSUB": {
|
||||
"include": [r'[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文|中字'],
|
||||
"exclude": []
|
||||
"include": [
|
||||
r'[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文|中字'],
|
||||
"exclude": [],
|
||||
"tmdb": {
|
||||
"original_language": "zh,cn"
|
||||
}
|
||||
},
|
||||
# 特效字幕
|
||||
"SPECSUB": {
|
||||
@@ -67,7 +72,7 @@ class FilterModule(_ModuleBase):
|
||||
},
|
||||
# 杜比
|
||||
"DOLBY": {
|
||||
"include": [r"DOLBY|DOVI|[\s.]+DV[\s.]+|杜比"],
|
||||
"include": [r"Dolby[\s.]+Vision|DOVI|[\s.]+DV[\s.]+|杜比视界"],
|
||||
"exclude": []
|
||||
},
|
||||
# HDR
|
||||
@@ -107,16 +112,19 @@ class FilterModule(_ModuleBase):
|
||||
|
||||
def filter_torrents(self, rule_string: str,
|
||||
torrent_list: List[TorrentInfo],
|
||||
season_episodes: Dict[int, list] = None) -> List[TorrentInfo]:
|
||||
season_episodes: Dict[int, list] = None,
|
||||
mediainfo: MediaInfo = None) -> List[TorrentInfo]:
|
||||
"""
|
||||
过滤种子资源
|
||||
:param rule_string: 过滤规则
|
||||
:param torrent_list: 资源列表
|
||||
:param season_episodes: 季集数过滤 {season:[episodes]}
|
||||
:param mediainfo: 媒体信息
|
||||
:return: 过滤后的资源列表,添加资源优先级
|
||||
"""
|
||||
if not rule_string:
|
||||
return torrent_list
|
||||
self.media = mediainfo
|
||||
# 返回种子列表
|
||||
ret_torrents = []
|
||||
for torrent in torrent_list:
|
||||
@@ -215,6 +223,11 @@ class FilterModule(_ModuleBase):
|
||||
if not self.rule_set.get(rule_name):
|
||||
# 规则不存在
|
||||
return False
|
||||
# TMDB规则
|
||||
tmdb = self.rule_set[rule_name].get("tmdb")
|
||||
# 符合TMDB规则的直接返回True,即不过滤
|
||||
if tmdb and self.__match_tmdb(tmdb):
|
||||
return True
|
||||
# 包含规则项
|
||||
includes = self.rule_set[rule_name].get("include") or []
|
||||
# 排除规则项
|
||||
@@ -236,3 +249,44 @@ class FilterModule(_ModuleBase):
|
||||
# FREE规则不匹配
|
||||
return False
|
||||
return True
|
||||
|
||||
def __match_tmdb(self, tmdb: dict) -> bool:
|
||||
"""
|
||||
判断种子是否匹配TMDB规则
|
||||
"""
|
||||
def __get_media_value(key: str):
|
||||
try:
|
||||
return getattr(self.media, key)
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
if not self.media:
|
||||
return False
|
||||
|
||||
for attr, value in tmdb.items():
|
||||
if not value:
|
||||
continue
|
||||
# 获取media信息的值
|
||||
info_value = __get_media_value(attr)
|
||||
if not info_value:
|
||||
# 没有该值,不匹配
|
||||
return False
|
||||
elif attr == "production_countries":
|
||||
# 国家信息
|
||||
info_values = [str(val.get("iso_3166_1")).upper() for val in info_value]
|
||||
else:
|
||||
# media信息转化为数组
|
||||
if isinstance(info_value, list):
|
||||
info_values = [str(val).upper() for val in info_value]
|
||||
else:
|
||||
info_values = [str(info_value).upper()]
|
||||
# 过滤值转化为数组
|
||||
if value.find(",") != -1:
|
||||
values = [str(val).upper() for val in value.split(",")]
|
||||
else:
|
||||
values = [str(value).upper()]
|
||||
# 没有交集为不匹配
|
||||
if not set(values).intersection(set(info_values)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -64,7 +64,7 @@ class JellyfinModule(_ModuleBase):
|
||||
if movie:
|
||||
logger.info(f"媒体库中已存在:{movie}")
|
||||
return ExistMediaInfo(type=MediaType.MOVIE)
|
||||
movies = self.jellyfin.get_movies(title=mediainfo.title, year=mediainfo.year)
|
||||
movies = self.jellyfin.get_movies(title=mediainfo.title, year=mediainfo.year, tmdb_id=mediainfo.tmdb_id)
|
||||
if not movies:
|
||||
logger.info(f"{mediainfo.title_year} 在媒体库中不存在")
|
||||
return None
|
||||
|
||||
@@ -248,11 +248,15 @@ class Jellyfin(metaclass=Singleton):
|
||||
return None
|
||||
return ""
|
||||
|
||||
def get_movies(self, title: str, year: str = None) -> Optional[List[dict]]:
|
||||
def get_movies(self,
|
||||
title: str,
|
||||
year: str = None,
|
||||
tmdb_id: int = None) -> Optional[List[dict]]:
|
||||
"""
|
||||
根据标题和年份,检查电影是否在Jellyfin中存在,存在则返回列表
|
||||
:param title: 标题
|
||||
:param year: 年份,为空则不过滤
|
||||
:param tmdb_id: TMDB ID
|
||||
:return: 含title、year属性的字典列表
|
||||
"""
|
||||
if not self._host or not self._apikey or not self.user:
|
||||
@@ -266,11 +270,19 @@ class Jellyfin(metaclass=Singleton):
|
||||
if res_items:
|
||||
ret_movies = []
|
||||
for res_item in res_items:
|
||||
item_tmdbid = res_item.get("ProviderIds", {}).get("Tmdb")
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(item_tmdbid) != str(tmdb_id):
|
||||
continue
|
||||
else:
|
||||
ret_movies.append(
|
||||
{'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))})
|
||||
continue
|
||||
if res_item.get('Name') == title and (
|
||||
not year or str(res_item.get('ProductionYear')) == str(year)):
|
||||
ret_movies.append(
|
||||
{'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))})
|
||||
return ret_movies
|
||||
return ret_movies
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items出错:" + str(e))
|
||||
return None
|
||||
@@ -378,25 +390,100 @@ class Jellyfin(metaclass=Singleton):
|
||||
def get_webhook_message(self, message: dict) -> WebhookEventInfo:
|
||||
"""
|
||||
解析Jellyfin报文
|
||||
{
|
||||
"ServerId": "d79d3a6261614419a114595a585xxxxx",
|
||||
"ServerName": "nyanmisaka-jellyfin1",
|
||||
"ServerVersion": "10.8.10",
|
||||
"ServerUrl": "http://xxxxxxxx:8098",
|
||||
"NotificationType": "PlaybackStart",
|
||||
"Timestamp": "2023-09-10T08:35:25.3996506+00:00",
|
||||
"UtcTimestamp": "2023-09-10T08:35:25.3996527Z",
|
||||
"Name": "慕灼华逃婚离开",
|
||||
"Overview": "慕灼华假装在读书,她害怕大娘子说她不务正业。",
|
||||
"Tagline": "",
|
||||
"ItemId": "4b92551344f53b560fb55cd6700xxxxx",
|
||||
"ItemType": "Episode",
|
||||
"RunTimeTicks": 27074985984,
|
||||
"RunTime": "00:45:07",
|
||||
"Year": 2023,
|
||||
"SeriesName": "灼灼风流",
|
||||
"SeasonNumber": 1,
|
||||
"SeasonNumber00": "01",
|
||||
"SeasonNumber000": "001",
|
||||
"EpisodeNumber": 1,
|
||||
"EpisodeNumber00": "01",
|
||||
"EpisodeNumber000": "001",
|
||||
"Provider_tmdb": "229210",
|
||||
"Video_0_Title": "4K HEVC SDR",
|
||||
"Video_0_Type": "Video",
|
||||
"Video_0_Codec": "hevc",
|
||||
"Video_0_Profile": "Main",
|
||||
"Video_0_Level": 150,
|
||||
"Video_0_Height": 2160,
|
||||
"Video_0_Width": 3840,
|
||||
"Video_0_AspectRatio": "16:9",
|
||||
"Video_0_Interlaced": false,
|
||||
"Video_0_FrameRate": 25,
|
||||
"Video_0_VideoRange": "SDR",
|
||||
"Video_0_ColorSpace": "bt709",
|
||||
"Video_0_ColorTransfer": "bt709",
|
||||
"Video_0_ColorPrimaries": "bt709",
|
||||
"Video_0_PixelFormat": "yuv420p",
|
||||
"Video_0_RefFrames": 1,
|
||||
"Audio_0_Title": "AAC - Stereo - Default",
|
||||
"Audio_0_Type": "Audio",
|
||||
"Audio_0_Language": "und",
|
||||
"Audio_0_Codec": "aac",
|
||||
"Audio_0_Channels": 2,
|
||||
"Audio_0_Bitrate": 125360,
|
||||
"Audio_0_SampleRate": 48000,
|
||||
"Audio_0_Default": true,
|
||||
"PlaybackPositionTicks": 1000000,
|
||||
"PlaybackPosition": "00:00:00",
|
||||
"MediaSourceId": "4b92551344f53b560fb55cd6700ebc86",
|
||||
"IsPaused": false,
|
||||
"IsAutomated": false,
|
||||
"DeviceId": "TW96aWxsxxxxxjA",
|
||||
"DeviceName": "Edge Chromium",
|
||||
"ClientName": "Jellyfin Web",
|
||||
"NotificationUsername": "Jeaven",
|
||||
"UserId": "9783d2432b0d40a8a716b6aa46xxxxx"
|
||||
}
|
||||
"""
|
||||
logger.info(f"接收到jellyfin webhook:{message}")
|
||||
eventItem = WebhookEventInfo(
|
||||
event=message.get('NotificationType', ''),
|
||||
item_id=message.get('ItemId'),
|
||||
item_name=message.get('Name'),
|
||||
item_type=message.get('ItemType'),
|
||||
item_favorite=message.get('Favorite'),
|
||||
save_reason=message.get('SaveReason'),
|
||||
tmdb_id=message.get('Provider_tmdb'),
|
||||
user_name=message.get('NotificationUsername'),
|
||||
channel="jellyfin"
|
||||
)
|
||||
eventItem.item_id = message.get('ItemId')
|
||||
eventItem.tmdb_id = message.get('Provider_tmdb')
|
||||
eventItem.overview = message.get('Overview')
|
||||
eventItem.device_name = message.get('DeviceName')
|
||||
eventItem.user_name = message.get('NotificationUsername')
|
||||
eventItem.client = message.get('ClientName')
|
||||
if message.get("ItemType") == "Episode":
|
||||
# 剧集
|
||||
eventItem.item_type = "TV"
|
||||
eventItem.season_id = message.get('SeasonNumber')
|
||||
eventItem.episode_id = message.get('EpisodeNumber')
|
||||
eventItem.item_name = "%s %s%s %s" % (
|
||||
message.get('SeriesName'),
|
||||
"S" + str(eventItem.season_id),
|
||||
"E" + str(eventItem.episode_id),
|
||||
message.get('Name'))
|
||||
else:
|
||||
# 电影
|
||||
eventItem.item_type = "MOV"
|
||||
eventItem.item_name = "%s %s" % (
|
||||
message.get('Name'), "(" + str(message.get('Year')) + ")")
|
||||
|
||||
# 获取消息图片
|
||||
if eventItem.item_id:
|
||||
# 根据返回的item_id去调用媒体服务器获取
|
||||
eventItem.image_url = self.get_remote_image_by_id(item_id=eventItem.item_id,
|
||||
image_type="Backdrop")
|
||||
eventItem.image_url = self.get_remote_image_by_id(
|
||||
item_id=eventItem.item_id,
|
||||
image_type="Backdrop"
|
||||
)
|
||||
|
||||
return eventItem
|
||||
|
||||
@@ -461,8 +548,8 @@ class Jellyfin(metaclass=Singleton):
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return None
|
||||
url = url.replace("{HOST}", self._host)\
|
||||
.replace("{APIKEY}", self._apikey)\
|
||||
url = url.replace("{HOST}", self._host) \
|
||||
.replace("{APIKEY}", self._apikey) \
|
||||
.replace("{USER}", self.user)
|
||||
try:
|
||||
return RequestUtils().get_res(url=url)
|
||||
|
||||
@@ -54,7 +54,10 @@ class PlexModule(_ModuleBase):
|
||||
if movie:
|
||||
logger.info(f"媒体库中已存在:{movie}")
|
||||
return ExistMediaInfo(type=MediaType.MOVIE)
|
||||
movies = self.plex.get_movies(title=mediainfo.title, year=mediainfo.year)
|
||||
movies = self.plex.get_movies(title=mediainfo.title,
|
||||
original_title=mediainfo.original_title,
|
||||
year=mediainfo.year,
|
||||
tmdb_id=mediainfo.tmdb_id)
|
||||
if not movies:
|
||||
logger.info(f"{mediainfo.title_year} 在媒体库中不存在")
|
||||
return None
|
||||
@@ -63,7 +66,9 @@ class PlexModule(_ModuleBase):
|
||||
return ExistMediaInfo(type=MediaType.MOVIE)
|
||||
else:
|
||||
tvs = self.plex.get_tv_episodes(title=mediainfo.title,
|
||||
original_title=mediainfo.original_title,
|
||||
year=mediainfo.year,
|
||||
tmdb_id=mediainfo.tmdb_id,
|
||||
item_id=itemid)
|
||||
if not tvs:
|
||||
logger.info(f"{mediainfo.title_year} 在媒体库中不存在")
|
||||
|
||||
@@ -128,11 +128,17 @@ class Plex(metaclass=Singleton):
|
||||
"EpisodeCount": EpisodeCount
|
||||
}
|
||||
|
||||
def get_movies(self, title: str, year: str = None) -> Optional[List[dict]]:
|
||||
def get_movies(self,
|
||||
title: str,
|
||||
original_title: str = None,
|
||||
year: str = None,
|
||||
tmdb_id: int = None) -> Optional[List[dict]]:
|
||||
"""
|
||||
根据标题和年份,检查电影是否在Plex中存在,存在则返回列表
|
||||
:param title: 标题
|
||||
:param original_title: 原产地标题
|
||||
:param year: 年份,为空则不过滤
|
||||
:param tmdb_id: TMDB ID
|
||||
:return: 含title、year属性的字典列表
|
||||
"""
|
||||
if not self._plex:
|
||||
@@ -140,22 +146,35 @@ class Plex(metaclass=Singleton):
|
||||
ret_movies = []
|
||||
if year:
|
||||
movies = self._plex.library.search(title=title, year=year, libtype="movie")
|
||||
# 根据原标题再查一遍
|
||||
if original_title and str(original_title) != str(title):
|
||||
movies.extend(self._plex.library.search(title=original_title, year=year, libtype="movie"))
|
||||
else:
|
||||
movies = self._plex.library.search(title=title, libtype="movie")
|
||||
for movie in movies:
|
||||
if original_title and str(original_title) != str(title):
|
||||
movies.extend(self._plex.library.search(title=original_title, year=year, libtype="movie"))
|
||||
for movie in set(movies):
|
||||
movie_tmdbid = self.__get_ids(movie.guids).get("tmdb_id")
|
||||
if tmdb_id and movie_tmdbid:
|
||||
if str(movie_tmdbid) != str(tmdb_id):
|
||||
continue
|
||||
ret_movies.append({'title': movie.title, 'year': movie.year})
|
||||
return ret_movies
|
||||
|
||||
def get_tv_episodes(self,
|
||||
item_id: str = None,
|
||||
title: str = None,
|
||||
original_title: str = None,
|
||||
year: str = None,
|
||||
tmdb_id: int = None,
|
||||
season: int = None) -> Optional[Dict[int, list]]:
|
||||
"""
|
||||
根据标题、年份、季查询电视剧所有集信息
|
||||
:param item_id: 媒体ID
|
||||
:param title: 标题
|
||||
:param original_title: 原产地标题
|
||||
:param year: 年份,可以为空,为空时不按年份过滤
|
||||
:param tmdb_id: TMDB ID
|
||||
:param season: 季号,数字
|
||||
:return: 所有集的列表
|
||||
"""
|
||||
@@ -164,13 +183,19 @@ class Plex(metaclass=Singleton):
|
||||
if item_id:
|
||||
videos = self._plex.fetchItem(item_id)
|
||||
else:
|
||||
# 根据标题和年份模糊搜索,该结果不够准确
|
||||
videos = self._plex.library.search(title=title, year=year, libtype="show")
|
||||
if not videos and original_title and str(original_title) != str(title):
|
||||
videos = self._plex.library.search(title=original_title, year=year, libtype="show")
|
||||
if not videos:
|
||||
return {}
|
||||
if isinstance(videos, list):
|
||||
episodes = videos[0].episodes()
|
||||
else:
|
||||
episodes = videos.episodes()
|
||||
videos = videos[0]
|
||||
video_tmdbid = self.__get_ids(videos.guids).get('tmdb_id')
|
||||
if tmdb_id and video_tmdbid:
|
||||
if str(video_tmdbid) != str(tmdb_id):
|
||||
return {}
|
||||
episodes = videos.episodes()
|
||||
season_episodes = {}
|
||||
for episode in episodes:
|
||||
if season and episode.seasonNumber != int(season):
|
||||
@@ -337,10 +362,104 @@ class Plex(metaclass=Singleton):
|
||||
item_name TV:琅琊榜 S1E6 剖心明志 虎口脱险
|
||||
MOV:猪猪侠大冒险(2001)
|
||||
overview 剧情描述
|
||||
{
|
||||
"event": "media.scrobble",
|
||||
"user": false,
|
||||
"owner": true,
|
||||
"Account": {
|
||||
"id": 31646104,
|
||||
"thumb": "https://plex.tv/users/xx",
|
||||
"title": "播放"
|
||||
},
|
||||
"Server": {
|
||||
"title": "Media-Server",
|
||||
"uuid": "xxxx"
|
||||
},
|
||||
"Player": {
|
||||
"local": false,
|
||||
"publicAddress": "xx.xx.xx.xx",
|
||||
"title": "MagicBook",
|
||||
"uuid": "wu0uoa1ujfq90t0c5p9f7fw0"
|
||||
},
|
||||
"Metadata": {
|
||||
"librarySectionType": "show",
|
||||
"ratingKey": "40294",
|
||||
"key": "/library/metadata/40294",
|
||||
"parentRatingKey": "40291",
|
||||
"grandparentRatingKey": "40275",
|
||||
"guid": "plex://episode/615580a9fa828e7f1a0caabd",
|
||||
"parentGuid": "plex://season/615580a9fa828e7f1a0caab8",
|
||||
"grandparentGuid": "plex://show/60e81fd8d8000e002d7d2976",
|
||||
"type": "episode",
|
||||
"title": "The World's Strongest Senior",
|
||||
"titleSort": "World's Strongest Senior",
|
||||
"grandparentKey": "/library/metadata/40275",
|
||||
"parentKey": "/library/metadata/40291",
|
||||
"librarySectionTitle": "动漫剧集",
|
||||
"librarySectionID": 7,
|
||||
"librarySectionKey": "/library/sections/7",
|
||||
"grandparentTitle": "范马刃牙",
|
||||
"parentTitle": "Combat Shadow Fighting Saga / Great Prison Battle Saga",
|
||||
"originalTitle": "Baki Hanma",
|
||||
"contentRating": "TV-MA",
|
||||
"summary": "The world is shaken by news of a man taking down a monstrous elephant with his bare hands. Back in Japan, Baki is confronted by a knife-wielding child.",
|
||||
"index": 1,
|
||||
"parentIndex": 1,
|
||||
"audienceRating": 8.5,
|
||||
"viewCount": 1,
|
||||
"lastViewedAt": 1694320444,
|
||||
"year": 2021,
|
||||
"thumb": "/library/metadata/40294/thumb/1693544504",
|
||||
"art": "/library/metadata/40275/art/1693952979",
|
||||
"parentThumb": "/library/metadata/40291/thumb/1691115271",
|
||||
"grandparentThumb": "/library/metadata/40275/thumb/1693952979",
|
||||
"grandparentArt": "/library/metadata/40275/art/1693952979",
|
||||
"duration": 1500000,
|
||||
"originallyAvailableAt": "2021-09-30",
|
||||
"addedAt": 1691115281,
|
||||
"updatedAt": 1693544504,
|
||||
"audienceRatingImage": "themoviedb://image.rating",
|
||||
"Guid": [
|
||||
{
|
||||
"id": "imdb://tt14765720"
|
||||
},
|
||||
{
|
||||
"id": "tmdb://3087250"
|
||||
},
|
||||
{
|
||||
"id": "tvdb://8530933"
|
||||
}
|
||||
],
|
||||
"Rating": [
|
||||
{
|
||||
"image": "themoviedb://image.rating",
|
||||
"value": 8.5,
|
||||
"type": "audience"
|
||||
}
|
||||
],
|
||||
"Director": [
|
||||
{
|
||||
"id": 115144,
|
||||
"filter": "director=115144",
|
||||
"tag": "Keiya Saito",
|
||||
"tagKey": "5f401c8d04a86500409ea6c1"
|
||||
}
|
||||
],
|
||||
"Writer": [
|
||||
{
|
||||
"id": 115135,
|
||||
"filter": "writer=115135",
|
||||
"tag": "Tatsuhiko Urahata",
|
||||
"tagKey": "5d7768e07a53e9001e6db1ce",
|
||||
"thumb": "https://metadata-static.plex.tv/f/people/f6f90dc89fa87d459f85d40a09720c05.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
message = json.loads(message_str)
|
||||
logger.info(f"接收到plex webhook:{message}")
|
||||
eventItem = WebhookEventInfo(event=message.get('Event', ''), channel="plex")
|
||||
eventItem = WebhookEventInfo(event=message.get('event', ''), channel="plex")
|
||||
if message.get('Metadata'):
|
||||
if message.get('Metadata', {}).get('type') == 'episode':
|
||||
eventItem.item_type = "TV"
|
||||
|
||||
@@ -36,19 +36,22 @@ class QbittorrentModule(_ModuleBase):
|
||||
if self.qbittorrent.is_inactive():
|
||||
self.qbittorrent = Qbittorrent()
|
||||
|
||||
def download(self, torrent_path: Path, download_dir: Path, cookie: str,
|
||||
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
|
||||
episodes: Set[int] = None, category: str = None) -> Optional[Tuple[Optional[str], str]]:
|
||||
"""
|
||||
根据种子文件,选择并添加下载任务
|
||||
:param torrent_path: 种子文件地址
|
||||
:param content: 种子文件地址或者磁力链接
|
||||
:param download_dir: 下载目录
|
||||
:param cookie: cookie
|
||||
:param episodes: 需要下载的集数
|
||||
:param category: 分类
|
||||
:return: 种子Hash,错误信息
|
||||
"""
|
||||
if not torrent_path or not torrent_path.exists():
|
||||
return None, f"种子文件不存在:{torrent_path}"
|
||||
if not content:
|
||||
return
|
||||
if isinstance(content, Path) and not content.exists():
|
||||
return None, f"种子文件不存在:{content}"
|
||||
|
||||
# 生成随机Tag
|
||||
tag = StringUtils.generate_random_str(10)
|
||||
if settings.TORRENT_TAG:
|
||||
@@ -58,19 +61,21 @@ class QbittorrentModule(_ModuleBase):
|
||||
# 如果要选择文件则先暂停
|
||||
is_paused = True if episodes else False
|
||||
# 添加任务
|
||||
state = self.qbittorrent.add_torrent(content=torrent_path.read_bytes(),
|
||||
download_dir=str(download_dir),
|
||||
is_paused=is_paused,
|
||||
tag=tags,
|
||||
cookie=cookie,
|
||||
category=category)
|
||||
state = self.qbittorrent.add_torrent(
|
||||
content=content.read_bytes() if isinstance(content, Path) else content,
|
||||
download_dir=str(download_dir),
|
||||
is_paused=is_paused,
|
||||
tag=tags,
|
||||
cookie=cookie,
|
||||
category=category
|
||||
)
|
||||
if not state:
|
||||
return None, f"添加种子任务失败:{torrent_path}"
|
||||
return None, f"添加种子任务失败:{content}"
|
||||
else:
|
||||
# 获取种子Hash
|
||||
torrent_hash = self.qbittorrent.get_torrent_id_by_tag(tags=tag)
|
||||
if not torrent_hash:
|
||||
return None, f"获取种子Hash失败:{torrent_path}"
|
||||
return None, f"获取种子Hash失败:{content}"
|
||||
else:
|
||||
if is_paused:
|
||||
# 种子文件
|
||||
|
||||
@@ -272,6 +272,7 @@ class Slack:
|
||||
link = torrent.page_url
|
||||
title = f"{meta.season_episode} " \
|
||||
f"{meta.resource_term} " \
|
||||
f"{meta.video_term} " \
|
||||
f"{meta.release_group}"
|
||||
title = re.sub(r"\s+", " ", title).strip()
|
||||
free = torrent.volume_factor
|
||||
|
||||
@@ -34,15 +34,22 @@ class SubtitleModule(_ModuleBase):
|
||||
def stop(self) -> None:
|
||||
pass
|
||||
|
||||
def download_added(self, context: Context, torrent_path: Path, download_dir: Path) -> None:
|
||||
def download_added(self, context: Context, download_dir: Path, torrent_path: Path = None) -> None:
|
||||
"""
|
||||
添加下载任务成功后,从站点下载字幕,保存到下载目录
|
||||
:param context: 上下文,包括识别信息、媒体信息、种子信息
|
||||
:param torrent_path: 种子文件地址
|
||||
:param download_dir: 下载目录
|
||||
:param torrent_path: 种子文件地址
|
||||
:return: None,该方法可被多个模块同时处理
|
||||
"""
|
||||
# 种子信息
|
||||
if not settings.DOWNLOAD_SUBTITLE:
|
||||
return None
|
||||
|
||||
# 没有种子文件不处理
|
||||
if not torrent_path:
|
||||
return
|
||||
|
||||
# 没有详情页不处理
|
||||
torrent = context.torrent_info
|
||||
if not torrent.page_url:
|
||||
return
|
||||
|
||||
@@ -153,6 +153,7 @@ class Telegram(metaclass=Singleton):
|
||||
link = torrent.page_url
|
||||
title = f"{meta.season_episode} " \
|
||||
f"{meta.resource_term} " \
|
||||
f"{meta.video_term} " \
|
||||
f"{meta.release_group}"
|
||||
title = re.sub(r"\s+", " ", title).strip()
|
||||
free = torrent.volume_factor
|
||||
|
||||
@@ -74,8 +74,8 @@ class TheMovieDbModule(_ModuleBase):
|
||||
mtype=meta.type,
|
||||
season_year=meta.year,
|
||||
season_number=meta.begin_season)
|
||||
if meta.year:
|
||||
# 非严格模式下去掉年份再查一次
|
||||
if not info:
|
||||
# 去掉年份再查一次
|
||||
info = self.tmdb.match(name=meta.name,
|
||||
mtype=meta.type)
|
||||
else:
|
||||
@@ -132,6 +132,12 @@ class TheMovieDbModule(_ModuleBase):
|
||||
else:
|
||||
logger.info(f"{tmdbid} 识别结果:{mediainfo.type.value} "
|
||||
f"{mediainfo.title_year}")
|
||||
|
||||
# 补充剧集年份
|
||||
if mediainfo.type == MediaType.TV:
|
||||
episode_years = self.tmdb.get_tv_episode_years(info.get("id"))
|
||||
if episode_years:
|
||||
mediainfo.season_years = episode_years
|
||||
return mediainfo
|
||||
else:
|
||||
logger.info(f"{meta.name if meta else tmdbid} 未匹配到媒体信息")
|
||||
|
||||
@@ -447,7 +447,8 @@ class TmdbHelper:
|
||||
ret_info = multi
|
||||
break
|
||||
# 类型变更
|
||||
if ret_info:
|
||||
if (ret_info
|
||||
and not isinstance(ret_info.get("media_type"), MediaType)):
|
||||
ret_info['media_type'] = MediaType.MOVIE if ret_info.get("media_type") == "movie" else MediaType.TV
|
||||
|
||||
return ret_info
|
||||
@@ -1167,3 +1168,35 @@ class TmdbHelper:
|
||||
清除缓存
|
||||
"""
|
||||
self.tmdb.cache_clear()
|
||||
|
||||
def get_tv_episode_years(self, tv_id: int):
|
||||
"""
|
||||
查询剧集组年份
|
||||
"""
|
||||
try:
|
||||
episode_groups = self.tv.episode_groups(tv_id)
|
||||
if not episode_groups:
|
||||
return {}
|
||||
episode_years = {}
|
||||
for episode_group in episode_groups:
|
||||
logger.info(f"正在获取剧集组年份:{episode_group.get('id')}...")
|
||||
if episode_group.get('type') != 6:
|
||||
# 只处理剧集部分
|
||||
continue
|
||||
group_episodes = self.tv.group_episodes(episode_group.get('id'))
|
||||
if not group_episodes:
|
||||
continue
|
||||
for group_episode in group_episodes:
|
||||
order = group_episode.get('order')
|
||||
episodes = group_episode.get('episodes')
|
||||
if not episodes or not order:
|
||||
continue
|
||||
# 当前季第一季时间
|
||||
first_date = episodes[0].get("air_date")
|
||||
if not first_date and str(first_date).split("-") != 3:
|
||||
continue
|
||||
episode_years[order] = str(first_date).split("-")[0]
|
||||
return episode_years
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return {}
|
||||
|
||||
@@ -33,6 +33,7 @@ class TV(TMDb):
|
||||
"on_the_air": "/tv/on_the_air",
|
||||
"popular": "/tv/popular",
|
||||
"top_rated": "/tv/top_rated",
|
||||
"group_episodes": "/tv/episode_group/%s",
|
||||
}
|
||||
|
||||
def details(self, tv_id, append_to_response="videos,trailers,images,credits,translations"):
|
||||
@@ -130,6 +131,17 @@ class TV(TMDb):
|
||||
key="results"
|
||||
)
|
||||
|
||||
def group_episodes(self, group_id):
|
||||
"""
|
||||
查询剧集组所有剧集
|
||||
:param group_id: int
|
||||
:return:
|
||||
"""
|
||||
return self._request_obj(
|
||||
self._urls["group_episodes"] % group_id,
|
||||
key="groups"
|
||||
)
|
||||
|
||||
def external_ids(self, tv_id):
|
||||
"""
|
||||
Get the external ids for a TV show.
|
||||
|
||||
@@ -36,17 +36,22 @@ class TransmissionModule(_ModuleBase):
|
||||
if not self.transmission.is_inactive():
|
||||
self.transmission = Transmission()
|
||||
|
||||
def download(self, torrent_path: Path, download_dir: Path, cookie: str,
|
||||
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
|
||||
episodes: Set[int] = None, category: str = None) -> Optional[Tuple[Optional[str], str]]:
|
||||
"""
|
||||
根据种子文件,选择并添加下载任务
|
||||
:param torrent_path: 种子文件地址
|
||||
:param content: 种子文件地址或者磁力链接
|
||||
:param download_dir: 下载目录
|
||||
:param cookie: cookie
|
||||
:param episodes: 需要下载的集数
|
||||
:param category: 分类,TR中未使用
|
||||
:return: 种子Hash
|
||||
"""
|
||||
if not content:
|
||||
return
|
||||
if isinstance(content, Path) and not content.exists():
|
||||
return None, f"种子文件不存在:{content}"
|
||||
|
||||
# 如果要选择文件则先暂停
|
||||
is_paused = True if episodes else False
|
||||
# 标签
|
||||
@@ -55,13 +60,15 @@ class TransmissionModule(_ModuleBase):
|
||||
else:
|
||||
labels = None
|
||||
# 添加任务
|
||||
torrent = self.transmission.add_torrent(content=torrent_path.read_bytes(),
|
||||
download_dir=str(download_dir),
|
||||
is_paused=is_paused,
|
||||
labels=labels,
|
||||
cookie=cookie)
|
||||
torrent = self.transmission.add_torrent(
|
||||
content=content.read_bytes() if isinstance(content, Path) else content,
|
||||
download_dir=str(download_dir),
|
||||
is_paused=is_paused,
|
||||
labels=labels,
|
||||
cookie=cookie
|
||||
)
|
||||
if not torrent:
|
||||
return None, f"添加种子任务失败:{torrent_path}"
|
||||
return None, f"添加种子任务失败:{content}"
|
||||
else:
|
||||
torrent_hash = torrent.hashString
|
||||
if is_paused:
|
||||
@@ -105,7 +112,7 @@ class TransmissionModule(_ModuleBase):
|
||||
title=torrent.name,
|
||||
path=Path(torrent.download_dir) / torrent.name,
|
||||
hash=torrent.hashString,
|
||||
tags=torrent.labels
|
||||
tags=",".join(torrent.labels or [])
|
||||
))
|
||||
elif status == TorrentStatus.TRANSFER:
|
||||
# 获取已完成且未整理的
|
||||
@@ -124,7 +131,7 @@ class TransmissionModule(_ModuleBase):
|
||||
title=torrent.name,
|
||||
path=Path(torrent.download_dir) / torrent.name,
|
||||
hash=torrent.hashString,
|
||||
tags=torrent.labels
|
||||
tags=",".join(torrent.labels or [])
|
||||
))
|
||||
elif status == TorrentStatus.DOWNLOADING:
|
||||
# 获取正在下载的任务
|
||||
|
||||
@@ -222,6 +222,7 @@ class WeChat(metaclass=Singleton):
|
||||
torrent_title = f"{index}.【{torrent.site_name}】" \
|
||||
f"{meta.season_episode} " \
|
||||
f"{meta.resource_term} " \
|
||||
f"{meta.video_term} " \
|
||||
f"{meta.release_group} " \
|
||||
f"{StringUtils.str_filesize(torrent.size)} " \
|
||||
f"{torrent.volume_factor} " \
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, List, Dict, Tuple
|
||||
from app.chain import ChainBase
|
||||
from app.core.config import settings
|
||||
from app.core.event import EventManager
|
||||
from app.db import ScopedSession
|
||||
from app.db import SessionFactory
|
||||
from app.db.models import Base
|
||||
from app.db.plugindata_oper import PluginDataOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
@@ -37,7 +37,7 @@ class _PluginBase(metaclass=ABCMeta):
|
||||
|
||||
def __init__(self):
|
||||
# 数据库连接
|
||||
self.db = ScopedSession()
|
||||
self.db = SessionFactory()
|
||||
# 插件数据
|
||||
self.plugindata = PluginDataOper(self.db)
|
||||
# 处理链
|
||||
@@ -184,3 +184,10 @@ class _PluginBase(metaclass=ABCMeta):
|
||||
channel=channel, mtype=mtype, title=title, text=text,
|
||||
image=image, link=link, userid=userid
|
||||
))
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
关闭数据库连接
|
||||
"""
|
||||
if self.db:
|
||||
self.db.close()
|
||||
|
||||
@@ -14,6 +14,7 @@ from ruamel.yaml import CommentedMap
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.core.event import EventManager, eventmanager, Event
|
||||
from app.db.models.site import Site
|
||||
from app.helper.browser import PlaywrightHelper
|
||||
from app.helper.cloudflare import under_challenge
|
||||
from app.helper.module import ModuleHelper
|
||||
@@ -85,11 +86,18 @@ class AutoSignIn(_PluginBase):
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
self._notify = config.get("notify")
|
||||
self._queue_cnt = config.get("queue_cnt") or 5
|
||||
self._sign_sites = config.get("sign_sites")
|
||||
self._login_sites = config.get("login_sites")
|
||||
self._sign_sites = config.get("sign_sites") or []
|
||||
self._login_sites = config.get("login_sites") or []
|
||||
self._retry_keyword = config.get("retry_keyword")
|
||||
self._clean = config.get("clean")
|
||||
|
||||
# 过滤掉已删除的站点
|
||||
all_sites = [site for site in self.sites.get_indexers() if not site.get("public")]
|
||||
self._sign_sites = [site.get("id") for site in all_sites if site.get("id") in self._sign_sites]
|
||||
self._login_sites = [site.get("id") for site in all_sites if site.get("id") in self._login_sites]
|
||||
# 保存配置
|
||||
self.__update_config()
|
||||
|
||||
# 加载模块
|
||||
if self._enabled or self._onlyonce:
|
||||
|
||||
@@ -237,8 +245,8 @@ class AutoSignIn(_PluginBase):
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
# 站点的可选项
|
||||
site_options = [{"title": site.get("name"), "value": site.get("id")}
|
||||
for site in self.sites.get_indexers()]
|
||||
site_options = [{"title": site.name, "value": site.id}
|
||||
for site in Site.list_order_by_pri(self.db)]
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
@@ -469,7 +477,7 @@ class AutoSignIn(_PluginBase):
|
||||
{
|
||||
'component': 'td',
|
||||
'props': {
|
||||
'class': 'whitespace-nowrap break-keep'
|
||||
'class': 'whitespace-nowrap break-keep text-high-emphasis'
|
||||
},
|
||||
'text': current_day
|
||||
},
|
||||
@@ -575,6 +583,7 @@ class AutoSignIn(_PluginBase):
|
||||
yesterday_str = yesterday.strftime('%Y-%m-%d')
|
||||
# 删除昨天历史
|
||||
self.del_data(key=type + "-" + yesterday_str)
|
||||
self.del_data(key=f"{yesterday.month}月{yesterday.day}日")
|
||||
|
||||
# 查看今天有没有签到|登录历史
|
||||
today = today.strftime('%Y-%m-%d')
|
||||
@@ -591,19 +600,14 @@ class AutoSignIn(_PluginBase):
|
||||
# 今日没数据
|
||||
if not today_history or self._clean:
|
||||
logger.info(f"今日 {today} 未{type},开始{type}已选站点")
|
||||
# 过滤删除的站点
|
||||
if type == "签到":
|
||||
self._sign_sites = [site.get("id") for site in do_sites if site]
|
||||
if type == "登录":
|
||||
self._login_sites = [site.get("id") for site in do_sites if site]
|
||||
if self._clean:
|
||||
# 关闭开关
|
||||
self._clean = False
|
||||
else:
|
||||
# 需要重试站点
|
||||
retry_sites = today_history.get("retry")
|
||||
retry_sites = today_history.get("retry") or []
|
||||
# 今天已签到|登录站点
|
||||
already_sites = today_history.get("sign")
|
||||
already_sites = today_history.get("do") or []
|
||||
|
||||
# 今日未签|登录站点
|
||||
no_sites = [site for site in do_sites if
|
||||
@@ -634,11 +638,22 @@ class AutoSignIn(_PluginBase):
|
||||
logger.info(f"站点{type}任务完成!")
|
||||
# 获取今天的日期
|
||||
key = f"{datetime.now().month}月{datetime.now().day}日"
|
||||
today_data = self.get_data(key)
|
||||
if today_data:
|
||||
if not isinstance(today_data, list):
|
||||
today_data = [today_data]
|
||||
for s in status:
|
||||
today_data.append({
|
||||
"site": s[0],
|
||||
"status": s[1]
|
||||
})
|
||||
else:
|
||||
today_data = [{
|
||||
"site": s[0],
|
||||
"status": s[1]
|
||||
} for s in status]
|
||||
# 保存数据
|
||||
self.save_data(key, [{
|
||||
"site": s[0],
|
||||
"status": s[1]
|
||||
} for s in status])
|
||||
self.save_data(key, today_data)
|
||||
|
||||
# 命中重试词的站点id
|
||||
retry_sites = []
|
||||
@@ -686,13 +701,13 @@ class AutoSignIn(_PluginBase):
|
||||
|
||||
if not self._retry_keyword:
|
||||
# 没设置重试关键词则重试已选站点
|
||||
retry_sites = self._sign_sites
|
||||
retry_sites = self._sign_sites if type == "签到" else self._login_sites
|
||||
logger.debug(f"下次{type}重试站点 {retry_sites}")
|
||||
|
||||
# 存入历史
|
||||
self.save_data(key=type + "-" + today,
|
||||
value={
|
||||
"sign": self._sign_sites,
|
||||
"do": self._sign_sites if type == "签到" else self._login_sites,
|
||||
"retry": retry_sites
|
||||
})
|
||||
|
||||
@@ -704,9 +719,9 @@ class AutoSignIn(_PluginBase):
|
||||
signin_message += retry_msg
|
||||
|
||||
signin_message = "\n".join([f'【{s[0]}】{s[1]}' for s in signin_message if s])
|
||||
self.post_message(title=f"站点自动{type}",
|
||||
self.post_message(title=f"【站点自动{type}】",
|
||||
mtype=NotificationType.SiteMessage,
|
||||
text=f"全部{type}数量: {len(list(self._sign_sites))} \n"
|
||||
text=f"全部{type}数量: {len(self._sign_sites if type == '签到' else self._login_sites)} \n"
|
||||
f"本次{type}数量: {len(do_sites)} \n"
|
||||
f"下次{type}数量: {len(retry_sites) if self._retry_keyword else 0} \n"
|
||||
f"{signin_message}"
|
||||
@@ -801,6 +816,10 @@ class AutoSignIn(_PluginBase):
|
||||
return f"无法通过Cloudflare!"
|
||||
return f"仿真登录失败,Cookie已失效!"
|
||||
else:
|
||||
# 判断是否已签到
|
||||
if re.search(r'已签|签到已得', page_source, re.IGNORECASE) \
|
||||
or SiteUtils.is_checkin(page_source):
|
||||
return f"签到成功"
|
||||
return "仿真签到成功"
|
||||
else:
|
||||
res = RequestUtils(cookies=site_cookie,
|
||||
@@ -930,30 +949,25 @@ class AutoSignIn(_PluginBase):
|
||||
site_id = event.event_data.get("site_id")
|
||||
config = self.get_config()
|
||||
if config:
|
||||
sign_sites = config.get("sign_sites")
|
||||
if sign_sites:
|
||||
if isinstance(sign_sites, str):
|
||||
sign_sites = [sign_sites]
|
||||
self._sign_sites = self.__remove_site_id(config.get("sign_sites") or [], site_id)
|
||||
self._login_sites = self.__remove_site_id(config.get("login_sites") or [], site_id)
|
||||
# 保存配置
|
||||
self.__update_config()
|
||||
|
||||
# 删除对应站点
|
||||
if site_id:
|
||||
sign_sites = [site for site in sign_sites if int(site) != int(site_id)]
|
||||
else:
|
||||
# 清空
|
||||
sign_sites = []
|
||||
def __remove_site_id(self, do_sites, site_id):
|
||||
if do_sites:
|
||||
if isinstance(do_sites, str):
|
||||
do_sites = [do_sites]
|
||||
|
||||
# 若无站点,则停止
|
||||
if len(sign_sites) == 0:
|
||||
self._enabled = False
|
||||
# 删除对应站点
|
||||
if site_id:
|
||||
do_sites = [site for site in do_sites if int(site) != int(site_id)]
|
||||
else:
|
||||
# 清空
|
||||
do_sites = []
|
||||
|
||||
# 保存配置
|
||||
self.update_config(
|
||||
{
|
||||
"enabled": self._enabled,
|
||||
"notify": self._notify,
|
||||
"cron": self._cron,
|
||||
"onlyonce": self._onlyonce,
|
||||
"queue_cnt": self._queue_cnt,
|
||||
"sign_sites": sign_sites
|
||||
}
|
||||
)
|
||||
# 若无站点,则停止
|
||||
if len(do_sites) == 0:
|
||||
self._enabled = False
|
||||
|
||||
return do_sites
|
||||
|
||||
@@ -9,7 +9,6 @@ import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
from app import schemas
|
||||
from app.chain.search import SearchChain
|
||||
from app.chain.torrents import TorrentsChain
|
||||
from app.core.config import settings
|
||||
from app.db.site_oper import SiteOper
|
||||
@@ -50,7 +49,6 @@ class BrushFlow(_PluginBase):
|
||||
siteshelper = None
|
||||
siteoper = None
|
||||
torrents = None
|
||||
searchchain = None
|
||||
qb = None
|
||||
tr = None
|
||||
# 添加种子定时
|
||||
@@ -90,7 +88,6 @@ class BrushFlow(_PluginBase):
|
||||
self.siteshelper = SitesHelper()
|
||||
self.siteoper = SiteOper()
|
||||
self.torrents = TorrentsChain()
|
||||
self.searchchain = SearchChain()
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._notify = config.get("notify")
|
||||
@@ -1245,7 +1242,8 @@ class BrushFlow(_PluginBase):
|
||||
torrents_size = 0
|
||||
# 读取统计数据
|
||||
statistic_info = self.get_data("statistic") or {
|
||||
"count": 0
|
||||
"count": 0,
|
||||
"deleted": 0,
|
||||
}
|
||||
# 处理所有站点
|
||||
for siteid in self._brushsites:
|
||||
@@ -1254,7 +1252,7 @@ class BrushFlow(_PluginBase):
|
||||
logger.warn(f"站点不存在:{siteid}")
|
||||
continue
|
||||
logger.info(f"开始获取站点 {siteinfo.name} 的新种子 ...")
|
||||
torrents = self.searchchain.browse(domain=siteinfo.domain)
|
||||
torrents = self.torrents.browse(domain=siteinfo.domain)
|
||||
if not torrents:
|
||||
logger.info(f"站点 {siteinfo.name} 没有获取到种子")
|
||||
continue
|
||||
@@ -1324,10 +1322,10 @@ class BrushFlow(_PluginBase):
|
||||
end_pubtime = 0
|
||||
# 将种子发布日志转换为与当前时间的差
|
||||
if begin_pubtime and not end_pubtime \
|
||||
and pubdate_minutes > int(begin_pubtime) * 60:
|
||||
and pubdate_minutes > int(begin_pubtime):
|
||||
continue
|
||||
elif begin_pubtime and end_pubtime \
|
||||
and not int(begin_pubtime) * 60 <= pubdate_minutes <= int(end_pubtime) * 60:
|
||||
and not int(begin_pubtime) <= pubdate_minutes <= int(end_pubtime):
|
||||
continue
|
||||
# 同时下载任务数
|
||||
downloads = self.__get_downloading_count()
|
||||
@@ -1480,9 +1478,10 @@ class BrushFlow(_PluginBase):
|
||||
torrent_title=torrent_info.get("title"),
|
||||
reason=f"下载耗时达到 {self._download_time} 小时")
|
||||
continue
|
||||
# 平均上传速度(KB / s)
|
||||
# 平均上传速度(KB / s),大于30分钟才有效
|
||||
if self._seed_avgspeed:
|
||||
if torrent_info.get("avg_upspeed") <= float(self._seed_avgspeed) * 1024:
|
||||
if torrent_info.get("avg_upspeed") <= float(self._seed_avgspeed) * 1024 and \
|
||||
torrent_info.get("seeding_time") >= 30 * 60:
|
||||
logger.info(f"平均上传速度低于 {self._seed_avgspeed} KB/s,删除种子:{torrent_info.get('title')}")
|
||||
downloader.delete_torrents(ids=torrent_hash, delete_file=True)
|
||||
remove_torrents.append(torrent_info)
|
||||
@@ -1503,6 +1502,8 @@ class BrushFlow(_PluginBase):
|
||||
continue
|
||||
# 统计删除状态
|
||||
if remove_torrents:
|
||||
if not statistic_info.get("deleted"):
|
||||
statistic_info["deleted"] = 0
|
||||
statistic_info["deleted"] += len(remove_torrents)
|
||||
# 删除任务记录
|
||||
for torrent in remove_torrents:
|
||||
@@ -1675,9 +1676,17 @@ class BrushFlow(_PluginBase):
|
||||
# 标题
|
||||
torrent_title = torrent.get("name")
|
||||
# 下载时间
|
||||
dltime = date_now - torrent.get("added_on") if torrent.get("added_on") else 0
|
||||
if (not torrent.get("added_on")
|
||||
or torrent.get("added_on") < 0):
|
||||
dltime = 0
|
||||
else:
|
||||
dltime = date_now - torrent.get("added_on")
|
||||
# 做种时间
|
||||
seeding_time = date_now - (torrent.get("completion_on") if torrent.get("completion_on") else 0)
|
||||
if (not torrent.get("completion_on")
|
||||
or torrent.get("completion_on") < 0):
|
||||
seeding_time = 0
|
||||
else:
|
||||
seeding_time = date_now - torrent.get("completion_on")
|
||||
# 分享率
|
||||
ratio = torrent.get("ratio") or 0
|
||||
# 上传量
|
||||
@@ -1688,7 +1697,11 @@ class BrushFlow(_PluginBase):
|
||||
else:
|
||||
avg_upspeed = uploaded
|
||||
# 已未活动 秒
|
||||
iatime = date_now - (torrent.get("last_activity") if torrent.get("last_activity") else 0)
|
||||
if (not torrent.get("last_activity")
|
||||
or torrent.get("last_activity") < 0):
|
||||
iatime = 0
|
||||
else:
|
||||
iatime = date_now - torrent.get("last_activity")
|
||||
# 下载量
|
||||
downloaded = torrent.get("downloaded")
|
||||
# 种子大小
|
||||
@@ -1702,12 +1715,14 @@ class BrushFlow(_PluginBase):
|
||||
# 标题
|
||||
torrent_title = torrent.name
|
||||
# 做种时间
|
||||
if not torrent.date_done or torrent.date_done.timestamp() < 1:
|
||||
if (not torrent.date_done
|
||||
or torrent.date_done.timestamp() < 1):
|
||||
seeding_time = 0
|
||||
else:
|
||||
seeding_time = date_now - int(torrent.date_done.timestamp())
|
||||
# 下载耗时
|
||||
if not torrent.date_added or torrent.date_added.timestamp() < 1:
|
||||
if (not torrent.date_added
|
||||
or torrent.date_added.timestamp() < 1):
|
||||
dltime = 0
|
||||
else:
|
||||
dltime = date_now - int(torrent.date_added.timestamp())
|
||||
@@ -1723,7 +1738,8 @@ class BrushFlow(_PluginBase):
|
||||
else:
|
||||
avg_upspeed = uploaded
|
||||
# 未活动时间
|
||||
if not torrent.date_active or torrent.date_active.timestamp() < 1:
|
||||
if (not torrent.date_active
|
||||
or torrent.date_active.timestamp() < 1):
|
||||
iatime = 0
|
||||
else:
|
||||
iatime = date_now - int(torrent.date_active.timestamp())
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import zipfile
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Dict, Any
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from python_hosts import Hosts, HostsEntry
|
||||
from requests import Response
|
||||
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
@@ -142,13 +147,35 @@ class CloudflareSpeedTest(_PluginBase):
|
||||
if err_flag:
|
||||
logger.info("正在进行CLoudflare CDN优选,请耐心等待")
|
||||
# 执行优选命令,-dd不测速
|
||||
cf_command = f'cd {self._cf_path} && chmod a+x {self._binary_name} && ./{self._binary_name} {self._additional_args} -o {self._result_file}' + (
|
||||
f' -f {self._cf_ipv4}' if self._ipv4 else '') + (f' -f {self._cf_ipv6}' if self._ipv6 else '')
|
||||
if SystemUtils.is_windows():
|
||||
cf_command = f'cd \"{self._cf_path}\" && CloudflareST {self._additional_args} -o \"{self._result_file}\"' + (
|
||||
f' -f \"{self._cf_ipv4}\"' if self._ipv4 else '') + (f' -f \"{self._cf_ipv6}\"' if self._ipv6 else '')
|
||||
else:
|
||||
cf_command = f'cd {self._cf_path} && chmod a+x {self._binary_name} && ./{self._binary_name} {self._additional_args} -o {self._result_file}' + (
|
||||
f' -f {self._cf_ipv4}' if self._ipv4 else '') + (f' -f {self._cf_ipv6}' if self._ipv6 else '')
|
||||
logger.info(f'正在执行优选命令 {cf_command}')
|
||||
os.system(cf_command)
|
||||
if SystemUtils.is_windows():
|
||||
process = subprocess.Popen(cf_command, shell=True)
|
||||
# 执行命令后无法退出 采用异步和设置超时方案
|
||||
# 设置超时时间为120秒
|
||||
if cf_command.__contains__("-dd"):
|
||||
time.sleep(120)
|
||||
else:
|
||||
time.sleep(600)
|
||||
# 如果没有在120秒内完成任务,那么杀死该进程
|
||||
if process.poll() is None:
|
||||
os.system('taskkill /F /IM CloudflareST.exe')
|
||||
else:
|
||||
os.system(cf_command)
|
||||
|
||||
# 获取优选后最优ip
|
||||
best_ip = SystemUtils.execute("sed -n '2,1p' " + self._result_file + " | awk -F, '{print $1}'")
|
||||
if SystemUtils.is_windows():
|
||||
powershell_command = f"powershell.exe -Command \"Get-Content \'{self._result_file}\' | Select-Object -Skip 1 -First 1 | Write-Output\""
|
||||
logger.info(f'正在执行powershell命令 {powershell_command}')
|
||||
best_ip = SystemUtils.execute(powershell_command)
|
||||
best_ip = best_ip.split(',')[0]
|
||||
else:
|
||||
best_ip = SystemUtils.execute("sed -n '2,1p' " + self._result_file + " | awk -F, '{print $1}'")
|
||||
logger.info(f"\n获取到最优ip==>[{best_ip}]")
|
||||
|
||||
# 替换自定义Hosts插件数据库hosts
|
||||
@@ -246,7 +273,10 @@ class CloudflareSpeedTest(_PluginBase):
|
||||
# 是否重新安装
|
||||
if self._re_install:
|
||||
install_flag = True
|
||||
os.system(f'rm -rf {self._cf_path}')
|
||||
if SystemUtils.is_windows():
|
||||
os.system(f'rd /s /q \"{self._cf_path}\"')
|
||||
else:
|
||||
os.system(f'rm -rf {self._cf_path}')
|
||||
logger.info(f'删除CloudflareSpeedTest目录 {self._cf_path},开始重新安装')
|
||||
|
||||
# 判断目录是否存在
|
||||
@@ -277,7 +307,8 @@ class CloudflareSpeedTest(_PluginBase):
|
||||
|
||||
# 重装后数据库有版本数据,但是本地没有则重装
|
||||
if not install_flag and release_version == self._version and not Path(
|
||||
f'{self._cf_path}/{self._binary_name}').exists():
|
||||
f'{self._cf_path}/{self._binary_name}').exists() and not Path(
|
||||
f'{self._cf_path}/CloudflareST.exe').exists():
|
||||
logger.warn(f"未检测到CloudflareSpeedTest本地版本,重新安装")
|
||||
install_flag = True
|
||||
|
||||
@@ -287,9 +318,11 @@ class CloudflareSpeedTest(_PluginBase):
|
||||
|
||||
# 检查环境、安装
|
||||
if SystemUtils.is_windows():
|
||||
# todo
|
||||
logger.error(f"CloudflareSpeedTest暂不支持windows平台")
|
||||
return False, None
|
||||
# windows
|
||||
cf_file_name = 'CloudflareST_windows_amd64.zip'
|
||||
download_url = f'{self._release_prefix}/{release_version}/{cf_file_name}'
|
||||
return self.__os_install(download_url, cf_file_name, release_version,
|
||||
f"ditto -V -x -k --sequesterRsrc {self._cf_path}/{cf_file_name} {self._cf_path}")
|
||||
elif SystemUtils.is_macos():
|
||||
# mac
|
||||
uname = SystemUtils.execute('uname -m')
|
||||
@@ -317,14 +350,31 @@ class CloudflareSpeedTest(_PluginBase):
|
||||
proxies = settings.PROXY
|
||||
https_proxy = proxies.get("https") if proxies and proxies.get("https") else None
|
||||
if https_proxy:
|
||||
os.system(
|
||||
f'wget -P {self._cf_path} --no-check-certificate -e use_proxy=yes -e https_proxy={https_proxy} {download_url}')
|
||||
if SystemUtils.is_windows():
|
||||
self.__get_windows_cloudflarest(download_url, proxies)
|
||||
else:
|
||||
os.system(
|
||||
f'wget -P {self._cf_path} --no-check-certificate -e use_proxy=yes -e https_proxy={https_proxy} {download_url}')
|
||||
else:
|
||||
os.system(f'wget -P {self._cf_path} https://ghproxy.com/{download_url}')
|
||||
if SystemUtils.is_windows():
|
||||
self.__get_windows_cloudflarest(download_url, proxies)
|
||||
else:
|
||||
os.system(f'wget -P {self._cf_path} https://ghproxy.com/{download_url}')
|
||||
|
||||
# 判断是否下载好安装包
|
||||
if Path(f'{self._cf_path}/{cf_file_name}').exists():
|
||||
try:
|
||||
if SystemUtils.is_windows():
|
||||
with zipfile.ZipFile(f'{self._cf_path}/{cf_file_name}', 'r') as zip_ref:
|
||||
# 解压ZIP文件中的所有文件到指定目录
|
||||
zip_ref.extractall(self._cf_path)
|
||||
if Path(f'{self._cf_path}\\CloudflareST.exe').exists():
|
||||
logger.info(f"CloudflareSpeedTest安装成功,当前版本:{release_version}")
|
||||
return True, release_version
|
||||
else:
|
||||
logger.error(f"CloudflareSpeedTest安装失败,请检查")
|
||||
os.system(f'rd /s /q \"{self._cf_path}\"')
|
||||
return False, None
|
||||
# 解压
|
||||
os.system(f'{unzip_command}')
|
||||
# 删除压缩包
|
||||
@@ -338,23 +388,42 @@ class CloudflareSpeedTest(_PluginBase):
|
||||
return False, None
|
||||
except Exception as err:
|
||||
# 如果升级失败但是有可执行文件CloudflareST,则可继续运行,反之停止
|
||||
if Path(f'{self._cf_path}/{self._binary_name}').exists():
|
||||
if Path(f'{self._cf_path}/{self._binary_name}').exists() or \
|
||||
Path(f'{self._cf_path}\\CloudflareST.exe').exists():
|
||||
logger.error(f"CloudflareSpeedTest安装失败:{str(err)},继续使用现版本运行")
|
||||
return True, None
|
||||
else:
|
||||
logger.error(f"CloudflareSpeedTest安装失败:{str(err)},无可用版本,停止运行")
|
||||
os.removedirs(self._cf_path)
|
||||
if SystemUtils.is_windows():
|
||||
os.system(f'rd /s /q \"{self._cf_path}\"')
|
||||
else:
|
||||
os.removedirs(self._cf_path)
|
||||
return False, None
|
||||
else:
|
||||
# 如果升级失败但是有可执行文件CloudflareST,则可继续运行,反之停止
|
||||
if Path(f'{self._cf_path}/{self._binary_name}').exists():
|
||||
if Path(f'{self._cf_path}/{self._binary_name}').exists() or \
|
||||
Path(f'{self._cf_path}\\CloudflareST.exe').exists():
|
||||
logger.warn(f"CloudflareSpeedTest安装失败,存在可执行版本,继续运行")
|
||||
return True, None
|
||||
else:
|
||||
logger.error(f"CloudflareSpeedTest安装失败,无可用版本,停止运行")
|
||||
os.removedirs(self._cf_path)
|
||||
if SystemUtils.is_windows():
|
||||
os.system(f'rd /s /q \"{self._cf_path}\"')
|
||||
else:
|
||||
os.removedirs(self._cf_path)
|
||||
return False, None
|
||||
|
||||
def __get_windows_cloudflarest(self, download_url, proxies):
|
||||
response = Response()
|
||||
try:
|
||||
response = requests.get(download_url, stream=True, proxies=proxies if proxies else None)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"CloudflareSpeedTest下载失败:{str(e)}")
|
||||
if response.status_code == 200:
|
||||
with open(f'{self._cf_path}\\CloudflareST_windows_amd64.zip', 'wb') as file:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
file.write(chunk)
|
||||
|
||||
@staticmethod
|
||||
def __get_release_version():
|
||||
"""
|
||||
|
||||
@@ -122,7 +122,14 @@ class DirMonitor(_PluginBase):
|
||||
continue
|
||||
|
||||
# 存储目的目录
|
||||
paths = mon_path.split(":")
|
||||
if SystemUtils.is_windows():
|
||||
if mon_path.count(":") > 1:
|
||||
paths = [mon_path.split(":")[0] + ":" + mon_path.split(":")[1],
|
||||
mon_path.split(":")[2] + ":" + mon_path.split(":")[3]]
|
||||
else:
|
||||
paths = [mon_path]
|
||||
else:
|
||||
paths = mon_path.split(":")
|
||||
target_path = None
|
||||
if len(paths) > 1:
|
||||
mon_path = paths[0]
|
||||
@@ -216,7 +223,7 @@ class DirMonitor(_PluginBase):
|
||||
for keyword in transfer_exclude_words:
|
||||
if not keyword:
|
||||
continue
|
||||
if keyword and re.findall(keyword, event_path):
|
||||
if keyword and re.search(r"%s" % keyword, event_path, re.IGNORECASE):
|
||||
logger.info(f"{event_path} 命中整理屏蔽词 {keyword},不处理")
|
||||
return
|
||||
|
||||
@@ -263,10 +270,10 @@ class DirMonitor(_PluginBase):
|
||||
|
||||
# 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title
|
||||
if not settings.SCRAP_FOLLOW_TMDB:
|
||||
transfer_historys = self.transferhis.get_by(tmdbid=mediainfo.tmdb_id,
|
||||
mtype=mediainfo.type.value)
|
||||
if transfer_historys:
|
||||
mediainfo.title = transfer_historys[0].title
|
||||
transfer_history = self.transferhis.get_by_type_tmdbid(tmdbid=mediainfo.tmdb_id,
|
||||
mtype=mediainfo.type.value)
|
||||
if transfer_history:
|
||||
mediainfo.title = transfer_history.title
|
||||
logger.info(f"{file_path.name} 识别为:{mediainfo.type.value} {mediainfo.title_year}")
|
||||
|
||||
# 更新媒体图片
|
||||
@@ -316,8 +323,9 @@ class DirMonitor(_PluginBase):
|
||||
)
|
||||
|
||||
# 刮削单个文件
|
||||
self.chain.scrape_metadata(path=transferinfo.target_path,
|
||||
mediainfo=mediainfo)
|
||||
if settings.SCRAP_METADATA:
|
||||
self.chain.scrape_metadata(path=transferinfo.target_path,
|
||||
mediainfo=mediainfo)
|
||||
|
||||
"""
|
||||
{
|
||||
@@ -379,7 +387,8 @@ class DirMonitor(_PluginBase):
|
||||
self._medias[mediainfo.title_year + " " + meta.season] = media_list
|
||||
|
||||
# 汇总刷新媒体库
|
||||
self.chain.refresh_mediaserver(mediainfo=mediainfo, file_path=transferinfo.target_path)
|
||||
if settings.REFRESH_MEDIASERVER:
|
||||
self.chain.refresh_mediaserver(mediainfo=mediainfo, file_path=transferinfo.target_path)
|
||||
# 广播事件
|
||||
self.eventmanager.send_event(EventType.TransferComplete, {
|
||||
'meta': file_meta,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Event
|
||||
@@ -11,6 +12,9 @@ from ruamel.yaml import CommentedMap
|
||||
|
||||
from app.core.config import settings
|
||||
from app.helper.sites import SitesHelper
|
||||
|
||||
from app.core.event import eventmanager
|
||||
from app.db.models.site import Site
|
||||
from app.helper.torrent import TorrentHelper
|
||||
from app.log import logger
|
||||
from app.modules.qbittorrent import Qbittorrent
|
||||
@@ -18,6 +22,7 @@ from app.modules.transmission import Transmission
|
||||
from app.plugins import _PluginBase
|
||||
from app.plugins.iyuuautoseed.iyuu_helper import IyuuHelper
|
||||
from app.schemas import NotificationType
|
||||
from app.schemas.types import EventType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
@@ -60,6 +65,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
_sites = []
|
||||
_notify = False
|
||||
_nolabels = None
|
||||
_nopaths = None
|
||||
_clearcache = False
|
||||
# 退出事件
|
||||
_event = Event()
|
||||
@@ -98,14 +104,20 @@ class IYUUAutoSeed(_PluginBase):
|
||||
self._cron = config.get("cron")
|
||||
self._token = config.get("token")
|
||||
self._downloaders = config.get("downloaders")
|
||||
self._sites = config.get("sites")
|
||||
self._sites = config.get("sites") or []
|
||||
self._notify = config.get("notify")
|
||||
self._nolabels = config.get("nolabels")
|
||||
self._nopaths = config.get("nopaths")
|
||||
self._clearcache = config.get("clearcache")
|
||||
self._permanent_error_caches = config.get("permanent_error_caches") or []
|
||||
self._error_caches = [] if self._clearcache else config.get("error_caches") or []
|
||||
self._success_caches = [] if self._clearcache else config.get("success_caches") or []
|
||||
|
||||
# 过滤掉已删除的站点
|
||||
self._sites = [site.get("id") for site in self.sites.get_indexers() if
|
||||
not site.get("public") and site.get("id") in self._sites]
|
||||
self.__update_config()
|
||||
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
|
||||
@@ -163,8 +175,8 @@ class IYUUAutoSeed(_PluginBase):
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
# 站点的可选项
|
||||
site_options = [{"title": site.get("name"), "value": site.get("id")}
|
||||
for site in self.sites.get_indexers()]
|
||||
site_options = [{"title": site.name, "value": site.id}
|
||||
for site in Site.list_order_by_pri(self.db)]
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
@@ -242,22 +254,6 @@ class IYUUAutoSeed(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'nolabels',
|
||||
'label': '不辅种标签',
|
||||
'placeholder': '使用,分隔多个标签'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -309,6 +305,44 @@ class IYUUAutoSeed(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'nolabels',
|
||||
'label': '不辅种标签',
|
||||
'placeholder': '使用,分隔多个标签'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextarea',
|
||||
'props': {
|
||||
'model': 'nopaths',
|
||||
'label': '不辅种数据文件目录',
|
||||
'rows': 3,
|
||||
'placeholder': '每一行一个目录'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
@@ -357,6 +391,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
"token": "",
|
||||
"downloaders": [],
|
||||
"sites": [],
|
||||
"nopaths": "",
|
||||
"nolabels": ""
|
||||
}
|
||||
|
||||
@@ -374,6 +409,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
"sites": self._sites,
|
||||
"notify": self._notify,
|
||||
"nolabels": self._nolabels,
|
||||
"nopaths": self._nopaths,
|
||||
"success_caches": self._success_caches,
|
||||
"error_caches": self._error_caches,
|
||||
"permanent_error_caches": self._permanent_error_caches
|
||||
@@ -397,6 +433,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
if not self.iyuuhelper:
|
||||
return
|
||||
logger.info("开始辅种任务 ...")
|
||||
|
||||
# 计数器初始化
|
||||
self.total = 0
|
||||
self.realtotal = 0
|
||||
@@ -426,13 +463,25 @@ class IYUUAutoSeed(_PluginBase):
|
||||
logger.info(f"种子 {hash_str} 辅种失败且已缓存,跳过 ...")
|
||||
continue
|
||||
save_path = self.__get_save_path(torrent, downloader)
|
||||
|
||||
if self._nopaths and save_path:
|
||||
# 过滤不需要转移的路径
|
||||
nopath_skip = False
|
||||
for nopath in self._nopaths.split('\n'):
|
||||
if os.path.normpath(save_path).startswith(os.path.normpath(nopath)):
|
||||
logger.info(f"种子 {hash_str} 保存路径 {save_path} 不需要辅种,跳过 ...")
|
||||
nopath_skip = True
|
||||
break
|
||||
if nopath_skip:
|
||||
continue
|
||||
|
||||
# 获取种子标签
|
||||
torrent_labels = self.__get_label(torrent, downloader)
|
||||
if torrent_labels and self._nolabels:
|
||||
is_skip = False
|
||||
for label in self._nolabels.split(','):
|
||||
if label in torrent_labels:
|
||||
logger.info(f"种子 {hash_str} 含有不转移标签 {label},跳过 ...")
|
||||
logger.info(f"种子 {hash_str} 含有不辅种标签 {label},跳过 ...")
|
||||
is_skip = True
|
||||
break
|
||||
if is_skip:
|
||||
@@ -671,6 +720,15 @@ class IYUUAutoSeed(_PluginBase):
|
||||
"info_hash": "a444850638e7a6f6220e2efdde94099c53358159"
|
||||
}
|
||||
"""
|
||||
|
||||
def __is_special_site(url):
|
||||
"""
|
||||
判断是否为特殊站点(是否需要添加https)
|
||||
"""
|
||||
if "hdsky.me" in url:
|
||||
return False
|
||||
return True
|
||||
|
||||
self.total += 1
|
||||
# 获取种子站点及下载地址模板
|
||||
site_url, download_page = self.iyuuhelper.get_torrent_url(seed.get("sid"))
|
||||
@@ -715,10 +773,11 @@ class IYUUAutoSeed(_PluginBase):
|
||||
self.cached += 1
|
||||
return False
|
||||
# 强制使用Https
|
||||
if "?" in torrent_url:
|
||||
torrent_url += "&https=1"
|
||||
else:
|
||||
torrent_url += "?https=1"
|
||||
if __is_special_site(torrent_url):
|
||||
if "?" in torrent_url:
|
||||
torrent_url += "&https=1"
|
||||
else:
|
||||
torrent_url += "?https=1"
|
||||
# 下载种子文件
|
||||
_, content, _, _, error_msg = self.torrent.download_torrent(
|
||||
url=torrent_url,
|
||||
@@ -870,6 +929,9 @@ class IYUUAutoSeed(_PluginBase):
|
||||
"""
|
||||
从详情页面获取下载链接
|
||||
"""
|
||||
if not site.get('url'):
|
||||
logger.warn(f"站点 {site.get('name')} 未获取站点地址,无法获取种子下载链接")
|
||||
return None
|
||||
try:
|
||||
page_url = f"{site.get('url')}details.php?id={seed.get('torrent_id')}&hit=1"
|
||||
logger.info(f"正在获取种子下载链接:{page_url} ...")
|
||||
@@ -922,3 +984,31 @@ class IYUUAutoSeed(_PluginBase):
|
||||
self._scheduler = None
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
@eventmanager.register(EventType.SiteDeleted)
|
||||
def site_deleted(self, event):
|
||||
"""
|
||||
删除对应站点选中
|
||||
"""
|
||||
site_id = event.event_data.get("site_id")
|
||||
config = self.get_config()
|
||||
if config:
|
||||
sites = config.get("sites")
|
||||
if sites:
|
||||
if isinstance(sites, str):
|
||||
sites = [sites]
|
||||
|
||||
# 删除对应站点
|
||||
if site_id:
|
||||
sites = [site for site in sites if int(site) != int(site_id)]
|
||||
else:
|
||||
# 清空
|
||||
sites = []
|
||||
|
||||
# 若无站点,则停止
|
||||
if len(sites) == 0:
|
||||
self._enabled = False
|
||||
|
||||
self._sites = sites
|
||||
# 保存配置
|
||||
self.__update_config()
|
||||
|
||||
@@ -8,6 +8,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.meta import MetaBase
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.db.transferhistory_oper import TransferHistoryOper
|
||||
from app.helper.nfo import NfoReader
|
||||
@@ -295,18 +296,21 @@ class LibraryScraper(_PluginBase):
|
||||
continue
|
||||
# 开始刮削目录
|
||||
if sub_path.is_dir():
|
||||
# 判断目录是不是媒体目录
|
||||
dir_meta = MetaInfo(sub_path.name)
|
||||
if not dir_meta.name or not dir_meta.year:
|
||||
logger.warn(f"{sub_path} 可能不是媒体目录,请检查刮削目录配置,跳过 ...")
|
||||
continue
|
||||
logger.info(f"开始刮削目录:{sub_path} ...")
|
||||
self.__scrape_dir(sub_path)
|
||||
self.__scrape_dir(path=sub_path, dir_meta=dir_meta)
|
||||
logger.info(f"目录 {sub_path} 刮削完成")
|
||||
logger.info(f"媒体库 {path} 刮削完成")
|
||||
|
||||
def __scrape_dir(self, path: Path):
|
||||
def __scrape_dir(self, path: Path, dir_meta: MetaBase):
|
||||
"""
|
||||
削刮一个目录,该目录必须是媒体文件目录
|
||||
"""
|
||||
|
||||
# 目录识别
|
||||
dir_meta = MetaInfo(path.name)
|
||||
# 媒体信息
|
||||
mediainfo = None
|
||||
|
||||
@@ -318,12 +322,15 @@ class LibraryScraper(_PluginBase):
|
||||
return
|
||||
|
||||
# 识别元数据
|
||||
meta_info = MetaInfo(file.name)
|
||||
meta_info = MetaInfo(file.stem)
|
||||
# 合并
|
||||
meta_info.merge(dir_meta)
|
||||
# 是否刮削
|
||||
scrap_metadata = settings.SCRAP_METADATA
|
||||
|
||||
# 识别媒体信息
|
||||
if not mediainfo:
|
||||
# 没有媒体信息或者名字出现变化时,需要重新识别
|
||||
if not mediainfo \
|
||||
or meta_info.name != dir_meta.name:
|
||||
# 优先读取本地nfo文件
|
||||
tmdbid = None
|
||||
if meta_info.type == MediaType.MOVIE:
|
||||
@@ -352,13 +359,14 @@ class LibraryScraper(_PluginBase):
|
||||
|
||||
# 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title
|
||||
if not settings.SCRAP_FOLLOW_TMDB:
|
||||
transfer_historys = self.transferhis.get_by(tmdbid=mediainfo.tmdb_id,
|
||||
mtype=mediainfo.type.value)
|
||||
if transfer_historys:
|
||||
mediainfo.title = transfer_historys[0].title
|
||||
transfer_history = self.transferhis.get_by_type_tmdbid(tmdbid=mediainfo.tmdb_id,
|
||||
mtype=mediainfo.type.value)
|
||||
if transfer_history:
|
||||
mediainfo.title = transfer_history.title
|
||||
|
||||
# 覆盖模式时,提前删除nfo
|
||||
if self._mode in ["force_all", "force_nfo"]:
|
||||
scrap_metadata = True
|
||||
nfo_files = SystemUtils.list_files(path, [".nfo"])
|
||||
for nfo_file in nfo_files:
|
||||
try:
|
||||
@@ -369,6 +377,7 @@ class LibraryScraper(_PluginBase):
|
||||
|
||||
# 覆盖模式时,提前删除图片文件
|
||||
if self._mode in ["force_all", "force_image"]:
|
||||
scrap_metadata = True
|
||||
image_files = SystemUtils.list_files(path, [".jpg", ".png"])
|
||||
for image_file in image_files:
|
||||
if ".actors" in str(image_file):
|
||||
@@ -380,7 +389,8 @@ class LibraryScraper(_PluginBase):
|
||||
print(str(err))
|
||||
|
||||
# 刮削单个文件
|
||||
self.chain.scrape_metadata(path=file, mediainfo=mediainfo)
|
||||
if scrap_metadata:
|
||||
self.chain.scrape_metadata(path=file, mediainfo=mediainfo)
|
||||
|
||||
@staticmethod
|
||||
def __get_tmdbid_from_nfo(file_path: Path):
|
||||
|
||||
@@ -57,6 +57,7 @@ class MediaSyncDel(_PluginBase):
|
||||
_notify = False
|
||||
_del_source = False
|
||||
_exclude_path = None
|
||||
_library_path = None
|
||||
_transferhis = None
|
||||
_downloadhis = None
|
||||
qb = None
|
||||
@@ -80,6 +81,7 @@ class MediaSyncDel(_PluginBase):
|
||||
self._notify = config.get("notify")
|
||||
self._del_source = config.get("del_source")
|
||||
self._exclude_path = config.get("exclude_path")
|
||||
self._library_path = config.get("library_path")
|
||||
|
||||
if self._enabled and str(self._sync_type) == "log":
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
@@ -231,6 +233,28 @@ class MediaSyncDel(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextarea',
|
||||
'props': {
|
||||
'model': 'library_path',
|
||||
'rows': '2',
|
||||
'label': '媒体库路径',
|
||||
'placeholder': '媒体服务器路径:MoviePilot路径(一行一个)'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
@@ -260,6 +284,7 @@ class MediaSyncDel(_PluginBase):
|
||||
"enabled": False,
|
||||
"notify": True,
|
||||
"del_source": False,
|
||||
"library_path": "",
|
||||
"sync_type": "webhook",
|
||||
"cron": "*/30 * * * *",
|
||||
"exclude_path": "",
|
||||
@@ -477,6 +502,7 @@ class MediaSyncDel(_PluginBase):
|
||||
"enabled": False,
|
||||
"del_source": self._del_source,
|
||||
"exclude_path": self._exclude_path,
|
||||
"library_path": self._library_path,
|
||||
"notify": self._notify,
|
||||
"cron": self._cron,
|
||||
"sync_type": self._sync_type,
|
||||
@@ -528,6 +554,7 @@ class MediaSyncDel(_PluginBase):
|
||||
# 查询转移记录
|
||||
msg, transfer_history = self.__get_transfer_his(media_type=media_type,
|
||||
media_name=media_name,
|
||||
media_path=media_path,
|
||||
tmdb_id=tmdb_id,
|
||||
season_num=season_num,
|
||||
episode_num=episode_num)
|
||||
@@ -626,12 +653,11 @@ class MediaSyncDel(_PluginBase):
|
||||
# 保存历史
|
||||
self.save_data("history", history)
|
||||
|
||||
def __get_transfer_his(self, media_type: str, media_name: str,
|
||||
def __get_transfer_his(self, media_type: str, media_name: str, media_path: str,
|
||||
tmdb_id: int, season_num: int, episode_num: int):
|
||||
"""
|
||||
查询转移记录
|
||||
"""
|
||||
|
||||
# 季数
|
||||
if season_num:
|
||||
season_num = str(season_num).rjust(2, '0')
|
||||
@@ -642,11 +668,19 @@ class MediaSyncDel(_PluginBase):
|
||||
# 类型
|
||||
mtype = MediaType.MOVIE if media_type in ["Movie", "MOV"] else MediaType.TV
|
||||
|
||||
# 处理路径映射 (处理同一媒体多分辨率的情况)
|
||||
if self._library_path:
|
||||
paths = self._library_path.split("\n")
|
||||
for path in paths:
|
||||
sub_paths = path.split(":")
|
||||
media_path = media_path.replace(sub_paths[0], sub_paths[1]).replace('\\', '/')
|
||||
|
||||
# 删除电影
|
||||
if mtype == MediaType.MOVIE:
|
||||
msg = f'电影 {media_name} {tmdb_id}'
|
||||
transfer_history: List[TransferHistory] = self._transferhis.get_by(tmdbid=tmdb_id,
|
||||
mtype=mtype.value)
|
||||
mtype=mtype.value,
|
||||
dest=media_path)
|
||||
# 删除电视剧
|
||||
elif mtype == MediaType.TV and not season_num and not episode_num:
|
||||
msg = f'剧集 {media_name} {tmdb_id}'
|
||||
@@ -670,7 +704,8 @@ class MediaSyncDel(_PluginBase):
|
||||
transfer_history: List[TransferHistory] = self._transferhis.get_by(tmdbid=tmdb_id,
|
||||
mtype=mtype.value,
|
||||
season=f'S{season_num}',
|
||||
episode=f'E{episode_num}')
|
||||
episode=f'E{episode_num}',
|
||||
dest=media_path)
|
||||
else:
|
||||
return "", []
|
||||
|
||||
@@ -723,13 +758,21 @@ class MediaSyncDel(_PluginBase):
|
||||
logger.info(f"媒体路径 {media_path} 已被排除,暂不处理")
|
||||
return
|
||||
|
||||
# 处理路径映射 (处理同一媒体多分辨率的情况)
|
||||
if self._library_path:
|
||||
paths = self._library_path.split("\n")
|
||||
for path in paths:
|
||||
sub_paths = path.split(":")
|
||||
media_path = media_path.replace(sub_paths[0], sub_paths[1]).replace('\\', '/')
|
||||
|
||||
# 获取删除的记录
|
||||
# 删除电影
|
||||
if media_type == "Movie":
|
||||
msg = f'电影 {media_name}'
|
||||
transfer_history: List[TransferHistory] = self._transferhis.get_by(
|
||||
title=media_name,
|
||||
year=media_year)
|
||||
year=media_year,
|
||||
dest=media_path)
|
||||
# 删除电视剧
|
||||
elif media_type == "Series":
|
||||
msg = f'剧集 {media_name}'
|
||||
@@ -750,7 +793,8 @@ class MediaSyncDel(_PluginBase):
|
||||
title=media_name,
|
||||
year=media_year,
|
||||
season=media_season,
|
||||
episode=media_episode)
|
||||
episode=media_episode,
|
||||
dest=media_path)
|
||||
else:
|
||||
continue
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ class MessageForward(_PluginBase):
|
||||
_enabled = False
|
||||
_wechat = None
|
||||
_pattern = None
|
||||
_clean: bool = False
|
||||
_pattern_token = {}
|
||||
|
||||
# 企业微信发送消息URL
|
||||
@@ -48,19 +47,9 @@ class MessageForward(_PluginBase):
|
||||
def init_plugin(self, config: dict = None):
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._clean = config.get("clear")
|
||||
self._wechat = config.get("wechat")
|
||||
self._pattern = config.get("pattern")
|
||||
|
||||
if self._clean:
|
||||
# 清理已有缓存
|
||||
self.del_data("wechat_token")
|
||||
self.update_config({
|
||||
"enabled": self._enabled,
|
||||
"clean": False,
|
||||
"wechat": self._wechat,
|
||||
"pattern": self._pattern
|
||||
})
|
||||
# 获取token存库
|
||||
if self._enabled and self._wechat:
|
||||
self.__save_wechat_token()
|
||||
@@ -102,22 +91,6 @@ class MessageForward(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'clear',
|
||||
'label': '清除缓存'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -168,7 +141,6 @@ class MessageForward(_PluginBase):
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"clear": False,
|
||||
"wechat": "",
|
||||
"pattern": ""
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ lock = Lock()
|
||||
|
||||
class RssSubscribe(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "RSS订阅"
|
||||
plugin_name = "自定义订阅"
|
||||
# 插件描述
|
||||
plugin_desc = "定时刷新RSS报文,识别内容后添加订阅或直接下载。"
|
||||
# 插件图标
|
||||
@@ -119,7 +119,7 @@ class RssSubscribe(_PluginBase):
|
||||
# 记录清理缓存设置
|
||||
self._clearflag = self._clear
|
||||
# 关闭清理缓存开关
|
||||
self._clearflag = False
|
||||
self._clear = False
|
||||
# 保存设置
|
||||
self.__update_config()
|
||||
|
||||
@@ -549,7 +549,7 @@ class RssSubscribe(_PluginBase):
|
||||
logger.error(f"未获取到RSS数据:{url}")
|
||||
return
|
||||
# 过滤规则
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.FilterRules)
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
|
||||
# 解析数据
|
||||
for result in results:
|
||||
try:
|
||||
@@ -593,7 +593,8 @@ class RssSubscribe(_PluginBase):
|
||||
if self._filter:
|
||||
result = self.chain.filter_torrents(
|
||||
rule_string=filter_rule,
|
||||
torrent_list=[torrentinfo]
|
||||
torrent_list=[torrentinfo],
|
||||
mediainfo=mediainfo
|
||||
)
|
||||
if not result:
|
||||
logger.info(f"{title} {description} 不匹配过滤规则")
|
||||
|
||||
@@ -15,6 +15,7 @@ from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.core.event import Event
|
||||
from app.core.event import eventmanager
|
||||
from app.db.models.site import Site
|
||||
from app.helper.browser import PlaywrightHelper
|
||||
from app.helper.module import ModuleHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
@@ -84,6 +85,11 @@ class SiteStatistic(_PluginBase):
|
||||
self._statistic_type = config.get("statistic_type") or "all"
|
||||
self._statistic_sites = config.get("statistic_sites") or []
|
||||
|
||||
# 过滤掉已删除的站点
|
||||
self._statistic_sites = [site.get("id") for site in self.sites.get_indexers() if
|
||||
not site.get("public") and site.get("id") in self._statistic_sites]
|
||||
self.__update_config()
|
||||
|
||||
if self._enabled or self._onlyonce:
|
||||
# 加载模块
|
||||
self._site_schema = ModuleHelper.load('app.plugins.sitestatistic.siteuserinfo',
|
||||
@@ -177,8 +183,8 @@ class SiteStatistic(_PluginBase):
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
# 站点的可选项
|
||||
site_options = [{"title": site.get("name"), "value": site.get("id")}
|
||||
for site in self.sites.get_indexers()]
|
||||
site_options = [{"title": site.name, "value": site.id}
|
||||
for site in Site.list_order_by_pri(self.db)]
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
@@ -966,6 +972,12 @@ class SiteStatistic(_PluginBase):
|
||||
# 发送通知,存在未读消息
|
||||
self.__notify_unread_msg(site_name, site_user_info, unread_msg_notify)
|
||||
|
||||
# 分享率接近1时,发送消息提醒
|
||||
if site_user_info.ratio and float(site_user_info.ratio) < 1:
|
||||
self.post_message(mtype=NotificationType.SiteMessage,
|
||||
title=f"【站点分享率低预警】",
|
||||
text=f"站点 {site_user_info.site_name} 分享率 {site_user_info.ratio},请注意!")
|
||||
|
||||
self._sites_data.update(
|
||||
{
|
||||
site_name: {
|
||||
@@ -1041,10 +1053,6 @@ class SiteStatistic(_PluginBase):
|
||||
else:
|
||||
refresh_sites = [site for site in self.sites.get_indexers() if
|
||||
site.get("id") in self._statistic_sites]
|
||||
|
||||
# 过滤掉已删除的站点
|
||||
self._statistic_sites = [site.get("id") for site in refresh_sites if site]
|
||||
self.__update_config()
|
||||
if not refresh_sites:
|
||||
return
|
||||
|
||||
|
||||
@@ -56,5 +56,6 @@ class NexusHhanclubSiteUserInfo(NexusPhpSiteUserInfo):
|
||||
|
||||
def _get_user_level(self, html):
|
||||
super()._get_user_level(html)
|
||||
|
||||
self.user_level = html.xpath('//*[@id="mainContent"]/div/div[2]/div[2]/div[4]/img/@title')[0]
|
||||
user_level_path = html.xpath('//*[@id="mainContent"]/div/div[2]/div[2]/div[4]/span[2]/img/@title')
|
||||
if user_level_path:
|
||||
self.user_level = user_level_path[0]
|
||||
|
||||
@@ -69,6 +69,7 @@ class SpeedLimiter(_PluginBase):
|
||||
self._play_down_speed = float(config.get("play_down_speed")) if config.get("play_down_speed") else 0
|
||||
self._noplay_up_speed = float(config.get("noplay_up_speed")) if config.get("noplay_up_speed") else 0
|
||||
self._noplay_down_speed = float(config.get("noplay_down_speed")) if config.get("noplay_down_speed") else 0
|
||||
self._current_state = f"U:{self._noplay_up_speed},D:{self._noplay_down_speed}"
|
||||
try:
|
||||
# 总带宽
|
||||
self._bandwidth = int(float(config.get("bandwidth") or 0)) * 1000000
|
||||
@@ -374,6 +375,8 @@ class SpeedLimiter(_PluginBase):
|
||||
"""
|
||||
if not self._qb and not self._tr:
|
||||
return
|
||||
if not self._enabled:
|
||||
return
|
||||
if event:
|
||||
event_data: WebhookEventInfo = event.event_data
|
||||
if event_data.event not in ["playback.start", "PlaybackStart", "media.play"]:
|
||||
@@ -592,7 +595,8 @@ class SpeedLimiter(_PluginBase):
|
||||
for allow_ipv6 in allow_ipv6s:
|
||||
if ipaddr in ipaddress.ip_network(allow_ipv6, strict=False):
|
||||
return True
|
||||
except Exception:
|
||||
except Exception as err:
|
||||
print(str(err))
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
@@ -605,7 +605,7 @@ class TorrentRemover(_PluginBase):
|
||||
if torrents and message_text and self._notify:
|
||||
self.post_message(
|
||||
mtype=NotificationType.SiteMessage,
|
||||
title=f"【自动删种任务执行完成】",
|
||||
title=f"【自动删种任务完成】",
|
||||
text=message_text
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -624,7 +624,7 @@ class TorrentRemover(_PluginBase):
|
||||
# 平均上传速度
|
||||
torrent_upload_avs = torrent.uploaded / torrent_seeding_time if torrent_seeding_time else 0
|
||||
# 大小 单位:GB
|
||||
sizes = self._size.split(',') if self._size else []
|
||||
sizes = self._size.split('-') if self._size else []
|
||||
minsize = sizes[0] * 1024 * 1024 * 1024 if sizes else 0
|
||||
maxsize = sizes[-1] * 1024 * 1024 * 1024 if sizes else 0
|
||||
# 分享率
|
||||
@@ -634,7 +634,7 @@ class TorrentRemover(_PluginBase):
|
||||
if self._time and torrent_seeding_time <= float(self._time) * 3600:
|
||||
return None
|
||||
# 文件大小
|
||||
if self._size and (torrent.size >= maxsize or torrent.size <= minsize):
|
||||
if self._size and (torrent.size >= int(maxsize) or torrent.size <= int(minsize)):
|
||||
return None
|
||||
if self._upspeed and torrent_upload_avs >= float(self._upspeed) * 1024:
|
||||
return None
|
||||
@@ -668,7 +668,7 @@ class TorrentRemover(_PluginBase):
|
||||
# 平均上传速茺
|
||||
torrent_upload_avs = torrent_uploaded / torrent_seeding_time if torrent_seeding_time else 0
|
||||
# 大小 单位:GB
|
||||
sizes = self._size.split(',') if self._size else []
|
||||
sizes = self._size.split('-') if self._size else []
|
||||
minsize = sizes[0] * 1024 * 1024 * 1024 if sizes else 0
|
||||
maxsize = sizes[-1] * 1024 * 1024 * 1024 if sizes else 0
|
||||
# 分享率
|
||||
@@ -676,7 +676,7 @@ class TorrentRemover(_PluginBase):
|
||||
return None
|
||||
if self._time and torrent_seeding_time <= float(self._time) * 3600:
|
||||
return None
|
||||
if self._size and (torrent.total_size >= maxsize or torrent.total_size <= minsize):
|
||||
if self._size and (torrent.total_size >= int(maxsize) or torrent.total_size <= int(minsize)):
|
||||
return None
|
||||
if self._upspeed and torrent_upload_avs >= float(self._upspeed) * 1024:
|
||||
return None
|
||||
@@ -736,14 +736,15 @@ class TorrentRemover(_PluginBase):
|
||||
name = remove_torrent.get("name")
|
||||
size = remove_torrent.get("size")
|
||||
for torrent in torrents:
|
||||
if torrent.name == name \
|
||||
and torrent.size == size \
|
||||
and torrent.hash not in [t.get("id") for t in remove_torrents]:
|
||||
remove_torrents_plus.append({
|
||||
"id": torrent.hash,
|
||||
"name": torrent.name,
|
||||
"site": StringUtils.get_url_sld(torrent.tracker),
|
||||
"size": torrent.size
|
||||
})
|
||||
if downloader == "qbittorrent":
|
||||
item_plus = self.__get_qb_torrent(torrent)
|
||||
else:
|
||||
item_plus = self.__get_tr_torrent(torrent)
|
||||
if not item_plus:
|
||||
continue
|
||||
if item_plus.get("name") == name \
|
||||
and item_plus.get("size") == size \
|
||||
and item_plus.get("id") not in [t.get("id") for t in remove_torrents]:
|
||||
remove_torrents_plus.append(item_plus)
|
||||
remove_torrents.extend(remove_torrents_plus)
|
||||
return remove_torrents
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any, List, Dict, Tuple, Optional
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from torrentool.torrent import Torrent
|
||||
from bencode import bdecode, bencode
|
||||
|
||||
from app.core.config import settings
|
||||
from app.helper.torrent import TorrentHelper
|
||||
@@ -582,26 +582,50 @@ class TorrentTransfer(_PluginBase):
|
||||
continue
|
||||
|
||||
# 如果源下载器是QB检查是否有Tracker,没有的话额外获取
|
||||
trackers = None
|
||||
if downloader == "qbittorrent":
|
||||
# 读取种子内容、解析种子文件
|
||||
content = torrent_file.read_bytes()
|
||||
if not content:
|
||||
logger.warn(f"读取种子文件失败:{torrent_file}")
|
||||
fail += 1
|
||||
continue
|
||||
# 读取trackers
|
||||
try:
|
||||
torrent_main = Torrent.from_file(torrent_file)
|
||||
main_announce = torrent_main.announce_urls
|
||||
torrent_main = bdecode(content)
|
||||
main_announce = torrent_main.get('announce')
|
||||
except Exception as err:
|
||||
logger.error(f"解析种子文件 {torrent_file} 失败:{err}")
|
||||
# 失败计数
|
||||
logger.warn(f"解析种子文件 {torrent_file} 失败:{err}")
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
if not main_announce:
|
||||
logger.info(f"{torrent_item.get('hash')} 未发现tracker信息,尝试补充tracker ...")
|
||||
# 从源下载任务信息中获取Tracker
|
||||
torrent = torrent_item.get('torrent')
|
||||
# 源trackers
|
||||
trackers = [tracker.get("url") for tracker in torrent.trackers
|
||||
if str(tracker.get("url")).startswith('http')]
|
||||
logger.info(f"获取到源tracker:{trackers}")
|
||||
logger.info(f"{torrent_item.get('hash')} 未发现tracker信息,尝试补充tracker信息...")
|
||||
# 读取fastresume文件
|
||||
fastresume_file = Path(self._fromtorrentpath) / f"{torrent_item.get('hash')}.fastresume"
|
||||
if not fastresume_file.exists():
|
||||
logger.warn(f"fastresume文件不存在:{fastresume_file}")
|
||||
fail += 1
|
||||
continue
|
||||
# 尝试补充trackers
|
||||
try:
|
||||
# 解析fastresume文件
|
||||
fastresume = fastresume_file.read_bytes()
|
||||
torrent_fastresume = bdecode(fastresume)
|
||||
# 读取trackers
|
||||
fastresume_trackers = torrent_fastresume.get('trackers')
|
||||
if isinstance(fastresume_trackers, list) \
|
||||
and len(fastresume_trackers) > 0 \
|
||||
and fastresume_trackers[0]:
|
||||
# 重新赋值
|
||||
torrent_main['announce'] = fastresume_trackers[0][0]
|
||||
# 替换种子文件路径
|
||||
torrent_file = settings.TEMP_PATH / f"{torrent_item.get('hash')}.torrent"
|
||||
# 编码并保存到临时文件
|
||||
torrent_file.write_bytes(bencode(torrent_main))
|
||||
except Exception as err:
|
||||
logger.error(f"解析fastresume文件 {fastresume_file} 出错:{err}")
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
# 发送到另一个下载器中下载:默认暂停、传输下载路径、关闭自动管理模式
|
||||
logger.info(f"添加转移做种任务到下载器 {todownloader}:{torrent_file}")
|
||||
@@ -617,11 +641,6 @@ class TorrentTransfer(_PluginBase):
|
||||
# 下载成功
|
||||
logger.info(f"成功添加转移做种任务,种子文件:{torrent_file}")
|
||||
|
||||
# 补充Tracker
|
||||
if trackers:
|
||||
logger.info(f"开始补充 {download_id} 的tracker:{trackers}")
|
||||
todownloader_obj.add_trackers(ids=[download_id], trackers=trackers)
|
||||
|
||||
# TR会自动校验,QB需要手动校验
|
||||
if todownloader == "qbittorrent":
|
||||
logger.info(f"qbittorrent 开始校验 {download_id} ...")
|
||||
|
||||
@@ -8,11 +8,10 @@ from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from app.chain import ChainBase
|
||||
from app.chain.cookiecloud import CookieCloudChain
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.chain.rss import RssChain
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.config import settings
|
||||
from app.db import ScopedSession
|
||||
from app.db import SessionFactory
|
||||
from app.log import logger
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.timer import TimerUtils
|
||||
@@ -40,7 +39,7 @@ class Scheduler(metaclass=Singleton):
|
||||
|
||||
def __init__(self):
|
||||
# 数据库连接
|
||||
self._db = ScopedSession()
|
||||
self._db = SessionFactory()
|
||||
# 调试模式不启动定时服务
|
||||
if settings.DEV:
|
||||
return
|
||||
@@ -71,15 +70,20 @@ class Scheduler(metaclass=Singleton):
|
||||
self._scheduler.add_job(SubscribeChain(self._db).search, "interval",
|
||||
hours=24, kwargs={'state': 'R'}, name="订阅搜索")
|
||||
|
||||
# 站点首页种子定时刷新缓存并匹配订阅
|
||||
triggers = TimerUtils.random_scheduler(num_executions=30)
|
||||
for trigger in triggers:
|
||||
self._scheduler.add_job(SubscribeChain(self._db).refresh, "cron",
|
||||
hour=trigger.hour, minute=trigger.minute, name="订阅刷新")
|
||||
|
||||
# 自定义订阅
|
||||
self._scheduler.add_job(RssChain(self._db).refresh, "interval",
|
||||
minutes=30, name="自定义订阅刷新")
|
||||
if settings.SUBSCRIBE_MODE == "spider":
|
||||
# 站点首页种子定时刷新模式
|
||||
triggers = TimerUtils.random_scheduler(num_executions=30)
|
||||
for trigger in triggers:
|
||||
self._scheduler.add_job(SubscribeChain(self._db).refresh, "cron",
|
||||
hour=trigger.hour, minute=trigger.minute, name="订阅刷新")
|
||||
else:
|
||||
# RSS订阅模式
|
||||
if not settings.SUBSCRIBE_RSS_INTERVAL:
|
||||
settings.SUBSCRIBE_RSS_INTERVAL = 30
|
||||
elif settings.SUBSCRIBE_RSS_INTERVAL < 5:
|
||||
settings.SUBSCRIBE_RSS_INTERVAL = 5
|
||||
self._scheduler.add_job(SubscribeChain(self._db).refresh, "interval",
|
||||
minutes=settings.SUBSCRIBE_RSS_INTERVAL, name="订阅刷新")
|
||||
|
||||
# 下载器文件转移(每5分钟)
|
||||
if settings.DOWNLOADER_MONITOR:
|
||||
@@ -106,3 +110,5 @@ class Scheduler(metaclass=Singleton):
|
||||
"""
|
||||
if self._scheduler.running:
|
||||
self._scheduler.shutdown()
|
||||
if self._db:
|
||||
self._db.close()
|
||||
|
||||
@@ -12,5 +12,4 @@ from .mediaserver import *
|
||||
from .message import *
|
||||
from .tmdb import *
|
||||
from .transfer import *
|
||||
from .rss import *
|
||||
from .file import *
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Rss(BaseModel):
|
||||
id: Optional[int]
|
||||
# 名称
|
||||
name: Optional[str]
|
||||
# RSS地址
|
||||
url: Optional[str]
|
||||
# 类型
|
||||
type: Optional[str]
|
||||
# 标题
|
||||
title: Optional[str]
|
||||
# 年份
|
||||
year: Optional[str]
|
||||
# TMDBID
|
||||
tmdbid: Optional[int]
|
||||
# 季号
|
||||
season: Optional[int]
|
||||
# 海报
|
||||
poster: Optional[str]
|
||||
# 背景图
|
||||
backdrop: Optional[str]
|
||||
# 评分
|
||||
vote: Optional[float]
|
||||
# 简介
|
||||
description: Optional[str]
|
||||
# 总集数
|
||||
total_episode: Optional[int]
|
||||
# 包含
|
||||
include: Optional[str]
|
||||
# 排除
|
||||
exclude: Optional[str]
|
||||
# 洗版
|
||||
best_version: Optional[int]
|
||||
# 是否使用代理服务器
|
||||
proxy: Optional[int]
|
||||
# 是否使用过滤规则
|
||||
filter: Optional[int]
|
||||
# 保存路径
|
||||
save_path: Optional[str]
|
||||
# 附加信息
|
||||
note: Optional[str]
|
||||
# 已处理数量
|
||||
processed: Optional[int]
|
||||
# 最后更新时间
|
||||
last_update: Optional[str]
|
||||
# 状态 0-停用,1-启用
|
||||
state: Optional[int]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
@@ -48,7 +48,7 @@ class SystemConfigKey(Enum):
|
||||
UserInstalledPlugins = "UserInstalledPlugins"
|
||||
# 搜索结果
|
||||
SearchResults = "SearchResults"
|
||||
# 索引站点范围
|
||||
# 搜索站点范围
|
||||
IndexerSites = "IndexerSites"
|
||||
# 订阅站点范围
|
||||
RssSites = "RssSites"
|
||||
@@ -60,10 +60,14 @@ class SystemConfigKey(Enum):
|
||||
CustomReleaseGroups = "CustomReleaseGroups"
|
||||
# 自定义识别词
|
||||
CustomIdentifiers = "CustomIdentifiers"
|
||||
# 过滤规则
|
||||
FilterRules = "FilterRules"
|
||||
# 搜索优先级规则
|
||||
SearchFilterRules = "SearchFilterRules"
|
||||
# 订阅优先级规则
|
||||
SubscribeFilterRules = "SubscribeFilterRules"
|
||||
# 洗版规则
|
||||
FilterRules2 = "FilterRules2"
|
||||
BestVersionFilterRules = "BestVersionFilterRules"
|
||||
# 默认包含与排除规则
|
||||
DefaultIncludeExcludeFilter = "DefaultIncludeExcludeFilter"
|
||||
# 转移屏蔽词
|
||||
TransferExcludeWords = "TransferExcludeWords"
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import urllib3
|
||||
from requests import Session, Response
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
from app.utils.ip import IpUtils
|
||||
|
||||
urllib3.disable_warnings(InsecureRequestWarning)
|
||||
|
||||
|
||||
|
||||
@@ -31,3 +31,32 @@ class SiteUtils:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def is_checkin(cls, html_text: str) -> bool:
|
||||
"""
|
||||
判断站点是否已经签到
|
||||
:return True已签到 False未签到
|
||||
"""
|
||||
html = etree.HTML(html_text)
|
||||
if not html:
|
||||
return False
|
||||
# 站点签到支持的识别XPATH
|
||||
xpaths = [
|
||||
'//a[@id="signed"]',
|
||||
'//a[contains(@href, "attendance")]',
|
||||
'//a[contains(text(), "签到")]',
|
||||
'//a/b[contains(text(), "签 到")]',
|
||||
'//span[@id="sign_in"]/a',
|
||||
'//a[contains(@href, "addbonus")]',
|
||||
'//input[@class="dt_button"][contains(@value, "打卡")]',
|
||||
'//a[contains(@href, "sign_in")]',
|
||||
'//a[contains(@onclick, "do_signin")]',
|
||||
'//a[@id="do-attendance"]',
|
||||
'//shark-icon-button[@href="attendance.php"]'
|
||||
]
|
||||
for xpath in xpaths:
|
||||
if html.xpath(xpath):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -106,7 +106,7 @@ class SystemUtils:
|
||||
|
||||
if directory.is_file():
|
||||
return [directory]
|
||||
|
||||
|
||||
if not min_filesize:
|
||||
min_filesize = 0
|
||||
|
||||
@@ -122,6 +122,36 @@ class SystemUtils:
|
||||
|
||||
return files
|
||||
|
||||
@staticmethod
|
||||
def exits_files(directory: Path, extensions: list, min_filesize: int = 0) -> bool:
|
||||
"""
|
||||
判断目录下是否存在指定扩展名的文件
|
||||
:return True存在 False不存在
|
||||
"""
|
||||
|
||||
if not min_filesize:
|
||||
min_filesize = 0
|
||||
|
||||
if not directory.exists():
|
||||
return False
|
||||
|
||||
if directory.is_file():
|
||||
return True
|
||||
|
||||
if not min_filesize:
|
||||
min_filesize = 0
|
||||
|
||||
pattern = r".*(" + "|".join(extensions) + ")$"
|
||||
|
||||
# 遍历目录及子目录
|
||||
for path in directory.rglob('**/*'):
|
||||
if path.is_file() \
|
||||
and re.match(pattern, path.name, re.IGNORECASE) \
|
||||
and path.stat().st_size >= min_filesize * 1024 * 1024:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def list_sub_files(directory: Path, extensions: list) -> List[Path]:
|
||||
"""
|
||||
@@ -311,7 +341,13 @@ class SystemUtils:
|
||||
# 创建 Docker 客户端
|
||||
client = docker.DockerClient(base_url='tcp://127.0.0.1:38379')
|
||||
# 获取当前容器的 ID
|
||||
container_id = open("/proc/self/cgroup", "r").read().split("/")[-1]
|
||||
with open('/proc/self/mountinfo', 'r') as f:
|
||||
data = f.read()
|
||||
index_resolv_conf = data.find("resolv.conf")
|
||||
if index_resolv_conf != -1:
|
||||
index_second_slash = data.rfind("/", 0, index_resolv_conf)
|
||||
index_first_slash = data.rfind("/", 0, index_second_slash) + 1
|
||||
container_id = data[index_first_slash:index_second_slash]
|
||||
if not container_id:
|
||||
return False, "获取容器ID失败!"
|
||||
# 重启当前容器
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
||||
Cython~=3.0.2
|
||||
pydantic~=1.10.8
|
||||
SQLAlchemy~=2.0.15
|
||||
uvicorn~=0.22.0
|
||||
@@ -40,7 +41,6 @@ slack_sdk~=3.21.3
|
||||
chardet~=4.0.0
|
||||
starlette~=0.27.0
|
||||
PyVirtualDisplay~=3.0
|
||||
Cython~=0.29.35
|
||||
psutil~=5.9.4
|
||||
python_hosts~=1.0.3
|
||||
watchdog~=3.0.0
|
||||
@@ -50,4 +50,6 @@ cacheout~=0.14.1
|
||||
click~=8.1.6
|
||||
requests_cache~=0.5.2
|
||||
parse~=1.19.0
|
||||
docker~=6.1.3
|
||||
docker~=6.1.3
|
||||
cachetools~=5.3.1
|
||||
fast-bencode==1.1.3
|
||||
2
update
2
update
@@ -31,7 +31,7 @@ if [ "${MOVIEPILOT_AUTO_UPDATE_DEV}" = "true" ]; then
|
||||
mv /tmp/app /app
|
||||
rm -rf /public
|
||||
mv /tmp/dist /public
|
||||
echo "程序更新成功,前端版本:${frontend_version},后端版本:${release_version}"
|
||||
echo "程序更新成功,前端版本:${frontend_version}"
|
||||
else
|
||||
echo "前端程序下载失败,继续使用旧的程序来启动..."
|
||||
fi
|
||||
|
||||
@@ -1 +1 @@
|
||||
APP_VERSION = 'v1.1.7'
|
||||
APP_VERSION = 'v1.2.3-1'
|
||||
|
||||
Reference in New Issue
Block a user