collection

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 20 Imported by: 0

README

bangumi-collection-go

Bangumi 公共用户收藏的匿名、只读 Go 客户端。

发布状态:本仓库正在准备首个 v0.1.0 契约;当前变更没有创建 tag、release,也没有发布模块版本。公开版本发布后才应使用固定的 v0.1.0

范围

  • 只读取无需登录即可访问的公共收藏。
  • 不接受或发送 access token、Authorization、Cookie,也不提供私有收藏或写接口。
  • Fetch 自动获取所有计划内页面,去重后按 (SubjectType, SubjectID, Type) 排序。
  • FetchPage 只获取一页,并保留该页的上游顺序。
  • 同一个 Client 的所有 FetchFetchPage 和重试共享 QPS 与在途请求上限。

模块的 Go 语言与兼容性下限是 Go 1.26.0;本地开发和 v0.1.0 发布验收必须使用 Go 1.26.5,默认的 GOTOOLCHAIN=auto 会按 go.mod 中的 toolchain 声明选择该补丁版本。

公开版本发布后的安装命令将是:

go get github.com/AcuLY/bangumi-collection-go@v0.1.0

快速开始

package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	collection "github.com/AcuLY/bangumi-collection-go"
)

func main() {
	client := collection.NewClient("example-app/1.0 (contact: you@example.com)")
	subjects, err := client.Fetch(
		context.Background(),
		"lucay126",
		collection.SubjectTypeAnime,
		collection.CollectionTypeDoing,
		collection.CollectionTypeDone,
	)
	if err != nil {
		if errors.Is(err, collection.ErrRateLimited) {
			log.Fatal("Bangumi 请求频率受限")
		}
		log.Fatal(err)
	}

	for _, subject := range subjects {
		name := subject.NameCn
		if name == "" {
			name = subject.Name
		}
		fmt.Printf(
			"%d | %s | 收藏状态=%d | 评分=%d | 更新时间=%s\n",
			subject.SubjectID,
			name,
			subject.Type,
			subject.Rate,
			subject.UpdatedAt.Format("2006-01-02 15:04:05Z07:00"),
		)
	}
}

客户端配置

client := collection.NewClient(
	userAgent,
	collection.WithConcurrencyLimit(10),
	collection.WithRateLimit(3, 1),
	collection.WithRequestTimeout(30*time.Second),
	collection.WithMaxRetries(3),
	collection.WithRetryInterval(time.Second),
	collection.WithMaxRetryDelay(30*time.Second),
	collection.WithHTTPClient(httpClient),
)

配置项:

Option 默认值 说明
WithConcurrencyLimit(int) 10 每个 Client 共享的最大在途请求数
WithRateLimit(float64, int) 3 req/s, burst 1 每个 Client 共享的 token bucket
WithRequestTimeout(time.Duration) 30s 每次 HTTP attempt 的独立超时
WithMaxRetries(int) 3 首次 attempt 之后的最大重试次数
WithRetryInterval(time.Duration) 1s full-jitter 指数退避基数
WithMaxRetryDelay(time.Duration) 30s 本地退避及 Retry-After 的共同上限
WithHTTPClient(*http.Client) 安全默认值 浅复制客户端并清除 Jar/Client.Timeout、拒绝 redirect
WithEndpoint(string) https://api.bgm.tv HTTPS 根地址;HTTP 仅接受 loopback 测试地址

既有非认证 option 的无效值继续保留默认值。新的 endpoint、rate、max-retry-delay 配置或 nil Option 无效时,Client 会被固定标记为 ErrInvalidConfiguration,所有操作都在 transport 前失败。

API

获取完整收藏
subjects, err := client.Fetch(
	ctx,
	"user-id",
	collection.SubjectTypeAnime,
	collection.CollectionTypeDoing,
	collection.CollectionTypeDone,
)

Fetch 要求至少一个收藏类型。重复类型会被合并;首个 50 条页面决定固定 page plan;任何页面失败都不会返回 partial data。

