mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-13 07:26:45 +00:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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
|
||||
|
||||
29
README.md
29
README.md
@@ -81,7 +81,7 @@ 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小时运行,推荐使用该模式。
|
||||
- **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`
|
||||
@@ -149,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
|
||||
@@ -234,14 +234,14 @@ def read_rss_sites(db: Session = Depends(get_db)) -> List[dict]:
|
||||
获取站点列表
|
||||
"""
|
||||
# 选中的rss站点
|
||||
rss_sites = SystemConfigOper().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)):
|
||||
"""
|
||||
查询系统环境变量,包括当前版本号
|
||||
"""
|
||||
@@ -83,7 +83,7 @@ def set_setting(key: str, value: Union[list, dict, str, int] = None,
|
||||
|
||||
|
||||
@router.get("/message", summary="实时消息")
|
||||
def get_progress(token: str):
|
||||
def get_message(token: str):
|
||||
"""
|
||||
实时获取系统消息,返回格式为SSE
|
||||
"""
|
||||
@@ -169,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().get(SystemConfigKey.FilterRules2)
|
||||
rule_string = SystemConfigOper().get(SystemConfigKey.BestVersionFilterRules)
|
||||
elif ruletype == "3":
|
||||
rule_string = SystemConfigOper().get(SystemConfigKey.SearchFilterRules)
|
||||
else:
|
||||
rule_string = SystemConfigOper().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
|
||||
})
|
||||
|
||||
@@ -223,16 +223,19 @@ 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, content: Union[Path, str], download_dir: Path, cookie: str,
|
||||
episodes: Set[int] = None, category: str = None
|
||||
|
||||
@@ -225,7 +225,7 @@ 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, download_dir=download_dir, torrent_path=torrent_file)
|
||||
# 广播事件
|
||||
|
||||
@@ -88,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} ...')
|
||||
@@ -130,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:
|
||||
@@ -185,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:
|
||||
@@ -236,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:
|
||||
@@ -253,6 +265,7 @@ class SearchChain(ChainBase):
|
||||
if not indexer_sites:
|
||||
logger.warn('未开启任何有效站点,无法搜索资源')
|
||||
return []
|
||||
|
||||
# 开始进度
|
||||
self.progress.start(ProgressKey.Search)
|
||||
# 开始计时
|
||||
|
||||
@@ -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]):
|
||||
@@ -496,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:
|
||||
@@ -550,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}')
|
||||
@@ -580,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):
|
||||
"""
|
||||
@@ -609,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,
|
||||
@@ -677,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):
|
||||
"""
|
||||
更新订阅剩余集数
|
||||
"""
|
||||
@@ -765,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: 开始季
|
||||
|
||||
@@ -108,7 +108,6 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
|
||||
site_proxy=site.get("proxy"),
|
||||
site_order=site.get("pri"),
|
||||
title=item.get("title"),
|
||||
description=item.get("description"),
|
||||
enclosure=item.get("enclosure"),
|
||||
page_url=item.get("link"),
|
||||
size=item.get("size"),
|
||||
@@ -130,7 +129,7 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
# 刷新站点
|
||||
if not sites:
|
||||
sites = [str(sid) for sid in (self.systemconfig.get(SystemConfigKey.RssSites) or [])]
|
||||
sites = self.systemconfig.get(SystemConfigKey.RssSites) or []
|
||||
|
||||
# 读取缓存
|
||||
torrents_cache = self.get_torrents()
|
||||
@@ -140,7 +139,7 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
|
||||
# 遍历站点缓存资源
|
||||
for indexer in indexers:
|
||||
# 未开启的站点不刷新
|
||||
if sites and str(indexer.get("id")) not in sites:
|
||||
if sites and indexer.get("id") not in sites:
|
||||
continue
|
||||
domain = StringUtils.get_url_domain(indexer.get("domain"))
|
||||
if stype == "spider":
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import glob
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
@@ -601,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):
|
||||
@@ -615,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(path.parent, settings.RMT_MEDIAEXT):
|
||||
# 当前路径下没有媒体文件则删除
|
||||
shutil.rmtree(parent_path)
|
||||
logger.warn(f"目录 {parent_path} 已删除")
|
||||
|
||||
@@ -222,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
|
||||
|
||||
@@ -72,7 +72,7 @@ class Settings(BaseSettings):
|
||||
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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -272,8 +272,8 @@ class Emby(metaclass=Singleton):
|
||||
return None
|
||||
return ""
|
||||
|
||||
def get_movies(self,
|
||||
title: str,
|
||||
def get_movies(self,
|
||||
title: str,
|
||||
year: str = None,
|
||||
tmdb_id: int = None) -> Optional[List[dict]]:
|
||||
"""
|
||||
@@ -338,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 = ""
|
||||
|
||||
@@ -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": {
|
||||
@@ -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
|
||||
|
||||
@@ -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, tmdb_id=mediainfo.tmdb_id)
|
||||
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,6 +66,7 @@ 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)
|
||||
|
||||
@@ -130,11 +130,13 @@ class Plex(metaclass=Singleton):
|
||||
|
||||
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属性的字典列表
|
||||
@@ -144,9 +146,14 @@ 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):
|
||||
@@ -157,6 +164,7 @@ class Plex(metaclass=Singleton):
|
||||
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]]:
|
||||
@@ -164,6 +172,7 @@ class Plex(metaclass=Singleton):
|
||||
根据标题、年份、季查询电视剧所有集信息
|
||||
:param item_id: 媒体ID
|
||||
:param title: 标题
|
||||
:param original_title: 原产地标题
|
||||
:param year: 年份,可以为空,为空时不按年份过滤
|
||||
:param tmdb_id: TMDB ID
|
||||
:param season: 季号,数字
|
||||
@@ -176,6 +185,8 @@ class Plex(metaclass=Singleton):
|
||||
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):
|
||||
|
||||
@@ -131,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:
|
||||
# 获取正在下载的任务
|
||||
|
||||
@@ -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',
|
||||
@@ -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,11 +600,6 @@ 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
|
||||
@@ -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 = []
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -398,10 +434,6 @@ class IYUUAutoSeed(_PluginBase):
|
||||
return
|
||||
logger.info("开始辅种任务 ...")
|
||||
|
||||
# 排除已删除站点
|
||||
self._sites = [site.get("id") for site in self.sites.get_indexers() if
|
||||
site.get("id") in self._sites]
|
||||
|
||||
# 计数器初始化
|
||||
self.total = 0
|
||||
self.realtotal = 0
|
||||
@@ -431,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:
|
||||
@@ -940,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,14 +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:
|
||||
|
||||
@@ -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',
|
||||
@@ -1047,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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
APP_VERSION = 'v1.2.0'
|
||||
APP_VERSION = 'v1.2.3'
|
||||
|
||||
Reference in New Issue
Block a user