feat: professionalize control plane and standalone delivery
This commit is contained in:
@ -0,0 +1,151 @@
|
||||
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
|
||||
|
||||
|
||||
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 = Path(str(settings["session_dir"])) / task.title
|
||||
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
|
||||
Reference in New Issue
Block a user