mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-09 07:42:40 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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. **进阶配置**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -185,19 +185,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:
|
||||
|
||||
@@ -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 = []
|
||||
@@ -308,12 +308,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 +335,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,
|
||||
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]):
|
||||
@@ -504,11 +509,13 @@ class SubscribeChain(ChainBase):
|
||||
torrent_list=[torrent_info])
|
||||
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:
|
||||
@@ -580,12 +587,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,
|
||||
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 +616,15 @@ 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} ...')
|
||||
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,7 +681,7 @@ class SubscribeChain(ChainBase):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __upate_lack_episodes(self, lefts: Dict[int, Dict[int, NotExistMediaInfo]],
|
||||
def __update_lack_episodes(self, lefts: Dict[int, Dict[int, NotExistMediaInfo]],
|
||||
subscribe: Subscribe,
|
||||
mediainfo: MediaInfo,
|
||||
update_date: bool = False):
|
||||
@@ -765,7 +769,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"),
|
||||
|
||||
@@ -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.
@@ -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):
|
||||
|
||||
@@ -575,6 +575,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')
|
||||
@@ -634,11 +635,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 +813,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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Event
|
||||
@@ -60,6 +61,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
_sites = []
|
||||
_notify = False
|
||||
_nolabels = None
|
||||
_nopaths = None
|
||||
_clearcache = False
|
||||
# 退出事件
|
||||
_event = Event()
|
||||
@@ -101,6 +103,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
self._sites = config.get("sites")
|
||||
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 []
|
||||
@@ -242,22 +245,6 @@ class IYUUAutoSeed(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'nolabels',
|
||||
'label': '不辅种标签',
|
||||
'placeholder': '使用,分隔多个标签'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -309,6 +296,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 +382,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
"token": "",
|
||||
"downloaders": [],
|
||||
"sites": [],
|
||||
"nopaths": "",
|
||||
"nolabels": ""
|
||||
}
|
||||
|
||||
@@ -374,6 +400,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
|
||||
@@ -431,13 +458,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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
APP_VERSION = 'v1.2.0'
|
||||
APP_VERSION = 'v1.2.1'
|
||||
|
||||
Reference in New Issue
Block a user