mirror of
https://github.com/bestnite/bilinovel-downloader.git
synced 2025-10-26 17:14:24 +00:00
Compare commits
5 Commits
v0.0.13
...
v0.0.15-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
17c3859e9e
|
|||
|
11fccdb05f
|
|||
|
af968cbc9a
|
|||
|
08e6280c34
|
|||
| 34179b4dc0 |
28
.github/workflows/release.yml
vendored
Normal file
28
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
version: latest
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
version: 2
|
||||||
project_name: bilinovel-downloader
|
project_name: bilinovel-downloader
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
|
- go install github.com/a-h/templ/cmd/templ@latest
|
||||||
- templ generate
|
- templ generate
|
||||||
builds:
|
builds:
|
||||||
- env:
|
- env:
|
||||||
@@ -12,16 +14,15 @@ builds:
|
|||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
|
||||||
- "386"
|
- "386"
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X bilinovel-downloader/cmd.Version={{ .Version }}
|
- -s -w -X bilinovel-downloader/cmd.Version={{ .Version }}
|
||||||
flags:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
archives:
|
archives:
|
||||||
- format: tar.gz
|
- formats: ["tar.gz"]
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- format: zip
|
- formats: ["zip"]
|
||||||
goos: windows
|
goos: windows
|
||||||
wrap_in_directory: true
|
wrap_in_directory: true
|
||||||
release:
|
release:
|
||||||
@@ -29,3 +30,17 @@ release:
|
|||||||
upx:
|
upx:
|
||||||
- enabled: true
|
- enabled: true
|
||||||
compress: best
|
compress: best
|
||||||
|
|
||||||
|
nfpms:
|
||||||
|
- id: bilinovel-downloader
|
||||||
|
homepage: https://github.com/bestnite/bilinovel-downloader
|
||||||
|
maintainer: Nite <admin@nite07.com>
|
||||||
|
license: "MIT"
|
||||||
|
formats:
|
||||||
|
- apk
|
||||||
|
- deb
|
||||||
|
- rpm
|
||||||
|
- termux.deb
|
||||||
|
- archlinux
|
||||||
|
provides:
|
||||||
|
- bilinovel-downloader
|
||||||
|
|||||||
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -23,9 +23,9 @@
|
|||||||
"program": "${workspaceFolder}",
|
"program": "${workspaceFolder}",
|
||||||
"args": [
|
"args": [
|
||||||
"download",
|
"download",
|
||||||
"-n=2727",
|
"-n=2388",
|
||||||
"-v=150098",
|
"-v=84522",
|
||||||
"--headless=false"
|
"--debug=true"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Nite
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -8,10 +8,12 @@ import (
|
|||||||
"bilinovel-downloader/text"
|
"bilinovel-downloader/text"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/playwright-community/playwright-go"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,9 +22,20 @@ var downloadCmd = &cobra.Command{
|
|||||||
Short: "Download a novel or volume",
|
Short: "Download a novel or volume",
|
||||||
Long: "Download a novel or volume",
|
Long: "Download a novel or volume",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
err := runDownloadNovel()
|
slog.Info("Installing playwright")
|
||||||
|
err := playwright.Install(&playwright.RunOptions{
|
||||||
|
Browsers: []string{"chromium"},
|
||||||
|
Stdout: io.Discard,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to download novel: %v", err)
|
slog.Error("failed to install playwright")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = runDownloadNovel()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to download novel", slog.Any("error", err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -32,8 +45,8 @@ type downloadCmdArgs struct {
|
|||||||
VolumeId int `validate:"required"`
|
VolumeId int `validate:"required"`
|
||||||
outputPath string
|
outputPath string
|
||||||
outputType string
|
outputType string
|
||||||
headless bool
|
|
||||||
concurrency int
|
concurrency int
|
||||||
|
debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -45,15 +58,15 @@ func init() {
|
|||||||
downloadCmd.Flags().IntVarP(&downloadArgs.VolumeId, "volume-id", "v", 0, "volume id")
|
downloadCmd.Flags().IntVarP(&downloadArgs.VolumeId, "volume-id", "v", 0, "volume id")
|
||||||
downloadCmd.Flags().StringVarP(&downloadArgs.outputPath, "output-path", "o", "novels", "output path")
|
downloadCmd.Flags().StringVarP(&downloadArgs.outputPath, "output-path", "o", "novels", "output path")
|
||||||
downloadCmd.Flags().StringVarP(&downloadArgs.outputType, "output-type", "t", "epub", "output type, epub or text")
|
downloadCmd.Flags().StringVarP(&downloadArgs.outputType, "output-type", "t", "epub", "output type, epub or text")
|
||||||
downloadCmd.Flags().BoolVar(&downloadArgs.headless, "headless", true, "headless mode")
|
downloadCmd.Flags().BoolVar(&downloadArgs.debug, "debug", false, "debug mode")
|
||||||
downloadCmd.Flags().IntVar(&downloadArgs.concurrency, "concurrency", 3, "concurrency of downloading volumes")
|
downloadCmd.Flags().IntVar(&downloadArgs.concurrency, "concurrency", 3, "concurrency of downloading volumes")
|
||||||
RootCmd.AddCommand(downloadCmd)
|
RootCmd.AddCommand(downloadCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDownloadNovel() error {
|
func runDownloadNovel() error {
|
||||||
downloader, err := bilinovel.New(bilinovel.BilinovelNewOption{
|
downloader, err := bilinovel.New(bilinovel.BilinovelNewOption{
|
||||||
Headless: downloadArgs.headless,
|
|
||||||
Concurrency: downloadArgs.concurrency,
|
Concurrency: downloadArgs.concurrency,
|
||||||
|
Debug: downloadArgs.debug,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create downloader: %v", err)
|
return fmt.Errorf("failed to create downloader: %v", err)
|
||||||
@@ -61,7 +74,7 @@ func runDownloadNovel() error {
|
|||||||
// 确保在函数结束时关闭资源
|
// 确保在函数结束时关闭资源
|
||||||
defer func() {
|
defer func() {
|
||||||
if closeErr := downloader.Close(); closeErr != nil {
|
if closeErr := downloader.Close(); closeErr != nil {
|
||||||
log.Printf("Failed to close downloader: %v", closeErr)
|
slog.Info("Failed to close downloader", slog.Any("error", closeErr))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var RootCmd = &cobra.Command{}
|
var RootCmd = &cobra.Command{
|
||||||
|
Use: "bilinovel-downloader",
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -40,11 +40,13 @@ type Bilinovel struct {
|
|||||||
pages map[string]playwright.Page
|
pages map[string]playwright.Page
|
||||||
concurrency int
|
concurrency int
|
||||||
concurrentChan chan any
|
concurrentChan chan any
|
||||||
|
|
||||||
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type BilinovelNewOption struct {
|
type BilinovelNewOption struct {
|
||||||
Headless bool
|
|
||||||
Concurrency int
|
Concurrency int
|
||||||
|
Debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(option BilinovelNewOption) (*Bilinovel, error) {
|
func New(option BilinovelNewOption) (*Bilinovel, error) {
|
||||||
@@ -54,6 +56,17 @@ func New(option BilinovelNewOption) (*Bilinovel, error) {
|
|||||||
}
|
}
|
||||||
restyClient := utils.NewRestyClient(50)
|
restyClient := utils.NewRestyClient(50)
|
||||||
|
|
||||||
|
var logLevel slog.Level
|
||||||
|
if option.Debug {
|
||||||
|
logLevel = slog.LevelDebug
|
||||||
|
} else {
|
||||||
|
logLevel = slog.LevelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerOptions := &slog.HandlerOptions{
|
||||||
|
Level: logLevel,
|
||||||
|
}
|
||||||
|
|
||||||
b := &Bilinovel{
|
b := &Bilinovel{
|
||||||
fontMapper: fontMapper,
|
fontMapper: fontMapper,
|
||||||
textOnly: false,
|
textOnly: false,
|
||||||
@@ -61,10 +74,11 @@ func New(option BilinovelNewOption) (*Bilinovel, error) {
|
|||||||
pages: make(map[string]playwright.Page),
|
pages: make(map[string]playwright.Page),
|
||||||
concurrency: option.Concurrency,
|
concurrency: option.Concurrency,
|
||||||
concurrentChan: make(chan any, option.Concurrency),
|
concurrentChan: make(chan any, option.Concurrency),
|
||||||
|
logger: slog.New(slog.NewTextHandler(os.Stdout, handlerOptions)),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化浏览器实例
|
// 初始化浏览器实例
|
||||||
err = b.initBrowser(option.Headless)
|
err = b.initBrowser(option.Debug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to init browser: %v", err)
|
return nil, fmt.Errorf("failed to init browser: %v", err)
|
||||||
}
|
}
|
||||||
@@ -81,13 +95,15 @@ func (b *Bilinovel) GetExtraFiles() []model.ExtraFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initBrowser 初始化浏览器实例
|
// initBrowser 初始化浏览器实例
|
||||||
func (b *Bilinovel) initBrowser(headless bool) error {
|
func (b *Bilinovel) initBrowser(debug bool) error {
|
||||||
pw, err := playwright.Run()
|
pw, err := playwright.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not start playwright: %w", err)
|
return fmt.Errorf("could not start playwright: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.browser, err = pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{
|
b.browser, err = pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{
|
||||||
Headless: playwright.Bool(headless),
|
Headless: playwright.Bool(!debug),
|
||||||
|
Devtools: playwright.Bool(debug),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not launch browser: %w", err)
|
return fmt.Errorf("could not launch browser: %w", err)
|
||||||
@@ -98,7 +114,7 @@ func (b *Bilinovel) initBrowser(headless bool) error {
|
|||||||
return fmt.Errorf("could not create browser context: %w", err)
|
return fmt.Errorf("could not create browser context: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Browser initialized successfully")
|
b.logger.Info("Browser initialized successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +122,7 @@ func (b *Bilinovel) initBrowser(headless bool) error {
|
|||||||
func (b *Bilinovel) Close() error {
|
func (b *Bilinovel) Close() error {
|
||||||
if b.browser != nil {
|
if b.browser != nil {
|
||||||
if err := b.browser.Close(); err != nil {
|
if err := b.browser.Close(); err != nil {
|
||||||
log.Printf("could not close browser: %v", err)
|
b.logger.Error("could not close browser", slog.Any("error", err))
|
||||||
}
|
}
|
||||||
b.browser = nil
|
b.browser = nil
|
||||||
b.browserContext = nil
|
b.browserContext = nil
|
||||||
@@ -122,7 +138,7 @@ func (b *Bilinovel) GetStyleCSS() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bilinovel) GetNovel(novelId int, skipChapterContent bool, skipVolumes []int) (*model.Novel, error) {
|
func (b *Bilinovel) GetNovel(novelId int, skipChapterContent bool, skipVolumes []int) (*model.Novel, error) {
|
||||||
log.Printf("Getting novel %v\n", novelId)
|
b.logger.Info("Getting novel", slog.Int("novelId", novelId))
|
||||||
|
|
||||||
novelUrl := fmt.Sprintf("https://www.bilinovel.com/novel/%v.html", novelId)
|
novelUrl := fmt.Sprintf("https://www.bilinovel.com/novel/%v.html", novelId)
|
||||||
resp, err := b.restyClient.R().Get(novelUrl)
|
resp, err := b.restyClient.R().Get(novelUrl)
|
||||||
@@ -161,7 +177,7 @@ func (b *Bilinovel) GetNovel(novelId int, skipChapterContent bool, skipVolumes [
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bilinovel) GetVolume(novelId int, volumeId int, skipChapterContent bool) (*model.Volume, error) {
|
func (b *Bilinovel) GetVolume(novelId int, volumeId int, skipChapterContent bool) (*model.Volume, error) {
|
||||||
log.Printf("Getting volume %v of novel %v\n", volumeId, novelId)
|
b.logger.Info("Getting volume of novel", slog.Int("volumeId", volumeId), slog.Int("novelId", novelId))
|
||||||
|
|
||||||
novelUrl := fmt.Sprintf("https://www.bilinovel.com/novel/%v/catalog", novelId)
|
novelUrl := fmt.Sprintf("https://www.bilinovel.com/novel/%v/catalog", novelId)
|
||||||
resp, err := b.restyClient.R().Get(novelUrl)
|
resp, err := b.restyClient.R().Get(novelUrl)
|
||||||
@@ -259,7 +275,7 @@ func (b *Bilinovel) GetVolume(novelId int, volumeId int, skipChapterContent bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bilinovel) getAllVolumes(novelId int, skipChapterContent bool, skipVolumes []int) ([]*model.Volume, error) {
|
func (b *Bilinovel) getAllVolumes(novelId int, skipChapterContent bool, skipVolumes []int) ([]*model.Volume, error) {
|
||||||
log.Printf("Getting all volumes of novel %v\n", novelId)
|
b.logger.Info("Getting all volumes of novel", slog.Int("novelId", novelId))
|
||||||
|
|
||||||
catelogUrl := fmt.Sprintf("https://www.bilinovel.com/novel/%v/catalog", novelId)
|
catelogUrl := fmt.Sprintf("https://www.bilinovel.com/novel/%v/catalog", novelId)
|
||||||
resp, err := b.restyClient.R().Get(catelogUrl)
|
resp, err := b.restyClient.R().Get(catelogUrl)
|
||||||
@@ -300,7 +316,7 @@ func (b *Bilinovel) getAllVolumes(novelId int, skipChapterContent bool, skipVolu
|
|||||||
|
|
||||||
volumeId, err := strconv.Atoi(volumeIdStr)
|
volumeId, err := strconv.Atoi(volumeIdStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to convert volume id %s: %v", volumeIdStr, err)
|
b.logger.Error("failed to convert volume id", slog.String("volumeIdStr", volumeIdStr), slog.Any("error", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if slices.Contains(skipVolumes, volumeId) {
|
if slices.Contains(skipVolumes, volumeId) {
|
||||||
@@ -308,7 +324,7 @@ func (b *Bilinovel) getAllVolumes(novelId int, skipChapterContent bool, skipVolu
|
|||||||
}
|
}
|
||||||
volume, err := b.GetVolume(novelId, volumeId, skipChapterContent)
|
volume, err := b.GetVolume(novelId, volumeId, skipChapterContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to get volume info for novel %d, volume %d: %v", novelId, volumeId, err)
|
b.logger.Error("failed to get volume info", slog.Int("novelId", novelId), slog.Int("volumeId", volumeId), slog.Any("error", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
volume.SeriesIdx = i
|
volume.SeriesIdx = i
|
||||||
@@ -340,7 +356,7 @@ func (b *Bilinovel) getAllVolumes(novelId int, skipChapterContent bool, skipVolu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bilinovel) GetChapter(novelId int, volumeId int, chapterId int) (*model.Chapter, error) {
|
func (b *Bilinovel) GetChapter(novelId int, volumeId int, chapterId int) (*model.Chapter, error) {
|
||||||
log.Printf("Getting chapter %v of novel %v\n", chapterId, novelId)
|
b.logger.Info("Getting chapter of novel", slog.Int("chapterId", chapterId), slog.Int("novelId", novelId))
|
||||||
|
|
||||||
pageNum := 1
|
pageNum := 1
|
||||||
chapter := &model.Chapter{
|
chapter := &model.Chapter{
|
||||||
@@ -370,8 +386,11 @@ func (b *Bilinovel) GetChapter(novelId int, volumeId int, chapterId int) (*model
|
|||||||
return chapter, nil
|
return chapter, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nextPageUrlRegexp = regexp.MustCompile(`url_next:\s?['"]([^'"]*?)['"]`)
|
||||||
|
var cleanNextPageUrlRegexp = regexp.MustCompile(`(_\d+)?\.html$`)
|
||||||
|
|
||||||
func (b *Bilinovel) getChapterByPage(pwPage playwright.Page, chapter *model.Chapter, pageNum int) (bool, error) {
|
func (b *Bilinovel) getChapterByPage(pwPage playwright.Page, chapter *model.Chapter, pageNum int) (bool, error) {
|
||||||
log.Printf("Getting chapter %v by page %v\n", chapter.Id, pageNum)
|
b.logger.Info("Getting chapter by page", slog.Int("chapter", chapter.Id), slog.Int("page", pageNum))
|
||||||
|
|
||||||
Url := strings.TrimSuffix(chapter.Url, ".html") + fmt.Sprintf("_%v.html", pageNum)
|
Url := strings.TrimSuffix(chapter.Url, ".html") + fmt.Sprintf("_%v.html", pageNum)
|
||||||
|
|
||||||
@@ -405,6 +424,17 @@ func (b *Bilinovel) getChapterByPage(pwPage playwright.Page, chapter *model.Chap
|
|||||||
return false, fmt.Errorf("failed to parse html: %w", err)
|
return false, fmt.Errorf("failed to parse html: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 判断章节是否有下一页
|
||||||
|
n := nextPageUrlRegexp.FindStringSubmatch(resortedHtml)
|
||||||
|
if len(n) != 2 {
|
||||||
|
return false, fmt.Errorf("failed to determine wether there is a next page")
|
||||||
|
}
|
||||||
|
|
||||||
|
s := cleanNextPageUrlRegexp.ReplaceAllString(n[1], "")
|
||||||
|
if strings.Contains(Url, s) {
|
||||||
|
hasNext = true
|
||||||
|
}
|
||||||
|
|
||||||
if pageNum == 1 {
|
if pageNum == 1 {
|
||||||
chapter.Title = doc.Find("#atitle").Text()
|
chapter.Title = doc.Find("#atitle").Text()
|
||||||
}
|
}
|
||||||
@@ -413,7 +443,7 @@ func (b *Bilinovel) getChapterByPage(pwPage playwright.Page, chapter *model.Chap
|
|||||||
content.Find("center").Remove()
|
content.Find("center").Remove()
|
||||||
content.Find(".google-auto-placed").Remove()
|
content.Find(".google-auto-placed").Remove()
|
||||||
|
|
||||||
if strings.Contains(resp.String(), `font-family: "read"`) {
|
if strings.Contains(resortedHtml, `font-family: "read"`) {
|
||||||
html, err := content.Find("p").Last().Html()
|
html, err := content.Find("p").Last().Html()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to get html: %v", err)
|
return false, fmt.Errorf("failed to get html: %v", err)
|
||||||
@@ -486,7 +516,7 @@ func (b *Bilinovel) getChapterByPage(pwPage playwright.Page, chapter *model.Chap
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bilinovel) getImg(url string) ([]byte, error) {
|
func (b *Bilinovel) getImg(url string) ([]byte, error) {
|
||||||
log.Printf("Getting img %v\n", url)
|
b.logger.Info("Getting img", slog.String("url", url))
|
||||||
resp, err := b.restyClient.R().SetHeader("Referer", "https://www.bilinovel.com").Get(url)
|
resp, err := b.restyClient.R().SetHeader("Referer", "https://www.bilinovel.com").Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -497,7 +527,15 @@ func (b *Bilinovel) getImg(url string) ([]byte, error) {
|
|||||||
|
|
||||||
// processContentWithPlaywright 使用复用的浏览器实例处理内容
|
// processContentWithPlaywright 使用复用的浏览器实例处理内容
|
||||||
func (b *Bilinovel) processContentWithPlaywright(page playwright.Page, htmlContent string) (string, error) {
|
func (b *Bilinovel) processContentWithPlaywright(page playwright.Page, htmlContent string) (string, error) {
|
||||||
tempFile, err := os.CreateTemp("", "bilinovel-temp-*.html")
|
// 替换 window.location.replace,防止页面跳转
|
||||||
|
htmlContent = strings.ReplaceAll(htmlContent, "window.location.replace", "console.log")
|
||||||
|
|
||||||
|
tempPath := filepath.Join(os.TempDir(), "bilinovel-downloader")
|
||||||
|
err := os.MkdirAll(tempPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create temp dir: %w", err)
|
||||||
|
}
|
||||||
|
tempFile, err := os.CreateTemp(tempPath, "temp-*.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -510,6 +548,34 @@ func (b *Bilinovel) processContentWithPlaywright(page playwright.Page, htmlConte
|
|||||||
tempFile.Close()
|
tempFile.Close()
|
||||||
tempFilePath := tempFile.Name()
|
tempFilePath := tempFile.Name()
|
||||||
|
|
||||||
|
// // 屏蔽请求
|
||||||
|
// googleAdsDomains := []string{
|
||||||
|
// "adtrafficquality.google",
|
||||||
|
// "doubleclick.net",
|
||||||
|
// "googlesyndication.com",
|
||||||
|
// "googletagmanager.com",
|
||||||
|
// "hm.baidu.com",
|
||||||
|
// "cloudflareinsights.com",
|
||||||
|
// "fsdoa.js", // adblock 检测
|
||||||
|
// "https://www.linovelib.com/novel/", // 阻止从本地文件跳转到在线页面
|
||||||
|
// }
|
||||||
|
// err = page.Route("**/*", func(route playwright.Route) {
|
||||||
|
// for _, d := range googleAdsDomains {
|
||||||
|
// if strings.Contains(route.Request().URL(), d) {
|
||||||
|
// b.logger.Debug("blocking request", slog.String("url", route.Request().URL()))
|
||||||
|
// err := route.Abort("aborted")
|
||||||
|
// if err != nil {
|
||||||
|
// b.logger.Debug("failed to block request", route.Request().URL(), err)
|
||||||
|
// }
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// _ = route.Continue()
|
||||||
|
// })
|
||||||
|
// if err != nil {
|
||||||
|
// return "", fmt.Errorf("failed to intercept requests: %w", err)
|
||||||
|
// }
|
||||||
|
|
||||||
_, err = page.ExpectResponse(func(url string) bool {
|
_, err = page.ExpectResponse(func(url string) bool {
|
||||||
return strings.Contains(url, "chapterlog.js")
|
return strings.Contains(url, "chapterlog.js")
|
||||||
}, func() error {
|
}, func() error {
|
||||||
@@ -519,14 +585,15 @@ func (b *Bilinovel) processContentWithPlaywright(page playwright.Page, htmlConte
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, playwright.PageExpectResponseOptions{
|
}, playwright.PageExpectResponseOptions{
|
||||||
Timeout: playwright.Float(5000),
|
Timeout: playwright.Float(10000),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to wait for network request finish")
|
return "", fmt.Errorf("failed to wait for network request finish")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = page.Locator("#acontent").WaitFor(playwright.LocatorWaitForOptions{
|
err = page.Locator("#acontent").WaitFor(playwright.LocatorWaitForOptions{
|
||||||
State: playwright.WaitForSelectorStateVisible,
|
State: playwright.WaitForSelectorStateVisible,
|
||||||
|
Timeout: playwright.Float(10000),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("could not wait for #acontent: %w", err)
|
return "", fmt.Errorf("could not wait for #acontent: %w", err)
|
||||||
@@ -562,7 +629,7 @@ func (b *Bilinovel) processContentWithPlaywright(page playwright.Page, htmlConte
|
|||||||
return "", fmt.Errorf("failed to remove hidden elements: %w", err)
|
return "", fmt.Errorf("failed to remove hidden elements: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Hidden elements removal result: %s", result)
|
b.logger.Debug("Hidden elements removal result", slog.Any("count", result))
|
||||||
|
|
||||||
processedHTML, err := page.Content()
|
processedHTML, err := page.Content()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
12
main.go
12
main.go
@@ -2,20 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bilinovel-downloader/cmd"
|
"bilinovel-downloader/cmd"
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/playwright-community/playwright-go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.Println("Installing playwright")
|
|
||||||
err := playwright.Install(&playwright.RunOptions{
|
|
||||||
Browsers: []string{"chromium"},
|
|
||||||
Stdout: io.Discard,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf("failed to install playwright")
|
|
||||||
}
|
|
||||||
_ = cmd.RootCmd.Execute()
|
_ = cmd.RootCmd.Execute()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestBilinovel_GetNovel(t *testing.T) {
|
func TestBilinovel_GetNovel(t *testing.T) {
|
||||||
bilinovel, err := bilinovel.New(bilinovel.BilinovelNewOption{Headless: false, Concurrency: 5})
|
bilinovel, err := bilinovel.New(bilinovel.BilinovelNewOption{Concurrency: 5})
|
||||||
bilinovel.SetTextOnly(true)
|
bilinovel.SetTextOnly(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create bilinovel: %v", err)
|
t.Fatalf("failed to create bilinovel: %v", err)
|
||||||
@@ -25,7 +25,7 @@ func TestBilinovel_GetNovel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBilinovel_GetVolume(t *testing.T) {
|
func TestBilinovel_GetVolume(t *testing.T) {
|
||||||
bilinovel, err := bilinovel.New(bilinovel.BilinovelNewOption{Headless: false, Concurrency: 1})
|
bilinovel, err := bilinovel.New(bilinovel.BilinovelNewOption{Concurrency: 1})
|
||||||
bilinovel.SetTextOnly(true)
|
bilinovel.SetTextOnly(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create bilinovel: %v", err)
|
t.Fatalf("failed to create bilinovel: %v", err)
|
||||||
@@ -42,7 +42,7 @@ func TestBilinovel_GetVolume(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBilinovel_GetChapter(t *testing.T) {
|
func TestBilinovel_GetChapter(t *testing.T) {
|
||||||
bilinovel, err := bilinovel.New(bilinovel.BilinovelNewOption{Headless: false, Concurrency: 1})
|
bilinovel, err := bilinovel.New(bilinovel.BilinovelNewOption{Concurrency: 1})
|
||||||
bilinovel.SetTextOnly(true)
|
bilinovel.SetTextOnly(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create bilinovel: %v", err)
|
t.Fatalf("failed to create bilinovel: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user