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
global.js
downloads
node_modules
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("remoteServerProtocol", api_cfg.RemoteServerProtocol)
interceptor_srv.Interceptor.AddVariable("remoteServerHostname", api_cfg.RemoteServerHostname)
interceptor_srv.Interceptor.AddVariable("remoteServerPort", api_cfg.RemoteServerPort)
} else {
interceptor_srv.Interceptor.AddPostPlugin(&proxy.Plugin{
Match: "api.weixin.qq.com",
@@ -145,9 +148,10 @@ func root_command(cfg *config.Config) {
} else {
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 {
color.Red(fmt.Sprintf("启动API服务失败,%s 被占用\n\n", api_cfg.Addr))
color.Red(fmt.Sprintf("启动API服务失败,%s 被占用\n\n", api_addr))
os.Exit(0)
return
}
+6 -4
View File
@@ -80,9 +80,10 @@ func serve_command() {
if mp_token_filepath != "" && err == nil {
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 {
color.Red(fmt.Sprintf("启动API服务失败,%s 被占用\n\n", api_cfg.Addr))
color.Red(fmt.Sprintf("启动API服务失败,%s 被占用\n\n", addr))
os.Exit(0)
return
}
@@ -189,13 +190,14 @@ var mp_status_cmd = &cobra.Command{
_ = remove_mp_pidfile()
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 {
PID int `json:"pid"`
Addr string `json:"addr"`
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)
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
official_cfg := officialaccount.NewOfficialAccountConfig(cfg.Original, cfg.RemoteServerMode)
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) {
// Initial tasks
tasks := downloader.GetTasks()
+6 -3
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/adrg/xdg"
@@ -19,7 +18,9 @@ type APIConfig struct {
DownloadDir string
PlayDoneAudio bool
MaxRunning int // 最多同时下载的任务数
Addr string
Protocol string
Hostname string
Port int
RemoteServerEnabled bool
RemoteServerProtocol string
RemoteServerHostname string
@@ -48,7 +49,9 @@ func NewAPIConfig(c *config.Config, remote_mode bool) *APIConfig {
DownloadDir: dir,
PlayDoneAudio: viper.GetBool("download.playDoneAudio"),
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"),
RemoteServerProtocol: viper.GetString("download.remoteServer.protocol"),
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 {
m := obj.ObjectDesc.Media[0]
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
}
@@ -808,6 +812,49 @@ func (c *APIClient) handleHighlightFileInFolder(ctx *gin.Context) {
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) {
dir := c.cfg.DownloadDir
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.GET("/play", c.handlePlay)
c.engine.GET("/video", c.handleStreamVideo)
// 公众号接口 远端和本地都有的接口
c.engine.GET("/api/mp/list", c.official.HandleFetchList)
c.engine.GET("/api/mp/msg/list", c.official.HandleFetchMsgList)
+2 -1
View File
@@ -3,6 +3,7 @@ package api
import (
"fmt"
"net"
"strconv"
"github.com/rs/zerolog"
@@ -15,7 +16,7 @@ type APIServer struct {
}
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)
srv.SetHandler(withCORS(client))
return &APIServer{
+1 -2
View File
@@ -44,9 +44,8 @@ type ChannelsClient struct {
OnConnected func(client *Client)
}
func NewChannelsClient(addr string) *ChannelsClient {
func NewChannelsClient() *ChannelsClient {
return &ChannelsClient{
// ServerAddr: addr,
ws_clients: make(map[*Client]bool),
requests: make(map[string]chan ClientWebsocketResponse),
// 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 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 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 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>`;
@@ -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;";
if (isCompleted) {
const isOpenExternal = WXU.config.remoteServerEnabled;
const openIcon = isOpenExternal ? ExternalLinkIcon : FolderIcon;
const openTitle = isOpenExternal ? "打开外部链接" : "打开文件夹";
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}">
${FolderIcon}
<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}">
${openIcon}
</a>
<a href="javascript:" class="wx-download-item-delete" aria-label="删除" title="删除" data-id="${t.id}" data-action="delete" style="${btnStyle}">
${DeleteIcon}
+21 -8
View File
@@ -117,15 +117,28 @@ var isWin = /Windows|Win/i.test(ua);
});
return;
}
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,
if (WXU.config.remoteServerEnabled) {
var u =
WXU.config.remoteServerProtocol +
"://" +
WXU.config.remoteServerHostname;
if (WXU.config.remoteServerPort !== 80) {
u += ":" + WXU.config.remoteServerPort;
}
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;
}