dm

package module
v0.0.0-...-fe29192 Latest Latest
Warning

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

Go to latest
Published: Feb 19, 2026 License: MIT Imports: 17 Imported by: 0

README

bilibili_dm_lib

Go library for subscribing to and sending Bilibili live room danmaku (弹幕) via WebSocket and HTTP API.

Features

  • Pub/Sub API — typed callbacks (OnDanmaku, OnGift, etc.) and channel-based subscription
  • Send danmaku — send messages via Client.SendDanmaku or standalone Sender
  • Auto-split — long messages split into chunks with rate limiting
  • Multiple rooms — subscribe to many rooms with a single client
  • Auto-reconnect — exponential backoff on disconnect
  • Brotli + Zlib — handles all Bilibili compression formats
  • Thread-safe — register handlers and send from any goroutine
  • Cookie support — optional authenticated access for richer data
  • Clean shutdown — context cancellation propagates everywhere

Install

go get github.com/MatchaCake/bilibili_dm_lib

Quick Start

Callback-based
package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"

    dm "github.com/MatchaCake/bilibili_dm_lib"
)

func main() {
    client := dm.NewClient(
        dm.WithRoomID(510), // short room IDs are resolved automatically
    )

    client.OnDanmaku(func(d *dm.Danmaku) {
        fmt.Printf("[弹幕] %s: %s\n", d.Sender, d.Content)
    })

    client.OnGift(func(g *dm.Gift) {
        fmt.Printf("[礼物] %s %s %s x%d\n", g.User, g.Action, g.GiftName, g.Num)
    })

    client.OnSuperChat(func(sc *dm.SuperChat) {
        fmt.Printf("[SC ¥%d] %s: %s\n", sc.Price, sc.User, sc.Message)
    })

    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
    defer stop()

    client.Start(ctx)
}
Channel-based
client := dm.NewClient(dm.WithRoomID(21452505))
events := client.Subscribe()

go client.Start(ctx)

for ev := range events {
    switch d := ev.Data.(type) {
    case *dm.Danmaku:
        fmt.Printf("%s: %s\n", d.Sender, d.Content)
    case *dm.Gift:
        fmt.Printf("Gift: %s x%d from %s\n", d.GiftName, d.Num, d.User)
    }
}
Multiple Rooms
client := dm.NewClient(
    dm.WithRoomID(510),
    dm.WithRoomID(21452505),
)

// Or add rooms dynamically after Start:
client.AddRoom(12345)
client.RemoveRoom(510)
Authenticated (with cookies)

Providing cookies enables richer danmaku data (full medal info, etc.):

client := dm.NewClient(
    dm.WithRoomID(510),
    dm.WithCookie("your_SESSDATA", "your_bili_jct"),
)
Sending Danmaku
Via Client
client := dm.NewClient(
    dm.WithRoomID(510),
    dm.WithCookie("your_SESSDATA", "your_bili_jct"),
)

// Send after the client is started.
go func() {
    ctx := context.Background()
    err := client.SendDanmaku(ctx, 510, "Hello from Go!")
    if err != nil {
        log.Println("send failed:", err)
    }
}()

client.Start(ctx)
Standalone Sender

Use NewSender when you only need to send without subscribing:

sender := dm.NewSender(
    dm.WithSenderCookie("your_SESSDATA", "your_bili_jct"),
    dm.WithMaxLength(30), // UL20+ users can send up to 30 chars
    dm.WithCooldown(3 * time.Second),
)

ctx := context.Background()

// Send with default scroll mode.
err := sender.Send(ctx, 510, "Hello!")

// Send with specific display mode.
err = sender.SendWithMode(ctx, 510, "Pinned!", dm.ModeTop)

Long messages are automatically split into chunks and sent with rate-limiting pauses between each chunk.

Event Types

CMD Callback Struct Description
DANMU_MSG OnDanmaku Danmaku Chat messages
SEND_GIFT OnGift Gift Gift events
SUPER_CHAT_MESSAGE OnSuperChat SuperChat Super Chat messages
GUARD_BUY OnGuardBuy GuardBuy Captain/Admiral/Governor purchases
LIVE OnLive LiveEvent Room goes live
PREPARING OnPreparing LiveEvent Room goes offline
INTERACT_WORD OnInteractWord InteractWord Entry, follow, share
(any) OnRawEvent []byte Catch-all for unrecognised commands

