mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 08:58:03 +00:00
feat: merge web/webfetch/bili into single browser plugin (search/fetch/render/video)
This commit is contained in:
@ -1,234 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
sdk *sdk.PluginSDK
|
||||
}
|
||||
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
p.sdk = s
|
||||
tp := p.name + "_"
|
||||
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "plugin." + p.name + ".output_dir", Default: "/tmp/bili_videos",
|
||||
Type: "string", DisplayName: "下载目录",
|
||||
Description: "B站视频下载后的保存目录",
|
||||
Category: p.name,
|
||||
})
|
||||
|
||||
s.RegisterTool(tp+"video", sdk.ToolDef{
|
||||
Name: tp + "video",
|
||||
Description: "使用 yt-dlp 下载B站视频到本地。支持查看视频信息后再下载。下载后返回文件路径。",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"url": map[string]interface{}{"type": "string", "description": "B站视频分享链接"},
|
||||
"info_only": map[string]interface{}{"type": "boolean", "description": "仅获取视频信息(标题、清晰度列表),不下载"},
|
||||
"format": map[string]interface{}{"type": "string", "description": "视频格式ID(如 30112=高清1080P, 30080=高清1080P, 30064=高清720P, 30032=清晰480P, 30016=流畅360P),不指定则自动选最优"},
|
||||
},
|
||||
"required": []string{"url"},
|
||||
},
|
||||
}, p.handleBiliVideo)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error { return nil }
|
||||
|
||||
type ytdlpFormat struct {
|
||||
FormatID string `json:"format_id"`
|
||||
FormatNote string `json:"format_note"`
|
||||
Ext string `json:"ext"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
TBR float64 `json:"tbr"`
|
||||
Filesize int64 `json:"filesize"`
|
||||
FilesizeApprox int64 `json:"filesize_approx"`
|
||||
VCodec string `json:"vcodec"`
|
||||
ACodec string `json:"acodec"`
|
||||
FPS float64 `json:"fps"`
|
||||
}
|
||||
|
||||
type ytdlpInfo struct {
|
||||
Title string `json:"title"`
|
||||
Duration float64 `json:"duration"`
|
||||
WebpageURL string `json:"webpage_url"`
|
||||
Filename string `json:"_filename"`
|
||||
Formats []ytdlpFormat `json:"formats"`
|
||||
}
|
||||
|
||||
func (p *Plugin) handleBiliVideo(args map[string]interface{}) (interface{}, error) {
|
||||
url, _ := args["url"].(string)
|
||||
if url == "" {
|
||||
return nil, fmt.Errorf("url is required")
|
||||
}
|
||||
infoOnly, _ := args["info_only"].(bool)
|
||||
format, _ := args["format"].(string)
|
||||
|
||||
outputDir := "/tmp/bili_videos"
|
||||
if p.sdk != nil {
|
||||
if v, _ := p.sdk.Settings().Get("plugin." + p.name + ".output_dir"); v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
outputDir = s
|
||||
}
|
||||
}
|
||||
}
|
||||
os.MkdirAll(outputDir, 0755)
|
||||
|
||||
var out bytes.Buffer
|
||||
ytdlpArgs := []string{"--no-warnings", "--dump-json", url}
|
||||
cmd := exec.Command("yt-dlp", ytdlpArgs...)
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
cmd.Env = append(os.Environ(), "HTTP_PROXY=http://127.0.0.1:7890", "HTTPS_PROXY=http://127.0.0.1:7890")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("yt-dlp info: %w\n%s", err, strings.TrimSpace(out.String()))
|
||||
}
|
||||
|
||||
var info ytdlpInfo
|
||||
if err := json.Unmarshal(out.Bytes(), &info); err != nil {
|
||||
return nil, fmt.Errorf("parse yt-dlp output: %w", err)
|
||||
}
|
||||
|
||||
if infoOnly {
|
||||
var filtered []ytdlpFormat
|
||||
for _, f := range info.Formats {
|
||||
if f.VCodec != "none" || f.ACodec != "none" {
|
||||
filtered = append(filtered, f)
|
||||
}
|
||||
}
|
||||
info.Formats = filtered
|
||||
|
||||
lines := []string{fmt.Sprintf("标题: %s", info.Title)}
|
||||
if info.Duration > 0 {
|
||||
lines = append(lines, fmt.Sprintf("时长: %.0f 秒", info.Duration))
|
||||
}
|
||||
|
||||
type fmtLine struct {
|
||||
ID string
|
||||
Note string
|
||||
Res string
|
||||
Ext string
|
||||
Size string
|
||||
}
|
||||
var seen []string
|
||||
var display []fmtLine
|
||||
for _, f := range info.Formats {
|
||||
if f.FormatNote == "" {
|
||||
continue
|
||||
}
|
||||
key := f.FormatNote + f.Ext
|
||||
if contains(seen, key) {
|
||||
continue
|
||||
}
|
||||
seen = append(seen, key)
|
||||
res := ""
|
||||
if f.Width > 0 && f.Height > 0 {
|
||||
res = fmt.Sprintf("%dx%d", f.Width, f.Height)
|
||||
}
|
||||
sz := ""
|
||||
fs := f.Filesize
|
||||
if fs == 0 {
|
||||
fs = f.FilesizeApprox
|
||||
}
|
||||
if fs > 0 {
|
||||
sz = fmt.Sprintf(" (%.1f MB)", float64(fs)/1048576)
|
||||
}
|
||||
display = append(display, fmtLine{ID: f.FormatID, Note: f.FormatNote, Res: res, Ext: f.Ext, Size: sz})
|
||||
}
|
||||
if len(display) > 0 {
|
||||
lines = append(lines, "清晰度列表:")
|
||||
for _, d := range display {
|
||||
r := d.Res
|
||||
if r != "" {
|
||||
r = " " + r
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf(" [%s] %s%s | %s%s", d.ID, d.Note, r, d.Ext, d.Size))
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{"content": strings.Join(lines, "\n")}, nil
|
||||
}
|
||||
|
||||
dlArgs := []string{
|
||||
"--no-warnings",
|
||||
"--socket-timeout", "30",
|
||||
"--retries", "3",
|
||||
"--fragment-retries", "3",
|
||||
"-o", filepath.Join(outputDir, "%(title)s.%(ext)s"),
|
||||
"--no-overwrites",
|
||||
}
|
||||
if format != "" {
|
||||
dlArgs = append(dlArgs, "-f", format)
|
||||
}
|
||||
dlArgs = append(dlArgs, url)
|
||||
cmd2 := exec.Command("yt-dlp", dlArgs...)
|
||||
cmd2.Env = append(os.Environ(), "HTTP_PROXY=http://127.0.0.1:7890", "HTTPS_PROXY=http://127.0.0.1:7890")
|
||||
var dlOut bytes.Buffer
|
||||
cmd2.Stdout = &dlOut
|
||||
cmd2.Stderr = &dlOut
|
||||
if err := cmd2.Run(); err != nil {
|
||||
return nil, fmt.Errorf("yt-dlp download: %w\n%s", err, strings.TrimSpace(dlOut.String()))
|
||||
}
|
||||
|
||||
entries, _ := os.ReadDir(outputDir)
|
||||
var newest string
|
||||
var newestTime int64
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
fi, _ := e.Info()
|
||||
if fi == nil {
|
||||
continue
|
||||
}
|
||||
t := fi.ModTime().Unix()
|
||||
if t > newestTime {
|
||||
newestTime = t
|
||||
newest = e.Name()
|
||||
}
|
||||
}
|
||||
if newest == "" {
|
||||
return map[string]interface{}{
|
||||
"content": "下载完成,但未找到视频文件",
|
||||
}, nil
|
||||
}
|
||||
dlPath := filepath.Join(outputDir, newest)
|
||||
fi, _ := os.Stat(dlPath)
|
||||
var fileSize int64
|
||||
if fi != nil {
|
||||
fileSize = fi.Size()
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"content": fmt.Sprintf("下载完成: %s (%.1f MB)\n路径: %s", newest, float64(fileSize)/1048576, dlPath),
|
||||
"file": dlPath,
|
||||
"filename": newest,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func contains(slice []string, s string) bool {
|
||||
for _, v := range slice {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return &Plugin{name: name}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user