feat: open the video in a new window when in remote server mode

This commit is contained in:
ltaoo committed 2026-01-18 22:26:59 +08:00
1 parent d0fa6a6b9c
commit 22d21f5a76
11 files changed
+99 -24

No files matched your search

+1
View File
@@ -11,6 +11,7 @@ wx_channel*
config.yaml config.yaml
global.js global.js
downloads
node_modules node_modules
docs/.vitepress/cache docs/.vitepress/cache
+6 -2
View File
@@ -117,6 +117,9 @@ func root_command(cfg *config.Config) {
}, },
}) })
interceptor_srv.Interceptor.AddVariable("remoteServerEnabled", api_cfg.RemoteServerEnabled) interceptor_srv.Interceptor.AddVariable("remoteServerEnabled", api_cfg.RemoteServerEnabled)
interceptor_srv.Interceptor.AddVariable("remoteServerProtocol", api_cfg.RemoteServerProtocol)
interceptor_srv.Interceptor.AddVariable("remoteServerHostname", api_cfg.RemoteServerHostname)
interceptor_srv.Interceptor.AddVariable("remoteServerPort", api_cfg.RemoteServerPort)
} else { } else {
interceptor_srv.Interceptor.AddPostPlugin(&proxy.Plugin{ interceptor_srv.Interceptor.AddPostPlugin(&proxy.Plugin{
Match: "api.weixin.qq.com", Match: "api.weixin.qq.com",
@@ -145,9 +148,10 @@ func root_command(cfg *config.Config) {
} else { } else {
fmt.Printf("下载目录 %s\n\n", color.New(color.Underline).Sprint(api_cfg.DownloadDir)) fmt.Printf("下载目录 %s\n\n", color.New(color.Underline).Sprint(api_cfg.DownloadDir))
} }
l, err := net.Listen("tcp", api_cfg.Addr) api_addr := fmt.Sprintf("%s:%d", api_cfg.Hostname, api_cfg.Port)
l, err := net.Listen("tcp", api_addr)
if err != nil { if err != nil {
color.Red(fmt.Sprintf("启动API服务失败,%s 被占用\n\n", api_cfg.Addr)) color.Red(fmt.Sprintf("启动API服务失败,%s 被占用\n\n", api_addr))
os.Exit(0) os.Exit(0)
return return
} }
+6 -4
View File
@@ -80,9 +80,10 @@ func serve_command() {
if mp_token_filepath != "" && err == nil { if mp_token_filepath != "" && err == nil {
fmt.Printf("公众号授权凭证文件 %s\n", color.New(color.Underline).Sprint(mp_token_filepath)) fmt.Printf("公众号授权凭证文件 %s\n", color.New(color.Underline).Sprint(mp_token_filepath))
} }
l, err := net.Listen("tcp", api_cfg.Addr) addr := fmt.Sprintf("%s:%d", api_cfg.Hostname, api_cfg.Port)
l, err := net.Listen("tcp", addr)
if err != nil { if err != nil {
color.Red(fmt.Sprintf("启动API服务失败,%s 被占用\n\n", api_cfg.Addr)) color.Red(fmt.Sprintf("启动API服务失败,%s 被占用\n\n", addr))
os.Exit(0) os.Exit(0)
return return
} }
@@ -189,13 +190,14 @@ var mp_status_cmd = &cobra.Command{
_ = remove_mp_pidfile() _ = remove_mp_pidfile()
return return
} }
ok := port_listening(api_cfg.Addr) addr := fmt.Sprintf("%s:%d", api_cfg.Hostname, api_cfg.Port)
ok := port_listening(addr)
type Status struct { type Status struct {
PID int `json:"pid"` PID int `json:"pid"`
Addr string `json:"addr"` Addr string `json:"addr"`
Listening bool `json:"listening"` Listening bool `json:"listening"`
} }
s := Status{PID: pid, Addr: api_cfg.Addr, Listening: ok} s := Status{PID: pid, Addr: addr, Listening: ok}
b, _ := json.Marshal(s) b, _ := json.Marshal(s)
fmt.Println(string(b)) fmt.Println(string(b))
}, },
+1 -1
View File
@@ -40,7 +40,7 @@ func NewAPIClient(cfg *APIConfig, parent_logger *zerolog.Logger) *APIClient {
var channels_client *channels.ChannelsClient var channels_client *channels.ChannelsClient
official_cfg := officialaccount.NewOfficialAccountConfig(cfg.Original, cfg.RemoteServerMode) official_cfg := officialaccount.NewOfficialAccountConfig(cfg.Original, cfg.RemoteServerMode)
officialaccount_client := officialaccount.NewOfficialAccountClient(official_cfg, parent_logger) officialaccount_client := officialaccount.NewOfficialAccountClient(official_cfg, parent_logger)
channels_client = channels.NewChannelsClient(cfg.Addr) channels_client = channels.NewChannelsClient()
channels_client.OnConnected = func(client *channels.Client) { channels_client.OnConnected = func(client *channels.Client) {
// Initial tasks // Initial tasks
tasks := downloader.GetTasks() tasks := downloader.GetTasks()
+6 -3
View File
@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/adrg/xdg" "github.com/adrg/xdg"
@@ -19,7 +18,9 @@ type APIConfig struct {
DownloadDir string DownloadDir string
PlayDoneAudio bool PlayDoneAudio bool
MaxRunning int // 最多同时下载的任务数 MaxRunning int // 最多同时下载的任务数
Addr string Protocol string
Hostname string
Port int
RemoteServerEnabled bool RemoteServerEnabled bool
RemoteServerProtocol string RemoteServerProtocol string
RemoteServerHostname string RemoteServerHostname string
@@ -48,7 +49,9 @@ func NewAPIConfig(c *config.Config, remote_mode bool) *APIConfig {
DownloadDir: dir, DownloadDir: dir,
PlayDoneAudio: viper.GetBool("download.playDoneAudio"), PlayDoneAudio: viper.GetBool("download.playDoneAudio"),
MaxRunning: 3, MaxRunning: 3,
Addr: viper.GetString("api.hostname") + ":" + strconv.Itoa(viper.GetInt("api.port")), Protocol: viper.GetString("api.protocol"),
Hostname: viper.GetString("api.hostname"),
Port: viper.GetInt("api.port"),
RemoteServerEnabled: viper.GetBool("download.remoteServer.enabled"), RemoteServerEnabled: viper.GetBool("download.remoteServer.enabled"),
RemoteServerProtocol: viper.GetString("download.remoteServer.protocol"), RemoteServerProtocol: viper.GetString("download.remoteServer.protocol"),
RemoteServerHostname: viper.GetString("download.remoteServer.hostname"), RemoteServerHostname: viper.GetString("download.remoteServer.hostname"),
+48 -1
View File
@@ -94,7 +94,11 @@ func (c *APIClient) handleFetchFeedListOfContactRSS(ctx *gin.Context) {
if len(obj.ObjectDesc.Media) > 0 { if len(obj.ObjectDesc.Media) > 0 {
m := obj.ObjectDesc.Media[0] m := obj.ObjectDesc.Media[0]
video_url := m.URL + m.URLToken video_url := m.URL + m.URLToken
mediaURL = "http://" + c.cfg.Addr + "/play?url=" + url.QueryEscape(video_url) + "&key=" + m.DecodeKey addr := c.cfg.Protocol + "://" + c.cfg.Hostname
if c.cfg.Port != 80 {
addr += ":" + strconv.Itoa(c.cfg.Port)
}
mediaURL = addr + "/play?url=" + url.QueryEscape(video_url) + "&key=" + m.DecodeKey
coverURL = m.CoverUrl coverURL = m.CoverUrl
} }
@@ -808,6 +812,49 @@ func (c *APIClient) handleHighlightFileInFolder(ctx *gin.Context) {
result.Ok(ctx, nil) result.Ok(ctx, nil)
} }
// 根据任务ID流式返回视频
func (c *APIClient) handleStreamVideo(ctx *gin.Context) {
task_id := ctx.Query("id")
if task_id == "" {
result.Err(ctx, 400, "missing task id")
return
}
task := c.downloader.GetTask(task_id)
if task == nil {
result.Err(ctx, 404, "task not found")
return
}
if task.Meta == nil || task.Meta.Req == nil {
result.Err(ctx, 400, "invalid task meta")
return
}
path := task.Meta.Opts.Path
name := task.Meta.Opts.Name
video_filepath := filepath.Join(path, name)
_, err := os.Stat(video_filepath)
if err != nil {
result.Err(ctx, 500, "找不到文件")
return
}
ctx.File(video_filepath)
}
func (c *APIClient) handleGetFileURL(ctx *gin.Context) {
id := ctx.Query("id")
if id == "" {
result.Err(ctx, 400, "missing id")
return
}
url := c.cfg.Protocol + "://" + c.cfg.Hostname
if c.cfg.Port != 80 {
url += ":" + strconv.Itoa(c.cfg.Port)
}
url += "/video?id=" + id
result.Ok(ctx, gin.H{
"url": url,
})
}
func (c *APIClient) handleTest(ctx *gin.Context) { func (c *APIClient) handleTest(ctx *gin.Context) {
dir := c.cfg.DownloadDir dir := c.cfg.DownloadDir
if err := system.Open(dir); err != nil { if err := system.Open(dir); err != nil {
+1
View File
@@ -36,6 +36,7 @@ func (c *APIClient) SetupRoutes() {
c.engine.POST("/api/task/clear", c.handleClearTasks) c.engine.POST("/api/task/clear", c.handleClearTasks)
// 文件操作 // 文件操作
c.engine.GET("/play", c.handlePlay) c.engine.GET("/play", c.handlePlay)
c.engine.GET("/video", c.handleStreamVideo)
// 公众号接口 远端和本地都有的接口 // 公众号接口 远端和本地都有的接口
c.engine.GET("/api/mp/list", c.official.HandleFetchList) c.engine.GET("/api/mp/list", c.official.HandleFetchList)
c.engine.GET("/api/mp/msg/list", c.official.HandleFetchMsgList) c.engine.GET("/api/mp/msg/list", c.official.HandleFetchMsgList)
+2 -1
View File
@@ -3,6 +3,7 @@ package api
import ( import (
"fmt" "fmt"
"net" "net"
"strconv"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@@ -15,7 +16,7 @@ type APIServer struct {
} }
func NewAPIServer(cfg *APIConfig, logger *zerolog.Logger) *APIServer { func NewAPIServer(cfg *APIConfig, logger *zerolog.Logger) *APIServer {
srv := manager.NewHTTPServer("API服务", "api", cfg.Addr) srv := manager.NewHTTPServer("API服务", "api", cfg.Hostname+":"+strconv.Itoa(cfg.Port))
client := NewAPIClient(cfg, logger) client := NewAPIClient(cfg, logger)
srv.SetHandler(withCORS(client)) srv.SetHandler(withCORS(client))
return &APIServer{ return &APIServer{
+1 -2
View File
@@ -44,9 +44,8 @@ type ChannelsClient struct {
OnConnected func(client *Client) OnConnected func(client *Client)
} }
func NewChannelsClient(addr string) *ChannelsClient { func NewChannelsClient() *ChannelsClient {
return &ChannelsClient{ return &ChannelsClient{
// ServerAddr: addr,
ws_clients: make(map[*Client]bool), ws_clients: make(map[*Client]bool),
requests: make(map[string]chan ClientWebsocketResponse), requests: make(map[string]chan ClientWebsocketResponse),
// engine: gin.Default(), // engine: gin.Default(),
@@ -184,6 +184,7 @@ var MP3Icon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColo
var MP4Icon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zM10 15V9l5 3-5 3z"/></svg>`; var MP4Icon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zM10 15V9l5 3-5 3z"/></svg>`;
var ImageIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>`; var ImageIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>`;
var FolderIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>`; var FolderIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>`;
var ExternalLinkIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M19 19H5V5h7V3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>`;
var PauseIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`; var PauseIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`;
var PlayIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`; var PlayIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`;
var DeleteIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>`; var DeleteIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>`;
@@ -359,9 +360,12 @@ function __wx_refresh_downloader(selector, tasks) {
"color: var(--weui-FG-0); opacity: 0.8; margin-left: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center;"; "color: var(--weui-FG-0); opacity: 0.8; margin-left: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center;";
if (isCompleted) { if (isCompleted) {
const isOpenExternal = WXU.config.remoteServerEnabled;
const openIcon = isOpenExternal ? ExternalLinkIcon : FolderIcon;
const openTitle = isOpenExternal ? "打开外部链接" : "打开文件夹";
actionButtons += ` actionButtons += `
<a href="javascript:" class="wx-download-item-open" aria-label="打开文件夹" title="打开文件夹" data-name="${t.name}" data-path="${t.path}" data-filepath="${t.filepath}" data-id="${t.id}" data-action="open" style="${btnStyle}"> <a href="javascript:" class="wx-download-item-open" aria-label="${openTitle}" title="${openTitle}" data-name="${t.name}" data-path="${t.path}" data-filepath="${t.filepath}" data-id="${t.id}" data-action="open" style="${btnStyle}">
${FolderIcon} ${openIcon}
</a> </a>
<a href="javascript:" class="wx-download-item-delete" aria-label="删除" title="删除" data-id="${t.id}" data-action="delete" style="${btnStyle}"> <a href="javascript:" class="wx-download-item-delete" aria-label="删除" title="删除" data-id="${t.id}" data-action="delete" style="${btnStyle}">
${DeleteIcon} ${DeleteIcon}
+21 -8
View File
@@ -117,15 +117,28 @@ var isWin = /Windows|Win/i.test(ua);
}); });
return; return;
} }
var [err, data] = await WXU.request({ if (WXU.config.remoteServerEnabled) {
method: "POST", var u =
url: "https://" + FakeAPIServerAddr + "/api/show_file", WXU.config.remoteServerProtocol +
body: { path, name, id }, "://" +
}); WXU.config.remoteServerHostname;
if (err) { if (WXU.config.remoteServerPort !== 80) {
WXU.error({ u += ":" + WXU.config.remoteServerPort;
msg: err.message, }
u += "/video?id=" + id;
window.open(u);
} else {
// Use original API for local file
var [err, data] = await WXU.request({
method: "POST",
url: "https://" + FakeAPIServerAddr + "/api/show_file",
body: { path, name, id },
}); });
if (err) {
WXU.error({
msg: err.message,
});
}
} }
return; return;
} }