Running the Example

go run ./cmd/example -room 510

With cookies:

go run ./cmd/example -room 510 -sessdata YOUR_SESSDATA -bili-jct YOUR_BILI_JCT

Architecture

Client (pub/sub hub + sender)
├── roomConn (room 510)     ← goroutine: connect → auth → read loop
│   ├── heartbeat goroutine ← sends heartbeat every 30s
│   └── auto-reconnect      ← exponential backoff on failure
├── roomConn (room 21452505)
│   └── ...
├── dispatch
│   ├── typed callbacks (OnDanmaku, OnGift, ...)
│   ├── raw event callback (OnRawEvent)
│   └── channel subscribers (Subscribe)
└── Sender (lazy init)      ← SendDanmaku → HTTP POST /msg/send
    ├── auto-split long messages
    └── per-room rate limiting

Protocol

This library implements the Bilibili live WebSocket danmaku protocol:

  1. Resolve short room ID → real room ID via HTTP API
  2. Fetch WSS server host + auth token via HTTP API
  3. Connect to wss://{host}:{port}/sub
  4. Send auth packet (16-byte header + JSON body, protover=3)
  5. Send heartbeat every 30 seconds
  6. Receive command packets (raw JSON, Brotli, or Zlib compressed)

Packets use a 16-byte big-endian header:

  • [0:4] Total size, [4:6] Header size (16), [6:8] Protocol version, [8:12] Operation type, [12:16] Sequence

License

MIT

Documentation

Overview

Example (Authenticated)
package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"

	dm "github.com/MatchaCake/bilibili_dm_lib"
)

func main() {
	// With cookies, you receive richer danmaku data (full medal info, etc.)
	client := dm.NewClient(
		dm.WithRoomID(510),
		dm.WithCookie("your_SESSDATA", "your_bili_jct"),
	)

	client.OnDanmaku(func(d *dm.Danmaku) {
		medal := ""
		if d.MedalName != "" {
			medal = fmt.Sprintf("[%s %d] ", d.MedalName, d.MedalLevel)
		}
		fmt.Printf("%s%s: %s\n", medal, d.Sender, d.Content)
	})

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	_ = client.Start(ctx)
}
Example (Callback)
package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"

	dm "github.com/MatchaCake/bilibili_dm_lib"
)

func main() {
	// Create a client for room 510 (short ID is resolved automatically).
	client := dm.NewClient(
		dm.WithRoomID(510),
	)

	// Register typed callbacks.
	client.OnDanmaku(func(d *dm.Danmaku) {
		fmt.Printf("[弹幕] %s: %s\n", d.Sender, d.Content)
	})

	client.OnGift(func(g *dm.Gift) {
		fmt.Printf("[礼物] %s %s %s x%d\n", g.User, g.Action, g.GiftName, g.Num)
	})

	client.OnSuperChat(func(sc *dm.SuperChat) {
		fmt.Printf("[SC ¥%d] %s: %s\n", sc.Price, sc.User, sc.Message)
	})

	// Start blocks until context is cancelled.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	if err := client.Start(ctx); err != nil && ctx.Err() == nil {
		fmt.Println("error:", err)
	}
}
Example (ChannelSubscribe)
package main

import (
	"context"
	"fmt"

	dm "github.com/MatchaCake/bilibili_dm_lib"
)

func main() {
	client := dm.NewClient(
		dm.WithRoomID(21452505),
	)

	// Channel-based subscription receives all events.
	events := client.Subscribe()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go func() {
		if err := client.Start(ctx); err != nil && ctx.Err() == nil {
			fmt.Println("error:", err)
		}
	}()

	// Process events from the channel.
	for ev := range events {
		switch d := ev.Data.(type) {
		case *dm.Danmaku:
			fmt.Printf("[%d] %s: %s\n", ev.RoomID, d.Sender, d.Content)
		case *dm.Gift:
			fmt.Printf("[%d] Gift: %s x%d from %s\n", ev.RoomID, d.GiftName, d.Num, d.User)
		}
	}
}
Example (MultiRoom)
package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"

	dm "github.com/MatchaCake/bilibili_dm_lib"
)