获取单页
page, err := client.FetchPage(
	ctx,
	"user-id",
	collection.SubjectTypeAnime,
	collection.CollectionTypeDone,
	50,
	0,
)

limit 会被限制到 1..50,负 offset 会变为 0

完整 DTO

Subject 表示一条收藏记录:

字段 含义
ID 兼容别名,始终等于 SubjectID
SubjectID 条目 ID
SubjectType 条目类型
Type 收藏状态
Name, NameCn 原名与中文名;上游省略 subject 时为空
Rate 用户评分,0..10
Comment 用户评论;上游省略或明确为 null 时为空字符串
Tags 必填的用户收藏标签;空数组有效,返回值始终为非 nil slice
UpdatedAt RFC3339 更新时间
VolStatus, EpStatus 卷数与话数进度
Private 上游 private 标记

官方条目类型映射为:书籍 1、动画 2、音乐 3、游戏 4、三次元 6。未打 tag 的原型曾把 SubjectTypeGameSubjectTypeMusic 的名称写反;首个 v0.1.0 契约在发布前纠正为 SubjectTypeMusic=3SubjectTypeGame=4,有效原始数值集合不变。收藏类型值保持不变:想看 1、看过 2、在看 3、搁置 4、抛弃 5

官方响应中的 comment 与嵌套 subject 是可选字段。省略或明确为 nullcomment 会映射为空字符串,其他已出现的值必须是字符串。省略 subject 时,ID 仍等于顶层 SubjectIDNameNameCn 为空;若 subject 出现,则必须是完整、非 null 的合法值。tags 是必填字段,省略、null 或类型错误都会作为协议错误返回。

错误处理

使用 errors.Is 判断稳定分类,使用 errors.As 读取类型化元数据:

var httpErr *collection.HTTPError
if errors.As(err, &httpErr) {
	fmt.Println("HTTP status:", httpErr.StatusCode)
	if errors.Is(err, collection.ErrRateLimited) {
		fmt.Println("Retry-After:", httpErr.RetryAfter)
	}
}

switch {
case errors.Is(err, collection.ErrRateLimited):
	// 429
case errors.Is(err, collection.ErrTimeout):
	// parent deadline 或单次 attempt timeout
}

稳定分类包括输入/配置、401、403、404、429、5xx、一般 HTTP 状态、transport、timeout、cancellation、decode、protocol、响应过大和 retry exhaustion。返回错误不会包含 UID、URL/query、headers、response body 或原始 transport 文本。

兼容字段 HTTPError.Body 仍然存在,但返回值始终为空字符串。404 同时匹配 ErrNotFound 和已弃用的 ErrInvalidUserID

v0.1.0 兼容边界

与未打 tag 的旧代码相比:

  • 保留 NewClientFetchFetchPage、收藏类型数值和所有非认证 options。
  • 纠正 SubjectTypeMusic=3SubjectTypeGame=4;未打 tag 的原型把这两个公开名称写反,使用这两个命名常量的调用会获得纠正后的官方语义。
  • 移除 WithAccessToken 以及所有 Authorization/Cookie 行为。
  • Subject 增加完整收藏字段;ID 保留并等于 SubjectID
  • Fetch 不再按 goroutine 完成顺序返回,改为稳定 canonical order。
  • Fetch 的空收藏类型列表从空成功改为 ErrNoCollectionTypes
  • HTTPError.Body 不再保存或输出上游 body。
  • 新增 endpoint、共享 rate limit 与最大 retry delay options。

这些变更定义首个公开版本的安全边界,不表示该版本已发布。

Documentation

Overview

Package collection provides an anonymous, read-only client for Bangumi public user collections.

A Client is safe for concurrent use after construction. Fetch retrieves all pages and returns a canonical order; FetchPage preserves one upstream page's order. Requests never send Authorization or Cookie headers.

Bangumi collection records require tags but may omit comment and the nested subject projection. An omitted comment becomes the empty string; an omitted subject keeps ID equal to SubjectID and leaves Name and NameCn empty. Present optional fields must still contain a valid non-null value.

