mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-07 00:30:20 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
871d1ec0d8 | ||
|
|
ca1dbdf843 | ||
|
|
e77bef7cf1 | ||
|
|
f4011d3ac2 |
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
@@ -11,6 +11,13 @@ on:
|
||||
# 允许手动触发
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: unit-tests-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -2785,9 +2785,16 @@ class SubscribeChain(ChainBase):
|
||||
# 更新剧集列表、开始集数、总集数
|
||||
if not episode_list:
|
||||
# 整季缺失
|
||||
episodes = []
|
||||
start_episode = start_episode or start
|
||||
total_episode = total_episode or total
|
||||
original_start = start if start is not None else 1
|
||||
# 空集列表会被下载链解释为整季下载;当订阅开始集裁掉季初范围时,需要转成显式集数。
|
||||
if start_episode and total_episode and start_episode > original_start:
|
||||
episodes = list(range(start_episode, total_episode + 1))
|
||||
if not episodes:
|
||||
return True, {}
|
||||
else:
|
||||
episodes = []
|
||||
else:
|
||||
# 部分缺失
|
||||
if not start_episode \
|
||||
|
||||
@@ -9,10 +9,10 @@ import sys
|
||||
import threading
|
||||
from asyncio import AbstractEventLoop
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_origin, get_args
|
||||
from urllib.parse import quote, urlencode, urlparse
|
||||
|
||||
from dotenv import set_key
|
||||
from dotenv import set_key, unset_key
|
||||
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
@@ -690,6 +690,18 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
|
||||
# 处理 Optional 类型:当值为空字符串且类型允许 None 时,转为 None
|
||||
# 兼容 typing.Union (Python 3.9) 与 types.UnionType (Python 3.10+ PEP 604)
|
||||
origin = get_origin(expected_type)
|
||||
is_union = origin is Union or getattr(origin, "__name__", None) == "UnionType"
|
||||
if (
|
||||
is_union
|
||||
and type(None) in get_args(expected_type)
|
||||
and isinstance(value, str)
|
||||
and not value
|
||||
):
|
||||
return default, str(default) != str(original_value)
|
||||
|
||||
try:
|
||||
if expected_type is bool:
|
||||
if isinstance(value, bool):
|
||||
@@ -812,13 +824,19 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
||||
logger.warning(message)
|
||||
return False, message
|
||||
else:
|
||||
# 当值为 None 时,从 env 文件中删除该键,恢复为默认值
|
||||
if converted_value is None:
|
||||
unset_key(
|
||||
dotenv_path=SystemUtils.get_env_path(),
|
||||
key_to_unset=field_name,
|
||||
)
|
||||
logger.info(f"配置项 '{field_name}' 已清空,从 'app.env' 中移除")
|
||||
return True, message
|
||||
# 如果是列表、字典或集合类型,将其转换为JSON字符串
|
||||
if isinstance(converted_value, (list, dict, set)):
|
||||
value_to_write = json.dumps(converted_value)
|
||||
else:
|
||||
value_to_write = (
|
||||
str(converted_value) if converted_value is not None else ""
|
||||
)
|
||||
value_to_write = str(converted_value)
|
||||
|
||||
set_key(
|
||||
dotenv_path=SystemUtils.get_env_path(),
|
||||
@@ -967,7 +985,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
||||
|
||||
@property
|
||||
def PROXY(self):
|
||||
if self.PROXY_HOST:
|
||||
if self.PROXY_HOST and self.PROXY_HOST.strip():
|
||||
return {
|
||||
"http": self.PROXY_HOST,
|
||||
"https": self.PROXY_HOST,
|
||||
@@ -1009,7 +1027,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
||||
|
||||
@property
|
||||
def PROXY_SERVER(self):
|
||||
if self.PROXY_HOST:
|
||||
if self.PROXY_HOST and self.PROXY_HOST.strip():
|
||||
try:
|
||||
parsed = urlparse(self.PROXY_HOST)
|
||||
if not parsed.scheme:
|
||||
|
||||
@@ -92,6 +92,9 @@ def _get_shared_async_transport(
|
||||
会话级状态由调用方在外层 AsyncClient(transport=...) 实例化时单独配置,
|
||||
每次调用用完即销毁,因此天然无 jar 累积串扰。
|
||||
"""
|
||||
# 规范化代理:拒绝空字符串等非法值,防止 httpx 抛出 Unknown scheme for proxy URL
|
||||
if proxy is not None and (not proxy or not proxy.strip()):
|
||||
proxy = None
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
@@ -899,12 +902,17 @@ class AsyncRequestUtils:
|
||||
|
||||
# 如果已经是字符串格式,直接返回
|
||||
if isinstance(proxies, str):
|
||||
return proxies
|
||||
return proxies.strip() or None
|
||||
|
||||
# 如果是字典格式,提取http或https代理
|
||||
if isinstance(proxies, dict):
|
||||
# 优先使用https代理,如果没有则使用http代理
|
||||
proxy_url = proxies.get("https") or proxies.get("http")
|
||||
# 先各自 strip,避免空白字符串阻断裂合取或回退到 http 代理
|
||||
https_proxy = proxies.get("https")
|
||||
http_proxy = proxies.get("http")
|
||||
https_proxy = https_proxy.strip() if isinstance(https_proxy, str) else None
|
||||
http_proxy = http_proxy.strip() if isinstance(http_proxy, str) else None
|
||||
proxy_url = https_proxy or http_proxy
|
||||
if proxy_url:
|
||||
return proxy_url
|
||||
|
||||
|
||||
@@ -456,6 +456,69 @@ class SubscribeChainTest(TestCase):
|
||||
self.assertEqual(SubscribeChain.get_best_version_current_priority(subscribe), 100)
|
||||
self.assertTrue(SubscribeChain.is_best_version_complete(subscribe))
|
||||
|
||||
def test_get_subscribe_no_exists_expands_whole_missing_when_custom_start_skips_existing_range(self):
|
||||
"""自定义开始集跳过季初集数时,缺失整季需要转成显式目标集。"""
|
||||
no_exists = {
|
||||
"media-key": {
|
||||
1: SimpleNamespace(season=1, episodes=[], total_episode=48, start_episode=1)
|
||||
}
|
||||
}
|
||||
|
||||
exist_flag, result = SubscribeChain._SubscribeChain__get_subscribe_no_exits(
|
||||
subscribe_name="主角 S01",
|
||||
no_exists=no_exists,
|
||||
mediakey="media-key",
|
||||
begin_season=1,
|
||||
total_episode=48,
|
||||
start_episode=44,
|
||||
)
|
||||
|
||||
self.assertFalse(exist_flag)
|
||||
self.assertEqual(result["media-key"][1].episodes, [44, 45, 46, 47, 48])
|
||||
self.assertEqual(result["media-key"][1].start_episode, 44)
|
||||
self.assertEqual(result["media-key"][1].total_episode, 48)
|
||||
|
||||
def test_get_subscribe_no_exists_keeps_whole_missing_when_custom_start_matches_original_start(self):
|
||||
"""自定义开始集没有缩小范围时,仍保留空集列表表示整季缺失。"""
|
||||
no_exists = {
|
||||
"media-key": {
|
||||
1: SimpleNamespace(season=1, episodes=[], total_episode=48, start_episode=1)
|
||||
}
|
||||
}
|
||||
|
||||
exist_flag, result = SubscribeChain._SubscribeChain__get_subscribe_no_exits(
|
||||
subscribe_name="主角 S01",
|
||||
no_exists=no_exists,
|
||||
mediakey="media-key",
|
||||
begin_season=1,
|
||||
total_episode=48,
|
||||
start_episode=1,
|
||||
)
|
||||
|
||||
self.assertFalse(exist_flag)
|
||||
self.assertEqual(result["media-key"][1].episodes, [])
|
||||
self.assertEqual(result["media-key"][1].start_episode, 1)
|
||||
self.assertEqual(result["media-key"][1].total_episode, 48)
|
||||
|
||||
def test_best_version_full_pack_first_keeps_whole_missing_for_custom_start_episode(self):
|
||||
"""分集洗版优先全集时,空集列表仍表示下载链按整季资源处理。"""
|
||||
subscribe = self._build_subscribe(
|
||||
best_version=1,
|
||||
best_version_full=0,
|
||||
start_episode=44,
|
||||
total_episode=48,
|
||||
episode_priority={str(episode): 80 for episode in range(44, 49)},
|
||||
)
|
||||
|
||||
result = SubscribeChain._SubscribeChain__build_full_pack_first_no_exists(
|
||||
subscribe=subscribe,
|
||||
mediakey="media-key",
|
||||
)
|
||||
|
||||
self.assertEqual(result["media-key"][1].episodes, [])
|
||||
self.assertEqual(result["media-key"][1].start_episode, 44)
|
||||
self.assertEqual(result["media-key"][1].total_episode, 48)
|
||||
|
||||
def test_is_episode_range_covered_matches_pending_episodes(self):
|
||||
subscribe = self._build_subscribe(
|
||||
total_episode=12,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = 'v2.13.5'
|
||||
APP_VERSION = 'v2.13.5-1'
|
||||
FRONTEND_VERSION = 'v2.13.5'
|
||||
|
||||
Reference in New Issue
Block a user