func main() {
	// Subscribe to multiple rooms at once.
	client := dm.NewClient(
		dm.WithRoomID(510),
		dm.WithRoomID(21452505),
	)

	client.OnDanmaku(func(d *dm.Danmaku) {
		fmt.Printf("%s: %s\n", d.Sender, d.Content)
	})

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	_ = client.Start(ctx)
}

Index

Examples

Constants

View Source
const (
	EventDanmaku   = "danmaku"
	EventGift      = "gift"
	EventSuperChat = "superchat"
	EventGuardBuy  = "guard"
	EventLive      = "live"
	EventPreparing = "preparing"
	EventInteract  = "interact"
	EventRaw       = "raw"
	EventHeartbeat = "heartbeat"
)

Event type constants.

View Source
const (
	ProtoCommand       uint16 = 0 // Raw JSON command
	ProtoSpecial       uint16 = 1 // Special (heartbeat, auth)
	ProtoCommandZlib   uint16 = 2 // Zlib-compressed commands
	ProtoCommandBrotli uint16 = 3 // Brotli-compressed commands
)

Packet protocol versions.

View Source
const (
	OpHeartbeat       uint32 = 2
	OpHeartbeatReply  uint32 = 3
	OpCommand         uint32 = 5
	OpCertificate     uint32 = 7
	OpCertificateResp uint32 = 8
)

Packet operation types.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

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

Client subscribes to danmaku streams from one or more Bilibili live rooms. It can also send danmaku via the built-in Sender (see SendDanmaku).

func NewClient

func NewClient(opts ...Option) *Client

NewClient creates a new danmaku client.

func (*Client) AddRoom

func (c *Client) AddRoom(roomID int64) error

AddRoom dynamically adds a room to the client. Safe to call after Start.

func (*Client) OnDanmaku

func (c *Client) OnDanmaku(fn func(*Danmaku))

OnDanmaku registers a callback for chat messages.

func (*Client) OnGift

func (c *Client) OnGift(fn func(*Gift))

OnGift registers a callback for gift events.

func (*Client) OnGuardBuy

func (c *Client) OnGuardBuy(fn func(*GuardBuy))

OnGuardBuy registers a callback for guard purchases.

func (*Client) OnHeartbeat

func (c *Client) OnHeartbeat(fn func(*HeartbeatData))

OnHeartbeat registers a callback for heartbeat reply (popularity) events.

func (*Client) OnInteractWord

func (c *Client) OnInteractWord(fn func(*InteractWord))

OnInteractWord registers a callback for user interactions (entry, follow, share).

func (*Client) OnLive

func (c *Client) OnLive(fn func(*LiveEvent))

OnLive registers a callback for when a room goes live.

func (*Client) OnPreparing

func (c *Client) OnPreparing(fn func(*LiveEvent))

OnPreparing registers a callback for when a room goes offline.

func (*Client) OnRawEvent

func (c *Client) OnRawEvent(fn func(cmd string, raw []byte))

OnRawEvent registers a catch-all callback for any command event. This receives events that are not parsed into typed structs.

func (*Client) OnSuperChat

func (c *Client) OnSuperChat(fn func(*SuperChat))

OnSuperChat registers a callback for Super Chat messages.

func (*Client) RemoveRoom

func (c *Client) RemoveRoom(roomID int64)

RemoveRoom disconnects from a room.

func (*Client) SendDanmaku

func (c *Client) SendDanmaku(ctx context.Context, roomID int64, msg string) error

SendDanmaku sends a danmaku message to the given room. It uses the Client's credentials (set via WithCookie) and sender settings (WithMaxDanmakuLength, WithSendCooldown). Long messages are auto-split.

func (*Client) Start

func (c *Client) Start(ctx context.Context) error

Start connects to all configured rooms and blocks until ctx is cancelled.

func (*Client) Subscribe

func (c *Client) Subscribe() <-chan Event

Subscribe returns a channel that receives all events. The channel is buffered (256). The caller should consume events promptly to avoid blocking. The channel is closed when the client stops.

type Danmaku

type Danmaku struct {
	Sender      string
	UID         int64
	Content     string
	Timestamp   time.Time
	MedalName   string
	MedalLevel  int
	EmoticonURL string
}

Danmaku represents a chat message.

type DanmakuMode

type DanmakuMode int

DanmakuMode controls how the danmaku is displayed in the live room.

