mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-03 22:04:16 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3e5be37fd | ||
|
|
d8f7fa70af | ||
|
|
6916ee0988 | ||
|
|
6fef533527 | ||
|
|
c57985d553 | ||
|
|
ec07379a67 |
@@ -17,7 +17,7 @@ from app.db.message_oper import MessageOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.db.user_oper import get_current_active_superuser
|
||||
from app.helper.service import ServiceConfigHelper
|
||||
from app.helper.webpush import is_webpush_subscription_gone
|
||||
from app.helper.webpush import is_webpush_subscription_gone, webpush_options_for_endpoint
|
||||
from app.log import logger
|
||||
from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt
|
||||
from app.schemas.types import MessageChannel, SystemConfigKey
|
||||
@@ -316,6 +316,7 @@ def send_notification(
|
||||
data=json.dumps(payload.model_dump()),
|
||||
vapid_private_key=settings.VAPID.get("privateKey"),
|
||||
vapid_claims={"sub": settings.VAPID.get("subject")},
|
||||
**webpush_options_for_endpoint(sub.get("endpoint")),
|
||||
)
|
||||
except WebPushException as err:
|
||||
logger.error(f"WebPush发送失败: {str(err)}")
|
||||
|
||||
@@ -1306,18 +1306,16 @@ class SubscribeChain(ChainBase):
|
||||
logger.debug(f"search Lock released at {datetime.now()}")
|
||||
|
||||
@staticmethod
|
||||
def __update_movie_best_version_download_priority(
|
||||
def __update_movie_download_priority(
|
||||
subscribe: Subscribe,
|
||||
mediainfo: MediaInfo,
|
||||
downloads: Optional[List[Context]],
|
||||
):
|
||||
"""
|
||||
记录电影洗版本轮下载资源优先级。
|
||||
记录电影本轮下载资源优先级,用作后续电影洗版的起始质量状态。
|
||||
"""
|
||||
if not downloads:
|
||||
return
|
||||
if not subscribe.best_version:
|
||||
return
|
||||
priority = max([item.torrent_info.pri_order for item in downloads])
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
@@ -1330,7 +1328,7 @@ class SubscribeChain(ChainBase):
|
||||
})
|
||||
subscribe.current_priority = priority
|
||||
subscribe.last_update = now
|
||||
if priority != 100:
|
||||
if subscribe.best_version and priority != 100:
|
||||
# 正在洗版,更新资源优先级
|
||||
logger.info(f'{mediainfo.title_year} 正在洗版,更新资源优先级为 {priority}')
|
||||
|
||||
@@ -1348,6 +1346,12 @@ class SubscribeChain(ChainBase):
|
||||
self.__record_subscribe_download_facts(subscribe=subscribe, mediainfo=mediainfo, downloads=downloads)
|
||||
elif downloads:
|
||||
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
|
||||
if downloads and meta.type == MediaType.MOVIE:
|
||||
self.__update_movie_download_priority(
|
||||
subscribe=subscribe,
|
||||
mediainfo=mediainfo,
|
||||
downloads=downloads,
|
||||
)
|
||||
# 是否完成订阅
|
||||
if not subscribe.best_version:
|
||||
# 普通订阅:先按 lefts 写 lack,再判断完成
|
||||
@@ -1366,13 +1370,6 @@ class SubscribeChain(ChainBase):
|
||||
logger.info(f'{mediainfo.title_year} 未下载完整,继续订阅 ...')
|
||||
return
|
||||
|
||||
if downloads and meta.type == MediaType.MOVIE:
|
||||
# 电影没有按集质量事实,只能用 current_priority 表达洗版下载质量。
|
||||
self.__update_movie_best_version_download_priority(
|
||||
subscribe=subscribe,
|
||||
mediainfo=mediainfo,
|
||||
downloads=downloads,
|
||||
)
|
||||
if meta.type == MediaType.TV:
|
||||
self.__refresh_subscribe_progress_with_no_exists(
|
||||
no_exists=lefts,
|
||||
|
||||
@@ -1543,7 +1543,13 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
if download_history:
|
||||
task.username = download_history.username
|
||||
# 识别媒体信息
|
||||
if download_history.tmdbid or download_history.doubanid:
|
||||
history_year_conflict = self._is_movie_year_conflict(
|
||||
task.meta, download_history
|
||||
)
|
||||
if (
|
||||
(download_history.tmdbid or download_history.doubanid)
|
||||
and not history_year_conflict
|
||||
):
|
||||
# 下载记录中已存在识别信息
|
||||
mediainfo: Optional[MediaInfo] = self.recognize_media(
|
||||
mtype=MediaType(download_history.type),
|
||||
@@ -1556,6 +1562,18 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
# 更新自定义媒体类别
|
||||
if download_history.media_category:
|
||||
mediainfo.category = download_history.media_category
|
||||
else:
|
||||
if history_year_conflict:
|
||||
logger.info(
|
||||
f"{task.fileitem.name} 文件年份 {task.meta.year} 与下载记录年份 "
|
||||
f"{download_history.year} 不一致,按文件名重新识别"
|
||||
)
|
||||
mediainfo = MediaChain().recognize_by_meta(
|
||||
task.meta,
|
||||
obtain_images=True,
|
||||
)
|
||||
if mediainfo and download_history.media_category:
|
||||
mediainfo.category = download_history.media_category
|
||||
else:
|
||||
# 识别媒体信息
|
||||
mediainfo = MediaChain().recognize_by_meta(
|
||||
@@ -2304,6 +2322,31 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _is_movie_year_conflict(
|
||||
file_meta: MetaBase, media: Union[DownloadHistory, MediaInfo]
|
||||
) -> bool:
|
||||
"""
|
||||
判断文件名年份是否与已识别电影年份冲突。
|
||||
|
||||
多电影合集只保存一条下载历史,不能把合集首部电影的媒体 ID 套用到其它年份的文件;
|
||||
电视剧季包仍应继续复用同一条下载历史。
|
||||
"""
|
||||
file_year = getattr(file_meta, "year", None)
|
||||
media_year = getattr(media, "year", None)
|
||||
if not file_meta or not media or not file_year or not media_year:
|
||||
return False
|
||||
media_type = getattr(media, "type", None)
|
||||
if not isinstance(media_type, MediaType):
|
||||
try:
|
||||
media_type = MediaType(media_type)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return (
|
||||
media_type == MediaType.MOVIE
|
||||
and str(file_year) != str(media_year)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __optional_attr_equal(
|
||||
source: MetaBase,
|
||||
@@ -2964,11 +3007,19 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
_downloader = downloader
|
||||
_download_hash = download_hash
|
||||
|
||||
# 自动整理预载的媒体信息来自整条下载历史;电影合集内文件年份冲突时逐文件识别。
|
||||
task_mediainfo = mediainfo
|
||||
if (
|
||||
not manual
|
||||
and self._is_movie_year_conflict(file_meta, task_mediainfo)
|
||||
):
|
||||
task_mediainfo = None
|
||||
|
||||
# 后台整理
|
||||
transfer_task = TransferTask(
|
||||
fileitem=file_item,
|
||||
meta=file_meta,
|
||||
mediainfo=mediainfo,
|
||||
mediainfo=task_mediainfo,
|
||||
target_directory=target_directory,
|
||||
target_storage=target_storage,
|
||||
target_path=target_path,
|
||||
|
||||
@@ -188,12 +188,19 @@ def set_or_refresh_resource_token_cookie(
|
||||
purpose="resource"
|
||||
)
|
||||
|
||||
# 判断请求是否为 HTTPS:直连协议为 https,或经反向代理转发时携带 X-Forwarded-Proto: https。
|
||||
# 无法确认为明文 HTTP 时按 fail-safe 默认设置 secure=True,避免代理终止 HTTPS 后以 HTTP 转发导致 Cookie 明文传输。
|
||||
is_https = (
|
||||
request.url.scheme == "https"
|
||||
or request.headers.get("x-forwarded-proto", "").lower() == "https"
|
||||
)
|
||||
|
||||
# 设置会话级别的 HttpOnly Cookie
|
||||
response.set_cookie(
|
||||
key=settings.PROJECT_NAME,
|
||||
value=resource_token,
|
||||
httponly=True,
|
||||
secure=request.url.scheme == "https", # 根据当前请求的协议设置 secure 属性
|
||||
secure=is_https, # 根据当前请求协议(含反向代理转发标识)设置 secure 属性
|
||||
samesite="lax" # 不同浏览器对 "Strict" 的处理可能不同,设置 SameSite 为 "Lax",以平衡安全性和兼容性
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ from typing import Any
|
||||
|
||||
from pywebpush import WebPushException
|
||||
|
||||
# WNS 默认 TTL(秒);ttl>0 时需配 X-WNS-Cache-Policy: cache
|
||||
_WNS_DEFAULT_TTL = 86400
|
||||
|
||||
|
||||
def is_webpush_subscription_gone(error: WebPushException) -> bool:
|
||||
"""
|
||||
@@ -10,3 +13,25 @@ def is_webpush_subscription_gone(error: WebPushException) -> bool:
|
||||
response: Any = getattr(error, "response", None)
|
||||
status_code = getattr(response, "status_code", None) or getattr(response, "status", None)
|
||||
return status_code in {404, 410}
|
||||
|
||||
|
||||
def is_wns_endpoint(endpoint: str | None) -> bool:
|
||||
"""
|
||||
判断是否为 Microsoft WNS(Edge/Windows)推送端点。
|
||||
"""
|
||||
return bool(endpoint and "notify.windows.com" in endpoint)
|
||||
|
||||
|
||||
def webpush_options_for_endpoint(endpoint: str | None) -> dict[str, Any]:
|
||||
"""
|
||||
按推送服务返回 pywebpush 额外参数。
|
||||
|
||||
WNS 要求 TTL 与 X-WNS-Cache-Policy 一致,否则返回 400。
|
||||
见 https://github.com/web-push-libs/pywebpush/issues/162
|
||||
"""
|
||||
if not is_wns_endpoint(endpoint):
|
||||
return {}
|
||||
return {
|
||||
"ttl": _WNS_DEFAULT_TTL,
|
||||
"headers": {"X-WNS-Cache-Policy": "cache"},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Union, Tuple
|
||||
from pywebpush import webpush, WebPushException
|
||||
|
||||
from app.core.config import global_vars, settings
|
||||
from app.helper.webpush import is_webpush_subscription_gone
|
||||
from app.helper.webpush import is_webpush_subscription_gone, webpush_options_for_endpoint
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase, _MessageBase
|
||||
from app.schemas import Notification
|
||||
@@ -95,6 +95,7 @@ class WebPushModule(_ModuleBase, _MessageBase):
|
||||
vapid_claims={
|
||||
"sub": settings.VAPID.get("subject")
|
||||
},
|
||||
**webpush_options_for_endpoint(sub.get("endpoint")),
|
||||
)
|
||||
except WebPushException as err:
|
||||
logger.error(f"WebPush发送失败: {str(err)}")
|
||||
|
||||
@@ -203,7 +203,7 @@ class StringUtils:
|
||||
忽略特殊字符
|
||||
"""
|
||||
# 需要忽略的特殊字符
|
||||
CONVERT_EMPTY_CHARS = r"[、.。,,·::;;!!'’\"“”()()\[\]【】「」\-—―\+\|\\_/&#~~]"
|
||||
CONVERT_EMPTY_CHARS = r"[、.。,,·::;;!!??'’\"“”()()\[\]【】「」\-—―\+\|\\_/&#~~]"
|
||||
if not text:
|
||||
return text
|
||||
if not isinstance(text, list):
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.core.metainfo import MetaInfo, MetaInfoPath, find_metainfo
|
||||
from app.helper.torrent import TorrentHelper
|
||||
from app.schemas.types import MediaType
|
||||
from tests.cases.meta import meta_cases
|
||||
|
||||
|
||||
@@ -108,6 +111,52 @@ def test_metainfo_preserves_original_name_when_custom_words_applied():
|
||||
assert meta.original_name == "电影测试替换名称"
|
||||
|
||||
|
||||
def test_torrent_title_match_ignores_question_mark_variants():
|
||||
"""问号差异不应导致番剧罗马字标题匹配失败。"""
|
||||
mediainfo = SimpleNamespace(
|
||||
title="哪里有温柔对待阿宅的辣妹!?",
|
||||
original_title="オタクに優しいギャルはいない!?",
|
||||
names=["Otaku ni Yasashii Gal wa Inai!?"],
|
||||
type=MediaType.TV,
|
||||
year=None,
|
||||
tmdb_id=None,
|
||||
douban_id=None,
|
||||
imdb_id=None,
|
||||
season_years={},
|
||||
)
|
||||
torrent_meta = SimpleNamespace(
|
||||
tmdbid=None,
|
||||
doubanid=None,
|
||||
cn_name=None,
|
||||
en_name="Otaku ni Yasashii Gal wa Inai",
|
||||
type=MediaType.TV,
|
||||
year=None,
|
||||
org_string=None,
|
||||
)
|
||||
torrent = SimpleNamespace(
|
||||
site_name="MiKan",
|
||||
title="[今晚月色真美][Otaku ni Yasashii Gal wa Inai!?][12][1080P]",
|
||||
category=MediaType.TV.value,
|
||||
imdbid=None,
|
||||
description=None,
|
||||
)
|
||||
|
||||
assert TorrentHelper.match_torrent(
|
||||
mediainfo=mediainfo,
|
||||
torrent_meta=torrent_meta,
|
||||
torrent=torrent,
|
||||
)
|
||||
|
||||
mediainfo.names = []
|
||||
torrent_meta.cn_name = "哪里有温柔对待阿宅的辣妹"
|
||||
torrent_meta.en_name = None
|
||||
assert TorrentHelper.match_torrent(
|
||||
mediainfo=mediainfo,
|
||||
torrent_meta=torrent_meta,
|
||||
torrent=torrent,
|
||||
)
|
||||
|
||||
|
||||
def test_custom_words_replace_then_episode_offset():
|
||||
"""测试复杂识别词仍按先替换、后集数偏移的顺序处理。"""
|
||||
custom_words = ["旧名 => 新名 && 第 <> 集 >> EP+1"]
|
||||
@@ -273,3 +322,39 @@ def test_metainfopath_cn_title_containing_keyword_not_cleared():
|
||||
path = Path("/Some Movie 2024/粤语残片.mkv")
|
||||
meta = MetaInfoPath(path)
|
||||
assert "粤语残片" in meta.cn_name
|
||||
|
||||
|
||||
def test_metainfopath_movie_collection_parent_does_not_override_file_title():
|
||||
"""电影合集父目录不应覆盖文件名中更具体的片名与年份。"""
|
||||
collection = (
|
||||
"/Unraid/Media/MoviePilot/电影/"
|
||||
"The.Hunger.Games.Complete.4-Film.Collection.2160p.UHD.Blu-ray."
|
||||
"DV.Atmos.TrueHD.7.1.x265-HDH"
|
||||
)
|
||||
cases = [
|
||||
(
|
||||
"The.Hunger.Games.2012.2160p.UHD.Blu-ray.DV.Atmos.TrueHD.7.1.x265-HDH.mkv",
|
||||
"The Hunger Games",
|
||||
"2012",
|
||||
),
|
||||
(
|
||||
"The.Hunger.Games.Catching.Fire.2013.2160p.UHD.Blu-ray.DV.Atmos.TrueHD.7.1.x265-HDH.mkv",
|
||||
"The Hunger Games Catching Fire",
|
||||
"2013",
|
||||
),
|
||||
(
|
||||
"The.Hunger.Games.Mockingjay.Part.1.2014.2160p.UHD.Blu-ray.DV.Atmos.TrueHD.7.1.x265-HDH.mkv",
|
||||
"The Hunger Games Mockingjay Part 1",
|
||||
"2014",
|
||||
),
|
||||
(
|
||||
"The.Hunger.Games.Mockingjay.Part.2.2015.2160p.UHD.Blu-ray.DV.Atmos.TrueHD.7.1.x265-HDH.mkv",
|
||||
"The Hunger Games Mockingjay Part 2",
|
||||
"2015",
|
||||
),
|
||||
]
|
||||
|
||||
for file_name, expected_name, expected_year in cases:
|
||||
meta = MetaInfoPath(Path(f"{collection}/{file_name}"))
|
||||
assert meta.name == expected_name
|
||||
assert meta.year == expected_year
|
||||
|
||||
38
tests/test_resource_token_cookie_secure_flag.py
Normal file
38
tests/test_resource_token_cookie_secure_flag.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from fastapi import Response
|
||||
|
||||
from app import schemas
|
||||
from app.core.security import set_or_refresh_resource_token_cookie
|
||||
|
||||
|
||||
class FakeURL:
|
||||
def __init__(self, scheme: str) -> None:
|
||||
self.scheme = scheme
|
||||
|
||||
|
||||
class FakeRequest:
|
||||
"""
|
||||
最小化的请求桩对象,仅提供 set_or_refresh_resource_token_cookie 所需属性。
|
||||
"""
|
||||
|
||||
def __init__(self, scheme: str, headers: dict | None = None) -> None:
|
||||
self.url = FakeURL(scheme)
|
||||
self.headers = headers or {}
|
||||
self.cookies: dict = {}
|
||||
|
||||
|
||||
class ResourceTokenCookieSecureFlagTest(TestCase):
|
||||
def test_secure_flag_set_when_https_terminated_at_reverse_proxy(self):
|
||||
"""
|
||||
当反向代理(如 nginx)终止 HTTPS 并以 HTTP 转发给后端时,
|
||||
资源令牌 Cookie 仍必须携带 secure 属性,不能因为直连请求协议是 http 就降级。
|
||||
"""
|
||||
request = FakeRequest(scheme="http", headers={"x-forwarded-proto": "https"})
|
||||
response = Response()
|
||||
payload = schemas.TokenPayload(sub=1, username="test", super_user=False, level=1)
|
||||
|
||||
set_or_refresh_resource_token_cookie(request, response, payload)
|
||||
|
||||
set_cookie_header = response.headers.get("set-cookie", "")
|
||||
self.assertIn("Secure", set_cookie_header)
|
||||
@@ -1903,7 +1903,7 @@ class SubscribeNoteTrackingTest(TestCase):
|
||||
|
||||
with patch.object(SUBSCRIBE_CHAIN_MODULE, "SubscribeOper", _SubscribeOper), patch.object(
|
||||
SubscribeChain,
|
||||
"_SubscribeChain__update_movie_best_version_download_priority",
|
||||
"_SubscribeChain__update_movie_download_priority",
|
||||
), patch.object(
|
||||
SubscribeChain,
|
||||
"_SubscribeChain__finish_subscribe",
|
||||
@@ -2819,3 +2819,44 @@ class SubscribeDownloadFactsTest(TestCase):
|
||||
)
|
||||
|
||||
refresh_mock.assert_not_called()
|
||||
|
||||
def test_movie_normal_download_records_current_priority_before_completion(self):
|
||||
subscribe = self._build_subscribe(
|
||||
type=MediaType.MOVIE.value,
|
||||
best_version=0,
|
||||
best_version_full=0,
|
||||
current_priority=None,
|
||||
episode_priority={},
|
||||
note=[],
|
||||
tmdbid=30003,
|
||||
total_episode=1,
|
||||
lack_episode=1,
|
||||
)
|
||||
download = self._download(episodes=[], pri_order=90)
|
||||
download.media_info = SimpleNamespace(type=MediaType.MOVIE, tmdb_id=30003, douban_id=None)
|
||||
download.meta_info = SimpleNamespace(episode_list=[], season_list=[])
|
||||
updates = []
|
||||
finished = []
|
||||
|
||||
class _SubscribeOper:
|
||||
def update(self, subscribe_id, payload):
|
||||
updates.append(payload)
|
||||
|
||||
chain = self.SubscribeChain()
|
||||
|
||||
def finish_probe(subscribe, **_kwargs):
|
||||
finished.append(subscribe.current_priority)
|
||||
|
||||
with patch.object(self.module, "SubscribeOper", return_value=_SubscribeOper()), \
|
||||
patch.object(chain, "_SubscribeChain__finish_subscribe", side_effect=finish_probe):
|
||||
chain.finish_subscribe_or_not(
|
||||
subscribe=subscribe,
|
||||
meta=SimpleNamespace(type=MediaType.MOVIE),
|
||||
mediainfo=SimpleNamespace(title_year="下载事实电影 (2026)"),
|
||||
downloads=[download],
|
||||
lefts={},
|
||||
)
|
||||
|
||||
self.assertEqual(subscribe.current_priority, 90)
|
||||
self.assertEqual(finished, [90])
|
||||
self.assertIn({"current_priority": 90, "last_update": subscribe.last_update}, updates)
|
||||
|
||||
192
tests/test_transfer_movie_collection.py
Normal file
192
tests/test_transfer_movie_collection.py
Normal file
@@ -0,0 +1,192 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import MediaInfo
|
||||
from app.schemas import DownloadHistory, FileItem, TransferTask
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
def _make_chain() -> TransferChain:
|
||||
"""构造不启动后台线程的整理链测试实例。"""
|
||||
chain = object.__new__(TransferChain)
|
||||
chain._media_exts = settings.RMT_MEDIAEXT
|
||||
chain._subtitle_exts = settings.RMT_SUBEXT
|
||||
chain._audio_exts = settings.RMT_AUDIOEXT
|
||||
chain._allowed_exts = (
|
||||
chain._media_exts + chain._subtitle_exts + chain._audio_exts
|
||||
)
|
||||
chain.jobview = SimpleNamespace(
|
||||
finish_task=lambda task: None,
|
||||
try_remove_job=lambda task: None,
|
||||
)
|
||||
chain._TransferChain__get_trans_fileitems = lambda fileitem, predicate: [
|
||||
(fileitem, False)
|
||||
]
|
||||
chain._TransferChain__put_to_jobview = lambda task: True
|
||||
chain._TransferChain__register_scrape_batch_task = lambda task: None
|
||||
chain._TransferChain__close_scrape_batch = lambda batch_id: None
|
||||
return chain
|
||||
|
||||
|
||||
def _make_file_meta(year: str = "2013") -> SimpleNamespace:
|
||||
"""构造电影合集文件的元数据。"""
|
||||
return SimpleNamespace(
|
||||
name="The Hunger Games Catching Fire",
|
||||
year=year,
|
||||
type=MediaType.UNKNOWN,
|
||||
begin_season=None,
|
||||
begin_episode=None,
|
||||
part=None,
|
||||
)
|
||||
|
||||
|
||||
def _make_history() -> SimpleNamespace:
|
||||
"""构造被合集首部电影占用的下载历史。"""
|
||||
return SimpleNamespace(
|
||||
id=1,
|
||||
download_hash="collection-hash",
|
||||
downloader="qbittorrent",
|
||||
type=MediaType.MOVIE.value,
|
||||
title="饥饿游戏",
|
||||
year="2012",
|
||||
tmdbid=70160,
|
||||
doubanid=None,
|
||||
episode_group=None,
|
||||
media_category=None,
|
||||
username=None,
|
||||
custom_words=None,
|
||||
note=None,
|
||||
)
|
||||
|
||||
|
||||
def test_movie_year_conflict_only_applies_to_movies():
|
||||
"""仅电影年份冲突应触发逐文件识别,电视剧季包仍复用下载历史。"""
|
||||
file_meta = _make_file_meta()
|
||||
movie_history = _make_history()
|
||||
tv_history = SimpleNamespace(type=MediaType.TV, year="2012")
|
||||
|
||||
assert TransferChain._is_movie_year_conflict(file_meta, movie_history)
|
||||
assert not TransferChain._is_movie_year_conflict(file_meta, tv_history)
|
||||
movie_history.year = "2013"
|
||||
assert not TransferChain._is_movie_year_conflict(file_meta, movie_history)
|
||||
|
||||
|
||||
def test_conflicting_download_history_recognizes_movie_by_file_meta(monkeypatch):
|
||||
"""手动整理未指定媒体时,冲突的合集历史应回退到文件元数据识别。"""
|
||||
chain = object.__new__(TransferChain)
|
||||
fallback_media = MediaInfo(
|
||||
type=MediaType.MOVIE,
|
||||
title="饥饿游戏2:星火燎原",
|
||||
year="2013",
|
||||
tmdb_id=101299,
|
||||
)
|
||||
recognized_meta = []
|
||||
chain.recognize_media = lambda **kwargs: pytest.fail("不应按合集历史 ID 识别")
|
||||
chain.jobview = SimpleNamespace(
|
||||
migrate_task=lambda task: False,
|
||||
try_remove_job=lambda task: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.chain.transfer.TransferHistoryOper",
|
||||
lambda: SimpleNamespace(get_by_type_tmdbid=lambda **kwargs: None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.chain.transfer.MediaChain",
|
||||
lambda: SimpleNamespace(
|
||||
recognize_by_meta=lambda meta, obtain_images: (
|
||||
recognized_meta.append(meta) or fallback_media
|
||||
)
|
||||
),
|
||||
)
|
||||
task = TransferTask(
|
||||
fileitem=FileItem(
|
||||
storage="local",
|
||||
path="/downloads/collection/The.Hunger.Games.Catching.Fire.2013.mkv",
|
||||
type="file",
|
||||
name="The.Hunger.Games.Catching.Fire.2013.mkv",
|
||||
extension="mkv",
|
||||
size=1024,
|
||||
),
|
||||
meta=_make_file_meta(),
|
||||
download_history=DownloadHistory(**vars(_make_history())),
|
||||
preview=True,
|
||||
)
|
||||
|
||||
state, message = chain._TransferChain__handle_transfer(task)
|
||||
|
||||
assert not state
|
||||
assert "已在整理队列中" in message
|
||||
assert recognized_meta == [task.meta]
|
||||
assert task.mediainfo.tmdb_id == 101299
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("manual", "expected_tmdb_id"),
|
||||
[
|
||||
(False, None),
|
||||
(True, 70160),
|
||||
],
|
||||
)
|
||||
def test_movie_collection_conflict_only_drops_automatic_media(
|
||||
monkeypatch, manual: bool, expected_tmdb_id: int
|
||||
):
|
||||
"""自动整理应丢弃冲突的合集媒体,手动明确指定的媒体仍应保留。"""
|
||||
chain = _make_chain()
|
||||
source_file = FileItem(
|
||||
storage="local",
|
||||
path=(
|
||||
"/downloads/The.Hunger.Games.Complete.4-Film.Collection/"
|
||||
"The.Hunger.Games.Catching.Fire.2013.mkv"
|
||||
),
|
||||
type="file",
|
||||
name="The.Hunger.Games.Catching.Fire.2013.mkv",
|
||||
extension="mkv",
|
||||
size=1024,
|
||||
)
|
||||
file_meta = _make_file_meta()
|
||||
history = _make_history()
|
||||
history_oper = SimpleNamespace(
|
||||
get_by_hash=lambda download_hash: history,
|
||||
get_file_by_fullpath=lambda fullpath: None,
|
||||
get_files_by_savepath=lambda savepath: [],
|
||||
get_by_path=lambda path: None,
|
||||
)
|
||||
captured_tasks = []
|
||||
|
||||
def fake_handle_transfer(task, callback=None):
|
||||
"""记录整理任务,避免执行真实文件操作。"""
|
||||
captured_tasks.append(task)
|
||||
return True, ""
|
||||
|
||||
chain._TransferChain__handle_transfer = fake_handle_transfer
|
||||
monkeypatch.setattr(
|
||||
"app.chain.transfer.TransferHistoryOper",
|
||||
lambda: SimpleNamespace(get_by_src=lambda src, storage=None: None),
|
||||
)
|
||||
monkeypatch.setattr("app.chain.transfer.DownloadHistoryOper", lambda: history_oper)
|
||||
monkeypatch.setattr(
|
||||
"app.chain.transfer.SystemConfigOper",
|
||||
lambda: SimpleNamespace(get=lambda key: None),
|
||||
)
|
||||
monkeypatch.setattr("app.chain.transfer.StorageChain", lambda: SimpleNamespace())
|
||||
monkeypatch.setattr("app.chain.transfer.MetaInfoPath", lambda *args, **kwargs: file_meta)
|
||||
|
||||
chain.do_transfer(
|
||||
fileitem=source_file,
|
||||
mediainfo=SimpleNamespace(
|
||||
tmdb_id=70160,
|
||||
type=MediaType.MOVIE,
|
||||
year="2012",
|
||||
),
|
||||
download_hash=history.download_hash,
|
||||
background=False,
|
||||
manual=manual,
|
||||
preview=True,
|
||||
)
|
||||
|
||||
assert len(captured_tasks) == 1
|
||||
task_media = captured_tasks[0].mediainfo
|
||||
assert getattr(task_media, "tmdb_id", None) == expected_tmdb_id
|
||||
43
tests/test_webpush_helper.py
Normal file
43
tests/test_webpush_helper.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from app.helper.webpush import (
|
||||
is_webpush_subscription_gone,
|
||||
is_wns_endpoint,
|
||||
webpush_options_for_endpoint,
|
||||
)
|
||||
from pywebpush import WebPushException
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, status_code: int):
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
def test_is_webpush_subscription_gone_for_expired_status_codes() -> None:
|
||||
for status_code in (404, 410):
|
||||
err = WebPushException("gone")
|
||||
err.response = _FakeResponse(status_code)
|
||||
assert is_webpush_subscription_gone(err)
|
||||
|
||||
|
||||
def test_is_webpush_subscription_gone_for_other_errors() -> None:
|
||||
err = WebPushException("bad request")
|
||||
err.response = _FakeResponse(400)
|
||||
assert not is_webpush_subscription_gone(err)
|
||||
|
||||
|
||||
def test_is_wns_endpoint_detects_windows_push_url() -> None:
|
||||
assert is_wns_endpoint("https://wns2-sg2p.notify.windows.com/w/?token=abc")
|
||||
assert not is_wns_endpoint("https://web.push.apple.com/abc")
|
||||
assert not is_wns_endpoint(None)
|
||||
assert not is_wns_endpoint("")
|
||||
|
||||
|
||||
def test_webpush_options_for_wns_endpoint() -> None:
|
||||
options = webpush_options_for_endpoint("https://wns2-pn1p.notify.windows.com/x")
|
||||
assert options == {
|
||||
"ttl": 86400,
|
||||
"headers": {"X-WNS-Cache-Policy": "cache"},
|
||||
}
|
||||
|
||||
|
||||
def test_webpush_options_for_non_wns_endpoint() -> None:
|
||||
assert webpush_options_for_endpoint("https://fcm.googleapis.com/fcm/send/abc") == {}
|
||||
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = 'v2.14.0'
|
||||
FRONTEND_VERSION = 'v2.14.0'
|
||||
APP_VERSION = 'v2.14.1'
|
||||
FRONTEND_VERSION = 'v2.14.1'
|
||||
|
||||
Reference in New Issue
Block a user