feat: support download article from Wechat OfficialAccount

This commit is contained in:
ltaoo committed 2026-02-10 17:25:20 +08:00
1 parent e8c8efc8b0
commit 956980102b
17 files changed
+1698 -112

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
package main
import (
"fmt"
"os"
officialaccountdownload "github.com/GopeedLab/gopeed/pkg/officialaccount"
)
func main() {
client := &officialaccountdownload.OfficialAccountDownload{}
// url := "https://mp.weixin.qq.com/s/dlWe4sStQMKhKXphf8kC2Q"
// url := "https://mp.weixin.qq.com/s/tEeZD35wa92VzEQmN6zXDg"
url := "https://mp.weixin.qq.com/s/906EyV6zAvdCPTUFn2oehw"
article, err := client.FetchArticle(url)
if err != nil {
fmt.Println(err)
return
}
content, err := client.BuildHTMLFromArticle(article, false)
if err != nil {
fmt.Println(err)
return
}
os.WriteFile("article.html", []byte(content), 0644)
}
+12 -2
View File
@@ -21,7 +21,8 @@ require (
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.21.0
github.com/zeebo/blake3 v0.2.4
golang.org/x/text v0.32.0
golang.org/x/net v0.48.0
golang.org/x/text v0.34.0
)
replace github.com/GopeedLab/gopeed => ./pkg/gopeed
@@ -30,11 +31,18 @@ require (
atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
github.com/JohannesKaufmann/dom v0.2.0 // indirect
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0 // indirect
github.com/PuerkitoBio/goquery v1.11.0 // indirect
github.com/Trisia/gosysproxy v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/chai2010/webp v1.4.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/console v1.0.5 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 // indirect
github.com/ebitengine/purego v0.9.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
@@ -43,6 +51,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
@@ -50,6 +59,7 @@ require (
github.com/google/go-github/v30 v30.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -97,7 +107,7 @@ require (
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/image v0.36.0 // indirect
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
+65 -4
View File
@@ -6,6 +6,10 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0 h1:mklaPbT4f/EiDr1Q+zPrEt9lgKAkVrIBtWf33d9GpVA=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0/go.mod h1:D56Cl9r8M5i3UwAchE+LlLc5hPN3kJtdZNVJn06lSHU=
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
@@ -15,12 +19,18 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/Trisia/gosysproxy v1.1.0 h1:rBU1mazMXLsZiiaAHXtRoDPt2gzWdA6uxjaLskzaoAA=
github.com/Trisia/gosysproxy v1.1.0/go.mod h1:PTPPgpRpyTJEL/FMxEE2OcQcGNbUm961xZcgihwraZM=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
@@ -28,6 +38,8 @@ github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQ
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
@@ -38,6 +50,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 h1:bVp3yUzvSAJzu9GqID+Z96P+eu5TKnIMJSV4QaZMauM=
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/ebitengine/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=
github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
@@ -65,6 +81,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
@@ -78,6 +96,7 @@ github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
@@ -88,6 +107,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
@@ -180,8 +201,11 @@ github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDc
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
@@ -242,6 +266,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
@@ -262,14 +288,23 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -278,6 +313,11 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -287,6 +327,10 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -306,15 +350,25 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -323,8 +377,12 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -332,6 +390,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
@@ -351,8 +411,9 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+34 -1
View File
@@ -211,7 +211,7 @@ type FeedDownloadTaskBody struct {
Suffix string `json:"suffix"`
}
func (c *APIClient) handleCreateTask(ctx *gin.Context) {
func (c *APIClient) handleCreateFeedDownloadTask(ctx *gin.Context) {
var body FeedDownloadTaskBody
if err := ctx.ShouldBindJSON(&body); err != nil {
result.Err(ctx, 400, "不合法的参数")
@@ -276,6 +276,39 @@ func (c *APIClient) handleCreateTask(ctx *gin.Context) {
result.Ok(ctx, gin.H{"id": id})
}
type DownloadTaskPayload struct {
URL string
Filename string
Dir string
Extra map[string]string
}
func (c *APIClient) handleCreateDownloadTask(ctx *gin.Context) {
var body DownloadTaskPayload
if err := ctx.ShouldBindJSON(&body); err != nil {
result.Err(ctx, 400, "不合法的参数")
return
}
id, err := c.downloader.CreateDirect(
&base.Request{
URL: body.URL,
Labels: body.Extra,
},
&base.Options{
Name: body.Filename,
Path: filepath.Join(c.cfg.DownloadDir, body.Dir),
Extra: &gopeedhttp.OptsExtra{
Connections: 1,
},
},
)
if err != nil {
result.Err(ctx, 500, "创建任务失败:"+err.Error())
return
}
result.Ok(ctx, gin.H{"id": id})
}
type DownloadTaskSummary struct {
ID string `json:"id"`
Title string `json:"title"`
+2 -1
View File
@@ -28,10 +28,11 @@ func (c *APIClient) SetupRoutes() {
c.engine.GET("/ws/channels", c.channels.HandleChannelsWebsocket)
c.engine.GET("/api/task/list", c.handleFetchTaskList)
c.engine.GET("/api/task/profile", c.handleFetchTaskProfile)
c.engine.POST("/api/task/create", c.handleCreateTask)
c.engine.POST("/api/task/create", c.handleCreateFeedDownloadTask)
c.engine.POST("/api/task/create_batch", c.handleBatchCreateTask)
c.engine.POST("/api/task/create_channels", c.handleCreateChannelsTask)
// c.engine.POST("/api/task/create_live", c.handleCreateLiveTask)
c.engine.POST("/api/task/create2", c.handleCreateDownloadTask)
c.engine.POST("/api/task/start", c.handleStartTask)
c.engine.POST("/api/task/pause", c.handlePauseTask)
c.engine.POST("/api/task/resume", c.handleResumeTask)
@@ -42,6 +42,10 @@ export default [
download_icon2: "writable",
download_icon3: "writable",
download_icon4: "writable",
DownloadIcon5: "writable",
DownloadIcon6: "writable",
DownloadIcon7: "writable",
DownloadIcon8: "writable",
RSSIcon: "writeable",
download_btn1: "writable",
download_btn2: "writable",
@@ -178,6 +178,10 @@ var download_icon1 = `<svg data-v-132dee25 class="svg-icon icon" viewBox="0 0 10
var download_icon2 = `<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1567" width="1em" height="1em"><path d="M510.507 161c136.08 0 254.917 86.968 298.77 212.61l0.947 2.772 0.209 0.056c85.945 23.138 148.202 101.3 149.545 193.033L960 572.5c0 112.114-90.482 203-202.098 203-9.832 0-23.115-1.567-39.848-4.7-8.686-1.626-14.409-9.985-12.782-18.67a16 16 0 0 1 0.316-1.361l10.739-38.438c2.221-7.951 10.128-12.907 18.253-11.44 9.632 1.74 17.406 2.609 23.322 2.609 72.028 0 130.418-58.65 130.418-131 0-64.593-46.855-119.203-109.508-129.32l-1.904-0.293-23.48-3.436-6.02-23.055C719.407 309.16 622.489 233 510.507 233c-106.271 0-199.349 68.62-232.37 168.113l-0.982 3.024-6.578 20.706-21.305 3.796C183.982 440.275 135.68 497.59 135.68 565c0 76.492 61.733 138.5 137.884 138.5 4.118 0 9.58-0.435 16.386-1.305 8.178-1.045 15.812 4.296 17.633 12.337l8.838 39.024c1.952 8.618-3.452 17.187-12.07 19.139a16 16 0 0 1-1.327 0.242c-12.264 1.709-22.084 2.563-29.46 2.563C157.824 775.5 64 681.256 64 565c0-94.273 62.11-175.514 149.425-201.7l2.4-0.701 1.368-3.441c47.328-116.945 160.436-196.452 289.156-198.13l4.158-0.028z" fill="currentColor" p-id="1568"></path><path d="M505.744 860.925c3.186 4.1 9.358 4.1 12.544 0l111.502-141.8c4.082-5.2 0.399-12.9-6.272-12.9h-73.77l-1.909 0.003L547.84 464a8 8 0 0 0-8-8h-55.68a8 8 0 0 0-8 8v242.322l-2.075 0.003h-73.571c-6.67 0-10.354 7.7-6.272 12.9z" fill="currentColor" p-id="1569"></path></svg>`;
var download_icon3 = `<svg data-v-132dee25 class="svg-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="currentColor" width="28" height="28"><path d="M213.333333 853.333333h597.333334v-85.333333H213.333333m597.333334-384h-170.666667V128H384v256H213.333333l298.666667 298.666667 298.666667-298.666667z"></path></svg>`;
var download_icon4 = `<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M512 706.608L781.968 436.64a32 32 0 1 0-45.248-45.256L544 584.096V192a32 32 0 0 0-64 0v392.096l-192.712-192.72a32 32 0 0 0-45.256 45.256L512 706.608z" fill="currentColor"></path><path d="M824 640a32 32 0 0 0-32 32v128.36c0 3.112 0 8.496-0.48 11.472l-1.008 1.024c-0.952 0.984-2.104 2.168-3.112 3.152h-538.48c-2.448-0.664-7.808-3.56-10.608-6.36-2.776-2.784-5.656-8.128-6.32-10.568V672a32 32 0 0 0-64 0v128c0 20.632 12.608 42.456 25.088 54.912C205.584 867.4 227.408 880 248 880h544c22.496 0 36.208-14.112 44.408-22.536l2.48-2.528c17.128-17.088 17.12-41.472 17.12-54.928V672A32.016 32.016 0 0 0 824 640z" fill="currentColor"></path></svg>`;
var DownloadIcon5 = `<svg class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em"><path d="M472.483146 751.161182c0.122866 0.124914 0.24778 0.248804 0.372694 0.370646 11.174684 10.873662 25.852081 16.831651 41.412066 16.831651 0.277473 0 0.554946-0.001024 0.832419-0.005119 15.837458-0.217064 30.643864-6.574368 41.702849-17.905707L891.545075 410.683475c9.525205-9.667525 9.408482-25.226487-0.259043-34.750668-9.666501-9.524181-25.227511-9.408482-34.750668 0.259043l-315.80719 320.551874L540.728174 87.434687c0-13.5716-11.001648-24.573248-24.573248-24.573248s-24.573248 11.001648-24.573248 24.573248l0 613.073864L170.858816 374.767626c-9.52111-9.670597-25.080071-9.791415-34.750668-0.269282-9.670597 9.52111-9.791415 25.080071-0.270306 34.750668L472.483146 751.161182z" fill="currentColor" p-id="1583"></path><path d="M879.012719 846.929272 149.753468 846.929272c-13.5716 0-24.573248 11.001648-24.573248 24.573248s11.001648 24.573248 24.573248 24.573248l729.260275 0c13.5716 0 24.573248-11.001648 24.573248-24.573248S892.584319 846.929272 879.012719 846.929272z" fill="currentColor" p-id="1584"></path></svg>`;
var DownloadIcon6 = `<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em"><path d="M495.5 679.4c4.7 4.7 10.9 7.2 17.5 7.2s12.8-2.6 17.5-7.2L733.9 476c4.7-4.7 7.2-10.9 7.2-17.5s-2.6-12.8-7.2-17.5c-9.3-9.3-25.6-9.3-34.9 0L537.7 602.3V218.7c0-13.6-11.1-24.7-24.7-24.7s-24.7 11.1-24.7 24.7v383.6L327 441c-9.4-9.3-25.6-9.3-34.9 0-4.7 4.7-7.2 10.9-7.2 17.5s2.6 12.8 7.2 17.5l203.4 203.4z" fill="currentColor" p-id="1734"></path><path d="M795.2 633c-13.6 0-24.7 11.1-24.7 24.7v103.8c0 8.5-6.7 15.4-15 15.4h-485c-8.3 0-15-6.9-15-15.4V657.7c0-13.6-11.1-24.7-24.7-24.7s-24.7 11.1-24.7 24.7v116.2c0 28.9 23.5 52.4 52.4 52.4h509.1c28.9 0 52.4-23.5 52.4-52.4V657.7c-0.1-13.6-11.2-24.7-24.8-24.7z" fill="currentColor" p-id="1735"></path></svg>`;
var DownloadIcon7 = `<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em"><path d="M966.599111 426.666667c-0.910222-21.390222-2.844444-43.292444-5.12-65.649778-15.872-155.022222-139.548444-282.168889-293.888-301.397333A339.000889 339.000889 0 0 0 304.241778 284.444444a216.860444 216.860444 0 0 0-19.342222-0.967111c-124.871111 0-228.124444 101.888-228.124445 226.986667v56.661333c0 31.004444 27.022222 56.206222 57.912889 56.206223h1.137778c30.890667 0 54.499556-26.567111 54.499555-57.514667V521.102222c-6.542222-67.527111 48.469333-124.359111 114.574223-124.359111 3.299556 0 6.542222 0.284444 9.784888 0.568889l43.804445-0.568889c25.713778 2.161778 49.436444-9.045333 57.969778-33.393778l12.288-35.100444c29.809778-85.276444 105.073778-149.219556 194.901333-157.184 139.093333-12.344889 259.242667 104.903111 249.969778 243.655111l-0.170667 11.889778v145.749333h0.568889c1.649778 29.639111 27.648 53.304889 57.628444 53.304889h1.137778c29.525333 0 52.053333-24.234667 54.101334-53.304889h0.398222V426.609778h-0.625778z" fill="currentColor" p-id="1885"></path><path d="M641.820444 700.074667l-73.216 74.922666V568.32A56.149333 56.149333 0 0 0 512.568889 512h-1.137778a56.149333 56.149333 0 0 0-56.035555 56.32v198.030222l-7.338667-1.194666-63.601778-63.374223c-20.081778-19.968-52.224-24.860444-75.207111-8.305777a56.32 56.32 0 0 0-7.224889 85.959111l171.918222 171.121777a56.888889 56.888889 0 0 0 80.213334 0l167.936-170.666666c22.129778-22.016 22.129778-57.742222 0-79.815111a56.888889 56.888889 0 0 0-80.213334 0z" fill="currentColor" p-id="1886"></path></svg>`;
var DownloadIcon8 = `<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em"><path d="M512 706.608L781.968 436.64a32 32 0 1 0-45.248-45.256L544 584.096V192a32 32 0 0 0-64 0v392.096l-192.712-192.72a32 32 0 0 0-45.256 45.256L512 706.608z" fill="currentColor" p-id="4839"></path><path d="M824 640a32 32 0 0 0-32 32v128.36c0 3.112 0 8.496-0.48 11.472l-1.008 1.024c-0.952 0.984-2.104 2.168-3.112 3.152h-538.48c-2.448-0.664-7.808-3.56-10.608-6.36-2.776-2.784-5.656-8.128-6.32-10.568V672a32 32 0 0 0-64 0v128c0 20.632 12.608 42.456 25.088 54.912C205.584 867.4 227.408 880 248 880h544c22.496 0 36.208-14.112 44.408-22.536l2.48-2.528c17.128-17.088 17.12-41.472 17.12-54.928V672A32.016 32.016 0 0 0 824 640z" fill="currentColor" p-id="4840"></path></svg>`;
var FileIcon = `<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-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>`;
var MP3Icon = `<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-6zm2 13.5c0 1.38-1.12 2.5-2.5 2.5S11 16.88 11 15.5 12.12 13 13.5 13c.57 0 1.08.19 1.5.51V9h3v2h-2v4.5z"/></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>`;
@@ -78,7 +78,7 @@
JSON.stringify({
id,
data: body,
})
}),
);
}
if (key === "key:fetch_account_home") {
@@ -118,7 +118,7 @@
JSON.stringify({
type: "ping",
data: page_title,
})
}),
);
} catch (e) {
// ...
@@ -131,7 +131,7 @@
JSON.stringify({
type: "ping",
data: page_title,
})
}),
);
} catch (e) {
// ...
@@ -200,6 +200,30 @@
};
return $btn;
}
function render_download_button() {
var $btn = document.createElement("div");
// $btn.className = "sns_opr_btn sns_write_comment_btn bar-expand-hotarea js_wx_tap_highlight wx_tap_link";
$btn.style.cssText = `display: flex; align-items: center; margin-left: 16px; font-size: 14px; cursor: pointer;`;
$btn.innerHTML = `<span style="position: relative; top: -6px; width: 24px; height: 24px; font-size: 24px;">${DownloadIcon8}</span><span class="sns_opr_gap" style="margin-left: 1px">下载</span>`;
$btn.onclick = async function () {
var [err, data] = await WXU.request({
method: "POST",
url: "https://" + FakeAPIServerAddr + "/api/task/create2",
body: {
url: `officialaccount://${window.location.href}`,
// filename: document.title,
},
});
if (err) {
WXU.error({
msg: err.message,
});
return;
}
WXU.toast("开始下载");
};
return $btn;
}
function insert_rss_button(acct) {
if (!acct.biz || !acct.key) {
return;
@@ -210,72 +234,82 @@
var $btn = render_rss_button(acct);
$container.appendChild($btn);
}
insert_style();
function insert_download_button() {
var $wraps = document.querySelectorAll(".interaction_bar");
var $container = $wraps[$wraps.length - 1];
if (!$container || !$container.lastElementChild) {
return;
}
var $btn = render_download_button();
$container.insertBefore($btn, $container.lastElementChild);
}
async function main() {
if (location.pathname === "/s") {
var _OfficialAccountCredentials = {
nickname: (() => {
if (window.nickname) {
return window.nickname;
}
if (window.cgiData) {
if (window.cgiData.nick_name) {
return window.cgiData.nick_name;
}
}
if (window.cgiDataNew) {
if (window.cgiDataNew.nick_name) {
return window.cgiDataNew.nick_name;
}
}
return "";
})(),
avatar_url: (() => {
if (window.headimg) {
return window.headimg;
}
if (window.cgiData) {
if (window.cgiData.round_head_img) {
return window.cgiData.round_head_img;
}
if (window.cgiData.hd_head_img) {
return window.cgiData.hd_head_img;
}
}
if (window.cgiDataNew) {
if (window.cgiDataNew.round_head_img) {
return window.cgiDataNew.round_head_img;
}
if (window.cgiDataNew.hd_head_img) {
return window.cgiDataNew.hd_head_img;
}
}
return "";
})(),
biz: window.biz || window.__biz,
uin: window.uin,
key: window.key,
refresh_uri: (() => {
const params = new URLSearchParams(window.location.search);
const biz = params.get("__biz");
const mid = params.get("mid");
const idx = params.get("idx");
const sn = params.get("sn");
return `https://mp.weixin.qq.com/s?__biz=${biz}&mid=${mid}&idx=${idx}&sn=${sn}`;
})(),
pass_ticket: window.pass_ticket,
appmsg_token: window.appmsg_token,
};
WXU.observe_node(".wx_follow_media", () => {
setTimeout(() => {
insert_style();
// insert_rss_button(_OfficialAccountCredentials);
insert_download_button();
}, 800);
});
}
}
WXU.onWindowLoaded(() => {
async function main() {
if (location.pathname === "/s") {
var _OfficialAccountCredentials = {
nickname: (() => {
if (window.nickname) {
return window.nickname;
}
if (window.cgiData) {
if (window.cgiData.nick_name) {
return window.cgiData.nick_name;
}
}
if (window.cgiDataNew) {
if (window.cgiDataNew.nick_name) {
return window.cgiDataNew.nick_name;
}
}
return "";
})(),
avatar_url: (() => {
if (window.headimg) {
return window.headimg;
}
if (window.cgiData) {
if (window.cgiData.round_head_img) {
return window.cgiData.round_head_img;
}
if (window.cgiData.hd_head_img) {
return window.cgiData.hd_head_img;
}
}
if (window.cgiDataNew) {
if (window.cgiDataNew.round_head_img) {
return window.cgiDataNew.round_head_img;
}
if (window.cgiDataNew.hd_head_img) {
return window.cgiDataNew.hd_head_img;
}
}
return "";
})(),
biz: window.biz || window.__biz,
uin: window.uin,
key: window.key,
refresh_uri: (() => {
const params = new URLSearchParams(window.location.search);
const biz = params.get("__biz");
const mid = params.get("mid");
const idx = params.get("idx");
const sn = params.get("sn");
return `https://mp.weixin.qq.com/s?__biz=${biz}&mid=${mid}&idx=${idx}&sn=${sn}`;
})(),
pass_ticket: window.pass_ticket,
appmsg_token: window.appmsg_token,
};
if (!WXU.config.officialServerDisabled) {
connect(_OfficialAccountCredentials);
}
WXU.observe_node(".wx_follow_media", () => {
setTimeout(() => {
insert_rss_button(_OfficialAccountCredentials);
}, 1200);
});
}
if (WXU.config.officialServerDisabled) {
return;
}
main();
});
+21 -21
View File
@@ -6,7 +6,6 @@ import (
"encoding/json"
"errors"
"fmt"
"html"
"io"
"net"
"net/http"
@@ -25,6 +24,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
"golang.org/x/net/html"
result "wx_channel/internal/util"
"wx_channel/pkg/cache"
@@ -184,26 +184,26 @@ func NewOfficialAccountClient(cfg *OfficialAccountConfig, parent_logger *zerolog
}
}()
}
if !cfg.RemoteMode && len(c.AccountIdsRefreshInterval) > 0 {
var valid_accounts []string
acct_mu.RLock()
for _, biz := range c.AccountIdsRefreshInterval {
if _, ok := accounts[biz]; ok {
valid_accounts = append(valid_accounts, biz)
}
}
acct_mu.RUnlock()
c.AccountIdsRefreshInterval = valid_accounts
if len(c.AccountIdsRefreshInterval) > 0 {
go func() {
ticker := time.NewTicker(28 * time.Minute)
defer ticker.Stop()
for range ticker.C {
c.RefreshSpecifiedOfficialAccountList(c.AccountIdsRefreshInterval)
}
}()
}
}
// if !cfg.RemoteMode && len(c.AccountIdsRefreshInterval) > 0 {
// var valid_accounts []string
// acct_mu.RLock()
// for _, biz := range c.AccountIdsRefreshInterval {
// if _, ok := accounts[biz]; ok {
// valid_accounts = append(valid_accounts, biz)
// }
// }
// acct_mu.RUnlock()
// c.AccountIdsRefreshInterval = valid_accounts
// if len(c.AccountIdsRefreshInterval) > 0 {
// go func() {
// ticker := time.NewTicker(28 * time.Minute)
// defer ticker.Stop()
// for range ticker.C {
// c.RefreshSpecifiedOfficialAccountList(c.AccountIdsRefreshInterval)
// }
// }()
// }
// }
return c
}
+15 -7
View File
@@ -1,20 +1,28 @@
module github.com/GopeedLab/gopeed
go 1.23.0
toolchain go1.23.1
go 1.24.0
require (
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0
github.com/PuerkitoBio/goquery v1.11.0
github.com/chai2010/webp v1.4.0
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3
github.com/matoous/go-nanoid/v2 v2.0.0
github.com/mattn/go-ieproxy v0.0.12
github.com/rs/zerolog v1.31.0
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
go.etcd.io/bbolt v1.3.11
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
golang.org/x/sync v0.13.0
golang.org/x/image v0.36.0
golang.org/x/sync v0.19.0
)
require (
github.com/JohannesKaufmann/dom v0.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/stretchr/testify v1.10.0 // indirect
)
@@ -22,7 +30,7 @@ require (
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0
golang.org/x/net v0.47.0
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.34.0
)
+102 -8
View File
@@ -1,8 +1,29 @@
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0 h1:mklaPbT4f/EiDr1Q+zPrEt9lgKAkVrIBtWf33d9GpVA=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.0/go.mod h1:D56Cl9r8M5i3UwAchE+LlLc5hPN3kJtdZNVJn06lSHU=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 h1:bVp3yUzvSAJzu9GqID+Z96P+eu5TKnIMJSV4QaZMauM=
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
@@ -21,28 +42,101 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad h1:QtuWk6dsrNXc/ugwCBEN0jG52q/4tXRzMvZm4fH6T9g=
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -0,0 +1,216 @@
package officialaccountdownload
import (
"net/http"
"net/url"
"path/filepath"
"strings"
"sync"
"time"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
officialaccountdownload "github.com/GopeedLab/gopeed/pkg/officialaccount"
)
type Fetcher struct {
fetcher.DefaultFetcher
mu sync.Mutex
closed bool
}
func (f *Fetcher) Setup(ctl *controller.Controller) {
if err := f.DefaultFetcher.Setup(ctl); err != nil {
panic(err)
}
if f.DefaultFetcher.Meta == nil {
f.DefaultFetcher.Meta = &fetcher.FetcherMeta{}
}
}
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.DefaultFetcher.Meta
}
func (f *Fetcher) Resolve(req *base.Request) error {
if f.DefaultFetcher.Meta == nil {
f.DefaultFetcher.Meta = &fetcher.FetcherMeta{}
}
f.DefaultFetcher.Meta.Req = req
oa := &officialaccountdownload.OfficialAccountDownload{}
// Handle custom scheme
real_url := req.URL
if strings.HasPrefix(strings.ToLower(real_url), "officialaccount://") {
real_url = real_url[len("officialaccount://"):]
if !strings.HasPrefix(real_url, "http") {
real_url = "https://" + real_url
}
// Create a copy of request with real URL
req_copy := *req
req_copy.URL = real_url
f.DefaultFetcher.Meta.Req = &req_copy
}
// Fetch the article to get the title
article, err := oa.FetchArticle(f.DefaultFetcher.Meta.Req.URL)
if err != nil {
return err
}
// Sanitize filename
filename := strings.ReplaceAll(article.Title, "/", "_")
filename = strings.ReplaceAll(filename, "\\", "_")
filename = filename + ".html"
size := int64(article.ContentLength)
if len(article.Images) > 0 {
var wg sync.WaitGroup
var mu sync.Mutex
for _, imgURL := range article.Images {
if imgURL == "" {
continue
}
wg.Add(1)
go func(u string) {
defer wg.Done()
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Head(u)
if err != nil {
return
}
defer resp.Body.Close()
if resp.ContentLength > 0 {
mu.Lock()
size += resp.ContentLength
mu.Unlock()
}
}(imgURL)
}
wg.Wait()
}
f.DefaultFetcher.Meta.Res = &base.Resource{
Name: filename,
// Size is unknown/variable because of images, but we can set 0 or estimate
Size: size,
Files: []*base.FileInfo{
{
Name: filename,
Path: "",
Size: size,
},
},
}
return nil
}
func (f *Fetcher) Create(opts *base.Options) error {
f.DefaultFetcher.Meta.Opts = opts
return nil
}
func (f *Fetcher) Start() error {
go func() {
oa := &officialaccountdownload.OfficialAccountDownload{}
// Use the URL from the request and Path from options
destPath := filepath.Join(f.DefaultFetcher.Meta.Opts.Path, f.DefaultFetcher.Meta.Opts.Name)
needCompress := false
if f.DefaultFetcher.Meta.Req.Labels != nil && f.DefaultFetcher.Meta.Req.Labels["compress"] == "true" {
needCompress = true
}
content, err := oa.BuildHTMLFromURL(f.DefaultFetcher.Meta.Req.URL, needCompress)
if err != nil {
f.DoneCh <- err
return
}
// Save content to file
file, err := f.DefaultFetcher.Ctl.Touch(destPath, int64(len(content)))
if err != nil {
f.DoneCh <- err
return
}
defer file.Close()
_, err = file.WriteString(content)
f.DoneCh <- err
}()
return nil
}
func (f *Fetcher) Pause() error {
return nil
}
func (f *Fetcher) Close() error {
f.mu.Lock()
defer f.mu.Unlock()
f.closed = true
return nil
}
func (f *Fetcher) Stats() any {
return nil
}
func (f *Fetcher) Progress() fetcher.Progress {
// Progress tracking might be difficult with current SaveURLAsHTML implementation
// as it doesn't report progress. We return 0 or maybe implemented later.
return fetcher.Progress{0}
}
// Manager
type FetcherManager struct{}
func (fm *FetcherManager) Name() string {
return "officialaccount"
}
func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
return []*fetcher.SchemeFilter{
{
Type: fetcher.FilterTypeUrl,
Pattern: "officialaccount",
},
}
}
func (fm *FetcherManager) Build() fetcher.Fetcher {
return &Fetcher{}
}
func (fm *FetcherManager) ParseName(u string) string {
// Simple parsing, actual name resolution happens in Resolve
parsed, err := url.Parse(u)
if err != nil {
return "article.html"
}
// Maybe extract something from URL if possible, but Resolve will do better
return parsed.Path
}
func (fm *FetcherManager) AutoRename() bool {
return true
}
func (fm *FetcherManager) DefaultConfig() any {
return nil
}
func (fm *FetcherManager) Store(f fetcher.Fetcher) (any, error) {
return nil, nil
}
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
return nil, nil
}
func (fm *FetcherManager) Close() error {
return nil
}
+2
View File
@@ -8,6 +8,7 @@ import (
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/protocol/http"
officialaccountdownload "github.com/GopeedLab/gopeed/internal/protocol/officialaccount"
"github.com/GopeedLab/gopeed/internal/protocol/stream"
"github.com/GopeedLab/gopeed/internal/protocol/zip"
"github.com/GopeedLab/gopeed/pkg/base"
@@ -158,6 +159,7 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
new(http.FetcherManager),
new(stream.FetcherManager),
new(zip.FetcherManager),
new(officialaccountdownload.FetcherManager),
}
}
if cfg.RefreshInterval == 0 {
@@ -0,0 +1,693 @@
package officialaccountdownload
import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"
htmltomarkdown "github.com/JohannesKaufmann/html-to-markdown/v2"
"github.com/PuerkitoBio/goquery"
"golang.org/x/net/html"
)
type OfficialAccountDownload struct {
article *WechatOfficialArticle
}
func (c *OfficialAccountDownload) SaveURLAsMarkdown(url string, dir_path string) error {
article, err := c.FetchArticle(url)
if err != nil {
return err
}
return c.ConvertHtmlToMarkdown(article, dir_path)
}
func (c *OfficialAccountDownload) BuildHTMLFromURL(url string, need_compress_img bool) (string, error) {
article := c.article
if article == nil {
r, err := c.FetchArticle(url)
if err != nil {
return "", err
}
article = r
}
return c.BuildHTMLFromArticle(article, need_compress_img)
}
func (c *OfficialAccountDownload) ConvertHtmlToMarkdown(article *WechatOfficialArticle, dir_path string) error {
// Update the receiver with the fetched article data
// Sanitize filename for the markdown file
filename := strings.ReplaceAll(article.Title, "/", "_")
filename = strings.ReplaceAll(filename, "\\", "_")
// Create images directory
imagesDirName := "images"
imagesDirPath := filepath.Join(dir_path, imagesDirName)
if err := os.MkdirAll(imagesDirPath, 0755); err != nil {
return err
}
// Process HTML content to download images and replace links
doc, err := goquery.NewDocumentFromReader(strings.NewReader(article.Content))
if err != nil {
return err
}
// Preserve newlines in text nodes by replacing them with a placeholder
// This is needed because HTML parsers and markdown converters often treat newlines as whitespace
newlinePlaceholder := "WECHATNEWLINEHOLDER"
var replaceNewlines func(*html.Node)
replaceNewlines = func(n *html.Node) {
if n.Type == html.TextNode {
if strings.Contains(n.Data, "\n") {
n.Data = strings.ReplaceAll(n.Data, "\n", newlinePlaceholder)
}
return
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode {
tag := strings.ToLower(c.Data)
// Skip pre-formatted blocks where newlines should be preserved naturally
if tag == "pre" || tag == "code" || tag == "script" || tag == "style" {
continue
}
}
replaceNewlines(c)
}
}
for _, n := range doc.Nodes {
replaceNewlines(n)
}
doc.Find("mp-common-mpaudio").Each(func(i int, s *goquery.Selection) {
voiceEncodeFileId := s.AttrOr("voice_encode_fileid", "")
if voiceEncodeFileId != "" {
audioURL := "https://res.wx.qq.com/voice/getvoice?mediaid=" + voiceEncodeFileId
s.AppendHtml(fmt.Sprintf(`<audio src="%s" controls="controls"></audio>`, audioURL))
}
})
doc.Find("iframe.video_iframe").Each(func(i int, s *goquery.Selection) {
vid := s.AttrOr("data-vid", "")
if vid == "" {
vid = s.AttrOr("vid", "")
}
if vid == "" {
vid = s.AttrOr("data-mpvid", "")
}
if vid != "" {
for _, video := range article.Videos {
if video.VideoID == vid {
if len(video.MpVideoTransInfo) > 0 {
videoURL := video.MpVideoTransInfo[0].Url
cover := s.AttrOr("data-cover", "")
posterAttr := ""
if cover != "" {
if decodedCover, err := url.QueryUnescape(cover); err == nil {
cover = decodedCover
}
posterAttr = fmt.Sprintf(` poster="%s"`, escapeHTML(cover))
}
videoHTML := fmt.Sprintf(`<video src="%s"%s controls="controls" style="width: 100%%; height: auto;"></video>`, videoURL, posterAttr)
s.ReplaceWithHtml(videoHTML)
}
break
}
}
}
})
doc.Find("img").Each(func(i int, s *goquery.Selection) {
imgURL := s.AttrOr("data-src", "")
if imgURL == "" {
imgURL = s.AttrOr("src", "")
}
if imgURL != "" {
// Download image
localFileName, err := c.downloadImage(imgURL, imagesDirPath)
if err == nil {
// Replace src with local relative path
relativePath := filepath.Join(imagesDirName, localFileName)
s.SetAttr("src", relativePath)
// Remove data-src to ensure markdown converter uses src
s.RemoveAttr("data-src")
} else {
fmt.Printf("Failed to download image %s: %v\n", imgURL, err)
}
}
})
newHTML, err := doc.Html()
if err != nil {
return err
}
// Workaround for <br> handling: Replace <br> with a placeholder to ensure it's preserved as a hard break
// html-to-markdown/v2 might handle <br> differently depending on context or configuration.
// We want explicit hard breaks (two spaces + newline) for every <br> tag.
brPlaceholder := "WECHATBRHOLDER"
// Replace the newline placeholder with the break placeholder
newHTML = strings.ReplaceAll(newHTML, "WECHATNEWLINEHOLDER", brPlaceholder)
// goquery normalizes to <br/> but we handle all cases just to be safe
newHTML = strings.ReplaceAll(newHTML, "<br/>", brPlaceholder)
newHTML = strings.ReplaceAll(newHTML, "<br>", brPlaceholder)
newHTML = strings.ReplaceAll(newHTML, "<br />", brPlaceholder)
markdown, err := htmltomarkdown.ConvertString(newHTML)
if err != nil {
return err
}
// Restore line breaks
markdown = strings.ReplaceAll(markdown, brPlaceholder, " \n")
// Process additional images from article.Images
if len(article.Images) > 0 {
markdown += "\n\n"
for _, imgURL := range article.Images {
localFileName, err := c.downloadImage(imgURL, imagesDirPath)
if err != nil {
fmt.Printf("Failed to download attached image %s: %v\n", imgURL, err)
continue
}
relative_path := filepath.Join(imagesDirName, localFileName)
markdown += fmt.Sprintf("\n![image](%s)\n", relative_path)
}
}
if err := os.MkdirAll(dir_path, 0755); err != nil {
return err
}
filePath := filepath.Join(dir_path, filename+".md")
if err := os.WriteFile(filePath, []byte(markdown), 0644); err != nil {
return err
}
return nil
}
func (c *OfficialAccountDownload) BuildHTMLFromArticle(article *WechatOfficialArticle, need_compress_img bool) (string, error) {
isImageArticle := article.Type == 2 && len(article.Images) > 0
bodyMaxWidth := "677px"
if isImageArticle {
bodyMaxWidth = "1024px"
}
var htmlContent strings.Builder
htmlContent.WriteString(`<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>`)
htmlContent.WriteString(escapeHTML(article.Title))
htmlContent.WriteString(`</title>
<style>
html { height: 100%; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
max-width: ` + bodyMaxWidth + `;
margin: 0 auto;
padding: 20px;
color: #333;`)
if isImageArticle {
htmlContent.WriteString(`
height: 100%;
overflow: hidden;
box-sizing: border-box;`)
}
htmlContent.WriteString(`
}
h1 { font-size: 1.8em; margin-bottom: 0.5em; }
.author { color: #666; margin-bottom: 20px; }
.author img { width: 24px; height: 24px; border-radius: 50%; vertical-align: middle; margin-right: 8px; }
img { max-width: 100%; height: auto; }
.rich_media_title {
font-size: 22px;
line-height: 1.4;
margin-bottom: 14px;
font-weight: 500;
}
.not_in_mm .rich_media_meta_list {
position: relative;
z-index: 1;
}
.rich_media_meta_list {
margin-bottom: 22px;
line-height: 20px;
font-size: 0;
word-wrap: break-word;
-webkit-hyphens: auto;
-ms-hyphens: auto;
hyphens: auto;
}
.rich_media_meta {
display: inline-block;
vertical-align: middle;
margin: 0 10px 10px 0;
font-size: 15px;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.rich_media_meta_text.article_modify_tag, .rich_media_meta_nickname {
position: relative;
}
.rich_media_meta_list em {
font-style: normal;
}
.audio_card {
display: flex;
align-items: center;
background-color: #f7f7f7;
border-radius: 6px;
padding: 12px;
margin: 20px 0;
border: 1px solid #ebebeb;
}
.audio_card_cover {
width: 64px;
height: 64px;
border-radius: 4px;
overflow: hidden;
flex-shrink: 0;
margin-right: 12px;
position: relative;
}
.audio_card_cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.audio_card_content {
flex-grow: 1;
overflow: hidden;
margin-right: 12px;
}
.audio_card_title {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.audio_card_meta {
font-size: 13px;
color: #999;
}
.audio_card audio {
height: 32px;
}
.additional-images {
margin-top: 0;
padding-top: 0;
border-top: none;
}
.additional-images img {
display: block;
width: 100%;
height: auto;
margin-bottom: 20px;
border-radius: 6px;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
/* Split layout styles */
.split-container {
display: flex;
gap: 40px;
align-items: flex-start;
justify-content: center;
height: 100%;
}
.split-left {
width: 600px;
flex: 0 0 600px;
height: 100%;
overflow-y: auto;
scrollbar-width: thin;
}
.split-right {
width: 344px;
flex: 0 0 344px;
height: 100%;
overflow-y: auto;
scrollbar-width: thin;
}
@media (max-width: 1000px) {
html, body {
height: auto !important;
overflow: visible !important;
}
.split-container {
display: block;
height: auto;
}
.split-left, .split-right {
width: 100%;
flex: none;
height: auto;
overflow-y: visible;
}
.split-left {
margin-bottom: 20px;
}
}
</style>
</head>
<body>`)
if isImageArticle {
htmlContent.WriteString(`<div class="split-container"><div class="split-left"><div class="additional-images">`)
for _, imgURL := range article.Images {
imgData, mimeType, err := downloadImageBytes(imgURL)
if err == nil {
if need_compress_img {
// Compress image to reduce size
compressedData, compressedMime, errCompress := compressImage(imgData)
if errCompress == nil {
fmt.Printf("Compressed image %s: %d -> %d bytes (%.2f%%)\n",
imgURL, len(imgData), len(compressedData), float64(len(compressedData))/float64(len(imgData))*100)
imgData = compressedData
mimeType = compressedMime
} else {
fmt.Printf("Failed to compress image %s: %v\n", imgURL, errCompress)
}
}
base64Str := base64.StdEncoding.EncodeToString(imgData)
imgSrc := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Str)
htmlContent.WriteString(fmt.Sprintf(" <img src=\"%s\" alt=\"\">\n", imgSrc))
} else {
fmt.Printf("Failed to download image for base64 %s: %v\n", imgURL, err)
}
}
htmlContent.WriteString(`</div></div><div class="split-right">`)
}
htmlContent.WriteString(`<h1 class="rich_media_title"><span>` + article.Title + "</span></h1>")
creator_html := ""
if article.Creator != "" {
creator_html = `<span class="rich_media_meta rich_media_meta_text">` + article.Creator + `</span>`
}
htmlContent.WriteString(`<div class="rich_media_meta_list">` + creator_html + `<span class="rich_media_meta rich_media_meta_nickname">` + article.AuthorNickname + `</span><span><em class="rich_media_meta rich_media_meta_text">` + article.PublishTimeStr + "</em></span></div>")
htmlContent.WriteString(`<div class="rich_media_content js_underline_content autoTypeSetting24psection fix_apple_default_style">`)
// Process HTML content to handle newlines
doc, err := goquery.NewDocumentFromReader(strings.NewReader(article.Content))
if err != nil {
htmlContent.WriteString(article.Content)
} else {
newlinePlaceholder := "WECHATNEWLINEHOLDER"
var replaceNewlines func(*html.Node)
replaceNewlines = func(n *html.Node) {
if n.Type == html.TextNode {
if strings.Contains(n.Data, "\n") {
n.Data = strings.ReplaceAll(n.Data, "\n", newlinePlaceholder)
}
return
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode {
tag := strings.ToLower(c.Data)
// Skip pre-formatted blocks
if tag == "pre" || tag == "code" || tag == "script" || tag == "style" {
continue
}
}
replaceNewlines(c)
}
}
for _, n := range doc.Nodes {
replaceNewlines(n)
}
doc.Find("mp-common-mpaudio").Each(func(i int, s *goquery.Selection) {
voiceEncodeFileId := s.AttrOr("voice_encode_fileid", "")
if voiceEncodeFileId != "" {
audioURL := "https://res.wx.qq.com/voice/getvoice?mediaid=" + voiceEncodeFileId
name := s.AttrOr("name", "音频")
poster := s.AttrOr("poster", "")
if poster == "" {
poster = s.AttrOr("cover", "")
}
html := fmt.Sprintf(`
<div class="audio_card">
<div class="audio_card_cover">
<img src="%s" alt="cover">
</div>
<div class="audio_card_content">
<div class="audio_card_title">%s</div>
<audio src="%s" controls></audio>
</div>
</div>`, escapeHTML(poster), escapeHTML(name), audioURL)
s.ReplaceWithHtml(html)
}
})
doc.Find("iframe.video_iframe").Each(func(i int, s *goquery.Selection) {
vid := s.AttrOr("data-vid", "")
if vid == "" {
vid = s.AttrOr("vid", "")
}
if vid == "" {
vid = s.AttrOr("data-mpvid", "")
}
if vid != "" {
for _, video := range article.Videos {
if video.VideoID == vid {
if len(video.MpVideoTransInfo) > 0 {
videoURL := video.MpVideoTransInfo[0].Url
cover := s.AttrOr("data-cover", "")
posterAttr := ""
if cover != "" {
if decodedCover, err := url.QueryUnescape(cover); err == nil {
cover = decodedCover
}
posterAttr = fmt.Sprintf(` poster="%s"`, escapeHTML(cover))
}
videoHTML := fmt.Sprintf(`<video src="%s"%s controls="controls" style="width: 100%%; height: auto;"></video>`, videoURL, posterAttr)
s.ReplaceWithHtml(videoHTML)
}
break
}
}
}
})
// Process images with data-src for base64 encoding
doc.Find("img").Each(func(i int, s *goquery.Selection) {
imgURL := s.AttrOr("data-src", "")
if imgURL != "" {
imgData, mimeType, err := downloadImageBytes(imgURL)
if err == nil {
if need_compress_img {
// Compress image to reduce size
compressedData, compressedMime, errCompress := compressImage(imgData)
if errCompress == nil {
fmt.Printf("Compressed image %s: %d -> %d bytes (%.2f%%)\n",
imgURL, len(imgData), len(compressedData), float64(len(compressedData))/float64(len(imgData))*100)
imgData = compressedData
mimeType = compressedMime
} else {
fmt.Printf("Failed to compress image %s: %v\n", imgURL, errCompress)
}
}
base64Str := base64.StdEncoding.EncodeToString(imgData)
imgSrc := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Str)
s.SetAttr("src", imgSrc)
s.RemoveAttr("data-src")
} else {
fmt.Printf("Failed to download image for base64 %s: %v\n", imgURL, err)
}
}
})
// Get the content inside <body>
newHTML, err := doc.Find("body").Html()
if err != nil {
htmlContent.WriteString(article.Content)
} else {
newHTML = strings.ReplaceAll(newHTML, newlinePlaceholder, "<br>")
htmlContent.WriteString(newHTML)
}
}
if isImageArticle {
htmlContent.WriteString(" </div></div>")
}
htmlContent.WriteString(`</body>
</html>`)
return htmlContent.String(), nil
}
func (c *OfficialAccountDownload) downloadImage(imgURL string, save_dir string) (string, error) {
// Generate filename based on hash of URL
hash := md5.Sum([]byte(imgURL))
hashStr := hex.EncodeToString(hash[:])
// Try to guess extension
ext := ".jpg" // Default
if strings.Contains(imgURL, "wx_fmt=png") {
ext = ".png"
} else if strings.Contains(imgURL, "wx_fmt=gif") {
ext = ".gif"
} else if strings.Contains(imgURL, "wx_fmt=jpeg") {
ext = ".jpg"
} else if strings.Contains(imgURL, "wx_fmt=webp") {
ext = ".webp"
} else {
// Try to parse from URL path if query param not present
u, err := url.Parse(imgURL)
if err == nil {
pathExt := filepath.Ext(u.Path)
if pathExt != "" {
ext = pathExt
}
}
}
filename := hashStr + ext
filePath := filepath.Join(save_dir, filename)
// Check if file already exists
if _, err := os.Stat(filePath); err == nil {
return filename, nil
}
client := &http.Client{}
req, err := http.NewRequest("GET", imgURL, nil)
if err != nil {
return "", err
}
// Set headers similar to Scrape to avoid anti-hotlinking
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
req.Header.Set("Referer", "https://mp.weixin.qq.com/")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("bad status: %s", resp.Status)
}
file, err := os.Create(filePath)
if err != nil {
return "", err
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
return "", err
}
return filename, nil
}
func (c *OfficialAccountDownload) FetchArticle(url string) (*WechatOfficialArticle, error) {
content, err := c.Scrape(url)
if err != nil {
return nil, err
}
content_str := string(content)
// Extract createTime
var publish_time_str string
re := regexp.MustCompile(`var\s+createTime\s*=\s*'([^']+)'`)
matches := re.FindStringSubmatch(content_str)
if len(matches) > 1 {
createTime := matches[1]
t, err := time.Parse("2006-01-02 15:04", createTime)
if err == nil {
publish_time_str = t.Format("2006年01月02日 15:04")
} else {
publish_time_str = createTime
}
}
data, err := parse_cgi_datanew(content_str)
if err != nil {
return nil, err
}
article := &WechatOfficialArticle{
Type: data.PageType,
Title: data.Title,
Content: data.ContentNoEncode,
PublishTimeStr: publish_time_str,
ContentLength: len(data.ContentNoEncode),
Creator: data.Author,
AuthorNickname: data.NickName,
AuthorAvatar: data.RoundHeadImg,
AuthorID: data.UserName,
Images: make([]string, 0),
Videos: data.VideoPageInfos,
}
// isImageArticle := data.PageType == 2
if len(data.PicturePageInfoList) > 1 {
for _, img := range data.PicturePageInfoList {
if img.CdnUrl != "" {
article.Images = append(article.Images, img.CdnUrl)
}
}
}
c.article = article
return article, nil
}
func (c *OfficialAccountDownload) Scrape(url string) ([]byte, error) {
if url == "" {
return nil, fmt.Errorf("url is empty")
}
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("cache-control", "no-cache")
req.Header.Set("pragma", "no-cache")
req.Header.Set("priority", "u=0, i")
req.Header.Set("sec-ch-ua", `"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform", `"macOS"`)
req.Header.Set("sec-fetch-dest", "document")
req.Header.Set("sec-fetch-mode", "navigate")
req.Header.Set("sec-fetch-site", "none")
req.Header.Set("sec-fetch-user", "?1")
req.Header.Set("upgrade-insecure-requests", "1")
req.Header.Set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
req.Header.Set("cookie", "ua_id=zfRTujE0WVWbxqqCAAAAAL38AtjljAqWH0xPz_up8gw=; mm_lang=zh_CN; wxuin=69477998648217; xid=27df1e40bdcb601a449dc3afb35016ba; rewardsn=; wxtokenkey=777")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, err
}
+168
View File
@@ -0,0 +1,168 @@
package officialaccountdownload
type CgiDataNew struct {
BaseResp struct {
Ret int `json:"ret"`
ErrMsg string `json:"errmsg"`
WxToken int `json:"wxtoken"`
CookieCount int `json:"cookie_count"`
SessionID string `json:"sessionid"`
} `json:"base_resp"`
UserName string `json:"user_name"`
NickName string `json:"nick_name"`
RoundHeadImg string `json:"round_head_img"`
Title string `json:"title"`
Desc string `json:"desc"`
ContentNoEncode string `json:"content_noencode"`
CreateTime string `json:"create_time"`
CdnUrl string `json:"cdn_url"`
Link string `json:"link"`
SourceUrl string `json:"source_url"`
CanShare int `json:"can_share"`
Alias string `json:"alias"`
Type int `json:"type"`
Author string `json:"author"`
IsLimitUser int `json:"is_limit_user"`
ShowCoverPic int `json:"show_cover_pic"`
AdvertisementNum int `json:"advertisement_num"`
AdvertisementInfo []any `json:"advertisement_info"`
OriCreateTime int `json:"ori_create_time"`
UserUin int `json:"user_uin"`
TotalItemNum int `json:"total_item_num"`
IsAsync int `json:"is_async"`
CommentID string `json:"comment_id"`
ImgFormat string `json:"img_format"`
SvrTime int `json:"svr_time"`
CopyrightInfo struct {
CopyrightStat int `json:"copyright_stat"`
IsCartoonCopyright int `json:"is_cartoon_copyright"`
} `json:"copyright_info"`
CanReward int `json:"can_reward"`
Signature string `json:"signature"`
RewardWording string `json:"reward_wording"`
InMm int `json:"in_mm"`
AppID string `json:"app_id"`
ShowComment int `json:"show_comment"`
CanUsePage int `json:"can_use_page"`
HdHeadImg string `json:"hd_head_img"`
DelReasonID int `json:"del_reason_id"`
SrcID string `json:"srcid"`
IsWxgStuffUin int `json:"is_wxg_stuff_uin"`
NeedReportCost int `json:"need_report_cost"`
BizUin string `json:"bizuin"`
Mid int `json:"mid"`
Idx int `json:"idx"`
Sn string `json:"sn"`
UseTxVideoPlayer int `json:"use_tx_video_player"`
IsOnlyRead int `json:"is_only_read"`
ReqID string `json:"req_id"`
UseOuterLink int `json:"use_outer_link"`
BanScene int `json:"ban_scene"`
CspNonceStr int `json:"csp_nonce_str"`
MsgDailyIdx int `json:"msg_daily_idx"`
OriHeadImgUrl string `json:"ori_head_img_url"`
FilterTime int `json:"filter_time"`
AppmsgFeFilter string `json:"appmsg_fe_filter"`
IsLogin int `json:"is_login"`
RewardMoney int `json:"reward_money"`
PageType int `json:"page_type"`
ItemShowType int `json:"item_show_type"`
VoiceInAppmsg []any `json:"voice_in_appmsg"`
VideoPageInfo struct {
MpVideoTransInfo []any `json:"mp_video_trans_info"`
DramaVideoInfo struct {
} `json:"drama_video_info"`
DramaInfo struct {
} `json:"drama_info"`
} `json:"video_page_info"`
MaliciousTitleReasonID int `json:"malicious_title_reason_id"`
VideoPageInfos []VideoPageInfoItem `json:"video_page_infos"`
PicturePageInfoList []PicturePageInfo `json:"picture_page_info_list"`
}
type VideoPageInfoItem struct {
VideoID string `json:"video_id"`
OriStatus int `json:"ori_status"`
SourceBizuin string `json:"source_bizuin"`
SourceNickname string `json:"source_nickname"`
SourceLink string `json:"source_link"`
IsMpVideo int `json:"is_mp_video"`
IsMpVideoDelete int `json:"is_mp_video_delete"`
MpVideoOverseasLimit int `json:"mp_video_overseas_limit"`
IsMpVideoForbid int `json:"is_mp_video_forbid"`
MpVideoForbidReason int `json:"mp_video_forbid_reason"`
HitUsername int `json:"hit_username"`
HitBizuin int `json:"hit_bizuin"`
HitVid string `json:"hit_vid"`
ContentNoencode string `json:"content_noencode"`
IsMpVideoUrgentState int `json:"is_mp_video_urgent_state"`
IsAppmsgUnauthorized int `json:"is_appmsg_unauthorized"`
CoverUrl string `json:"cover_url"`
CoverUrl11 string `json:"cover_url_1_1"`
CoverUrl169 string `json:"cover_url_16_9"`
VideoidUploadtime int `json:"videoid_uploadtime"`
VideoidBizuin int `json:"videoid_bizuin"`
MpVideoTransInfo []MpVideoTransInfo `json:"mp_video_trans_info"`
}
type MpVideoTransInfo struct {
DurationMs int `json:"duration_ms"`
Filesize any `json:"filesize"`
FormatID int `json:"format_id"`
Height int `json:"height"`
Url string `json:"url"`
VideoQualityLevel int `json:"video_quality_level"`
VideoQualityWording string `json:"video_quality_wording"`
Width int `json:"width"`
}
type PicturePageInfo struct {
CdnUrl string `json:"cdn_url"`
Width int `json:"width"`
Height int `json:"height"`
ThemeColor string `json:"theme_color"`
IsQrCode int `json:"is_qr_code"`
PoiInfo []any `json:"poi_info"`
WxaInfo []any `json:"wxa_info"`
LivePhoto struct {
FormatInfo []any `json:"format_info"`
} `json:"live_photo"`
DisableThemeColor bool `json:"disable_theme_color"`
BindAdInfo []any `json:"bind_ad_info"`
CpsAdInfo []any `json:"cps_ad_info"`
PicWindowProduct struct {
ProductEncryptKey string `json:"product_encrypt_key"`
ProductType int `json:"product_type"`
Title string `json:"title"`
DataType int `json:"data_type"`
ProductID string `json:"product_id"`
} `json:"pic_window_product"`
ShowWatermark bool `json:"show_watermark"`
BottomRightBrightness float64 `json:"bottom_right_brightness"`
WatermarkInfo struct {
CdnUrl string `json:"cdn_url"`
IsUploader bool `json:"is_uploader"`
} `json:"watermark_info"`
SpotProductInfo []any `json:"spot_product_info"`
ShareCover struct {
FileID int `json:"file_id"`
Width int `json:"width"`
Height int `json:"height"`
CdnUrl string `json:"cdn_url"`
CropInfo string `json:"crop_info"`
} `json:"share_cover"`
}
type WechatOfficialArticle struct {
Type int `json:"type"`
Title string `json:"title"`
Content string `json:"content"`
ContentLength int `json:"content_length"`
Images []string `json:"images"`
Creator string `json:"creator"`
AuthorNickname string `json:"author_nickname"`
AuthorAvatar string `json:"author_avatar"`
AuthorID string `json:"author_id"`
PublishTimeStr string `json:"publish_time_str"`
Videos []VideoPageInfoItem `json:"videos"`
}
+220
View File
@@ -0,0 +1,220 @@
package officialaccountdownload
import (
"bytes"
"encoding/json"
"fmt"
"image"
"image/jpeg"
"io"
"net/http"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/chai2010/webp"
"github.com/dop251/goja"
"golang.org/x/image/draw"
)
func parse_cgi_datanew(htmlContent string) (*CgiDataNew, error) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
return nil, err
}
var scripts []string
doc.Find("script").Each(func(i int, s *goquery.Selection) {
text := s.Text()
if strings.Contains(text, "window.cgiDataNew =") || strings.Contains(text, "var videoPageInfos =") {
scripts = append(scripts, text)
}
})
if len(scripts) == 0 {
return nil, fmt.Errorf("cgiDataNew script not found")
}
vm := goja.New()
// Mock window
vm.RunString("var window = {};")
// Mock document and basic DOM/jQuery environment to prevent script errors
vm.RunString(`
var document = {
getElementById: function() { return {}; },
getElementsByTagName: function() { return []; },
createElement: function() { return {}; },
head: {}
};
var console = { log: function() {}, warn: function() {}, error: function() {} };
var mockDom = {
html: function() { return mockDom; },
text: function() { return mockDom; },
val: function() { return mockDom; },
attr: function() { return mockDom; },
find: function() { return mockDom; },
css: function() { return mockDom; },
addClass: function() { return mockDom; },
removeClass: function() { return mockDom; },
show: function() { return mockDom; },
hide: function() { return mockDom; },
append: function() { return mockDom; }
};
var $ = function() { return mockDom; };
var jQuery = $;
`)
// Mock JsDecode
// In the browser, JsDecode seems to decode strings, but the strings in the script
// are often already just string literals. If they contain escape sequences,
// the JS parser handles them.
// We'll treat it as an identity function for now.
vm.Set("JsDecode", func(call goja.FunctionCall) goja.Value {
return call.Argument(0)
})
// Run the scripts
var scriptErrs []error
for _, script := range scripts {
_, err = vm.RunString(script)
if err != nil {
scriptErrs = append(scriptErrs, err)
}
}
// Extract cgiDataNew
val := vm.Get("window").ToObject(vm).Get("cgiDataNew")
if val == nil {
if len(scriptErrs) > 0 {
for _, err := range scriptErrs {
fmt.Printf("failed to run script: %v\n", err)
}
}
return nil, fmt.Errorf("window.cgiDataNew is nil")
}
// Convert to JSON string
jsonStr, err := vm.RunString("JSON.stringify(window.cgiDataNew)")
if err != nil {
return nil, fmt.Errorf("failed to stringify cgiDataNew: %v", err)
}
data := &CgiDataNew{}
if err := json.Unmarshal([]byte(jsonStr.String()), data); err != nil {
return nil, fmt.Errorf("failed to unmarshal cgiDataNew: %v", err)
}
// Check if PicturePageInfoList is empty, if so check global window.picture_page_info_list
if len(data.PicturePageInfoList) == 0 {
valList := vm.Get("window").ToObject(vm).Get("picture_page_info_list")
if valList != nil && !goja.IsNull(valList) && !goja.IsUndefined(valList) {
jsonList, err := vm.RunString("JSON.stringify(window.picture_page_info_list)")
if err == nil {
var list []PicturePageInfo
if err := json.Unmarshal([]byte(jsonList.String()), &list); err == nil {
data.PicturePageInfoList = list
}
}
}
}
// Check for videoPageInfos
valVideo := vm.Get("videoPageInfos")
if valVideo != nil && !goja.IsNull(valVideo) && !goja.IsUndefined(valVideo) {
jsonVideo, err := vm.RunString("JSON.stringify(videoPageInfos)")
if err == nil {
var videoInfos []VideoPageInfoItem
if err := json.Unmarshal([]byte(jsonVideo.String()), &videoInfos); err == nil {
data.VideoPageInfos = videoInfos
}
}
}
return data, nil
}
func escapeHTML(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "\"", "&quot;")
s = strings.ReplaceAll(s, "'", "&#39;")
return s
}
func compressImage(data []byte) ([]byte, string, error) {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, "", err
}
// Resize if needed
bounds := img.Bounds()
width := bounds.Dx()
height := bounds.Dy()
maxWidth := 640 // Reduced from 800 to 640
var newImg image.Image = img
if width > maxWidth {
newHeight := height * maxWidth / width
dst := image.NewRGBA(image.Rect(0, 0, maxWidth, newHeight))
draw.CatmullRom.Scale(dst, dst.Bounds(), img, bounds, draw.Over, nil)
newImg = dst
}
// Encode to WebP
var buf bytes.Buffer
// Quality 50 provides a good balance between size and visual quality for web usage
err = webp.Encode(&buf, newImg, &webp.Options{Lossless: false, Quality: 50})
if err != nil {
// Fallback to JPEG if WebP encoding fails (though unlikely)
fmt.Printf("WebP encoding failed, falling back to JPEG: %v\n", err)
buf.Reset()
// Create white background for JPEG
bg := image.NewRGBA(newImg.Bounds())
draw.Draw(bg, bg.Bounds(), &image.Uniform{image.White}, image.Point{}, draw.Src)
draw.Draw(bg, bg.Bounds(), newImg, newImg.Bounds().Min, draw.Over)
finalImg := bg
err = jpeg.Encode(&buf, finalImg, &jpeg.Options{Quality: 40})
if err != nil {
return nil, "", err
}
return buf.Bytes(), "image/jpeg", nil
}
return buf.Bytes(), "image/webp", nil
}
func downloadImageBytes(imgURL string) ([]byte, string, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", imgURL, nil)
if err != nil {
return nil, "", err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
req.Header.Set("Referer", "https://mp.weixin.qq.com/")
resp, err := client.Do(req)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("bad status: %s", resp.Status)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, "", err
}
contentType := resp.Header.Get("Content-Type")
if contentType == "" {
contentType = http.DetectContentType(data)
}
return data, contentType, nil
}
@@ -0,0 +1,11 @@
package officialaccount
type ReqExtra struct {
}
type OptsExtra struct {
}
// Stats for download
type Stats struct {
}