diff --git a/.github/scripts/select_plugin_release_dir.sh b/.github/scripts/select_plugin_release_dir.sh new file mode 100644 index 00000000..6073b5e4 --- /dev/null +++ b/.github/scripts/select_plugin_release_dir.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +package_file="${1:?package file is required}" +plugin_id="${2:?lowercase plugin id is required}" + +case "$(basename "$package_file")" in + package.json) + plugin_dir="plugins/${plugin_id}" + ;; + package.v2.json) + plugin_dir="plugins.v2/${plugin_id}" + ;; + *) + echo "Unsupported package file: ${package_file}" >&2 + exit 2 + ;; +esac + +test -d "$plugin_dir" +printf '%s\n' "$plugin_dir" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2bca0a47..c16fbc50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,13 +57,7 @@ jobs: plugin_version="${entry##*|}" plugin_id_lc="$(echo "$plugin_id" | tr '[:upper:]' '[:lower:]')" - dir1="plugins/${plugin_id_lc}" - dir2="plugins.v2/${plugin_id_lc}" - plugin_dir="" - if [ -d "$dir1" ]; then plugin_dir="$dir1"; fi - if [ -d "$dir2" ]; then plugin_dir="$dir2"; fi - - if [ -z "$plugin_dir" ]; then + if ! plugin_dir="$(bash .github/scripts/select_plugin_release_dir.sh "$pkg_file" "$plugin_id_lc")"; then echo "WARN: directory for $plugin_id not found under plugins/ or plugins.v2/, skip." continue fi @@ -80,8 +74,12 @@ jobs: continue fi - # Existing per-plugin tags make unchanged directories a no-op. - prev_tag="$(git tag --list "${plugin_id}_v*" --sort=-version:refname | head -n 1 || true)" + # 同名插件可能跨代维护独立版本线;已有当前版本 tag 时用它判断该代目录是否变化。 + if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then + prev_tag="$tag" + else + prev_tag="$(git tag --list "${plugin_id}_v*" --sort=-version:refname | head -n 1 || true)" + fi changed=1 if [ -n "$prev_tag" ]; then diff --git a/package.v2.json b/package.v2.json index 45d88033..fe9aa6f0 100644 --- a/package.v2.json +++ b/package.v2.json @@ -412,11 +412,12 @@ "name": "PlexMatch", "description": "实现入库时添加 .plexmatch 文件,提高识别准确率。", "labels": "Plex,刮削", - "version": "1.2", + "version": "1.3", "icon": "https://raw.githubusercontent.com/InfinityPacer/MoviePilot-Plugins/main/icons/plexmatch.png", "author": "InfinityPacer", "level": 1, "history": { + "v1.3": "修复 PostgreSQL 下历史记录查询错误,并保持 SQLite 兼容", "v1.2": "MoviePilot V2 版本PlexMatch插件" }, "release": true diff --git a/plugins.v2/plexmatch/__init__.py b/plugins.v2/plexmatch/__init__.py index 4f9f5209..26cdb8d3 100644 --- a/plugins.v2/plexmatch/__init__.py +++ b/plugins.v2/plexmatch/__init__.py @@ -29,7 +29,7 @@ class PlexMatch(_PluginBase): # 插件图标 plugin_icon = "https://raw.githubusercontent.com/InfinityPacer/MoviePilot-Plugins/main/icons/plexmatch.png" # 插件版本 - plugin_version = "1.2" + plugin_version = "1.3" # 插件作者 plugin_author = "InfinityPacer" # 作者主页 @@ -398,9 +398,10 @@ def __check_external_interrupt(self, service: str) -> bool: @staticmethod @db_query def __list_transfer_histories(db: Optional[Session]) -> list[Type[TransferHistory]]: - """获取TMDBID 不为 0 并成功的历史记录列表""" + """获取具有有效 TMDB ID 且整理成功的历史记录。""" result = db.query(TransferHistory).filter(and_( - TransferHistory.tmdbid.is_not(0), + TransferHistory.tmdbid.is_not(None), + TransferHistory.tmdbid != 0, TransferHistory.status) ).all() return result diff --git a/tests/ci/test_plugin_release_directory.py b/tests/ci/test_plugin_release_directory.py new file mode 100644 index 00000000..f7b51a51 --- /dev/null +++ b/tests/ci/test_plugin_release_directory.py @@ -0,0 +1,50 @@ +from pathlib import Path +import subprocess + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SELECTOR = REPO_ROOT / ".github/scripts/select_plugin_release_dir.sh" +WORKFLOW = REPO_ROOT / ".github/workflows/release.yml" + + +def _select(tmp_path: Path, package_file: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["bash", str(SELECTOR), package_file, "example"], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + + +def test_release_directory_keeps_v1_and_v2_assets_separate(tmp_path: Path) -> None: + (tmp_path / "plugins/example").mkdir(parents=True) + (tmp_path / "plugins.v2/example").mkdir(parents=True) + + assert _select(tmp_path, "package.json").stdout.strip() == "plugins/example" + assert _select(tmp_path, "package.v2.json").stdout.strip() == "plugins.v2/example" + + +def test_v2_release_directory_does_not_fall_back_to_v1_source(tmp_path: Path) -> None: + (tmp_path / "plugins/example").mkdir(parents=True) + + result = _select(tmp_path, "package.v2.json") + + assert result.returncode != 0 + assert result.stdout == "" + + +def test_release_directory_rejects_unknown_package_file(tmp_path: Path) -> None: + result = _select(tmp_path, "package.beta.json") + + assert result.returncode == 2 + assert "Unsupported package file" in result.stderr + + +def test_release_workflow_uses_generation_aware_directory_selector() -> None: + workflow = WORKFLOW.read_text(encoding="utf-8") + + assert 'select_plugin_release_dir.sh "$pkg_file" "$plugin_id_lc"' in workflow + assert 'git rev-parse -q --verify "refs/tags/$tag"' in workflow + assert 'prev_tag="$tag"' in workflow + assert 'if [ -d "$dir2" ]; then plugin_dir="$dir2"; fi' not in workflow diff --git a/tests/v2/plexmatch/test_transfer_history_query.py b/tests/v2/plexmatch/test_transfer_history_query.py new file mode 100644 index 00000000..5da438a1 --- /dev/null +++ b/tests/v2/plexmatch/test_transfer_history_query.py @@ -0,0 +1,72 @@ +from sqlalchemy import Boolean, Integer, column, table +from sqlalchemy.dialects import postgresql, sqlite +from sqlalchemy.orm import Session + +import plexmatch + + +_TRANSFER_HISTORY = table( + "transferhistory", + column("tmdbid", Integer), + column("status", Boolean), +) + + +class _V2153TransferHistory: + """保留 MoviePilot v2.15.3 中查询依赖的整理记录字段契约。""" + + tmdbid = _TRANSFER_HISTORY.c.tmdbid + status = _TRANSFER_HISTORY.c.status + + +class _RecordingQuery: + """记录插件提交给 ORM 的筛选条件,不访问真实数据库。""" + + def __init__(self) -> None: + self.criterion = None + + def filter(self, criterion): + self.criterion = criterion + return self + + def all(self) -> list: + return [] + + +class _RecordingSession(Session): + """提供满足 db_query 契约的 Session,并暴露生成的查询条件。""" + + def __init__(self) -> None: + super().__init__() + self.recorded_query = _RecordingQuery() + + def query(self, *entities, **kwargs): + return self.recorded_query + + +def _compile_history_filter(monkeypatch, dialect) -> str: + monkeypatch.setattr(plexmatch, "TransferHistory", _V2153TransferHistory) + session = _RecordingSession() + try: + plexmatch.PlexMatch._PlexMatch__list_transfer_histories(db=session) + criterion = session.recorded_query.criterion + assert criterion is not None + return str(criterion.compile(dialect=dialect, compile_kwargs={"literal_binds": True})) + finally: + session.close() + + +def test_history_filter_uses_numeric_comparison_on_postgresql(monkeypatch) -> None: + sql = _compile_history_filter(monkeypatch, postgresql.dialect()) + + assert "tmdbid IS NOT NULL" in sql + assert "tmdbid != 0" in sql + assert "tmdbid IS NOT 0" not in sql + + +def test_history_filter_keeps_sqlite_compatibility(monkeypatch) -> None: + sql = _compile_history_filter(monkeypatch, sqlite.dialect()) + + assert "tmdbid IS NOT NULL" in sql + assert "tmdbid != 0" in sql + assert "tmdbid IS NOT 0" not in sql