公开 API
集成示例
可执行的 RSS 条件请求和有界删除轮询。
公开 RSS 返回带 .torrent 下载链接的 XML。番剧与字幕组过滤可组合;其他类资源使用独立分类 RSS。
https://anibt.net/rss/anime.xml?bgmId=543360&groupSlug=kirara-fantasia
https://anibt.net/rss/anime.xml?bgmId=543360&resolution=1080p&language=CHS
https://anibt.net/rss/group/kirara-fantasia.xml
https://anibt.net/rss/other/music.xml以下脚本使用 Python 3 标准库。先保存正文,再保存 ETag;304 只复用本地已有正文。
import os
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen
url = os.environ.get("FEED_URL", "https://anibt.net/rss/anime.xml?bgmId=543360")
feed_file = Path("feed.xml")
etag_file = Path("feed.etag")
headers = {"User-Agent": "AniBT-RSS-example/1.0"}
if feed_file.exists() and etag_file.exists():
headers["If-None-Match"] = etag_file.read_text().strip()
try:
with urlopen(Request(url, headers=headers), timeout=30) as response:
body = response.read()
etag = response.headers.get("ETag", "")
status = response.status
except HTTPError as error:
if error.code == 304:
print("Unchanged; reuse feed.xml")
raise SystemExit(0)
raise SystemExit(f"HTTP {error.code}; Retry-After={error.headers.get('Retry-After', 'unknown')}")
if status != 200:
raise SystemExit(f"Unexpected HTTP {status}")
# Commit the body before its validator; never mark an undownloaded body current.
feed_file.with_suffix(".tmp").write_bytes(body)
feed_file.with_suffix(".tmp").replace(feed_file)
etag_file.write_text(etag)
print("Saved feed.xml")删除示例从环境变量读取 RELEASE_ID 和 ANIBT_API_KEY,最多轮询 180 秒。只把明确的 completed 当作完成,HTTP 错误、failed 或无效正文都会停止。
import json
import os
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen
base = os.environ.get("ANIBT_BASE_URL", "https://anibt.net").rstrip("/")
release_id = os.environ["RELEASE_ID"]
key = os.environ["ANIBT_API_KEY"]
deadline = time.monotonic() + 180
def call(method, path):
request = Request(base + path, method=method, headers={"Authorization": f"Bearer {key}"})
try:
response = urlopen(request, timeout=20)
except HTTPError as error:
response = error
with response:
raw = response.read()
status = response.status
retry_after = response.headers.get("Retry-After")
request_id = response.headers.get("X-Request-ID", "unknown")
if status == 429:
wait = int(retry_after) if retry_after and retry_after.isdigit() else 5
return None, max(1, wait)
try:
data = json.loads(raw)
except ValueError:
raise SystemExit(f"Non-JSON response: HTTP {status}, requestId={request_id}")
if status not in (200, 202) or data.get("ok") is not True:
code = data.get("error", {}).get("code", "unknown")
raise SystemExit(f"HTTP {status}: {code}, requestId={request_id}")
result = data.get("result")
if not isinstance(result, dict) or result.get("state") not in ("pending", "completed", "failed"):
raise SystemExit("Invalid deletion receipt")
wait = int(retry_after) if retry_after and retry_after.isdigit() else 2
return result, max(1, wait)
receipt, wait = call("DELETE", f"/api/releases/{release_id}")
if receipt is None:
raise SystemExit(f"DELETE rate limited; retry after {wait} seconds")
while True:
if receipt is not None and receipt["state"] == "completed":
print("Deletion completed")
break
if receipt is not None and receipt["state"] == "failed":
raise SystemExit(f"Deletion failed: {receipt.get('failure', {}).get('code', 'unknown')}")
remaining = deadline - time.monotonic()
if remaining <= wait:
raise SystemExit("Polling timed out; keep the releaseId and check its status later")
time.sleep(wait)
receipt, wait = call("GET", f"/api/releases/{release_id}/deletion")