Subject types follow the official mapping: Book 1, Anime 2, Music 3, Game 4, and Real 6. Music and Game intentionally correct the reversed names in the untagged prototype before the first public version.

The first public contract retains NewClient, Fetch, FetchPage, collection enum values, and existing non-authentication options. It intentionally removes WithAccessToken, rejects an empty Fetch collection-type list, extends Subject to the complete collection DTO, and keeps HTTPError.Body empty. New options configure a test endpoint, a shared rate limit, and a maximum retry delay.

This repository is preparing the first v0.1.0 contract. The package has not been tagged or published by this change.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidUserID         = errors.New("invalid user id")
	ErrUnauthorized          = errors.New("unauthorized")
	ErrForbidden             = errors.New("forbidden")
	ErrRateLimited           = errors.New("rate limited")
	ErrServerError           = errors.New("server error")
	ErrEmptyUserID           = errors.New("user id cannot be empty")
	ErrNilContext            = errors.New("nil context")
	ErrNoCollectionTypes     = errors.New("no collection types")
	ErrInvalidSubjectType    = errors.New("invalid subject type")
	ErrInvalidCollectionType = errors.New("invalid collection type")
	ErrInvalidConfiguration  = errors.New("invalid client configuration")
	ErrNotFound              = errors.New("not found")
	ErrHTTPStatus            = errors.New("unexpected http status")
	ErrTransport             = errors.New("transport failure")
	ErrTimeout               = errors.New("request timeout")
	ErrCanceled              = errors.New("request canceled")
	ErrDecode                = errors.New("response decode failure")
	ErrProtocol              = errors.New("response protocol violation")
	ErrResponseTooLarge      = errors.New("response too large")
	ErrRetryExhausted        = errors.New("retry exhausted")
)

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client retrieves Bangumi public collection data.

Configuration is fixed when NewClient returns. The same Client shares its request-rate and in-flight limits across all Fetch and FetchPage calls.

func NewClient

func NewClient(userAgent string, options ...Option) *Client

NewClient creates an anonymous Bangumi public-collection client.

userAgent is required and must be valid UTF-8, contain no control rune, and be between 1 and 256 bytes. Since this constructor preserves its historical signature, invalid configuration is retained on the Client and returned as ErrInvalidConfiguration by every operation before transport.

func (*Client) Fetch

func (c *Client) Fetch(
	ctx context.Context,
	userID string,
	subjectType SubjectType,
	collectionTypes ...CollectionType,
) ([]*Subject, error)

Fetch retrieves all planned pages for the requested collection states.

Repeated states are normalized. Results are deduplicated and sorted by (SubjectType, SubjectID, Type), independent of completion order.

func (*Client) FetchPage

func (c *Client) FetchPage(
	ctx context.Context,
	userID string,
	subjectType SubjectType,
	collectionType CollectionType,
	limit int,
	offset int,
) (*PageResult, error)

FetchPage retrieves one validated page. limit is clamped to 1..50 and a negative offset is clamped to zero for compatibility.

type CollectionType

type CollectionType int

CollectionType is a user's collection state.

const (
	CollectionTypeWish    CollectionType = 1
	CollectionTypeDone    CollectionType = 2
	CollectionTypeDoing   CollectionType = 3
	CollectionTypeOnHold  CollectionType = 4
	CollectionTypeDropped CollectionType = 5
)

type DecodeError

type DecodeError struct {
	// contains filtered or unexported fields
}

DecodeError indicates that a bounded success body could not be read or was not exactly one JSON value. It intentionally exposes no upstream content.

func (*DecodeError) Error

func (e *DecodeError) Error() string

func (*DecodeError) Unwrap

func (e *DecodeError) Unwrap() error

type HTTPError

type HTTPError struct {
	StatusCode int
	Body       string
	RetryAfter time.Duration
}

HTTPError describes a non-200 response without retaining response content. Body remains for source compatibility and is always empty on returned errors.

func (*HTTPError) Error

func (e *HTTPError) Error() string

func (*HTTPError) Is

func (e *HTTPError) Is(target error) bool

type NetworkError