const (
	ModeScroll DanmakuMode = 1 // scrolling (default)
	ModeBottom DanmakuMode = 4 // pinned at bottom
	ModeTop    DanmakuMode = 5 // pinned at top
)

type Event

type Event struct {
	RoomID int64
	Type   string
	Data   interface{}
}

Event is the unified envelope delivered to subscribers.

type Gift

type Gift struct {
	User     string
	UID      int64
	GiftName string
	GiftID   int64
	Num      int
	Price    int64 // in gold/silver coins
	CoinType string
	Action   string
}

Gift represents a gift event.

type GuardBuy

type GuardBuy struct {
	User       string
	UID        int64
	GuardLevel int // 1=总督, 2=提督, 3=舰长
	Price      int64
	Num        int
}

GuardBuy represents a captain/admiral/governor purchase.

type HeartbeatData

type HeartbeatData struct {
	Popularity uint32
}

HeartbeatData carries the popularity value from heartbeat responses.

type InteractWord

type InteractWord struct {
	User    string
	UID     int64
	MsgType int // 1=entry, 2=follow, 3=share
}

InteractWord represents user interactions (entry, follow, share).

type LiveEvent

type LiveEvent struct {
	RoomID int64
	Live   bool
}

LiveEvent represents a room going live or offline.

type Option

type Option func(*clientConfig)

Option configures a Client.

func WithCookie

func WithCookie(sessdata, biliJCT string) Option

WithCookie sets the SESSDATA and bili_jct cookies for authenticated access. Authenticated connections receive richer danmaku data (e.g., full medal info).

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient overrides the default HTTP client used for API calls.

func WithMaxDanmakuLength

func WithMaxDanmakuLength(n int) Option

WithMaxDanmakuLength sets the maximum rune length per danmaku message for the Client's built-in Sender. Default is 20; UL20+ users can set 30.

func WithRoomID

func WithRoomID(roomID int64) Option

WithRoomID adds a room to connect to on Start.

func WithSendCooldown

func WithSendCooldown(d time.Duration) Option

WithSendCooldown sets the minimum interval between sends to the same room for the Client's built-in Sender. Default is 5 seconds.

type Packet

type Packet struct {
	Protocol uint16
	OpType   uint32
	Sequence uint32
	Body     []byte
}

Packet represents a single Bilibili danmaku protocol packet.

type SendError

type SendError struct {
	Code    int
	Message string
}

SendError is returned when the Bilibili API responds with a non-zero code.

func (*SendError) Error

func (e *SendError) Error() string

type Sender

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

Sender sends danmaku messages to Bilibili live rooms. It is safe for concurrent use.

func NewSender

func NewSender(opts ...SenderOption) *Sender

NewSender creates a standalone Sender for sending danmaku without subscribing.

func (*Sender) Send

func (s *Sender) Send(ctx context.Context, roomID int64, msg string) error

Send sends a danmaku message to the given room using the default scroll mode. Long messages are automatically split into chunks of maxLength runes, with cooldown pauses between each chunk.

func (*Sender) SendWithMode

func (s *Sender) SendWithMode(ctx context.Context, roomID int64, msg string, mode DanmakuMode) error

SendWithMode sends a danmaku message with the specified display mode.

type SenderOption

type SenderOption func(*senderConfig)

SenderOption configures a Sender.

func WithCooldown

func WithCooldown(d time.Duration) SenderOption

WithCooldown sets the minimum interval between sends to the same room. Default is 5 seconds.

func WithMaxLength

func WithMaxLength(n int) SenderOption

WithMaxLength sets the maximum rune length per danmaku message. Messages exceeding this limit are auto-split into multiple sends. Default is 20. Users with UL20+ can set this to 30.

func WithSenderCookie

func WithSenderCookie(sessdata, biliJCT string) SenderOption

WithSenderCookie sets the SESSDATA and bili_jct cookies for sending. Both values are required — bili_jct is used as the CSRF token.

func WithSenderHTTPClient

func WithSenderHTTPClient(hc *http.Client) SenderOption

WithSenderHTTPClient overrides the default HTTP client used by the Sender.

type SuperChat

type SuperChat struct {
	User     string
	UID      int64
	Message  string
	Price    int64 // in CNY
	Duration int   // display duration in seconds
}

SuperChat represents a Super Chat message.

Directories

Path Synopsis
cmd
example command

Jump to

Keyboard shortcuts

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