153 lines
6.7 KiB
Python
153 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import random
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from biliup_next.core.errors import ModuleError
|
|
from biliup_next.core.models import Task
|
|
from biliup_next.core.providers import ProviderManifest
|
|
from biliup_next.infra.adapters.bilibili_api import BilibiliApiAdapter
|
|
from biliup_next.infra.adapters.full_video_locator import resolve_full_video_bvid
|
|
from biliup_next.infra.workspace_paths import resolve_task_work_dir
|
|
|
|
|
|
class BilibiliCollectionProvider:
|
|
def __init__(self, bilibili_api: BilibiliApiAdapter | None = None) -> None:
|
|
self.bilibili_api = bilibili_api or BilibiliApiAdapter()
|
|
self._section_cache: dict[int, int | None] = {}
|
|
|
|
manifest = ProviderManifest(
|
|
id="bilibili_collection",
|
|
name="Bilibili Collection Provider",
|
|
version="0.1.0",
|
|
provider_type="collection_provider",
|
|
entrypoint="biliup_next.modules.collection.providers.bilibili_collection:BilibiliCollectionProvider",
|
|
capabilities=["collection"],
|
|
enabled_by_default=True,
|
|
)
|
|
|
|
def sync(self, task: Task, target: str, settings: dict[str, Any]) -> dict[str, object]:
|
|
session_dir = resolve_task_work_dir(task)
|
|
cookies = self.bilibili_api.load_cookies(Path(str(settings["cookies_file"])))
|
|
csrf = cookies.get("bili_jct")
|
|
if not csrf:
|
|
raise ModuleError(code="COOKIE_CSRF_MISSING", message="Cookie 缺少 bili_jct", retryable=False)
|
|
|
|
session = self.bilibili_api.build_session(
|
|
cookies=cookies,
|
|
referer="https://member.bilibili.com/platform/upload-manager/distribution",
|
|
)
|
|
|
|
if target == "a":
|
|
season_id = int(settings["season_id_a"])
|
|
bvid = resolve_full_video_bvid(task.title, session_dir, settings)
|
|
if not bvid:
|
|
(session_dir / "collection_a_done.flag").touch()
|
|
return {"status": "skipped", "reason": "full_video_bvid_not_found"}
|
|
flag_path = session_dir / "collection_a_done.flag"
|
|
else:
|
|
season_id = int(settings["season_id_b"])
|
|
bvid_path = session_dir / "bvid.txt"
|
|
if not bvid_path.exists():
|
|
raise ModuleError(code="COLLECTION_BVID_MISSING", message=f"缺少 bvid.txt: {session_dir}", retryable=True)
|
|
bvid = bvid_path.read_text(encoding="utf-8").strip()
|
|
flag_path = session_dir / "collection_b_done.flag"
|
|
|
|
if season_id <= 0:
|
|
flag_path.touch()
|
|
return {"status": "skipped", "reason": "season_disabled"}
|
|
|
|
section_id = self._resolve_section_id(session, season_id)
|
|
if not section_id:
|
|
raise ModuleError(code="COLLECTION_SECTION_NOT_FOUND", message=f"未找到合集 section: {season_id}", retryable=True)
|
|
|
|
info = self._get_video_info(session, bvid)
|
|
add_result = self._add_videos_batch(session, csrf, section_id, [info])
|
|
if add_result["status"] == "failed":
|
|
raise ModuleError(
|
|
code="COLLECTION_ADD_FAILED",
|
|
message=str(add_result["message"]),
|
|
retryable=True,
|
|
details=add_result,
|
|
)
|
|
|
|
flag_path.touch()
|
|
if add_result["status"] == "added":
|
|
append_key = "append_collection_a_new_to_end" if target == "a" else "append_collection_b_new_to_end"
|
|
if settings.get(append_key, True):
|
|
self._move_videos_to_section_end(session, csrf, section_id, [int(info["aid"])])
|
|
return {"status": add_result["status"], "target": target, "bvid": bvid, "season_id": season_id}
|
|
|
|
def _resolve_section_id(self, session, season_id: int) -> int | None: # type: ignore[no-untyped-def]
|
|
if season_id in self._section_cache:
|
|
return self._section_cache[season_id]
|
|
result = self.bilibili_api.list_seasons(session)
|
|
if result.get("code") != 0:
|
|
return None
|
|
for season in result.get("data", {}).get("seasons", []):
|
|
if season.get("season", {}).get("id") == season_id:
|
|
sections = season.get("sections", {}).get("sections", [])
|
|
section_id = sections[0]["id"] if sections else None
|
|
self._section_cache[season_id] = section_id
|
|
return section_id
|
|
self._section_cache[season_id] = None
|
|
return None
|
|
|
|
def _get_video_info(self, session, bvid: str) -> dict[str, object]: # type: ignore[no-untyped-def]
|
|
data = self.bilibili_api.get_video_view(
|
|
session,
|
|
bvid,
|
|
error_code="COLLECTION_VIDEO_INFO_FAILED",
|
|
error_message="获取视频信息失败",
|
|
)
|
|
return {"aid": data["aid"], "cid": data["cid"], "title": data["title"], "charging_pay": 0}
|
|
|
|
def _add_videos_batch(self, session, csrf: str, section_id: int, episodes: list[dict[str, object]]) -> dict[str, object]: # type: ignore[no-untyped-def]
|
|
time.sleep(random.uniform(5.0, 10.0))
|
|
result = self.bilibili_api.add_section_episodes(
|
|
session,
|
|
csrf=csrf,
|
|
section_id=section_id,
|
|
episodes=episodes,
|
|
)
|
|
if result.get("code") == 0:
|
|
return {"status": "added"}
|
|
if result.get("code") == 20080:
|
|
return {"status": "already_exists", "message": result.get("message", "")}
|
|
return {"status": "failed", "message": result.get("message", "unknown error"), "code": result.get("code")}
|
|
|
|
def _move_videos_to_section_end(self, session, csrf: str, section_id: int, added_aids: list[int]) -> bool: # type: ignore[no-untyped-def]
|
|
detail = self.bilibili_api.get_section_detail(session, section_id=section_id)
|
|
if detail.get("code") != 0:
|
|
return False
|
|
section = detail.get("data", {}).get("section", {})
|
|
episodes = detail.get("data", {}).get("episodes", []) or []
|
|
if not episodes:
|
|
return True
|
|
target_aids = {int(aid) for aid in added_aids}
|
|
existing = []
|
|
appended = []
|
|
for episode in episodes:
|
|
item = {"id": episode.get("id")}
|
|
if item["id"] is None:
|
|
continue
|
|
if episode.get("aid") in target_aids:
|
|
appended.append(item)
|
|
else:
|
|
existing.append(item)
|
|
ordered = existing + appended
|
|
payload = {
|
|
"section": {
|
|
"id": section["id"],
|
|
"seasonId": section["seasonId"],
|
|
"title": section["title"],
|
|
"type": section["type"],
|
|
},
|
|
"sorts": [{"id": item["id"], "sort": index + 1} for index, item in enumerate(ordered)],
|
|
}
|
|
result = self.bilibili_api.edit_section(session, csrf=csrf, payload=payload)
|
|
return result.get("code") == 0
|