type NetworkError struct {
	Err     error
	Timeout bool
	// contains filtered or unexported fields
}

NetworkError describes a sanitized runtime request failure.

Returned Err values are restricted to context.Canceled, context.DeadlineExceeded, or ErrTransport.

func (*NetworkError) Error

func (e *NetworkError) Error() string

func (*NetworkError) Is

func (e *NetworkError) Is(target error) bool

func (*NetworkError) Unwrap

func (e *NetworkError) Unwrap() error

type Option

type Option func(*Client)

Option configures a Client before it becomes available to callers.

func WithConcurrencyLimit

func WithConcurrencyLimit(limit int) Option

WithConcurrencyLimit sets the maximum number of in-flight requests shared by all operations on one Client. Non-positive values preserve the default.

func WithEndpoint

func WithEndpoint(endpoint string) Option

WithEndpoint replaces the API root. It accepts an absolute HTTPS root, or an HTTP loopback root for local tests. Invalid values poison the Client with ErrInvalidConfiguration.

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

WithHTTPClient supplies a custom HTTP client.

The supplied value is shallow-cloned. The package-owned clone has no cookie jar, has no Client.Timeout, and refuses redirects at the first response. WithRequestTimeout remains authoritative for every attempt regardless of option order. A nil client preserves the default for compatibility.

func WithMaxRetries

func WithMaxRetries(maxRetries int) Option

WithMaxRetries sets retries after the initial attempt. A negative value preserves the default; zero disables retries.

func WithMaxRetryDelay

func WithMaxRetryDelay(delay time.Duration) Option

WithMaxRetryDelay caps every local and Retry-After-derived retry wait.

func WithRateLimit

func WithRateLimit(requestsPerSecond float64, burst int) Option

WithRateLimit sets the shared token-bucket rate and burst. Both values must be finite and positive.

func WithRequestTimeout

func WithRequestTimeout(timeout time.Duration) Option

WithRequestTimeout sets the timeout for each individual HTTP attempt. Non-positive values preserve the default.

func WithRetryInterval

func WithRetryInterval(interval time.Duration) Option

WithRetryInterval sets the base exponential-backoff interval. Non-positive values preserve the default.

type PageResult

type PageResult struct {
	Data   []*Subject
	Total  int
	Limit  int
	Offset int
}

PageResult is one validated upstream page. Data preserves upstream order.

type ProtocolError

type ProtocolError struct{}

ProtocolError indicates that decoded data violated the collection contract. It intentionally exposes no upstream values.

func (*ProtocolError) Error

func (e *ProtocolError) Error() string

func (*ProtocolError) Unwrap

func (e *ProtocolError) Unwrap() error

type RetryError

type RetryError struct {
	Attempts int
	Err      error
}

RetryError reports exhaustion while preserving the last sanitized error.

func (*RetryError) Error

func (e *RetryError) Error() string

func (*RetryError) Is

func (e *RetryError) Is(target error) bool

func (*RetryError) Unwrap

func (e *RetryError) Unwrap() error

type Subject

type Subject struct {
	ID          int            `json:"id"`
	SubjectID   int            `json:"subject_id"`
	SubjectType SubjectType    `json:"subject_type"`
	Type        CollectionType `json:"type"`
	Name        string         `json:"name"`
	NameCn      string         `json:"name_cn"`
	Rate        int            `json:"rate"`
	Comment     string         `json:"comment"`
	Tags        []string       `json:"tags"`
	UpdatedAt   time.Time      `json:"updated_at"`
	VolStatus   int            `json:"vol_status"`
	EpStatus    int            `json:"ep_status"`
	Private     bool           `json:"private"`
}

Subject is one complete public collection record.

ID is retained as a compatibility alias and always equals SubjectID.

type SubjectType

type SubjectType int

SubjectType is a Bangumi subject category.

const (
	SubjectTypeBook  SubjectType = 1
	SubjectTypeAnime SubjectType = 2
	SubjectTypeMusic SubjectType = 3
	SubjectTypeGame  SubjectType = 4
	SubjectTypeReal  SubjectType = 6
)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL