本页内容
Golang 代码示例
Go 用标准库的 net/http 加 encoding/json 就能调完这套接口,不需要任何第三方 SDK。
本页给两段:先是一个能直接 go run 的最小例子,再是一份把响应结构体、错误类型和客户端都定义好的封装,后者可以整段复制进项目。
🚀 快速开始
用来验证 Key 与网络是否通,跑通了再往下抄封装那段。例子为了短把几处 err 用 _ 忽略了,搬进生产要逐个处理。真正要改的是 http.Client{}:零值没有超时,目标平台卡住时这个请求会一直挂着,务必设 Timeout(一次提取偶尔要十几秒,别设太短)。
defer resp.Body.Close() 也不能省,而且要把 body 读完,否则这条连接进不了复用池。非 200 的响应体统一是 {message, code?, retryable?}(只有 message 一定有,code 与 retryable 是提取失败的 400 才带的,401、402、422、500 只有 message),这段只原样打印,字段怎么落进结构体见下一节。
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
apiURL := "https://api.meowload.net/openapi/v1/extract/post"
apiKey := "<你的 API Key>"
// 构造请求
payload := map[string]string{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Accept-Language", "zh")
// 发送请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("❌ 网络错误: %v\n", err)
return
}
defer resp.Body.Close()
// 处理响应
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 200 {
fmt.Println("✅ 请求成功")
fmt.Println(string(body))
} else {
// 非 200 的响应体统一是 { message, code?, retryable? }
fmt.Printf("❌ 请求失败 (%d)\n", resp.StatusCode)
fmt.Println(string(body))
}
}📦 完整封装示例
长期使用的形态:ExtractResponse 描述成功响应,APIError 把 HTTP 状态码和错误体一起带出去,调用方用 errors.As 取出来看 Retryable 决定是否退避重试。可选字段都带 omitempty,用之前要判空——Title 与 Text 都可能是空串,有标题的平台两个都可能有、没标题的只给 Text,展示文案走下面的 Caption() 先取 Title 再回退 Text;Author 是指针,先判 nil 再取。
媒体在 Medias 里,同一媒体的多档清晰度在 Variants 里,IsDefault 为 true 的那档是推荐清晰度;VideoURL 与 AudioURL 同时非空说明是音视频分离的流,要各自下完再合并。Media.Headers 非空时,下载请求必须原样带上这些头。另外例子里解析错误体的 json.Unmarshal 忽略了返回值,万一响应体不是 JSON(例如中间代理返回的 HTML 错误页)会得到一个空 message,生产代码至少要把原始 body 留在日志里。与上一节的裸请求不同,NewClient 这里给 http.Client 设了 60 秒 Timeout——这个客户端活得比单次请求久得多,不设的话一次卡住的提取会永远占着一个 goroutine。
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
)
type MeowLoadClient struct {
APIKey string
BaseURL string
Client *http.Client
}
type ExtractRequest struct {
URL string `json:"url"`
}
// Variant 是同一媒体的一个清晰度档或音轨。
// VideoURL 与 AudioURL 同时非空表示音视频分离,需要各自下载后合并。
type Variant struct {
Quality int `json:"quality,omitempty"` // 高度像素,9999 表示原画
QualityLabel string `json:"quality_label,omitempty"`
VideoURL string `json:"video_url,omitempty"`
VideoExt string `json:"video_ext,omitempty"`
VideoFilesize int64 `json:"video_filesize,omitempty"`
AudioURL string `json:"audio_url,omitempty"`
AudioExt string `json:"audio_ext,omitempty"`
AudioFilesize int64 `json:"audio_filesize,omitempty"`
IsDefault bool `json:"is_default,omitempty"`
LanguageTag string `json:"language_tag,omitempty"`
LanguageName string `json:"language_name,omitempty"`
}
type Media struct {
MediaType string `json:"media_type"`
ResourceURL string `json:"resource_url"`
PreviewURL string `json:"preview_url,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Duration float64 `json:"duration,omitempty"`
Variants []Variant `json:"variants,omitempty"`
}
type Profile struct {
Username string `json:"username"`
DisplayName string `json:"display_name,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
}
type ExtractResponse struct {
Site string `json:"site,omitempty"`
Title string `json:"title,omitempty"` // 有标题的平台才有
Text string `json:"text,omitempty"` // 正文 / 文案
Medias []Media `json:"medias"`
ID string `json:"id,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
Author *Profile `json:"author,omitempty"`
PostURL string `json:"post_url,omitempty"`
}
// Caption 返回展示用文案:有标题取标题,否则取正文
func (r *ExtractResponse) Caption() string {
if r.Title != "" {
return r.Title
}
return r.Text
}
// ErrorResponse 是所有非 200 响应的统一形状。Code 与 Retryable 在 400 时一定有
type ErrorResponse struct {
Message string `json:"message"`
Code string `json:"code,omitempty"`
Retryable bool `json:"retryable,omitempty"`
}
// APIError 把 HTTP 状态码与错误体一起带出去,调用方据此决定要不要重试
type APIError struct {
StatusCode int
ErrorResponse
}
func (e *APIError) Error() string {
if e.Code != "" {
return fmt.Sprintf("请求失败 (%d): %s [code=%s retryable=%t]", e.StatusCode, e.Message, e.Code, e.Retryable)
}
return fmt.Sprintf("请求失败 (%d): %s", e.StatusCode, e.Message)
}
// NewClient 创建新的 API 客户端
func NewClient(apiKey string) *MeowLoadClient {
return &MeowLoadClient{
APIKey: apiKey,
BaseURL: "https://api.meowload.net/openapi/v1",
// 客户端活得比单次请求久得多,零值 http.Client 没有超时,卡住的提取会一直占着 goroutine
Client: &http.Client{Timeout: 60 * time.Second},
}
}
// ExtractPost 提取单个帖子
func (c *MeowLoadClient) ExtractPost(url string) (*ExtractResponse, error) {
endpoint := c.BaseURL + "/extract/post"
// 构造请求
reqBody := ExtractRequest{URL: url}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("JSON 序列化失败: %w", err)
}
// 创建 HTTP 请求
req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("创建请求失败: %w", err)
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.APIKey)
req.Header.Set("Accept-Language", "zh")
// 发送请求
resp, err := c.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %w", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
// 处理错误响应
if resp.StatusCode != 200 {
apiErr := &APIError{StatusCode: resp.StatusCode}
json.Unmarshal(body, &apiErr.ErrorResponse)
return nil, apiErr
}
// 解析成功响应
var result ExtractResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
return &result, nil
}
func main() {
// 创建客户端
client := NewClient("<你的 API Key>")
// 提取帖子
result, err := client.ExtractPost("https://www.bilibili.com/video/BV1sG4y1p7TA/")
if err != nil {
fmt.Printf("❌ 错误: %v\n", err)
var apiErr *APIError
if errors.As(err, &apiErr) && apiErr.Retryable {
fmt.Println(" 临时失败,可稍后重试")
}
return
}
fmt.Println("✅ 提取成功")
fmt.Printf("平台: %s\n", result.Site)
fmt.Printf("文案: %s\n", result.Caption())
if result.Author != nil {
fmt.Printf("作者: %s\n", result.Author.Username)
}
fmt.Printf("媒体数量: %d\n", len(result.Medias))
for idx, media := range result.Medias {
fmt.Printf("\n媒体 %d:\n", idx+1)
fmt.Printf(" 类型: %s\n", media.MediaType)
fmt.Printf(" 地址: %s\n", media.ResourceURL)
for _, v := range media.Variants {
split := ""
if v.VideoURL != "" && v.AudioURL != "" {
split = "(需合并)"
}
fmt.Printf(" - %s%s\n", v.QualityLabel, split)
}
}
}接下来这几页会用得上:
- 快速开始:接口全貌与第一个 curl 请求
- POST /extract/post 参考:结构体 tag 的权威来源,改字段前先对一遍
- 单帖提取接口:本页示例对应的接口
- 字幕提取接口:鉴权与
APIError这层可以照搬,响应是subtitles[]不是medias,结构体要另写 - 错误码:
APIError里那个Code的全部取值