├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── app ├── base.go ├── berkeley.go ├── berlin.go ├── bluk.go ├── cafaedu.go ├── cuhk.go ├── dzicnlib.go ├── emuseum.go ├── familysearch.go ├── gzlib.go ├── hannomnlv.go ├── harvard.go ├── hathitrust.go ├── hkulib.go ├── huawen.go ├── idp.go ├── iiif.go ├── image_downloader.go ├── keio.go ├── khirin.go ├── kokusho.go ├── korea.go ├── kyotou.go ├── kyudbsnu.go ├── loc.go ├── lodnlgokr.go ├── luoyang.go ├── nationaljp.go ├── ncltw.go ├── ncpssd.go ├── ndljp.go ├── niiac.go ├── njuedu.go ├── nlc.go ├── nlcguji.go ├── nomfoundation.go ├── onbdigital.go ├── ouroots.go ├── oxacuk.go ├── princeton.go ├── queue.go ├── rslru.go ├── ryukoku.go ├── sammlungen.go ├── sdutcm.go ├── siedu.go ├── stanford.go ├── szlib.go ├── template.go ├── template2.go ├── tianyige.go ├── tjlswx.go ├── tnm.go ├── usthk.go ├── utokyo.go ├── war1931.go ├── waseda.go ├── wzlib.go ├── yndfz.go ├── yonezawa.go └── zhucheng.go ├── bookget-gui ├── .clang-format ├── .editorconfig ├── .gitignore ├── BrowserWindow.h ├── BrowserWindowCore.cpp ├── BrowserWindowDownload.cpp ├── BrowserWindowFilepath.cpp ├── BrowserWindowMessage.cpp ├── BrowserWindowThread.cpp ├── BrowserWindowWebView.cpp ├── CheckFailure.cpp ├── CheckFailure.h ├── Config.cpp ├── Config.h ├── Downloader.cpp ├── Downloader.h ├── HttpClient.cpp ├── HttpClient.h ├── Resource.h ├── SharedMemory.cpp ├── SharedMemory.h ├── Tab.cpp ├── Tab.h ├── Util.cpp ├── Util.h ├── bookget.ico ├── bookgetApp.cpp ├── bookgetApp.h ├── bookgetApp.rc ├── bookgetApp.sln ├── bookgetApp.vcxproj ├── bookgetApp.vcxproj.filters ├── config.yaml ├── env.cpp ├── env.h ├── framework.h ├── gui │ ├── commands.js │ ├── content_ui │ │ ├── favorites.html │ │ ├── favorites.js │ │ ├── history.css │ │ ├── history.html │ │ ├── history.js │ │ ├── img │ │ │ ├── close.png │ │ │ ├── favorites.png │ │ │ ├── history.png │ │ │ └── settings.png │ │ ├── items.css │ │ ├── settings.css │ │ ├── settings.html │ │ ├── settings.js │ │ └── styles.css │ └── controls_ui │ │ ├── address-bar.css │ │ ├── controls.css │ │ ├── default.css │ │ ├── default.html │ │ ├── default.js │ │ ├── favorites.js │ │ ├── history.js │ │ ├── img │ │ ├── cancel.png │ │ ├── favicon.png │ │ ├── favorite.png │ │ ├── favorited.png │ │ ├── goBack.png │ │ ├── goBack_disabled.png │ │ ├── goForward.png │ │ ├── goForward_disabled.png │ │ ├── insecure.png │ │ ├── neutral.png │ │ ├── options.png │ │ ├── reload.png │ │ ├── secure.png │ │ └── unknown.png │ │ ├── options.css │ │ ├── options.html │ │ ├── options.js │ │ ├── storage.js │ │ ├── strip.css │ │ ├── styles.css │ │ └── tabs.js ├── scripts │ ├── guji.nlc.cn.js │ ├── rbook.ncl.edu.tw.js │ └── szlib.clcn.net.cn.js └── targetver.h ├── cmd └── bookget.go ├── config ├── conf.go ├── config.go ├── constant.go └── init.go ├── go.mod ├── go.sum ├── model ├── cuhk │ └── cuhk.go ├── family │ └── family.go ├── iiif │ ├── dzi.go │ └── iiif.go ├── korea │ └── korea.go ├── loc │ └── loc.go ├── njuedu │ └── njuedu.go ├── nlc │ └── nlc.go ├── onbdigital │ └── onbdigital.go ├── ouroots │ └── ouroots.go ├── princeton │ └── princeton.go ├── rslru │ └── rslru.go ├── sdutcm │ └── sdutcm.go ├── sillokgokr │ └── sillokgokr.go ├── szLib │ └── szLib.go ├── tianyige │ └── tianyige.go ├── usthk │ └── usthk.go ├── war │ └── war.go ├── wzlib │ └── wzlib.go └── yndfz │ └── yndfz.go ├── pkg ├── chttp │ ├── cookie.go │ └── header.go ├── crypt │ ├── aes.go │ └── escape.go ├── downloader │ ├── base.go │ ├── downloader.go │ └── iiif.go ├── file │ └── file.go ├── gohttp │ ├── chunk.go │ ├── download.go │ ├── filename.go │ ├── gohttp.go │ ├── multithread.go │ ├── options.go │ ├── request.go │ └── response.go ├── hash │ ├── hash.go │ └── hash_test.go ├── progressbar │ ├── progressbar.go │ └── spinners.go ├── queue │ └── queue.go ├── quickxorhash │ ├── quickxorhash.go │ ├── quickxorhash_test.go │ ├── xor.go │ └── xor_1.20.go ├── sharedmemory │ ├── sharedmemory_unix.go │ ├── sharedmemory_windows.go │ ├── windowsmem_unix.go │ └── windowsmem_windows.go ├── util │ ├── cmd_unix.go │ ├── cmd_windows.go │ ├── cn2number.go │ ├── file.go │ ├── file_type.go │ ├── number.go │ ├── sort.go │ └── text.go └── version │ └── version_check.go └── router └── interface.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Release bookget 2 | 3 | on: 4 | push: 5 | tags: [ "v*" ] 6 | jobs: 7 | build: 8 | strategy: 9 | matrix: 10 | platform: 11 | - linux-amd64 12 | - linux-arm64 13 | - darwin-amd64 14 | - darwin-arm64 15 | - windows-amd64 16 | runs-on: ${{ 17 | contains(matrix.platform, 'linux') && 'ubuntu-latest' || 18 | contains(matrix.platform, 'darwin') && 'macos-latest' || 19 | 'windows-latest'}} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v4 27 | with: 28 | go-version: '1.23' 29 | 30 | - name: Install Make (Windows) 31 | if: runner.os == 'Windows' 32 | run: choco install make -y 33 | 34 | - name: Build 35 | run: | 36 | make ${{ matrix.platform }} 37 | 38 | - name: Verify artifacts 39 | run: ls -R dist/ 40 | 41 | - name: Upload artifact 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: bookget-${{ matrix.platform }} 45 | path: dist/${{ matrix.platform }}/* 46 | 47 | release: 48 | needs: build 49 | runs-on: ubuntu-latest 50 | permissions: 51 | contents: write # 必须添加这个权限 52 | steps: 53 | - name: Download artifacts 54 | uses: actions/download-artifact@v4 55 | with: 56 | path: dist 57 | 58 | - name: List files for debugging 59 | run: find dist/ -type f 60 | 61 | - name: Create Release 62 | uses: softprops/action-gh-release@v1 63 | with: 64 | token: ${{ secrets.RELEASE_TOKEN }} # 替换默认的GITHUB_TOKEN 65 | tag_name: ${{ github.ref_name }} 66 | name: "bookget ${{ github.ref_name }}" 67 | body: "| 68 | 🚀 Automated release by **github-actions[bot]** 69 | 70 | **Assets:** 71 | - Linux: `bookget-linux` / `bookget-linux-arm64` 72 | - Windows: `bookget.exe` 73 | - MacOS: `bookget-macos` / `bookget-macos-arm64` 74 | " 75 | files: | 76 | dist/**/bookget* 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /dist/ 3 | /target/ 4 | /dependencies/ 5 | *.exe 6 | /.idea/ 7 | .fleet/ 8 | *.xml 9 | *.7z 10 | *.bak 11 | *.zip -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 编译配置 2 | GO ?= go 3 | GOFLAGS ?= -trimpath 4 | LDFLAGS ?= -s -w 5 | TARGET ?= bookget 6 | DIST_DIR ?= dist 7 | 8 | # 构建目标 9 | .PHONY: build 10 | build: 11 | @echo "Building $(TARGET) for $(GOOS)-$(GOARCH)" 12 | @mkdir -p $(DIST_DIR)/$(GOOS)-$(GOARCH) 13 | CGO_ENABLED=0 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" \ 14 | -o $(DIST_DIR)/$(GOOS)-$(GOARCH)/$(TARGET)$(SUFFIX) ./cmd/ 15 | 16 | # 跨平台构建(调用示例) 17 | .PHONY: release 18 | release: linux-amd64 linux-arm64 darwin-amd64 darwin-arm64 windows-amd64 19 | 20 | linux-amd64: 21 | @$(MAKE) build GOOS=linux GOARCH=amd64 SUFFIX=-linux 22 | 23 | linux-arm64: 24 | @$(MAKE) build GOOS=linux GOARCH=arm64 SUFFIX=-linux-arm64 25 | 26 | darwin-amd64: 27 | @$(MAKE) build GOOS=darwin GOARCH=amd64 SUFFIX=-macos 28 | 29 | darwin-arm64: 30 | @$(MAKE) build GOOS=darwin GOARCH=arm64 SUFFIX=-macos-arm64 31 | 32 | windows-amd64: 33 | @$(MAKE) build GOOS=windows GOARCH=amd64 SUFFIX=.exe 34 | 35 | # 清理 36 | .PHONY: clean 37 | clean: 38 | @rm -rf $(DIST_DIR) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 簡介 2 | 3 | bookget 数字古籍图书下载工具,已支持约 50+ 个数字图书馆。 4 | 5 | ### 使用説明 6 | 1. 打开 [https://github.com/deweizhu/bookget/releases](https://github.com/deweizhu/bookget/releases/latest) 下载最新版。 7 | 1. [必读]使用手册wiki https://github.com/deweizhu/bookget/wiki 8 | 1. 此项目代码仅供学习研究使用,欢迎有能力的朋友git clone 代码二次开发维护您自己的版本。 9 | 10 | #### 源碼編譯 11 | 从源码构建,仅对计算机程序员参考。普通用户可直接跳过阅读。 12 | 阅读 [golang 官方文档](https://golang.google.cn/doc/install) ,给您的电脑安装 golang 开发环境。 13 | ```shell 14 | git clone https://github.com/deweizhu/bookget.git 15 | cd bookget 16 | 17 | # 本地开发时可直接运行 18 | make linux-amd64 # 编译Linux版本 19 | make windows-amd64 # 编译Windows版本 20 | make release # 编译所有平台 21 | ``` 22 | 23 | - For Win可用环境:windows 10 x64 / windows 11 (自2024/01/12日起,引入bookget-gui只适用于Win10/11 x64系统)。 24 | 25 | # 新書推薦 26 | 27 | 本書即以清代避諱為例,試探如何運用融合數位與傳統的e考據之法,重新定義避諱學2.0該有的研究模式。這將是治清代的文史工作者案頭不可或缺的一本專著。[^1] 28 | - [清代避諱研究](https://gpi.culture.tw/books/1011300273) 29 | - 作者: 黃一農 / 黄一农 30 | - 出版社: 台灣清華大學出版社 31 | - 副标题: e考據的學術實踐 32 | - 出版年: 2024-4 33 | - ISBN: 9786269724987 34 | [^1]:[https://gpi.culture.tw/books/1011300273](https://gpi.culture.tw/books/1011300273) 35 | 36 | ### 第三方社區/網站 37 | - 书格 https://www.shuge.org 38 | ### Arch Linux AUR 可选包 39 | 以下是热心网友维护的 Linux 软件仓库,便于 Arch Linux 用户安装使用。 40 | - https://aur.archlinux.org/packages/bookget 41 | - https://aur.archlinux.org/packages/bookget-bin 42 | - https://aur.archlinux.org/packages/bookget-git 43 | - [bookget - Arch Linux 中文维基](https://wiki.archlinuxcn.org/wiki/Bookget) 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/base.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bookget/config" 5 | "bookget/pkg/chttp" 6 | ) 7 | 8 | func BuildRequestHeader() map[string]string { 9 | httpHeaders := map[string]string{"User-Agent": config.Conf.UserAgent} 10 | cookies, _ := chttp.ReadCookiesFromFile(config.Conf.CookieFile) 11 | if cookies != "" { 12 | httpHeaders["Cookie"] = cookies 13 | } 14 | 15 | headers, err := chttp.ReadHeadersFromFile(config.Conf.HeaderFile) 16 | if err == nil { 17 | for key, value := range headers { 18 | httpHeaders[key] = value 19 | } 20 | } 21 | return httpHeaders 22 | } 23 | -------------------------------------------------------------------------------- /app/gzlib.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bookget/config" 5 | "bookget/pkg/chttp" 6 | "bookget/pkg/downloader" 7 | "context" 8 | "crypto/tls" 9 | "fmt" 10 | "io" 11 | "log" 12 | "net/http" 13 | "net/http/cookiejar" 14 | "net/url" 15 | "regexp" 16 | "time" 17 | ) 18 | 19 | type Gzlib struct { 20 | dm *downloader.DownloadManager 21 | ctx context.Context 22 | cancel context.CancelFunc 23 | client *http.Client 24 | 25 | rawUrl string 26 | parsedUrl *url.URL 27 | savePath string 28 | bookId string 29 | } 30 | 31 | func NewGzlib() *Gzlib { 32 | ctx, cancel := context.WithCancel(context.Background()) 33 | dm := downloader.NewDownloadManager(ctx, cancel, config.Conf.MaxConcurrent) 34 | 35 | // 创建自定义 Transport 忽略 SSL 验证 36 | tr := &http.Transport{ 37 | TLSClientConfig: &tls.Config{ 38 | InsecureSkipVerify: true, 39 | }, 40 | } 41 | jar, _ := cookiejar.New(nil) 42 | return &Gzlib{ 43 | // 初始化字段 44 | dm: dm, 45 | client: &http.Client{Timeout: config.Conf.Timeout * time.Second, Jar: jar, Transport: tr}, 46 | ctx: ctx, 47 | cancel: cancel, 48 | } 49 | } 50 | 51 | func (r *Gzlib) GetRouterInit(sUrl string) (map[string]interface{}, error) { 52 | r.rawUrl = sUrl 53 | r.parsedUrl, _ = url.Parse(sUrl) 54 | msg, err := r.Run() 55 | return map[string]interface{}{ 56 | "url": sUrl, 57 | "msg": msg, 58 | }, err 59 | } 60 | 61 | func (r *Gzlib) getBookId() (bookId string) { 62 | m := regexp.MustCompile(`(?i)id=([A-z0-9_-]+)`).FindStringSubmatch(r.rawUrl) 63 | if m != nil { 64 | bookId = m[1] 65 | } 66 | return bookId 67 | } 68 | 69 | func (r *Gzlib) Run() (msg string, err error) { 70 | r.bookId = r.getBookId() 71 | if r.bookId == "" { 72 | return "[err=getBookId]", err 73 | } 74 | r.savePath = config.Conf.Directory 75 | 76 | apiUrl := fmt.Sprintf("https://%s/attach/GZDD/Attach/%s.pdf", r.parsedUrl.Hostname(), r.bookId) 77 | fileName := fmt.Sprintf("%s.pdf", r.bookId) 78 | 79 | headers := BuildRequestHeader() 80 | r.dm.UseSizeBar = true 81 | // 添加GET下载任务 82 | r.dm.AddTask( 83 | apiUrl, 84 | "GET", 85 | headers, 86 | nil, 87 | r.savePath, 88 | fileName, 89 | config.Conf.Threads, 90 | ) 91 | r.dm.Start() 92 | 93 | return "", err 94 | } 95 | 96 | func (r *Gzlib) getBody(sUrl string) ([]byte, error) { 97 | req, err := http.NewRequest("GET", sUrl, nil) 98 | if err != nil { 99 | return nil, err 100 | } 101 | req.Header.Set("User-Agent", config.Conf.UserAgent) 102 | req.Header.Set("Origin", "https://"+r.parsedUrl.Host) 103 | req.Header.Set("Referer", r.rawUrl) 104 | 105 | cookies, _ := chttp.ReadCookiesFromFile(config.Conf.CookieFile) 106 | if cookies != "" { 107 | req.Header.Set("Cookie", cookies) 108 | } 109 | 110 | headers, err := chttp.ReadHeadersFromFile(config.Conf.HeaderFile) 111 | if err == nil { 112 | for key, value := range headers { 113 | req.Header.Set(key, value) 114 | } 115 | } 116 | 117 | resp, err := r.client.Do(req.WithContext(r.ctx)) 118 | if err != nil { 119 | return nil, err 120 | } 121 | defer func() { 122 | if err := resp.Body.Close(); err != nil { 123 | log.Printf("close body err=%v", err) 124 | } 125 | }() 126 | 127 | if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { 128 | err = fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode) 129 | return nil, err 130 | } 131 | body, err := io.ReadAll(resp.Body) 132 | if err != nil { 133 | return nil, err 134 | } 135 | return body, nil 136 | } 137 | -------------------------------------------------------------------------------- /app/hannomnlv.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bookget/config" 5 | "bookget/pkg/gohttp" 6 | "context" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net/http/cookiejar" 11 | "net/url" 12 | "path" 13 | "regexp" 14 | "sync" 15 | ) 16 | 17 | type HannomNlv struct { 18 | dt *DownloadTask 19 | body []byte 20 | } 21 | 22 | func NewHannomNlv() *HannomNlv { 23 | return &HannomNlv{ 24 | // 初始化字段 25 | dt: new(DownloadTask), 26 | } 27 | } 28 | 29 | func (r *HannomNlv) GetRouterInit(sUrl string) (map[string]interface{}, error) { 30 | msg, err := r.Run(sUrl) 31 | return map[string]interface{}{ 32 | "url": sUrl, 33 | "msg": msg, 34 | }, err 35 | } 36 | 37 | func (r *HannomNlv) Run(sUrl string) (msg string, err error) { 38 | r.dt.UrlParsed, err = url.Parse(sUrl) 39 | r.dt.Url = sUrl 40 | r.dt.Jar, _ = cookiejar.New(nil) 41 | r.dt.BookId = r.getBookId(r.dt.Url) 42 | if r.dt.BookId == "" { 43 | return "requested URL was not found.", err 44 | } 45 | return r.download() 46 | } 47 | 48 | func (r *HannomNlv) getBookId(sUrl string) (bookId string) { 49 | var err error 50 | r.body, err = getBody(sUrl, r.dt.Jar) 51 | if err != nil { 52 | return "" 53 | } 54 | m := regexp.MustCompile(`var[\s+]documentOID[\s+]=[\s+]['"]([^“]+?)['"];`).FindSubmatch(r.body) 55 | if m != nil { 56 | return string(m[1]) 57 | } 58 | return "" 59 | } 60 | 61 | func (r *HannomNlv) download() (msg string, err error) { 62 | log.Printf("Get %s\n", r.dt.Url) 63 | r.dt.SavePath = config.Conf.Directory 64 | canvases, err := r.getCanvases(r.dt.Url, r.dt.Jar) 65 | if err != nil || canvases == nil { 66 | fmt.Println(err) 67 | } 68 | log.Printf(" %d pages \n", len(canvases)) 69 | r.do(canvases) 70 | return "", nil 71 | } 72 | 73 | func (r *HannomNlv) do(imgUrls []string) (msg string, err error) { 74 | if imgUrls == nil { 75 | return "", nil 76 | } 77 | size := len(imgUrls) 78 | fmt.Println() 79 | var wg sync.WaitGroup 80 | q := QueueNew(int(config.Conf.Threads)) 81 | for i, uri := range imgUrls { 82 | if uri == "" || !config.PageRange(i, size) { 83 | continue 84 | } 85 | sortId := fmt.Sprintf("%04d", i+1) 86 | filename := sortId + config.Conf.FileExt 87 | dest := path.Join(r.dt.SavePath, filename) 88 | if FileExist(dest) { 89 | continue 90 | } 91 | imgUrl := uri 92 | fmt.Println() 93 | log.Printf("Get %d/%d %s\n", i+1, size, imgUrl) 94 | wg.Add(1) 95 | q.Go(func() { 96 | defer wg.Done() 97 | ctx := context.Background() 98 | opts := gohttp.Options{ 99 | DestFile: dest, 100 | Overwrite: false, 101 | Concurrency: 1, 102 | CookieFile: config.Conf.CookieFile, 103 | HeaderFile: config.Conf.HeaderFile, 104 | CookieJar: r.dt.Jar, 105 | Headers: map[string]interface{}{ 106 | "User-Agent": config.Conf.UserAgent, 107 | }, 108 | } 109 | gohttp.FastGet(ctx, imgUrl, opts) 110 | fmt.Println() 111 | }) 112 | } 113 | wg.Wait() 114 | fmt.Println() 115 | return "", nil 116 | } 117 | 118 | func (r *HannomNlv) getVolumes(sUrl string, jar *cookiejar.Jar) (volumes []string, err error) { 119 | //TODO implement me 120 | panic("implement me") 121 | } 122 | 123 | func (r *HannomNlv) getCanvases(sUrl string, jar *cookiejar.Jar) (canvases []string, err error) { 124 | matches := regexp.MustCompile(`'([^']+)':\{'w':([0-9]+),'h':([0-9]+)\}`).FindAllSubmatch(r.body, -1) 125 | if matches == nil { 126 | return nil, errors.New("No image") 127 | } 128 | apiUrl := r.dt.UrlParsed.Scheme + "://" + r.dt.UrlParsed.Host 129 | match := regexp.MustCompile(`imageserverPageTileImageRequest[\s+]=[\s+]['"]([^;]+)['"];`).FindSubmatch(r.body) 130 | if match != nil { 131 | apiUrl += string(match[1]) 132 | } else { 133 | apiUrl += "/hannom/cgi-bin/imageserver/imageserver.pl?color=all&ext=jpg" 134 | } 135 | for _, m := range matches { 136 | imgUrl := apiUrl + fmt.Sprintf("&oid=%s.%s&key=&width=%s&crop=0,0,%s,%s", r.dt.BookId, m[1], m[2], m[2], m[3]) 137 | canvases = append(canvases, imgUrl) 138 | } 139 | return canvases, err 140 | } 141 | 142 | func (r *HannomNlv) getBody(sUrl string, jar *cookiejar.Jar) ([]byte, error) { 143 | //TODO implement me 144 | panic("implement me") 145 | } 146 | 147 | func (r *HannomNlv) postBody(sUrl string, d []byte) ([]byte, error) { 148 | //TODO implement me 149 | panic("implement me") 150 | } 151 | -------------------------------------------------------------------------------- /app/huawen.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bookget/config" 5 | "bookget/pkg/gohttp" 6 | "bookget/pkg/util" 7 | "context" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "net/http/cookiejar" 12 | "net/url" 13 | "path" 14 | "regexp" 15 | "strings" 16 | ) 17 | 18 | type Huawen struct { 19 | dt *DownloadTask 20 | } 21 | 22 | func NewHuawen() *Huawen { 23 | return &Huawen{ 24 | // 初始化字段 25 | dt: new(DownloadTask), 26 | } 27 | } 28 | 29 | func (r *Huawen) GetRouterInit(sUrl string) (map[string]interface{}, error) { 30 | msg, err := r.Run(sUrl) 31 | return map[string]interface{}{ 32 | "url": sUrl, 33 | "msg": msg, 34 | }, err 35 | } 36 | 37 | func (r *Huawen) Run(sUrl string) (msg string, err error) { 38 | if !strings.Contains(sUrl, "/reader") && strings.Contains(sUrl, "/zh-tw/book/") { 39 | sUrl += "/reader" 40 | } 41 | 42 | r.dt.UrlParsed, err = url.Parse(sUrl) 43 | r.dt.Url = sUrl 44 | 45 | r.dt.BookId = getBookId(r.dt.Url) 46 | if r.dt.BookId == "" { 47 | return "requested URL was not found.", err 48 | } 49 | r.dt.Jar, _ = cookiejar.New(nil) 50 | return r.download() 51 | } 52 | 53 | func (r *Huawen) getBookId(sUrl string) (bookId string) { 54 | //TODO implement me 55 | panic("implement me") 56 | } 57 | 58 | func (r *Huawen) download() (msg string, err error) { 59 | log.Printf("Get %s\n", r.dt.Url) 60 | 61 | respVolume, err := r.getVolumes(r.dt.Url, r.dt.Jar) 62 | if err != nil { 63 | fmt.Println(err) 64 | return "getVolumes", err 65 | } 66 | for i, vol := range respVolume { 67 | if !config.VolumeRange(i) { 68 | continue 69 | } 70 | r.dt.SavePath = config.Conf.Directory 71 | log.Printf(" %d/%d PDFs \n", i+1, len(respVolume)) 72 | r.do(vol) 73 | } 74 | return "", nil 75 | } 76 | 77 | func (r *Huawen) do(pdfUrl string) (msg string, err error) { 78 | filename := util.FileName(pdfUrl) 79 | dest := path.Join(r.dt.SavePath, filename) 80 | if FileExist(dest) { 81 | return "", nil 82 | } 83 | u, err := url.Parse(pdfUrl) 84 | ctx := context.Background() 85 | opts := gohttp.Options{ 86 | DestFile: dest, 87 | Overwrite: false, 88 | Concurrency: 1, 89 | CookieFile: config.Conf.CookieFile, 90 | HeaderFile: config.Conf.HeaderFile, 91 | CookieJar: r.dt.Jar, 92 | Headers: map[string]interface{}{ 93 | "User-Agent": config.Conf.UserAgent, 94 | "Referer": "https://" + r.dt.UrlParsed.Host + "/pdfjs/web/viewer.html?file=" + u.Path, 95 | }, 96 | } 97 | _, err = gohttp.FastGet(ctx, pdfUrl, opts) 98 | if err != nil { 99 | fmt.Println(err) 100 | } 101 | util.PrintSleepTime(config.Conf.Sleep) 102 | fmt.Println() 103 | return "", nil 104 | } 105 | 106 | func (r *Huawen) getVolumes(sUrl string, jar *cookiejar.Jar) (volumes []string, err error) { 107 | bs, err := r.getBody(sUrl, jar) 108 | if err != nil { 109 | return 110 | } 111 | matches := regexp.MustCompile(`(?i)viewer.html\?file=([^"]+)"`).FindAllSubmatch(bs, -1) 112 | if matches == nil { 113 | return 114 | } 115 | for _, match := range matches { 116 | sPath := strings.TrimSpace(string(match[1])) 117 | if pos := strings.Index(sPath, "&"); pos > 0 { 118 | sPath = sPath[:pos] 119 | } 120 | pdfUrl := "https://" + r.dt.UrlParsed.Host + sPath 121 | volumes = append(volumes, pdfUrl) 122 | } 123 | return volumes, nil 124 | } 125 | 126 | func (r *Huawen) getCanvases(sUrl string, jar *cookiejar.Jar) (canvases []string, err error) { 127 | //TODO implement me 128 | panic("implement me") 129 | } 130 | 131 | func (r *Huawen) getBody(apiUrl string, jar *cookiejar.Jar) ([]byte, error) { 132 | referer := url.QueryEscape(apiUrl) 133 | ctx := context.Background() 134 | cli := gohttp.NewClient(ctx, gohttp.Options{ 135 | CookieFile: config.Conf.CookieFile, 136 | CookieJar: jar, 137 | Headers: map[string]interface{}{ 138 | "User-Agent": config.Conf.UserAgent, 139 | "Referer": referer, 140 | }, 141 | }) 142 | resp, err := cli.Get(apiUrl) 143 | if err != nil { 144 | return nil, err 145 | } 146 | bs, _ := resp.GetBody() 147 | if resp.GetStatusCode() != 200 || bs == nil { 148 | return nil, errors.New(fmt.Sprintf("ErrCode:%d, %s", resp.GetStatusCode(), resp.GetReasonPhrase())) 149 | } 150 | return bs, nil 151 | } 152 | -------------------------------------------------------------------------------- /app/idp.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bookget/config" 5 | "bookget/pkg/gohttp" 6 | "bookget/pkg/progressbar" 7 | "context" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "net/http/cookiejar" 12 | "net/url" 13 | "path/filepath" 14 | "regexp" 15 | ) 16 | 17 | type Idp struct { 18 | dt *DownloadTask 19 | bar *progressbar.ProgressBar 20 | } 21 | 22 | func NewIdp() *Idp { 23 | return &Idp{ 24 | // 初始化字段 25 | dt: new(DownloadTask), 26 | } 27 | } 28 | 29 | func (r *Idp) GetRouterInit(sUrl string) (map[string]interface{}, error) { 30 | msg, err := r.Run(sUrl) 31 | return map[string]interface{}{ 32 | "url": sUrl, 33 | "msg": msg, 34 | }, err 35 | } 36 | 37 | func (r *Idp) Run(sUrl string) (msg string, err error) { 38 | r.dt.UrlParsed, err = url.Parse(sUrl) 39 | r.dt.Url = sUrl 40 | r.dt.BookId = r.getBookId(r.dt.Url) 41 | if r.dt.BookId == "" { 42 | return "requested URL was not found.", err 43 | } 44 | r.dt.Jar, _ = cookiejar.New(nil) 45 | return r.download() 46 | } 47 | 48 | func (r *Idp) getBookId(sUrl string) (bookId string) { 49 | m := regexp.MustCompile(`uid=([A-Za-z0-9]+)`).FindStringSubmatch(sUrl) 50 | if m != nil { 51 | bookId = m[1] 52 | } 53 | return bookId 54 | } 55 | 56 | func (r *Idp) download() (msg string, err error) { 57 | log.Printf("Get %s\n", r.dt.Url) 58 | 59 | canvases, err := r.getCanvases(r.dt.BookId, r.dt.Jar) 60 | if err != nil || canvases == nil { 61 | fmt.Println(err) 62 | return "requested URL was not found.", err 63 | } 64 | //不按卷下载,所有图片存一个目录 65 | r.dt.SavePath = config.Conf.Directory 66 | sizeCanvases := len(canvases) 67 | fmt.Println() 68 | ext := ".jpg" 69 | r.bar = progressbar.Default(int64(sizeCanvases), "downloading") 70 | ctx := context.Background() 71 | for i, imgUrl := range canvases { 72 | if !config.PageRange(i, sizeCanvases) || imgUrl == "" { 73 | continue 74 | } 75 | sortId := fmt.Sprintf("%04d", i+1) 76 | dest := filepath.Join(r.dt.SavePath, sortId+ext) 77 | cli := gohttp.NewClient(ctx, gohttp.Options{ 78 | DestFile: dest, 79 | CookieJar: r.dt.Jar, 80 | CookieFile: config.Conf.CookieFile, 81 | Headers: map[string]interface{}{ 82 | "User-Agent": config.Conf.UserAgent, 83 | }, 84 | }) 85 | _, err = cli.Get(imgUrl) 86 | if err != nil { 87 | log.Println(err) 88 | break 89 | } 90 | r.bar.Add(1) 91 | } 92 | return "", nil 93 | } 94 | 95 | func (r *Idp) do(imgUrls []string) (msg string, err error) { 96 | //TODO implement me 97 | panic("implement me") 98 | } 99 | 100 | func (r *Idp) getVolumes(sUrl string, jar *cookiejar.Jar) (volumes []string, err error) { 101 | //TODO implement me 102 | panic("implement me") 103 | } 104 | 105 | func (r *Idp) getCanvases(sUrl string, jar *cookiejar.Jar) ([]string, error) { 106 | bs, err := r.getBody(sUrl, jar) 107 | if err != nil { 108 | return nil, err 109 | } 110 | //imageUrls[0] = "/image_IDP.a4d?type=loadRotatedMainImage;recnum=31305;rotate=0;imageType=_M"; 111 | //imageRecnum[0] = "31305"; 112 | m := regexp.MustCompile(`imageRecnum\[\d+\][ \S]?=[ \S]?"(\d+)";`).FindAllSubmatch(bs, -1) 113 | if m == nil { 114 | return []string{}, nil 115 | } 116 | canvases := make([]string, 0, len(m)) 117 | for _, v := range m { 118 | id := string(v[1]) 119 | imgUrl := fmt.Sprintf("%s://%s/image_IDP.a4d?type=loadRotatedMainImage;recnum=%s;rotate=0;imageType=_L", 120 | r.dt.UrlParsed.Scheme, r.dt.UrlParsed.Host, id) 121 | canvases = append(canvases, imgUrl) 122 | } 123 | return canvases, nil 124 | } 125 | 126 | func (r *Idp) getBody(sUrl string, jar *cookiejar.Jar) ([]byte, error) { 127 | ctx := context.Background() 128 | cli := gohttp.NewClient(ctx, gohttp.Options{ 129 | CookieFile: config.Conf.CookieFile, 130 | CookieJar: jar, 131 | Headers: map[string]interface{}{ 132 | "User-Agent": config.Conf.UserAgent, 133 | }, 134 | }) 135 | resp, err := cli.Get(sUrl) 136 | if err != nil { 137 | return nil, err 138 | } 139 | bs, _ := resp.GetBody() 140 | if resp.GetStatusCode() != 200 || bs == nil { 141 | return nil, errors.New(fmt.Sprintf("ErrCode:%d, %s", resp.GetStatusCode(), resp.GetReasonPhrase())) 142 | } 143 | return bs, nil 144 | } 145 | 146 | func (r *Idp) postBody(sUrl string, d []byte) ([]byte, error) { 147 | //TODO implement me 148 | panic("implement me") 149 | } 150 | -------------------------------------------------------------------------------- /app/korea.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bookget/config" 5 | "bookget/model/korea" 6 | "bookget/pkg/gohttp" 7 | "bookget/pkg/util" 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "log" 12 | "net/http/cookiejar" 13 | "net/url" 14 | "path" 15 | "regexp" 16 | "sync" 17 | ) 18 | 19 | type Korea struct { 20 | dt *DownloadTask 21 | body []byte 22 | } 23 | 24 | func NewKorea() *Korea { 25 | return &Korea{ 26 | // 初始化字段 27 | dt: new(DownloadTask), 28 | } 29 | } 30 | 31 | func (r *Korea) GetRouterInit(sUrl string) (map[string]interface{}, error) { 32 | msg, err := r.Run(sUrl) 33 | return map[string]interface{}{ 34 | "url": sUrl, 35 | "msg": msg, 36 | }, err 37 | } 38 | 39 | func (r *Korea) Run(sUrl string) (msg string, err error) { 40 | r.dt = new(DownloadTask) 41 | r.dt.UrlParsed, err = url.Parse(sUrl) 42 | r.dt.Url = sUrl 43 | r.dt.BookId = r.getBookId(r.dt.Url) 44 | if r.dt.BookId == "" { 45 | return "requested URL was not found.", err 46 | } 47 | r.dt.Jar, _ = cookiejar.New(nil) 48 | return r.download() 49 | } 50 | 51 | func (r *Korea) getBookId(sUrl string) (bookId string) { 52 | m := regexp.MustCompile(`uci=([^&]+)`).FindStringSubmatch(sUrl) 53 | if m != nil { 54 | return m[1] 55 | } 56 | return "" 57 | } 58 | 59 | func (r *Korea) download() (msg string, err error) { 60 | log.Printf("Get %s\n", r.dt.Url) 61 | 62 | respVolume, err := r.getVolumes(r.dt.Url, r.dt.Jar) 63 | if err != nil { 64 | fmt.Println(err) 65 | return "getVolumes", err 66 | } 67 | sizeVol := len(respVolume) 68 | for i, vol := range respVolume { 69 | if !config.VolumeRange(i) { 70 | continue 71 | } 72 | if sizeVol == 1 { 73 | r.dt.SavePath = config.Conf.Directory 74 | } else { 75 | vid := fmt.Sprintf("%04d", i+1) 76 | r.dt.SavePath = CreateDirectory(vid) 77 | } 78 | if err != nil || vol.Canvases == nil { 79 | continue 80 | } 81 | log.Printf(" %d/%d volume, %d pages \n", i+1, sizeVol, len(vol.Canvases)) 82 | r.do(vol.Canvases) 83 | } 84 | return "", nil 85 | } 86 | 87 | func (r *Korea) do(imgUrls []string) (msg string, err error) { 88 | if imgUrls == nil { 89 | return 90 | } 91 | fmt.Println() 92 | size := len(imgUrls) 93 | 94 | var wg sync.WaitGroup 95 | q := QueueNew(int(config.Conf.Threads)) 96 | for i, uri := range imgUrls { 97 | if uri == "" || !config.PageRange(i, size) { 98 | continue 99 | } 100 | sortId := fmt.Sprintf("%04d", i+1) 101 | filename := sortId + config.Conf.FileExt 102 | dest := path.Join(r.dt.SavePath, filename) 103 | if FileExist(dest) { 104 | continue 105 | } 106 | imgUrl := uri 107 | log.Printf("Get %d/%d, %s\n", i+1, size, imgUrl) 108 | wg.Add(1) 109 | q.Go(func() { 110 | defer wg.Done() 111 | ctx := context.Background() 112 | opts := gohttp.Options{ 113 | DestFile: dest, 114 | Overwrite: false, 115 | Concurrency: 1, 116 | CookieFile: config.Conf.CookieFile, 117 | HeaderFile: config.Conf.HeaderFile, 118 | CookieJar: r.dt.Jar, 119 | Headers: map[string]interface{}{ 120 | "User-Agent": config.Conf.UserAgent, 121 | }, 122 | } 123 | gohttp.FastGet(ctx, imgUrl, opts) 124 | util.PrintSleepTime(config.Conf.Sleep) 125 | fmt.Println() 126 | }) 127 | } 128 | wg.Wait() 129 | fmt.Println() 130 | return "", err 131 | } 132 | 133 | func (r *Korea) getVolumes(sUrl string, jar *cookiejar.Jar) (volumes []korea.PartialCanvases, err error) { 134 | bs, err := getBody(sUrl, jar) 135 | if err != nil { 136 | return nil, err 137 | } 138 | matches := regexp.MustCompile(`var[\s+]bookInfos[\s+]=[\s+]([^;]+);`).FindSubmatch(bs) 139 | if matches == nil { 140 | return 141 | } 142 | resp := make([]korea.Response, 0, 100) 143 | if err = json.Unmarshal(matches[1], &resp); err != nil { 144 | return nil, err 145 | } 146 | ossHost := fmt.Sprintf("%s://%s/data/des/%s/IMG/", r.dt.UrlParsed.Scheme, r.dt.UrlParsed.Host, r.dt.BookId) 147 | for _, match := range resp { 148 | vol := korea.PartialCanvases{ 149 | Directory: "", 150 | Title: "", 151 | Canvases: make([]string, 0, len(match.ImgInfos)), 152 | } 153 | for _, m := range match.ImgInfos { 154 | imgUrl := ossHost + m.BookPath + "/" + m.Fname 155 | vol.Canvases = append(vol.Canvases, imgUrl) 156 | } 157 | volumes = append(volumes, vol) 158 | } 159 | return 160 | } 161 | -------------------------------------------------------------------------------- /app/luoyang.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bookget/config" 5 | "bookget/pkg/gohttp" 6 | "bookget/pkg/util" 7 | "context" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "net/http/cookiejar" 12 | "net/url" 13 | "path/filepath" 14 | "regexp" 15 | ) 16 | 17 | type Luoyang struct { 18 | dt *DownloadTask 19 | } 20 | 21 | func NewLuoyang() *Luoyang { 22 | return &Luoyang{ 23 | // 初始化字段 24 | dt: new(DownloadTask), 25 | } 26 | } 27 | 28 | func (r *Luoyang) GetRouterInit(sUrl string) (map[string]interface{}, error) { 29 | msg, err := r.Run(sUrl) 30 | return map[string]interface{}{ 31 | "url": sUrl, 32 | "msg": msg, 33 | }, err 34 | } 35 | 36 | func (p *Luoyang) Run(sUrl string) (msg string, err error) { 37 | p.dt.UrlParsed, err = url.Parse(sUrl) 38 | p.dt.Url = sUrl 39 | p.dt.BookId = p.getBookId(p.dt.Url) 40 | if p.dt.BookId == "" { 41 | return "requested URL was not found.", err 42 | } 43 | p.dt.Jar, _ = cookiejar.New(nil) 44 | return p.download() 45 | } 46 | 47 | func (p *Luoyang) getBookId(sUrl string) (bookId string) { 48 | if m := regexp.MustCompile(`&id=(\d+)`).FindStringSubmatch(sUrl); m != nil { 49 | bookId = m[1] 50 | } 51 | return bookId 52 | } 53 | 54 | func (p *Luoyang) download() (msg string, err error) { 55 | log.Printf("Get %s\n", p.dt.Url) 56 | respVolume, err := p.getVolumes(p.dt.Url, p.dt.Jar) 57 | if err != nil { 58 | fmt.Println(err) 59 | return "getVolumes", err 60 | } 61 | p.dt.SavePath = config.Conf.Directory 62 | for i, vol := range respVolume { 63 | if !config.VolumeRange(i) { 64 | continue 65 | } 66 | log.Printf(" %d/%d volume, %s \n", i+1, len(respVolume), vol) 67 | fName := util.FileName(vol) 68 | sortId := fmt.Sprintf("%04d", i+1) 69 | dest := filepath.Join(p.dt.SavePath, sortId+"."+fName) 70 | p.do(dest, vol) 71 | util.PrintSleepTime(config.Conf.Sleep) 72 | } 73 | return msg, err 74 | } 75 | 76 | func (p *Luoyang) do(dest, pdfUrl string) (msg string, err error) { 77 | ctx := context.Background() 78 | opts := gohttp.Options{ 79 | DestFile: dest, 80 | Overwrite: false, 81 | Concurrency: config.Conf.Threads, 82 | CookieFile: config.Conf.CookieFile, 83 | HeaderFile: config.Conf.HeaderFile, 84 | CookieJar: p.dt.Jar, 85 | Headers: map[string]interface{}{ 86 | "User-Agent": config.Conf.UserAgent, 87 | }, 88 | } 89 | _, err = gohttp.FastGet(ctx, pdfUrl, opts) 90 | if err != nil { 91 | fmt.Println(err) 92 | } 93 | return "", err 94 | } 95 | 96 | func (p *Luoyang) getVolumes(sUrl string, jar *cookiejar.Jar) (volumes []string, err error) { 97 | bs, err := p.getBody(sUrl, jar) 98 | if err != nil { 99 | return 100 | } 101 | //取册数 102 | matches := regexp.MustCompile(`href=["']viewer.php\?pdf=(.+?)\.pdf&`).FindAllStringSubmatch(string(bs), -1) 103 | if matches == nil { 104 | return 105 | } 106 | ids := make([]string, 0, len(matches)) 107 | for _, match := range matches { 108 | ids = append(ids, match[1]) 109 | } 110 | hostUrl := util.GetHostUrl(sUrl) 111 | volumes = make([]string, 0, len(ids)) 112 | for _, v := range ids { 113 | s := fmt.Sprintf("%s%s.pdf", hostUrl, v) 114 | volumes = append(volumes, s) 115 | } 116 | return volumes, nil 117 | } 118 | 119 | func (p *Luoyang) getCanvases(sUrl string, jar *cookiejar.Jar) (canvases []string, err error) { 120 | //TODO implement me 121 | panic("implement me") 122 | } 123 | 124 | func (p *Luoyang) getBody(sUrl string, jar *cookiejar.Jar) ([]byte, error) { 125 | referer := url.QueryEscape(sUrl) 126 | ctx := context.Background() 127 | cli := gohttp.NewClient(ctx, gohttp.Options{ 128 | CookieFile: config.Conf.CookieFile, 129 | CookieJar: jar, 130 | Headers: map[string]interface{}{ 131 | "User-Agent": config.Conf.UserAgent, 132 | "Referer": referer, 133 | }, 134 | }) 135 | resp, err := cli.Get(sUrl) 136 | if err != nil { 137 | return nil, err 138 | } 139 | bs, _ := resp.GetBody() 140 | if bs == nil { 141 | return nil, errors.New(fmt.Sprintf("ErrCode:%d, %s", resp.GetStatusCode(), resp.GetReasonPhrase())) 142 | } 143 | return bs, nil 144 | } 145 | 146 | func (p *Luoyang) postBody(sUrl string, d []byte) ([]byte, error) { 147 | //TODO implement me 148 | panic("implement me") 149 | } 150 | -------------------------------------------------------------------------------- /app/nationaljp.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bookget/config" 5 | "bookget/pkg/gohttp" 6 | "context" 7 | "fmt" 8 | "log" 9 | "net/http/cookiejar" 10 | "net/url" 11 | "path" 12 | "regexp" 13 | ) 14 | 15 | type Nationaljp struct { 16 | dt *DownloadTask 17 | extId string 18 | } 19 | 20 | func NewNationaljp() *Nationaljp { 21 | return &Nationaljp{ 22 | // 初始化字段 23 | dt: new(DownloadTask), 24 | } 25 | } 26 | 27 | func (r *Nationaljp) GetRouterInit(sUrl string) (map[string]interface{}, error) { 28 | msg, err := r.Run(sUrl) 29 | return map[string]interface{}{ 30 | "url": sUrl, 31 | "msg": msg, 32 | }, err 33 | } 34 | 35 | func (r *Nationaljp) Run(sUrl string) (msg string, err error) { 36 | r.dt.UrlParsed, err = url.Parse(sUrl) 37 | r.dt.Url = sUrl 38 | r.dt.BookId = r.getBookId(r.dt.Url) 39 | if r.dt.BookId == "" { 40 | return "requested URL was not found.", err 41 | } 42 | r.dt.Jar, _ = cookiejar.New(nil) 43 | r.extId = "jp2" 44 | return r.download() 45 | } 46 | 47 | func (r *Nationaljp) getBookId(sUrl string) (bookId string) { 48 | m := regexp.MustCompile(`(?i)BID=([A-z0-9_-]+)`).FindStringSubmatch(sUrl) 49 | if m != nil { 50 | return m[1] 51 | } 52 | return "" 53 | } 54 | 55 | func (r *Nationaljp) download() (msg string, err error) { 56 | log.Printf("Get %s\n", r.dt.Url) 57 | 58 | respVolume, err := r.getVolumes() 59 | if err != nil { 60 | fmt.Println(err) 61 | return "getVolumes", err 62 | } 63 | r.dt.SavePath = config.Conf.Directory 64 | for i, vol := range respVolume { 65 | if !config.VolumeRange(i) { 66 | continue 67 | } 68 | vid := fmt.Sprintf("%04d", i+1) 69 | fileName := vid + ".zip" 70 | dest := path.Join(r.dt.SavePath, fileName) 71 | if FileExist(dest) { 72 | continue 73 | } 74 | log.Printf(" %d/%d volume, %s\n", i+1, len(respVolume), r.extId) 75 | r.do(i+1, vol, dest) 76 | fmt.Println() 77 | } 78 | return msg, err 79 | } 80 | 81 | func (r *Nationaljp) do(index int, id, dest string) (msg string, err error) { 82 | apiUrl := "https://" + r.dt.UrlParsed.Host + "/acv/auto_conversion/download" 83 | data := fmt.Sprintf("DL_TYPE=%s&id_%d=%s", r.extId, index, id) 84 | ctx := context.Background() 85 | opts := gohttp.Options{ 86 | DestFile: dest, 87 | Overwrite: false, 88 | Concurrency: 1, 89 | CookieFile: config.Conf.CookieFile, 90 | HeaderFile: config.Conf.HeaderFile, 91 | CookieJar: r.dt.Jar, 92 | Headers: map[string]interface{}{ 93 | "User-Agent": config.Conf.UserAgent, 94 | "Content-Type": "application/x-www-form-urlencoded", 95 | }, 96 | Body: []byte(data), 97 | } 98 | _, err = gohttp.Post(ctx, apiUrl, opts) 99 | return "", err 100 | } 101 | 102 | func (r *Nationaljp) getVolumes() (volumes []string, err error) { 103 | apiUrl := fmt.Sprintf("https://%s/DAS/meta/listPhoto?LANG=default&BID=%s&ID=&NO=&TYPE=dljpeg&DL_TYPE=jpeg", r.dt.UrlParsed.Host, r.dt.BookId) 104 | bs, err := getBody(apiUrl, nil) 105 | if err != nil { 106 | return 107 | } 108 | //]+posi=["']([0-9]+)["'][^>]+value=["']([A-Za-z0-9]+)["']`).FindAllStringSubmatch(string(bs), -1) 111 | if matches == nil { 112 | return 113 | } 114 | iLen := len(matches) 115 | for _, match := range matches { 116 | //跳过全选复选框 117 | if iLen > 1 && (match[1] == "0" || match[2] == "") { 118 | continue 119 | } 120 | volumes = append(volumes, match[2]) 121 | } 122 | return volumes, nil 123 | } 124 | -------------------------------------------------------------------------------- /app/queue.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | type QueueLimit struct { 4 | n int 5 | c chan interface{} 6 | } 7 | 8 | func QueueNew(n int) *QueueLimit { 9 | return &QueueLimit{ 10 | n: n, 11 | c: make(chan interface{}, n), 12 | } 13 | } 14 | func (q *QueueLimit) Go(f func()) { 15 | q.c <- 0 16 | go func() { 17 | f() 18 | <-q.c 19 | }() 20 | } 21 | -------------------------------------------------------------------------------- /app/sammlungen.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http/cookiejar" 7 | "net/url" 8 | "regexp" 9 | ) 10 | 11 | type Sammlungen struct { 12 | dt *DownloadTask 13 | } 14 | 15 | func NewSammlungen() *Sammlungen { 16 | return &Sammlungen{ 17 | // 初始化字段 18 | dt: new(DownloadTask), 19 | } 20 | } 21 | 22 | func (r *Sammlungen) GetRouterInit(sUrl string) (map[string]interface{}, error) { 23 | msg, err := r.Run(sUrl) 24 | return map[string]interface{}{ 25 | "url": sUrl, 26 | "msg": msg, 27 | }, err 28 | } 29 | 30 | func (r *Sammlungen) Run(sUrl string) (msg string, err error) { 31 | 32 | r.dt.UrlParsed, err = url.Parse(sUrl) 33 | r.dt.Url = sUrl 34 | 35 | r.dt.BookId = r.getBookId(r.dt.Url) 36 | if r.dt.BookId == "" { 37 | return "requested URL was not found.", err 38 | } 39 | r.dt.Jar, _ = cookiejar.New(nil) 40 | return r.download() 41 | } 42 | 43 | func (r *Sammlungen) getBookId(sUrl string) (bookId string) { 44 | m := regexp.MustCompile(`/view/([A-z\d]+)`).FindStringSubmatch(sUrl) 45 | if m != nil { 46 | bookId = m[1] 47 | } 48 | return bookId 49 | } 50 | 51 | func (r *Sammlungen) download() (msg string, err error) { 52 | log.Printf("Get %s\n", r.dt.Url) 53 | manifestUrl := fmt.Sprintf("https://api.digitale-sammlungen.de/iiif/presentation/v2/%s/manifest", r.dt.BookId) 54 | var iiif IIIF 55 | return iiif.InitWithId(r.dt.Index, manifestUrl, r.dt.BookId) 56 | } 57 | -------------------------------------------------------------------------------- /app/tnm.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bookget/config" 5 | "bookget/pkg/downloader" 6 | "bookget/pkg/gohttp" 7 | "context" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "log" 12 | "net/http/cookiejar" 13 | "net/url" 14 | "path" 15 | "regexp" 16 | ) 17 | 18 | type Tnm struct { 19 | dt *DownloadTask 20 | ctx context.Context 21 | } 22 | 23 | func NewTnm() *Tnm { 24 | return &Tnm{ 25 | // 初始化字段 26 | dt: new(DownloadTask), 27 | ctx: context.Background(), 28 | } 29 | } 30 | 31 | func (r *Tnm) GetRouterInit(sUrl string) (map[string]interface{}, error) { 32 | msg, err := r.Run(sUrl) 33 | return map[string]interface{}{ 34 | "url": sUrl, 35 | "msg": msg, 36 | }, err 37 | } 38 | 39 | func (r *Tnm) Run(sUrl string) (msg string, err error) { 40 | 41 | r.dt.UrlParsed, err = url.Parse(sUrl) 42 | r.dt.Url = sUrl 43 | 44 | r.dt.BookId = r.getBookId(r.dt.Url) 45 | if r.dt.BookId == "" { 46 | return "requested URL was not found.", err 47 | } 48 | r.dt.Jar, _ = cookiejar.New(nil) 49 | return r.download() 50 | } 51 | 52 | func (r *Tnm) getBookId(sUrl string) (bookId string) { 53 | m := regexp.MustCompile(`(?i)/dlib/detail/([A-z0-9_-]+)`).FindStringSubmatch(sUrl) 54 | if m != nil { 55 | bookId = m[1] 56 | } 57 | return bookId 58 | } 59 | 60 | func (r *Tnm) download() (msg string, err error) { 61 | log.Printf("Get %s\n", r.dt.Url) 62 | 63 | r.dt.SavePath = config.Conf.Directory 64 | apiUrl := fmt.Sprintf("%s://%s/dlib/pages/%s", r.dt.UrlParsed.Scheme, r.dt.UrlParsed.Host, r.dt.BookId) 65 | canvases, err := r.getCanvases(apiUrl, r.dt.Jar) 66 | if err != nil { 67 | fmt.Println(err.Error()) 68 | return 69 | } 70 | log.Printf(" %d pages \n", len(canvases)) 71 | return r.do(canvases) 72 | } 73 | 74 | func (r *Tnm) do(dziUrls []string) (msg string, err error) { 75 | if dziUrls == nil { 76 | return "", err 77 | } 78 | referer := url.QueryEscape(r.dt.Url) 79 | 80 | args := []string{ 81 | "-H", "Origin:" + referer, 82 | "-H", "Referer:" + referer, 83 | } 84 | size := len(dziUrls) 85 | iiifDownloader := downloader.NewIIIFDownloader(&config.Conf) 86 | for i, uri := range dziUrls { 87 | if uri == "" || !config.PageRange(i, size) { 88 | continue 89 | } 90 | sortId := fmt.Sprintf("%04d", i+1) 91 | filename := sortId + config.Conf.FileExt 92 | dest := path.Join(r.dt.SavePath, filename) 93 | if FileExist(dest) { 94 | continue 95 | } 96 | log.Printf("Get %s %s\n", sortId, uri) 97 | iiifDownloader.Dezoomify(r.ctx, uri, dest, args) 98 | } 99 | return "", err 100 | } 101 | 102 | func (r *Tnm) getCanvases(sUrl string, jar *cookiejar.Jar) (canvases []string, err error) { 103 | bs, err := r.getBody(sUrl, jar) 104 | if err != nil { 105 | return 106 | } 107 | type ResponseBody struct { 108 | ImageType string `json:"imageType"` 109 | Imageid string `json:"imageid"` 110 | Ready bool `json:"ready"` 111 | Id int `json:"id"` 112 | Path string `json:"path"` 113 | } 114 | var result []ResponseBody 115 | if err = json.Unmarshal(bs, &result); err != nil { 116 | return 117 | } 118 | for _, v := range result { 119 | xmlUrl := fmt.Sprintf("%s://%s/dlib/img/%s/tiles/%s/ImageProperties.xml", 120 | r.dt.UrlParsed.Scheme, r.dt.UrlParsed.Host, r.dt.BookId, v.Imageid) 121 | canvases = append(canvases, xmlUrl) 122 | } 123 | return canvases, nil 124 | } 125 | 126 | func (r *Tnm) getBody(apiUrl string, jar *cookiejar.Jar) ([]byte, error) { 127 | referer := url.QueryEscape(apiUrl) 128 | ctx := context.Background() 129 | cli := gohttp.NewClient(ctx, gohttp.Options{ 130 | CookieFile: config.Conf.CookieFile, 131 | CookieJar: jar, 132 | Headers: map[string]interface{}{ 133 | "User-Agent": config.Conf.UserAgent, 134 | "Referer": referer, 135 | }, 136 | }) 137 | resp, err := cli.Get(apiUrl) 138 | if err != nil { 139 | return nil, err 140 | } 141 | bs, _ := resp.GetBody() 142 | if resp.GetStatusCode() == 202 || bs == nil { 143 | return nil, errors.New(fmt.Sprintf("ErrCode:%d, %s", resp.GetStatusCode(), resp.GetReasonPhrase())) 144 | } 145 | return bs, nil 146 | } 147 | -------------------------------------------------------------------------------- /app/utokyo.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bookget/config" 5 | "bookget/pkg/gohttp" 6 | "bookget/pkg/util" 7 | "context" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "net/http/cookiejar" 12 | "net/url" 13 | "path/filepath" 14 | "regexp" 15 | ) 16 | 17 | type Utokyo struct { 18 | dt *DownloadTask 19 | } 20 | 21 | func NewUtokyo() *Utokyo { 22 | return &Utokyo{ 23 | // 初始化字段 24 | dt: new(DownloadTask), 25 | } 26 | } 27 | 28 | func (r *Utokyo) GetRouterInit(sUrl string) (map[string]interface{}, error) { 29 | msg, err := r.Run(sUrl) 30 | return map[string]interface{}{ 31 | "url": sUrl, 32 | "msg": msg, 33 | }, err 34 | } 35 | 36 | func (p *Utokyo) Run(sUrl string) (msg string, err error) { 37 | p.dt.UrlParsed, err = url.Parse(sUrl) 38 | p.dt.Url = sUrl 39 | p.dt.BookId = p.getBookId(p.dt.Url) 40 | if p.dt.BookId == "" { 41 | return "requested URL was not found.", err 42 | } 43 | p.dt.Jar, _ = cookiejar.New(nil) 44 | return p.download() 45 | } 46 | 47 | func (p *Utokyo) getBookId(sUrl string) (bookId string) { 48 | if m := regexp.MustCompile(`nu=([A-Za-z0-9]+)`).FindStringSubmatch(sUrl); m != nil { 49 | bookId = m[1] 50 | } 51 | return bookId 52 | } 53 | 54 | func (p *Utokyo) download() (msg string, err error) { 55 | log.Printf("Get %s\n", p.dt.Url) 56 | respVolume, err := p.getVolumes(p.dt.Url, p.dt.Jar) 57 | if err != nil { 58 | fmt.Println(err) 59 | return "getVolumes", err 60 | } 61 | p.dt.SavePath = config.Conf.Directory 62 | for i, vol := range respVolume { 63 | if !config.VolumeRange(i) { 64 | continue 65 | } 66 | log.Printf(" %d/%d volume, %s \n", i+1, len(respVolume), vol) 67 | fName := util.FileName(vol) 68 | sortId := fmt.Sprintf("%04d", i+1) 69 | dest := filepath.Join(p.dt.SavePath, sortId+fName) 70 | p.do(dest, vol) 71 | util.PrintSleepTime(config.Conf.Sleep) 72 | } 73 | return msg, err 74 | } 75 | 76 | func (p *Utokyo) do(dest, pdfUrl string) (msg string, err error) { 77 | ctx := context.Background() 78 | opts := gohttp.Options{ 79 | DestFile: dest, 80 | Overwrite: false, 81 | Concurrency: config.Conf.Threads, 82 | CookieFile: config.Conf.CookieFile, 83 | HeaderFile: config.Conf.HeaderFile, 84 | CookieJar: p.dt.Jar, 85 | Headers: map[string]interface{}{ 86 | "User-Agent": config.Conf.UserAgent, 87 | }, 88 | } 89 | resp, err := gohttp.FastGet(ctx, pdfUrl, opts) 90 | if err != nil || resp.GetStatusCode() != 200 { 91 | fmt.Println(err) 92 | } 93 | return "", err 94 | } 95 | 96 | func (p *Utokyo) getVolumes(sUrl string, jar *cookiejar.Jar) (volumes []string, err error) { 97 | bs, err := p.getBody(sUrl, jar) 98 | if err != nil { 99 | return 100 | } 101 | //取册数 102 | matches := regexp.MustCompile(` 3 | DisableFormat: true 4 | -------------------------------------------------------------------------------- /bookget-gui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Visual Studio 生成了具有 C++ 设置的 .editorconfig 文件。 2 | root = true 3 | 4 | [*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] 5 | 6 | # Visual C++ 代码样式设置 7 | 8 | cpp_generate_documentation_comments = xml 9 | 10 | # Visual C++ 格式设置 11 | 12 | indent_style = tab 13 | indent_size = 4 14 | tab_width= 4 15 | cpp_indent_braces = false 16 | cpp_indent_multi_line_relative_to = statement_begin 17 | cpp_indent_within_parentheses = indent 18 | cpp_indent_preserve_within_parentheses = true 19 | cpp_indent_case_contents = false 20 | cpp_indent_case_labels = false 21 | cpp_indent_case_contents_when_block = false 22 | cpp_indent_lambda_braces_when_parameter = false 23 | cpp_indent_goto_labels = none 24 | cpp_indent_preprocessor = none 25 | cpp_indent_access_specifiers = false 26 | cpp_indent_namespace_contents = false 27 | cpp_indent_preserve_comments = false 28 | cpp_new_line_before_open_brace_namespace = ignore 29 | cpp_new_line_before_open_brace_type = ignore 30 | cpp_new_line_before_open_brace_function = ignore 31 | cpp_new_line_before_open_brace_block = ignore 32 | cpp_new_line_before_open_brace_lambda = ignore 33 | cpp_new_line_scope_braces_on_separate_lines = false 34 | cpp_new_line_close_brace_same_line_empty_type = true 35 | cpp_new_line_close_brace_same_line_empty_function = true 36 | cpp_new_line_before_catch = false 37 | cpp_new_line_before_else = false 38 | cpp_new_line_before_while_in_do_while = false 39 | cpp_space_before_function_open_parenthesis = ignore 40 | cpp_space_within_parameter_list_parentheses = false 41 | cpp_space_between_empty_parameter_list_parentheses = false 42 | cpp_space_after_keywords_in_control_flow_statements = false 43 | cpp_space_within_control_flow_statement_parentheses = false 44 | cpp_space_before_lambda_open_parenthesis = false 45 | cpp_space_within_cast_parentheses = false 46 | cpp_space_after_cast_close_parenthesis = false 47 | cpp_space_within_expression_parentheses = false 48 | cpp_space_before_block_open_brace = false 49 | cpp_space_between_empty_braces = false 50 | cpp_space_before_initializer_list_open_brace = false 51 | cpp_space_within_initializer_list_braces = false 52 | cpp_space_preserve_in_initializer_list = false 53 | cpp_space_before_open_square_bracket = false 54 | cpp_space_within_square_brackets = false 55 | cpp_space_before_empty_square_brackets = false 56 | cpp_space_between_empty_square_brackets = false 57 | cpp_space_group_square_brackets = false 58 | cpp_space_within_lambda_brackets = false 59 | cpp_space_between_empty_lambda_brackets = false 60 | cpp_space_before_comma = false 61 | cpp_space_after_comma = false 62 | cpp_space_remove_around_member_operators = false 63 | cpp_space_before_inheritance_colon = false 64 | cpp_space_before_constructor_colon = false 65 | cpp_space_remove_before_semicolon = false 66 | cpp_space_after_semicolon = false 67 | cpp_space_remove_around_unary_operator = false 68 | cpp_space_around_binary_operator = ignore 69 | cpp_space_around_assignment_operator = ignore 70 | cpp_space_pointer_reference_alignment = ignore 71 | cpp_space_around_ternary_operator = ignore 72 | cpp_use_unreal_engine_macro_formatting = false 73 | cpp_wrap_preserve_blocks = never 74 | 75 | # Visual C++ 包含清理设置 76 | 77 | cpp_include_cleanup_add_missing_error_tag_type = suggestion 78 | cpp_include_cleanup_remove_unused_error_tag_type = dimmed 79 | cpp_include_cleanup_optimize_unused_error_tag_type = suggestion 80 | cpp_include_cleanup_sort_after_edits = false 81 | cpp_sort_includes_error_tag_type = none 82 | cpp_sort_includes_priority_case_sensitive = false 83 | cpp_sort_includes_priority_style = quoted 84 | cpp_includes_style = default 85 | cpp_includes_use_forward_slash = false 86 | 87 | # zhudw 88 | end_of_line = lf # 行尾 UNIX 格式 LF 89 | charset = utf-8 # 文件编码字符集为 UTF-8 90 | trim_trailing_whitespace = true # 删除文件末尾空格 91 | insert_final_newline = true # 末尾插入新行 92 | indent_style = space # 以空格代替 tab 93 | indent_size = 4 # 代替 tab 的空格数量 -------------------------------------------------------------------------------- /bookget-gui/.gitignore: -------------------------------------------------------------------------------- 1 | *.vcxproj.user 2 | Release_x64/ 3 | Release_x86/ 4 | Debug_x64/ 5 | Debug_x86/ 6 | packages/ 7 | dist/ 8 | *.vscode 9 | .vscode/ 10 | *.idea 11 | .idea/ 12 | Release/ 13 | Debug/ 14 | ipch/ 15 | x64/ 16 | x86/ 17 | .vs/ 18 | downloads/ 19 | */downloads 20 | 21 | // Ignore the binary generated version of the resource (.rc) file 22 | *.aps 23 | 24 | # Prerequisites 25 | *.d 26 | 27 | # Compiled Object files 28 | *.slo 29 | *.lo 30 | *.o 31 | *.obj 32 | 33 | # Precompiled Headers 34 | *.gch 35 | *.pch 36 | 37 | # Compiled Dynamic libraries 38 | *.so 39 | *.dylib 40 | *.dll 41 | 42 | # Fortran module files 43 | *.mod 44 | *.smod 45 | 46 | # Compiled Static libraries 47 | *.lai 48 | *.la 49 | *.a 50 | *.lib 51 | 52 | # Executables 53 | *.exe 54 | *.out 55 | *.app 56 | -------------------------------------------------------------------------------- /bookget-gui/BrowserWindowDownload.cpp: -------------------------------------------------------------------------------- 1 | #include "BrowserWindow.h" 2 | #include "Config.h" 3 | #include 4 | #pragma comment(lib, "wininet.lib") 5 | 6 | 7 | 8 | // 资源响应处理 9 | HRESULT BrowserWindow::HandleTabWebResourceResponseReceived(ICoreWebView2* sender, ICoreWebView2WebResourceResponseReceivedEventArgs* args) 10 | { 11 | wil::com_ptr request; 12 | RETURN_IF_FAILED(args->get_Request(&request)); 13 | 14 | wil::unique_cotaskmem_string uri; 15 | RETURN_IF_FAILED(request->get_Uri(&uri)); 16 | 17 | if (!m_downloader.ShouldInterceptResponse(uri.get())) 18 | return S_OK; 19 | 20 | wil::com_ptr response; 21 | RETURN_IF_FAILED(args->get_Response(&response)); 22 | 23 | std::wstring sUrl(uri.get()); 24 | 25 | if (response) { 26 | wil::com_ptr headers; 27 | if (FAILED(response->get_Headers(&headers)) || !headers) { 28 | return S_OK; 29 | } 30 | 31 | wil::unique_cotaskmem_string contentType; 32 | wil::unique_cotaskmem_string contentLength; 33 | 34 | SUCCEEDED(headers->GetHeader(L"Content-Type", &contentType)); 35 | SUCCEEDED(headers->GetHeader(L"Content-Length", &contentLength)); 36 | 37 | std::wstring contentTypeStr = contentType ? contentType.get() : L""; 38 | std::wstring contentLengthStr = contentLength ? contentLength.get() : L""; 39 | 40 | if (!m_downloader.ShouldInterceptContentType(contentTypeStr, contentLengthStr)) 41 | return S_OK; 42 | 43 | response->GetContent( 44 | Callback( 45 | [this, sUrl](HRESULT errorCode, IStream* content) -> HRESULT { 46 | if (SUCCEEDED(errorCode) && content) 47 | { 48 | this->DownloadFile(sUrl, content); 49 | } 50 | return S_OK; 51 | }).Get()); 52 | } 53 | 54 | return S_OK; 55 | } 56 | 57 | 58 | 59 | bool BrowserWindow::DownloadFile(const std::wstring& sUrl, IStream *content) 60 | { 61 | std::wstring filePath = m_downloader.GetFilePath(sUrl); 62 | bool ret = Util::FileWrite(filePath, content); 63 | 64 | int sleepTime = Config::GetInstance().GetSleepTime(); 65 | if (sleepTime > 0) 66 | std::this_thread::sleep_for(std::chrono::seconds(sleepTime)); // 3 秒延时 67 | 68 | int mode = m_downloader.GetDownloaderMode(); 69 | if (mode == 0) { 70 | // 下载完成后继续下一个 71 | PostMessage(m_hWnd, WM_APP_DOWNLOAD_NEXT, 0, 0); 72 | } 73 | else if (mode == 1) { 74 | //执行 javascript 脚本 75 | std::wstring scriptPath; 76 | std::string narrow_url = Util::WideToUtf8(sUrl); 77 | for (const auto& site : Config::GetInstance().GetSiteConfigs()) { 78 | if (site.intercept == 1 && Util::matchUrlPattern(site.url, narrow_url) ) { 79 | scriptPath = GetFullPathFor(Util::Utf8ToWide(site.script).c_str()); 80 | break; 81 | } 82 | } 83 | if(scriptPath.length() > 0) { 84 | this->ExecuteScriptFile(scriptPath, m_tabs.at(m_activeTabId)->m_contentWebView.get()); 85 | } 86 | } 87 | else if (mode == 2) { 88 | // 写入共享内存 89 | SharedMemory::GetInstance().WriteImagePath(filePath.c_str()); 90 | } 91 | return ret; 92 | } 93 | 94 | bool BrowserWindow::DownloadFile(const std::wstring& sUrl,ICoreWebView2HttpRequestHeaders *headers) 95 | { 96 | int mode = m_downloader.GetDownloaderMode(); 97 | if (mode == 1) { 98 | if (!m_downloader.ShouldInterceptRequest(sUrl)) 99 | return false; 100 | 101 | return m_downloader.DownloadFile(sUrl.c_str(), headers); 102 | } 103 | return false; 104 | } 105 | -------------------------------------------------------------------------------- /bookget-gui/BrowserWindowFilepath.cpp: -------------------------------------------------------------------------------- 1 | #include "BrowserWindow.h" 2 | 3 | std::wstring BrowserWindow::GetAppDataDirectory() 4 | { 5 | TCHAR path[MAX_PATH]; 6 | std::wstring dataDirectory; 7 | HRESULT hr = SHGetFolderPath(nullptr, CSIDL_APPDATA, NULL, 0, path); 8 | if (SUCCEEDED(hr)) 9 | { 10 | dataDirectory = std::wstring(path); 11 | dataDirectory.append(L"\\Bookget\\"); 12 | } 13 | else 14 | { 15 | dataDirectory = std::wstring(L".\\"); 16 | } 17 | 18 | //dataDirectory.append(s_title); 19 | return dataDirectory; 20 | } 21 | 22 | std::wstring BrowserWindow::GetFullPathFor(LPCWSTR relativePath) 23 | { 24 | WCHAR path[MAX_PATH]; 25 | GetModuleFileNameW(m_hInst, path, MAX_PATH); 26 | std::wstring pathName(path); 27 | 28 | std::size_t index = pathName.find_last_of(L"\\") + 1; 29 | pathName.replace(index, pathName.length(), relativePath); 30 | 31 | return pathName; 32 | } 33 | 34 | std::wstring BrowserWindow::GetFilePathAsURI(std::wstring fullPath) 35 | { 36 | std::wstring fileURI; 37 | ComPtr uri; 38 | DWORD uriFlags = Uri_CREATE_ALLOW_IMPLICIT_FILE_SCHEME; 39 | HRESULT hr = CreateUri(fullPath.c_str(), uriFlags, 0, &uri); 40 | 41 | if (SUCCEEDED(hr)) 42 | { 43 | wil::unique_bstr absoluteUri; 44 | uri->GetAbsoluteUri(&absoluteUri); 45 | fileURI = std::wstring(absoluteUri.get()); 46 | } 47 | 48 | return fileURI; 49 | } 50 | 51 | std::wstring BrowserWindow::GetUserDataDirectory() { 52 | std::wstring userDataDirectory = GetAppDataDirectory(); 53 | userDataDirectory.append(L"\\User Data"); 54 | return userDataDirectory; 55 | } 56 | -------------------------------------------------------------------------------- /bookget-gui/BrowserWindowMessage.cpp: -------------------------------------------------------------------------------- 1 | #include "BrowserWindow.h" 2 | 3 | 4 | HRESULT BrowserWindow::ClearContentCache() 5 | { 6 | return m_tabs.at(m_activeTabId)->m_contentWebView->CallDevToolsProtocolMethod(L"Network.clearBrowserCache", L"{}", nullptr); 7 | } 8 | 9 | HRESULT BrowserWindow::ClearControlsCache() 10 | { 11 | return m_controlsWebView->CallDevToolsProtocolMethod(L"Network.clearBrowserCache", L"{}", nullptr); 12 | } 13 | 14 | HRESULT BrowserWindow::ClearContentCookies() 15 | { 16 | return m_tabs.at(m_activeTabId)->m_contentWebView->CallDevToolsProtocolMethod(L"Network.clearBrowserCookies", L"{}", nullptr); 17 | } 18 | 19 | HRESULT BrowserWindow::ClearControlsCookies() 20 | { 21 | return m_controlsWebView->CallDevToolsProtocolMethod(L"Network.clearBrowserCookies", L"{}", nullptr); 22 | } 23 | 24 | 25 | HRESULT BrowserWindow::PostJsonToWebView(web::json::value jsonObj, ICoreWebView2* webview) 26 | { 27 | utility::stringstream_t stream; 28 | jsonObj.serialize(stream); 29 | 30 | return webview->PostWebMessageAsJson(stream.str().c_str()); 31 | } 32 | -------------------------------------------------------------------------------- /bookget-gui/BrowserWindowThread.cpp: -------------------------------------------------------------------------------- 1 | #include "BrowserWindow.h" 2 | #include "SharedMemory.h" 3 | 4 | void BrowserWindow::StartBackgroundThread() { 5 | std::lock_guard lock(m_threadMutex); 6 | StopBackgroundThread(); // 确保之前的线程已停止 7 | 8 | m_sharedMemoryThread = std::thread([this]() { 9 | while (!m_stopThread.load(std::memory_order_relaxed)) { 10 | // 读取精简后的共享内存数据 11 | auto data = SharedMemory::GetInstance().Read(); 12 | 13 | // 检查是否有新图片路径需要处理 14 | if (data.URLReady && data.ImageReady && data.PID != GetCurrentProcessId()) { 15 | auto* sharedData = new SharedMemoryDataMini(); 16 | sharedData->ImageReady = data.ImageReady; 17 | sharedData->URLReady = data.URLReady; 18 | sharedData->PID = data.PID; 19 | wcsncpy_s(sharedData->ImagePath, _countof(sharedData->ImagePath), data.ImagePath, _TRUNCATE); 20 | wcsncpy_s(sharedData->URL, _countof(sharedData->URL), data.URL, _TRUNCATE); 21 | 22 | PostMessage(m_hWnd, WM_APP_UPDATE_UI, 0, reinterpret_cast(sharedData)); 23 | } 24 | // 检查是否有新URL需要处理 25 | else if (data.URLReady && data.PID != GetCurrentProcessId()) { 26 | // 仅传递必要数据到UI线程 27 | auto* sharedData = new SharedMemoryDataMini(); 28 | sharedData->URLReady = data.URLReady; 29 | sharedData->PID = data.PID; 30 | wcsncpy_s(sharedData->URL, _countof(sharedData->URL), data.URL, _TRUNCATE); 31 | 32 | PostMessage(m_hWnd, WM_APP_UPDATE_UI, 0, reinterpret_cast(sharedData)); 33 | } 34 | 35 | // 降低CPU使用率 36 | std::this_thread::sleep_for(std::chrono::milliseconds(200)); 37 | } 38 | }); 39 | 40 | m_downloader.Start(m_hWnd); 41 | } 42 | 43 | void BrowserWindow::StopBackgroundThread() { 44 | m_stopThread = true; // 先设置停止标志 45 | // 确保线程已完全停止 46 | if (m_sharedMemoryThread.joinable()) { 47 | m_sharedMemoryThread.join(); 48 | } 49 | 50 | m_downloader.Stop(); 51 | 52 | m_stopThread = false; // 重置标志 53 | } 54 | 55 | 56 | 57 | void BrowserWindow::HandleSharedMemoryUpdate(LPARAM lParam) { 58 | // 获取传递过来的数据 59 | auto* data = reinterpret_cast(lParam); 60 | 61 | auto* sharedData = SharedMemory::GetInstance().GetMutex(); 62 | if (sharedData == nullptr) { 63 | delete data; 64 | return; 65 | } 66 | 67 | // 处理图片下载模式 68 | if (sharedData->URLReady && sharedData->ImageReady && m_tabs.find(m_activeTabId) != m_tabs.end() && sharedData->PID != GetCurrentProcessId()) { 69 | // 重置标志位 70 | sharedData->URLReady = false; 71 | m_downloader.Reset(sharedData->URL, 2); 72 | // 配置下载处理器 73 | m_tabs.at(m_activeTabId)->SetupWebViewListeners(); 74 | // 导航到URL 75 | m_tabs.at(m_activeTabId)->m_contentWebView->Navigate(sharedData->URL); 76 | } 77 | // 处理普通URL导航 78 | else if (sharedData->URLReady && m_tabs.find(m_activeTabId) != m_tabs.end()) { 79 | // 重置标志位 80 | sharedData->URLReady = false; 81 | 82 | // 导航到URL 83 | m_tabs.at(m_activeTabId)->m_contentWebView->Navigate(sharedData->URL); 84 | } 85 | 86 | 87 | // 清理内存 88 | delete data; 89 | SharedMemory::GetInstance().ReleaseMutex(); 90 | 91 | } 92 | 93 | -------------------------------------------------------------------------------- /bookget-gui/CheckFailure.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #include "framework.h" 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | 18 | // Notify the user of a failure with a message box. 19 | void ShowFailure(HRESULT hr, const std::wstring& message) 20 | { 21 | std::wstringstream formattedMessage; 22 | formattedMessage << message << ": 0x" << std::hex << std::setw(8) << hr << " (" 23 | << winrt::hresult_error(hr).message().c_str() << ")"; 24 | MessageBox(nullptr, formattedMessage.str().c_str(), nullptr, MB_OK); 25 | } 26 | 27 | // If something failed, show the error code and fail fast. 28 | void CheckFailure(HRESULT hr, const std::wstring& message) 29 | { 30 | if (FAILED(hr)) 31 | { 32 | ShowFailure(hr, message); 33 | FAIL_FAST(); 34 | } 35 | } 36 | 37 | void FeatureNotAvailable() 38 | { 39 | MessageBox(nullptr, 40 | L"This feature is not available in the browser version currently being used.", 41 | L"Feature Not Available", MB_OK); 42 | } 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /bookget-gui/CheckFailure.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | #ifndef CHECK_FAILURE_H 5 | #define CHECK_FAILURE_H 6 | 7 | #include "framework.h" 8 | 9 | #include 10 | // Notify the user of a failure with a message box. 11 | void ShowFailure(HRESULT hr, const std::wstring& message = L"Error"); 12 | 13 | // If something failed, show the error code and fail fast. 14 | void CheckFailure(HRESULT hr, const std::wstring& message = L"Error"); 15 | 16 | // Needs to be a separate macro because the preprocessor is weird 17 | #define CHECK_FAILURE_STRINGIFY(arg) #arg 18 | 19 | // If we use a function-like macro to wrap a function call, the macro expansion covers the 20 | // entire function call, and if that function call contains a lambda which spans many lines, 21 | // it makes error messages, the __LINE__ macro, and debuggers less accurate. Instead, 22 | // we make it a term-like macro which generates a partially-applied function. In effect, 23 | // CHECK_FAILURE(MultiLineFunctionCall(...)); 24 | // becomes 25 | // ([](HRESULT hr){ CheckFailure(hr, "error message"); })(MultiLineFunctionCall(...)); 26 | // so that MultiLineFunctionCall(...) doesn't have to be part of the macro expansion. 27 | #define CHECK_FAILURE_FILE_LINE(file, line) ([](HRESULT hr){ CheckFailure(hr, L"Failure at " CHECK_FAILURE_STRINGIFY(file) L"(" CHECK_FAILURE_STRINGIFY(line) L")"); }) 28 | #define CHECK_FAILURE CHECK_FAILURE_FILE_LINE(__FILE__, __LINE__) 29 | #define CHECK_FAILURE_BOOL(value) CHECK_FAILURE((value) ? S_OK : E_UNEXPECTED) 30 | 31 | // Show a message box indicating that an interface isn't available in this browser version. 32 | // Only call this in direct response to a specific user action. 33 | void FeatureNotAvailable(); 34 | 35 | // Wraps the above in a conditional. 36 | #define CHECK_FEATURE_RETURN(feature) { if (!feature) { FeatureNotAvailable(); return true; } } 37 | 38 | // Returns nothing, which is different from CHECK_FEATURE_RETURN 39 | #define CHECK_FEATURE_RETURN_EMPTY(feature) { if (!feature) { FeatureNotAvailable(); return; } } 40 | #endif // !CHECK_FAILURE_H 41 | -------------------------------------------------------------------------------- /bookget-gui/Config.cpp: -------------------------------------------------------------------------------- 1 | #include "Config.h" 2 | #include 3 | 4 | // 构造函数初始化PIMPL 5 | Config::Config() : pImpl(std::make_unique()) {} 6 | 7 | // 加载 YAML 配置文件 8 | bool Config::ConfigImpl::Load(const std::string& configPath) { 9 | try { 10 | YAML::Node config = YAML::LoadFile(configPath); 11 | 12 | // 解析全局设置 13 | if (config["global_settings"]) { 14 | auto global = config["global_settings"]; 15 | if (global["download_dir"]) downloadDir = global["download_dir"].as(); 16 | if (global["max_downloads"]) maxDownloads = global["max_downloads"].as(); 17 | if (global["sleep_time"]) sleepTime = global["sleep_time"].as(); 18 | if (global["downloader_mode"]) downloaderMode = global["downloader_mode"].as(); 19 | if (global["ext"]) fileExt = global["ext"].as(); 20 | } 21 | 22 | // 解析站点配置 23 | if (config["sites"]) { 24 | for (const auto& site : config["sites"]) { 25 | SiteConfig siteConfig; 26 | if (site["url"]) siteConfig.url = site["url"].as(); 27 | if (site["script"]) siteConfig.script = site["script"].as(); 28 | 29 | siteConfig.ext = site["ext"] ? site["ext"].as() : fileExt; 30 | 31 | if (site["intercept"]) siteConfig.intercept = site["intercept"].as(); 32 | if (site["description"]) siteConfig.description = site["description"].as(); 33 | 34 | //if (site["metadata"] && site["metadata"]["description"]) { 35 | //siteConfig.description = site["metadata"]["description"].as(); 36 | //} 37 | siteConfigs.push_back(siteConfig); 38 | } 39 | } 40 | 41 | return true; 42 | } catch (...) { 43 | OutputDebugString(L"Failed to parse YAML config file\n"); 44 | return false; 45 | } 46 | } 47 | 48 | bool Config::Load(const std::string& configPath) { 49 | return pImpl->Load(configPath); 50 | } 51 | 52 | std::string Config::GetDownloadDir() { 53 | return pImpl->downloadDir; 54 | } 55 | 56 | std::string Config::GetDefaultExt() { 57 | return pImpl->fileExt; 58 | } 59 | 60 | int Config::GetMaxDownloads() { 61 | return pImpl->maxDownloads; 62 | } 63 | 64 | int Config::GetSleepTime() { 65 | return pImpl->sleepTime; 66 | } 67 | int Config::GetDownloaderMode() { 68 | return pImpl->downloaderMode; 69 | } 70 | 71 | const std::vector& Config::GetSiteConfigs() { 72 | return pImpl->siteConfigs; 73 | } 74 | 75 | -------------------------------------------------------------------------------- /bookget-gui/Config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class Config 7 | { 8 | public: 9 | // 删除拷贝构造函数和赋值运算符 10 | Config(const Config&) = delete; 11 | Config& operator=(const Config&) = delete; 12 | 13 | // 获取单例实例 14 | static Config& GetInstance() { 15 | static Config instance; 16 | return instance; 17 | } 18 | 19 | // 配置管理 20 | struct SiteConfig { 21 | std::string url; 22 | std::string script; 23 | int intercept; 24 | std::string ext; 25 | std::string description; 26 | int downloaderMode; 27 | }; 28 | 29 | // 公共接口 30 | bool Load(const std::string& configPath); 31 | std::string GetDownloadDir(); 32 | std::string GetDefaultExt(); 33 | int GetMaxDownloads(); 34 | int GetSleepTime(); 35 | int GetDownloaderMode(); 36 | const std::vector& GetSiteConfigs(); 37 | 38 | private: 39 | // PIMPL 实现 40 | struct ConfigImpl { 41 | // 全局设置 42 | std::string downloadDir = "downloads"; 43 | int maxDownloads = 1000; 44 | int sleepTime = 3; 45 | int downloaderMode = 1; //下载模式 0=urls.txt | 1=自动监听图片 | 2 = 共享内存URL 46 | std::string fileExt = ".jpg"; 47 | 48 | std::vector siteConfigs; 49 | 50 | // 加载 YAML 配置文件 51 | bool Load(const std::string& configPath); 52 | }; 53 | 54 | Config(); // 私有构造函数 55 | ~Config() = default; 56 | 57 | // PIMPL实现 58 | std::unique_ptr pImpl; 59 | }; 60 | -------------------------------------------------------------------------------- /bookget-gui/Downloader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "framework.h" 3 | #include 4 | #include 5 | 6 | class Downloader { 7 | public: 8 | ~Downloader(); 9 | void Start(HWND hWnd); 10 | void Stop(); 11 | 12 | void RequestDownload(const std::wstring& url); 13 | 14 | bool DownloadFile(const wchar_t* url, ICoreWebView2HttpRequestHeaders* headers); 15 | 16 | void LoadImageUrlsFromFile(const std::wstring& sUrlsFilePath); 17 | void DownloadNextImage(HWND hWnd); 18 | 19 | bool ShouldInterceptRequest(const std::wstring& sUrl); 20 | bool ShouldInterceptResponse(const std::wstring& sUrl); 21 | bool ShouldInterceptContentType(const std::wstring& contentType, const std::wstring& contentLength); 22 | std::wstring GetFilePath(const std::wstring& sUrl); 23 | 24 | void Reset(std::wstring sUrl, int runMode) { 25 | m_targetUrls.clear(); 26 | m_targetUrls.emplace_back(sUrl); 27 | m_downloaderMode = runMode; 28 | } 29 | int GetDownloaderMode(){return m_downloaderMode;}; 30 | 31 | private: 32 | std::thread m_downloaderThread; 33 | std::atomic m_stopThread{false}; 34 | DWORD m_workerThreadId = 0; 35 | 36 | int m_sleepTime = 0; 37 | int m_maxDownloads = 0; 38 | std::wstring m_downloadDir; 39 | std::wstring m_filePath; 40 | 41 | std::vector m_targetUrls; 42 | std::atomic m_downloadCounter = 0; 43 | std::mutex m_downloadCounterMutex; 44 | int m_downloaderMode = 1; // 0=urls.txt, 1=自动图片, 2=共享内存URL 45 | 46 | std::vector m_targetExtensions = { 47 | L".jpg", 48 | L".jp2", 49 | L".png", 50 | L".pdf", 51 | L".jpeg", 52 | L".tif", 53 | L".tiff", 54 | L".bmp", 55 | L".webp" 56 | }; 57 | 58 | std::vector m_targetContentTypes = { 59 | L"image/", 60 | L"application/pdf", 61 | L"application/octet-stream", 62 | }; 63 | 64 | 65 | }; 66 | 67 | -------------------------------------------------------------------------------- /bookget-gui/HttpClient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "framework.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #pragma comment(lib, "libssl.lib") 9 | #pragma comment(lib, "libcrypto.lib") 10 | #pragma comment(lib, "crypt32.lib") 11 | #pragma comment(lib, "ws2_32.lib") 12 | 13 | 14 | #include 15 | #include 16 | 17 | /* 18 | 19 | asio::io_context io_context; 20 | ssl::context ssl_ctx(ssl::context::tls_client); 21 | ssl_ctx.set_verify_mode(SSL_VERIFY_NONE); 22 | HttpClient httpClient(io_context, ssl_ctx); 23 | 24 | // Simple GET request 25 | std::string response = httpClient.get("https://example.com/api/data"); 26 | 27 | // GET with custom headers 28 | std::vector> headers = { 29 | {"Authorization", "Bearer token123"}, 30 | {"Accept", "application/json"} 31 | }; 32 | std::string response = httpClient.get("https://api.example.com/data", headers); 33 | 34 | // POST request with JSON body 35 | std::string json_body = "{\"name\":\"John\", \"age\":30}"; 36 | headers.push_back({"Content-Type", "application/json"}); 37 | std::string response = httpClient.post("https://api.example.com/users", json_body, headers); 38 | */ 39 | 40 | 41 | using asio::ip::tcp; 42 | namespace ssl = asio::ssl; 43 | 44 | const int MAX_HEADERS = 4096; 45 | 46 | class HttpClient { 47 | public: 48 | HttpClient(asio::io_context& io_context, ssl::context& ssl_ctx) 49 | : resolver_(io_context), 50 | socket_(io_context), 51 | ssl_socket_(io_context, ssl_ctx) {}; 52 | 53 | std::string get(const std::string& url, 54 | const std::vector>& headers = {}); 55 | 56 | std::string post(const std::string& url, 57 | const std::string& body, 58 | const std::vector>& headers = {}); 59 | 60 | bool download(const std::string& url, 61 | const std::string& output_filename, 62 | const std::vector>& headers = {}); 63 | void reset(); 64 | 65 | private: 66 | 67 | std::string build_request(const std::string& host, 68 | const std::string& path, 69 | const std::vector>& headers, 70 | const std::string& method = "GET", 71 | const std::string& body = ""); 72 | 73 | std::string build_post_request(const std::string& host, 74 | const std::string& path, 75 | const std::vector>& headers, 76 | const std::string& body); 77 | 78 | template 79 | bool handle_response(SocketType& socket, const std::string& output_filename); 80 | 81 | template 82 | std::string handle_string_response(SocketType& socket); 83 | 84 | tcp::resolver resolver_; 85 | tcp::socket socket_; 86 | ssl::stream ssl_socket_; 87 | 88 | std::vector> m_headers = {}; 89 | }; 90 | 91 | 92 | -------------------------------------------------------------------------------- /bookget-gui/Resource.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | //{{NO_DEPENDENCIES}} 6 | // Microsoft Visual C++ generated include file. 7 | // Used by bookgetApp.rc 8 | // 9 | #define IDC_MYICON 2 10 | #define IDD_WEBVIEWBROWSERAPP_DIALOG 102 11 | #define IDS_APP_TITLE 103 12 | #define IDM_ABOUT 104 13 | #define IDI_WEBVIEWBROWSERAPP 107 14 | #define IDI_SMALL 108 15 | #define IDC_BOOKGETAPP 109 16 | #define IDR_MAINFRAME 128 17 | #define IDC_STATIC -1 18 | 19 | // Next default values for new objects 20 | // 21 | #ifdef APSTUDIO_INVOKED 22 | #ifndef APSTUDIO_READONLY_SYMBOLS 23 | #define _APS_NO_MFC 1 24 | #define _APS_NEXT_RESOURCE_VALUE 131 25 | #define _APS_NEXT_COMMAND_VALUE 32771 26 | #define _APS_NEXT_CONTROL_VALUE 1000 27 | #define _APS_NEXT_SYMED_VALUE 110 28 | #endif 29 | #endif 30 | 31 | -------------------------------------------------------------------------------- /bookget-gui/SharedMemory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | // 共享内存结构 6 | #include 7 | #include 8 | #include 9 | 10 | #pragma pack(push, 1) // 确保无填充字节 11 | struct SharedMemoryData { 12 | uint32_t URLReady; 13 | uint32_t HTMLReady; 14 | uint32_t CookiesReady; 15 | uint32_t ImageReady; 16 | uint32_t PID; 17 | wchar_t URL[1024]; // 固定大小缓冲区 18 | wchar_t ImagePath[1024]; // MAX_PATH for image path 19 | wchar_t Cookies[4096]; // 4KB for cookies 20 | wchar_t HTML[1024 * 1024 * 8]; // 8MB for HTML 21 | }; 22 | struct SharedMemoryDataMini { 23 | uint32_t URLReady; 24 | uint32_t HTMLReady; 25 | uint32_t CookiesReady; 26 | uint32_t ImageReady; 27 | uint32_t PID; 28 | wchar_t URL[1024]; // 固定大小缓冲区 29 | wchar_t ImagePath[1024]; // MAX_PATH for image path 30 | }; 31 | #pragma pack(pop) // 恢复默认对齐 32 | 33 | 34 | class SharedMemory 35 | { 36 | 37 | public: 38 | // 获取单例实例 39 | static SharedMemory& GetInstance() { 40 | static SharedMemory instance; 41 | return instance; 42 | } 43 | 44 | 45 | bool Init(); 46 | void Cleanup(); 47 | void WriteHtml(const std::wstring& html); 48 | void WriteCookies(const std::wstring& cookies); 49 | void WriteImagePath(const std::wstring& imagePath); 50 | SharedMemoryDataMini Read(); 51 | SharedMemoryData* Get(); 52 | SharedMemoryData* GetMutex(); 53 | void ReleaseMutex(); 54 | 55 | 56 | private: 57 | 58 | HANDLE m_hSharedMemory; 59 | LPVOID m_pSharedMemory; 60 | HANDLE m_hSharedMemoryMutex; 61 | 62 | // 优化:用 constexpr 替代 static const 63 | const wchar_t* m_sharedMemoryName = L"Local\\WebView2SharedMemory"; 64 | const wchar_t* m_sharedMemoryMutexName = L"Local\\WebView2SharedMemoryMutex"; 65 | DWORD m_sharedMemorySize = sizeof(SharedMemoryData); 66 | }; 67 | 68 | -------------------------------------------------------------------------------- /bookget-gui/Tab.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #pragma once 6 | 7 | #include "framework.h" 8 | 9 | class Tab 10 | { 11 | public: 12 | // WebView资源 13 | wil::com_ptr m_contentEnv; 14 | 15 | wil::com_ptr m_contentController; 16 | wil::com_ptr m_contentWebView; 17 | wil::com_ptr m_securityStateChangedReceiver; 18 | wil::com_ptr m_cookieManager; 19 | 20 | wil::com_ptr m_webview22; 21 | 22 | 23 | static std::unique_ptr CreateNewTab(HWND hWnd, ICoreWebView2Environment* env, size_t id, bool shouldBeActive); 24 | HRESULT ResizeWebView(); 25 | 26 | HRESULT GetCookies(std::wstring uri); 27 | static std::wstring CookieToString(ICoreWebView2Cookie* cookie); 28 | 29 | protected: 30 | 31 | HWND m_parentHWnd = nullptr; 32 | size_t m_tabId = INVALID_TAB_ID; 33 | EventRegistrationToken m_historyUpdateForwarderToken = {0}; 34 | EventRegistrationToken m_uriUpdateForwarderToken = {0}; 35 | EventRegistrationToken m_navStartingToken = {0}; 36 | EventRegistrationToken m_navCompletedToken = {0}; 37 | EventRegistrationToken m_securityUpdateToken = {0}; 38 | EventRegistrationToken m_messageBrokerToken = {0}; // Message broker for browser pages loaded in a tab 39 | wil::com_ptr m_messageBroker; 40 | 41 | 42 | HRESULT Init(ICoreWebView2Environment* env, bool shouldBeActive); 43 | void SetMessageBroker(); 44 | 45 | public: 46 | EventRegistrationToken m_newWindowRequestedToken = {0}; 47 | EventRegistrationToken m_webResourceRequestedToken = {0}; 48 | EventRegistrationToken m_webResourceResponseReceivedToken = {0}; 49 | EventRegistrationToken m_navigationToken = {0}; 50 | 51 | 52 | void SetupWebViewListeners(); 53 | 54 | HRESULT CreateModifiedResponse(const std::wstring &url, 55 | ICoreWebView2WebResourceRequestedEventArgs* args, 56 | ICoreWebView2WebResourceResponse** response); 57 | 58 | }; 59 | 60 | -------------------------------------------------------------------------------- /bookget-gui/Util.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #pragma once 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | class Util 15 | { 16 | public: 17 | static std::wstring UnixEpochToDateTime(double value); 18 | 19 | static bool FileWrite(const std::wstring &filePath, IStream *content); 20 | static bool ReadFileToString(const std::wstring& filePath, std::wstring& content); 21 | 22 | static bool fileWrite(std::wstring filename, std::wstring data); 23 | static bool fileAppend(std::wstring filename, std::wstring data); 24 | static std::wstring fileRead(std::wstring filename); 25 | static std::wstring GetCurrentExeDirectory(); 26 | static std::wstring GetUserHomeDirectory(); 27 | 28 | static std::wstring BoolToString(BOOL value); 29 | static std::wstring EncodeQuote(std::wstring raw); 30 | 31 | static std::wstring Utf8ToWide(const std::string& utf8); 32 | static std::string WideToUtf8(const std::wstring& wide); 33 | 34 | static std::string Utf16ToUtf8(const std::wstring& utf16); 35 | static std::wstring Utf8ToUtf16(const std::string& utf8); 36 | 37 | static bool FindProcessExist(const std::wstring); 38 | static void Trim(std::wstring& str); 39 | static bool CheckIfUrlsFileExists(); 40 | 41 | static bool IsImageUrl(const std::wstring& url); 42 | 43 | static std::wstring GetFileNameFromUrl(const std::wstring& url); 44 | static bool IsImageContentType(const wchar_t* contentType); 45 | static std::wstring ParseJsonBool(const wchar_t* json); 46 | 47 | static std::string replace_fast(const std::string& str, 48 | const std::string& from, 49 | const std::string& to); 50 | 51 | static std::string replace(const std::string& str, 52 | const std::string& from, 53 | const std::string& to); 54 | 55 | static bool matchUrlPattern(const std::string& pattern, const std::string& url); 56 | static bool matchUrlPattern(const std::wstring& pattern, const std::wstring& url); 57 | static bool IsLocalUri(const std::wstring& url); 58 | static std::wstring GetFullPathFor(HINSTANCE hInst, LPCWSTR relativePath); 59 | 60 | 61 | static void DebugPrintException(const std::exception& e); 62 | 63 | static std::string removeDisableDevtoolJsCode(const std::string& input); 64 | static std::string ReadStreamToString(IStream* stream); 65 | }; 66 | 67 | -------------------------------------------------------------------------------- /bookget-gui/bookget.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/bookget.ico -------------------------------------------------------------------------------- /bookget-gui/bookgetApp.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // bookgetApp.cpp : Defines the entry point for the application. 6 | // 7 | 8 | #include "BrowserWindow.h" 9 | #include "bookgetApp.h" 10 | #include "env.h" 11 | #include "Util.h" 12 | 13 | using namespace Microsoft::WRL; 14 | 15 | void tryLaunchWindow(HINSTANCE hInstance, int nCmdShow); 16 | 17 | int APIENTRY wWinMain(_In_ HINSTANCE hInstance, 18 | _In_opt_ HINSTANCE hPrevInstance, 19 | _In_ LPWSTR lpCmdLine, 20 | _In_ int nCmdShow) 21 | { 22 | UNREFERENCED_PARAMETER(hPrevInstance); 23 | UNREFERENCED_PARAMETER(lpCmdLine); 24 | 25 | // 解析命令行参数 26 | LPWSTR argumentStart = GetCommandLine(); 27 | //LPWSTR argumentEnd; 28 | int cArgs; 29 | LPWSTR* arguments = CommandLineToArgvW(argumentStart, &cArgs); 30 | 31 | for (int i = 0; i < cArgs; i++) { 32 | if (i == 0) continue; 33 | std::wstring cmd; 34 | cmd = arguments[i]; 35 | if (cmd == L"-i") { 36 | std::wstring sUrl = arguments[i+1]; 37 | std::error_code ec; 38 | if (std::filesystem::exists(sUrl, ec)) { 39 | g_urlsFile = sUrl; 40 | } 41 | else { 42 | g_HomeUrl = sUrl; 43 | } 44 | g_arguments.push_back(std::make_pair(cmd, sUrl)); 45 | i++; 46 | } 47 | } 48 | LocalFree(arguments); 49 | 50 | // Call SetProcessDPIAware() instead when using Windows 7 or any version 51 | // below 1703 (Windows 10). 52 | SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); 53 | 54 | BrowserWindow::RegisterClass(hInstance); 55 | 56 | tryLaunchWindow(hInstance, nCmdShow); 57 | 58 | HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_BOOKGETAPP)); 59 | 60 | MSG msg; 61 | 62 | // Main message loop: 63 | while (GetMessage(&msg, nullptr, 0, 0)) 64 | { 65 | if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) 66 | { 67 | TranslateMessage(&msg); 68 | DispatchMessage(&msg); 69 | } 70 | } 71 | return (int) msg.wParam; 72 | } 73 | 74 | void tryLaunchWindow(HINSTANCE hInstance, int nCmdShow) 75 | { 76 | BOOL launched = BrowserWindow::LaunchWindow(hInstance, nCmdShow); 77 | if (!launched) 78 | { 79 | DWORD err = GetLastError(); 80 | 81 | std::wstringstream fmtMessage; 82 | fmtMessage << "Could not launch the browser [ " << err << " ]."; 83 | int msgboxID = MessageBox(NULL, fmtMessage.str().c_str(), L"Error", MB_RETRYCANCEL); 84 | 85 | switch (msgboxID) 86 | { 87 | case IDRETRY: 88 | tryLaunchWindow(hInstance, nCmdShow); 89 | break; 90 | case IDCANCEL: 91 | default: 92 | PostQuitMessage(0); 93 | } 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /bookget-gui/bookgetApp.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #pragma once 6 | 7 | #include "framework.h" 8 | -------------------------------------------------------------------------------- /bookget-gui/bookgetApp.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #include "resource.h" 4 | 5 | #define APSTUDIO_READONLY_SYMBOLS 6 | ///////////////////////////////////////////////////////////////////////////// 7 | // 8 | // Generated from the TEXTINCLUDE 2 resource. 9 | // 10 | #ifndef APSTUDIO_INVOKED 11 | #include "targetver.h" 12 | #endif 13 | #define APSTUDIO_HIDDEN_SYMBOLS 14 | #include "windows.h" 15 | #undef APSTUDIO_HIDDEN_SYMBOLS 16 | 17 | ///////////////////////////////////////////////////////////////////////////// 18 | #undef APSTUDIO_READONLY_SYMBOLS 19 | 20 | ///////////////////////////////////////////////////////////////////////////// 21 | // English (United States) resources 22 | 23 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 24 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 25 | 26 | ///////////////////////////////////////////////////////////////////////////// 27 | // 28 | // Icon 29 | // 30 | 31 | // Icon with lowest ID value placed first to ensure application icon 32 | // remains consistent on all systems. 33 | IDI_WEBVIEWBROWSERAPP ICON "bookget.ico" 34 | 35 | IDI_SMALL ICON "bookget.ico" 36 | 37 | 38 | ///////////////////////////////////////////////////////////////////////////// 39 | // 40 | // Accelerator 41 | // 42 | 43 | IDC_BOOKGETAPP ACCELERATORS 44 | BEGIN 45 | "?", IDM_ABOUT, ASCII, ALT 46 | "/", IDM_ABOUT, ASCII, ALT 47 | END 48 | 49 | 50 | #ifdef APSTUDIO_INVOKED 51 | ///////////////////////////////////////////////////////////////////////////// 52 | // 53 | // TEXTINCLUDE 54 | // 55 | 56 | 1 TEXTINCLUDE 57 | BEGIN 58 | "resource.h\0" 59 | END 60 | 61 | 2 TEXTINCLUDE 62 | BEGIN 63 | "#ifndef APSTUDIO_INVOKED\r\n" 64 | "#include ""targetver.h""\r\n" 65 | "#endif\r\n" 66 | "#define APSTUDIO_HIDDEN_SYMBOLS\r\n" 67 | "#include ""windows.h""\r\n" 68 | "#undef APSTUDIO_HIDDEN_SYMBOLS\r\n" 69 | "\0" 70 | END 71 | 72 | 3 TEXTINCLUDE 73 | BEGIN 74 | "\r\n" 75 | "\0" 76 | END 77 | 78 | #endif // APSTUDIO_INVOKED 79 | 80 | 81 | ///////////////////////////////////////////////////////////////////////////// 82 | // 83 | // String Table 84 | // 85 | 86 | STRINGTABLE 87 | BEGIN 88 | IDS_APP_TITLE "bookget" 89 | IDC_BOOKGETAPP "BOOKGETAPP" 90 | END 91 | 92 | #endif // English (United States) resources 93 | ///////////////////////////////////////////////////////////////////////////// 94 | 95 | 96 | 97 | #ifndef APSTUDIO_INVOKED 98 | ///////////////////////////////////////////////////////////////////////////// 99 | // 100 | // Generated from the TEXTINCLUDE 3 resource. 101 | // 102 | 103 | 104 | ///////////////////////////////////////////////////////////////////////////// 105 | #endif // not APSTUDIO_INVOKED 106 | 107 | -------------------------------------------------------------------------------- /bookget-gui/bookgetApp.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29001.49 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebView2Browser", "bookgetApp.vcxproj", "{D65018E5-6B31-4DC7-AFAC-7999384BA4BD}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Debug|x64.ActiveCfg = Debug|x64 17 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Debug|x64.Build.0 = Debug|x64 18 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Debug|x86.ActiveCfg = Debug|Win32 19 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Debug|x86.Build.0 = Debug|Win32 20 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Release|x64.ActiveCfg = Release|x64 21 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Release|x64.Build.0 = Release|x64 22 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Release|x86.ActiveCfg = Release|Win32 23 | {D65018E5-6B31-4DC7-AFAC-7999384BA4BD}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {0C9921DB-4634-415A-9AB1-087EA9B2EB24} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /bookget-gui/bookgetApp.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Header Files 20 | 21 | 22 | Header Files 23 | 24 | 25 | Header Files 26 | 27 | 28 | Header Files 29 | 30 | 31 | Header Files 32 | 33 | 34 | Header Files 35 | 36 | 37 | Header Files 38 | 39 | 40 | Header Files 41 | 42 | 43 | Header Files 44 | 45 | 46 | Header Files 47 | 48 | 49 | Header Files 50 | 51 | 52 | Header Files 53 | 54 | 55 | Header Files 56 | 57 | 58 | 59 | 60 | Source Files 61 | 62 | 63 | Source Files 64 | 65 | 66 | Source Files 67 | 68 | 69 | Source Files 70 | 71 | 72 | Source Files 73 | 74 | 75 | Source Files 76 | 77 | 78 | Source Files 79 | 80 | 81 | Source Files 82 | 83 | 84 | Source Files 85 | 86 | 87 | Source Files 88 | 89 | 90 | Source Files 91 | 92 | 93 | Source Files 94 | 95 | 96 | Source Files 97 | 98 | 99 | Source Files 100 | 101 | 102 | Source Files 103 | 104 | 105 | 106 | 107 | Resource Files 108 | 109 | 110 | 111 | 112 | Resource Files 113 | 114 | 115 | Resource Files 116 | 117 | 118 | -------------------------------------------------------------------------------- /bookget-gui/config.yaml: -------------------------------------------------------------------------------- 1 | # bookget 配置文件config.yaml 2 | global_settings: 3 | download_dir: "downloads" 4 | max_downloads: 1000 # 最大下载次数(翻页) 5 | sleep_time: 3 # 间隔睡眠几秒(翻页) 6 | downloader_mode: 1 # 下载模式 0=从 urls.txt 加载图片URL | 1=自动监听 sites 图片URL | 2 = bookget 共享内存URL 7 | ext: ".jpg" # 保存的文件扩展名 8 | 9 | sites: 10 | - url: "http://read.nlc.cn/menhu/OutOpenBook/getReaderNew*" 11 | ext: ".pdf" #保存的文件扩展名(不使用全局设置,单独为这个URL指定) 12 | intercept: 0 #拦截模式 0=发http请求之前,1=发http请求之后 13 | description: "中国国家图书馆·中国国家数字图书馆" 14 | 15 | - url: "http://szlib.clcn.net.cn/api/commonApi/jpgViewer*" 16 | script: "scripts\\szlib.clcn.net.cn.js" #页面加载完执行的JS代码 17 | intercept: 1 #拦截模式 0=发http请求之前,1=发http请求之后 18 | description: "首都圖書館古籍數字平台" 19 | 20 | - url: "https://rbook.ncl.edu.tw/NCLSearch/WaterMark/GetVideoImage*" 21 | script: "scripts\\rbook.ncl.edu.tw.js" #页面加载完执行的JS代码 22 | intercept: 1 #拦截模式 0=发http请求之前,1=发http请求之后 23 | description: "古籍與特藏文獻資源 - (中國台灣省)國家圖書館" 24 | 25 | - url: "https://guji.nlc.cn/api/common/jpgViewer*" 26 | script: "scripts\\guji.nlc.cn.js" #页面加载完执行的JS代码 27 | intercept: 1 #拦截模式 0=发http请求之前,1=发http请求之后 28 | description: "中華古籍智慧服務平台" 29 | -------------------------------------------------------------------------------- /bookget-gui/env.cpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "env.h" 3 | 4 | std::vector> g_arguments; 5 | std::wstring g_HomeUrl(L"about:blank"); 6 | std::wstring g_urlsFile; 7 | -------------------------------------------------------------------------------- /bookget-gui/env.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "framework.h" 3 | 4 | extern std::vector> g_arguments; 5 | extern std::wstring g_HomeUrl; 6 | extern std::wstring g_urlsFile; 7 | -------------------------------------------------------------------------------- /bookget-gui/framework.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #pragma once 6 | 7 | #include "targetver.h" 8 | #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers 9 | #define _WIN32_WINNT 0x0A00 10 | 11 | // Windows Header Files 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | // C RunTime Header Files 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | //C++ 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | // App specific includes 38 | #include "resource.h" 39 | //#define WEBVIEW2_STATIC 40 | //#define WEBVIEW2_IMPLEMENTATION 41 | #include "WebView2.h" 42 | #include "WebView2EnvironmentOptions.h" 43 | 44 | 45 | // DPI 46 | #define DEFAULT_DPI 96 47 | #define MIN_WINDOW_WIDTH 510 48 | #define MIN_WINDOW_HEIGHT 75 49 | 50 | #define MAX_LOADSTRING 256 51 | 52 | #define INVALID_TAB_ID 0 53 | #define MG_NAVIGATE 1 54 | #define MG_UPDATE_URI 2 55 | #define MG_GO_FORWARD 3 56 | #define MG_GO_BACK 4 57 | #define MG_NAV_STARTING 5 58 | #define MG_NAV_COMPLETED 6 59 | #define MG_RELOAD 7 60 | #define MG_CANCEL 8 61 | #define MG_CREATE_TAB 10 62 | #define MG_UPDATE_TAB 11 63 | #define MG_SWITCH_TAB 12 64 | #define MG_CLOSE_TAB 13 65 | #define MG_CLOSE_WINDOW 14 66 | #define MG_SHOW_OPTIONS 15 67 | #define MG_HIDE_OPTIONS 16 68 | #define MG_OPTIONS_LOST_FOCUS 17 69 | #define MG_OPTION_SELECTED 18 70 | #define MG_SECURITY_UPDATE 19 71 | #define MG_UPDATE_FAVICON 20 72 | #define MG_GET_SETTINGS 21 73 | #define MG_GET_FAVORITES 22 74 | #define MG_REMOVE_FAVORITE 23 75 | #define MG_CLEAR_CACHE 24 76 | #define MG_CLEAR_COOKIES 25 77 | #define MG_GET_HISTORY 26 78 | #define MG_REMOVE_HISTORY_ITEM 27 79 | #define MG_CLEAR_HISTORY 28 80 | 81 | // 82 | #define WM_APP_DOWNLOAD_COMPLETE (WM_APP + 100) 83 | #define WM_APP_DOWNLOAD_NEXT (WM_APP + 101) 84 | #define WM_APP_DO_WORK (WM_APP + 102) 85 | #define WM_APP_UPDATE_UI (WM_APP + 103) 86 | #define WM_APP_HANDLE_URL (WM_APP + 104) 87 | #define WM_NAVIGATE_URL (WM_APP + 105) 88 | #define WM_DOWNLOAD_URL (WM_APP + 106) 89 | -------------------------------------------------------------------------------- /bookget-gui/gui/commands.js: -------------------------------------------------------------------------------- 1 | const commands = { 2 | MG_NAVIGATE: 1, 3 | MG_UPDATE_URI: 2, 4 | MG_GO_FORWARD: 3, 5 | MG_GO_BACK: 4, 6 | MG_NAV_STARTING: 5, 7 | MG_NAV_COMPLETED: 6, 8 | MG_RELOAD: 7, 9 | MG_CANCEL: 8, 10 | MG_CREATE_TAB: 10, 11 | MG_UPDATE_TAB: 11, 12 | MG_SWITCH_TAB: 12, 13 | MG_CLOSE_TAB: 13, 14 | MG_CLOSE_WINDOW: 14, 15 | MG_SHOW_OPTIONS: 15, 16 | MG_HIDE_OPTIONS: 16, 17 | MG_OPTIONS_LOST_FOCUS: 17, 18 | MG_OPTION_SELECTED: 18, 19 | MG_SECURITY_UPDATE: 19, 20 | MG_UPDATE_FAVICON: 20, 21 | MG_GET_SETTINGS: 21, 22 | MG_GET_FAVORITES: 22, 23 | MG_REMOVE_FAVORITE: 23, 24 | MG_CLEAR_CACHE: 24, 25 | MG_CLEAR_COOKIES: 25, 26 | MG_GET_HISTORY: 26, 27 | MG_REMOVE_HISTORY_ITEM: 27, 28 | MG_CLEAR_HISTORY: 28 29 | }; 30 | -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/favorites.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Favorites 4 | 5 | 6 | 7 | 8 | 9 | Favorites 10 | 11 | You don't have any favorites. 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/favorites.js: -------------------------------------------------------------------------------- 1 | const messageHandler = event => { 2 | var message = event.data.message; 3 | var args = event.data.args; 4 | 5 | switch (message) { 6 | case commands.MG_GET_FAVORITES: 7 | loadFavorites(args.favorites); 8 | break; 9 | default: 10 | console.log(`Unexpected message: ${JSON.stringify(event.data)}`); 11 | break; 12 | } 13 | }; 14 | 15 | function requestFavorites() { 16 | let message = { 17 | message: commands.MG_GET_FAVORITES, 18 | args: {} 19 | }; 20 | 21 | window.chrome.webview.postMessage(message); 22 | } 23 | 24 | function removeFavorite(uri) { 25 | let message = { 26 | message: commands.MG_REMOVE_FAVORITE, 27 | args: { 28 | uri: uri 29 | } 30 | }; 31 | 32 | window.chrome.webview.postMessage(message); 33 | } 34 | 35 | function loadFavorites(payload) { 36 | let fragment = document.createDocumentFragment(); 37 | 38 | if (payload.length > 0) { 39 | let container = document.getElementById('entries-container'); 40 | container.textContent = ''; 41 | } 42 | 43 | payload.map(favorite => { 44 | let favoriteContainer = document.createElement('div'); 45 | favoriteContainer.className = 'item-container'; 46 | let favoriteElement = document.createElement('div'); 47 | favoriteElement.className = 'item'; 48 | 49 | let faviconElement = document.createElement('div'); 50 | faviconElement.className = 'favicon'; 51 | let faviconImage = document.createElement('img'); 52 | faviconImage.src = favorite.favicon; 53 | faviconElement.appendChild(faviconImage); 54 | 55 | let labelElement = document.createElement('div'); 56 | labelElement.className = 'label-title'; 57 | let linkElement = document.createElement('a'); 58 | linkElement.textContent = favorite.title; 59 | linkElement.href = favorite.uri; 60 | linkElement.title = favorite.title; 61 | labelElement.appendChild(linkElement); 62 | 63 | let uriElement = document.createElement('div'); 64 | uriElement.className = 'label-uri'; 65 | let textElement = document.createElement('p'); 66 | textElement.textContent = favorite.uriToShow || favorite.uri; 67 | textElement.title = favorite.uriToShow || favorite.uri; 68 | uriElement.appendChild(textElement); 69 | 70 | let buttonElement = document.createElement('div'); 71 | buttonElement.className = 'btn-close'; 72 | buttonElement.addEventListener('click', function(e) { 73 | favoriteContainer.parentNode.removeChild(favoriteContainer); 74 | removeFavorite(favorite.uri); 75 | }); 76 | 77 | favoriteElement.appendChild(faviconElement); 78 | favoriteElement.appendChild(labelElement); 79 | favoriteElement.appendChild(uriElement); 80 | favoriteElement.appendChild(buttonElement); 81 | 82 | favoriteContainer.appendChild(favoriteElement); 83 | fragment.appendChild(favoriteContainer); 84 | }); 85 | 86 | let container = document.getElementById('entries-container'); 87 | container.appendChild(fragment); 88 | } 89 | 90 | function init() { 91 | window.chrome.webview.addEventListener('message', messageHandler); 92 | requestFavorites(); 93 | } 94 | 95 | init(); 96 | -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/history.css: -------------------------------------------------------------------------------- 1 | .header-date { 2 | font-weight: 400; 3 | font-size: 14px; 4 | color: rgb(16, 16, 16); 5 | line-height: 20px; 6 | padding-top: 10px; 7 | padding-bottom: 4px; 8 | margin: 0; 9 | } 10 | 11 | #btn-clear { 12 | font-size: 14px; 13 | color: rgb(0, 97, 171); 14 | cursor: pointer; 15 | line-height: 20px; 16 | } 17 | 18 | #btn-clear.hidden { 19 | display: none; 20 | } 21 | 22 | #overlay { 23 | position: fixed; 24 | top: 0; 25 | left: 0; 26 | height: 100%; 27 | width: 100%; 28 | background-color: rgba(0, 0, 0, 0.2); 29 | } 30 | 31 | #overlay.hidden { 32 | display: none; 33 | } 34 | 35 | #prompt-box { 36 | display: flex; 37 | box-sizing: border-box; 38 | flex-direction: column; 39 | position: fixed; 40 | left: calc(50% - 130px); 41 | top: calc(50% - 70px); 42 | width: 260px; 43 | height: 140px; 44 | padding: 20px; 45 | border-radius: 5px; 46 | background-color: white; 47 | 48 | box-shadow: rgba(0, 0, 0, 0.13) 0px 1.6px 20px, rgba(0, 0, 0, 0.11) 0px 0.3px 10px; 49 | } 50 | 51 | #prompt-options { 52 | flex: 1; 53 | display: flex; 54 | justify-content: flex-end; 55 | 56 | user-select: none; 57 | } 58 | 59 | .prompt-btn { 60 | flex: 1; 61 | flex-grow: 0; 62 | align-self: flex-end; 63 | cursor: pointer; 64 | font-family: 'system-ui', sans-serif; 65 | display: inline-block; 66 | padding: 2px 7px; 67 | font-size: 14px; 68 | line-height: 20px; 69 | border-radius: 3px; 70 | font-weight: 400; 71 | } 72 | 73 | #prompt-true { 74 | background-color: rgb(0, 112, 198); 75 | color: white; 76 | } 77 | 78 | #prompt-false { 79 | background-color: rgb(210, 210, 210); 80 | margin-right: 5px; 81 | } 82 | -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/history.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | History 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Clear history? 13 | 14 | Cancel 15 | Clear 16 | 17 | 18 | 19 | History 20 | 21 | Clear history 22 | 23 | 24 | Loading... 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/content_ui/img/close.png -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/img/favorites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/content_ui/img/favorites.png -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/img/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/content_ui/img/history.png -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/img/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/content_ui/img/settings.png -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/items.css: -------------------------------------------------------------------------------- 1 | .item-container { 2 | box-sizing: border-box; 3 | padding: 4px 0; 4 | height: 48px; 5 | } 6 | 7 | .item { 8 | display: flex; 9 | width: 100%; 10 | max-width: 820px; 11 | height: 40px; 12 | width: 100%; 13 | border-radius: 4px; 14 | box-sizing: border-box; 15 | box-shadow: rgba(0, 0, 0, 0.13) 0px 1.6px 3.6px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px; 16 | align-items: center; 17 | background: rgb(255, 255, 255); 18 | } 19 | 20 | .item:hover { 21 | box-shadow: 0px 4.8px 10.8px rgba(0,0,0,0.13), 0px 0.9px 2.7px rgba(0,0,0,0.11); 22 | } 23 | 24 | .favicon { 25 | display: flex; 26 | height: 40px; 27 | width: 40px; 28 | justify-content: center; 29 | align-items: center; 30 | } 31 | 32 | .favicon img { 33 | width: 16px; 34 | height: 16px; 35 | } 36 | 37 | .label-title, .label-uri { 38 | display: flex; 39 | flex: 1; 40 | min-width: 0; 41 | height: 40px; 42 | align-items: center; 43 | } 44 | 45 | .label-title a { 46 | cursor: pointer; 47 | display: inline-block; 48 | white-space: nowrap; 49 | text-overflow: ellipsis; 50 | font-weight: 600; 51 | font-size: 12px; 52 | line-height: 16px; 53 | color: rgb(16, 16, 16); 54 | border-width: initial; 55 | border-style: none; 56 | border-color: initial; 57 | border-image: initial; 58 | overflow: hidden; 59 | background: none; 60 | text-decoration: none; 61 | } 62 | 63 | .label-title a:hover { 64 | text-decoration: underline; 65 | } 66 | 67 | .label-uri, .label-time { 68 | margin: 0 6px; 69 | line-height: 16px; 70 | font-size: 12px; 71 | color: rgb(115, 115, 115); 72 | } 73 | 74 | .label-uri p, .label-time p { 75 | display: block; 76 | white-space: nowrap; 77 | overflow: hidden; 78 | text-overflow: ellipsis; 79 | } 80 | 81 | .btn-close { 82 | height: 28px; 83 | width: 28px; 84 | margin: 6px; 85 | border-radius: 2px; 86 | background-image: url('img/close.png'); 87 | background-size: 100%; 88 | } 89 | 90 | .btn-close:hover { 91 | background-color: rgb(243, 243, 243); 92 | } 93 | -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/settings.css: -------------------------------------------------------------------------------- 1 | .settings-entry { 2 | display: block; 3 | height: 48px; 4 | width: 100%; 5 | max-width: 500px; 6 | 7 | background: none; 8 | border: none; 9 | padding: 4px 0; 10 | font: inherit; 11 | cursor: pointer; 12 | } 13 | 14 | #entry-script, #entry-popups { 15 | display: none; 16 | } 17 | 18 | .entry { 19 | display: block; 20 | height: 100%; 21 | text-align: left; 22 | border-radius: 5px; 23 | } 24 | 25 | .entry:hover { 26 | background-color: rgb(220, 220, 220); 27 | } 28 | 29 | .entry:focus { 30 | outline: none; 31 | } 32 | 33 | .entry-name, .entry-value { 34 | display: inline-flex; 35 | height: 100%; 36 | vertical-align: middle; 37 | } 38 | 39 | .entry-name span, .entry-value span { 40 | flex: 1; 41 | align-self: center; 42 | } 43 | 44 | .entry-name { 45 | padding-left: 10px; 46 | } 47 | 48 | .entry-value { 49 | float: right; 50 | vertical-align: middle; 51 | margin: 0 15px; 52 | font-size: 0.8em; 53 | color: gray; 54 | } 55 | -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Settings 4 | 5 | 6 | 7 | 8 | 9 | Settings 10 | 11 | 12 | 13 | 14 | Clear cache 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Clear cookies 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Toggle JavaScript 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Block pop-ups 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/settings.js: -------------------------------------------------------------------------------- 1 | const messageHandler = event => { 2 | var message = event.data.message; 3 | var args = event.data.args; 4 | 5 | switch (message) { 6 | case commands.MG_GET_SETTINGS: 7 | loadSettings(args.settings); 8 | break; 9 | case commands.MG_CLEAR_CACHE: 10 | if (args.content && args.controls) { 11 | updateLabelForEntry('entry-cache', 'Cleared'); 12 | } else { 13 | updateLabelForEntry('entry-cache', 'Try again'); 14 | } 15 | break; 16 | case commands.MG_CLEAR_COOKIES: 17 | if (args.content && args.controls) { 18 | updateLabelForEntry('entry-cookies', 'Cleared'); 19 | } else { 20 | updateLabelForEntry('entry-cookies', 'Try again'); 21 | } 22 | break; 23 | default: 24 | console.log(`Unexpected message: ${JSON.stringify(event.data)}`); 25 | break; 26 | } 27 | }; 28 | 29 | function addEntriesListeners() { 30 | let cacheEntry = document.getElementById('entry-cache'); 31 | cacheEntry.addEventListener('click', function(e) { 32 | let message = { 33 | message: commands.MG_CLEAR_CACHE, 34 | args: {} 35 | }; 36 | 37 | window.chrome.webview.postMessage(message); 38 | }); 39 | 40 | let cookiesEntry = document.getElementById('entry-cookies'); 41 | cookiesEntry.addEventListener('click', function(e) { 42 | let message = { 43 | message: commands.MG_CLEAR_COOKIES, 44 | args: {} 45 | }; 46 | 47 | window.chrome.webview.postMessage(message); 48 | }); 49 | 50 | let scriptEntry = document.getElementById('entry-script'); 51 | scriptEntry.addEventListener('click', function(e) { 52 | // Toggle script support 53 | }); 54 | 55 | let popupsEntry = document.getElementById('entry-popups'); 56 | popupsEntry.addEventListener('click', function(e) { 57 | // Toggle popups 58 | }); 59 | } 60 | 61 | function requestBrowserSettings() { 62 | let message = { 63 | message: commands.MG_GET_SETTINGS, 64 | args: {} 65 | }; 66 | 67 | window.chrome.webview.postMessage(message); 68 | } 69 | 70 | function loadSettings(settings) { 71 | if (settings.scriptsEnabled) { 72 | updateLabelForEntry('entry-script', 'Enabled'); 73 | } else { 74 | updateLabelForEntry('entry-script', 'Disabled'); 75 | } 76 | 77 | if (settings.blockPopups) { 78 | updateLabelForEntry('entry-popups', 'Blocked'); 79 | } else { 80 | updateLabelForEntry('entry-popups', 'Allowed'); 81 | } 82 | } 83 | 84 | function updateLabelForEntry(elementId, label) { 85 | let entryElement = document.getElementById(elementId); 86 | if (!entryElement) { 87 | console.log(`Element with id ${elementId} does not exist`); 88 | return; 89 | } 90 | 91 | let labelSpan = entryElement.querySelector(`.entry-value span`); 92 | 93 | if (!labelSpan) { 94 | console.log(`${elementId} does not have a label`); 95 | return; 96 | } 97 | 98 | labelSpan.textContent = label; 99 | } 100 | 101 | function init() { 102 | window.chrome.webview.addEventListener('message', messageHandler); 103 | requestBrowserSettings(); 104 | addEntriesListeners(); 105 | } 106 | 107 | init(); 108 | -------------------------------------------------------------------------------- /bookget-gui/gui/content_ui/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | border: none; 4 | padding: 0; 5 | 6 | font-family: Arial, Helvetica, sans-serif; 7 | background-color: rgb(240, 240, 242); 8 | } 9 | 10 | p { 11 | margin: 0; 12 | } 13 | 14 | body { 15 | padding: 32px 50px; 16 | font-family: 'system-ui', sans-serif; 17 | } 18 | 19 | .main-title { 20 | display: block; 21 | margin-bottom: 18px; 22 | font-size: 24px; 23 | font-weight: 600; 24 | color: rgb(16, 16, 16); 25 | } 26 | -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/address-bar.css: -------------------------------------------------------------------------------- 1 | #address-bar-container { 2 | display: flex; 3 | height: calc(100% - 10px); 4 | width: 80%; 5 | max-width: calc(100% - 160px); 6 | 7 | background-color: white; 8 | border: 1px solid gray; 9 | border-radius: 5px; 10 | 11 | position: relative; 12 | align-self: center; 13 | } 14 | 15 | #address-bar-container:focus-within { 16 | outline: none; 17 | box-shadow: 0 0 3px dodgerblue; 18 | } 19 | 20 | #address-bar-container:focus-within #btn-clear { 21 | display: block; 22 | } 23 | 24 | #security-label { 25 | display: inline-flex; 26 | height: 100%; 27 | margin-left: 2px; 28 | 29 | vertical-align: top; 30 | } 31 | 32 | #security-label span { 33 | font-family: Arial; 34 | font-size: 0.9em; 35 | color: gray; 36 | vertical-align: middle; 37 | flex: 1; 38 | align-self: center; 39 | text-align: left; 40 | padding-left: 5px; 41 | white-space: nowrap; 42 | } 43 | 44 | .icn { 45 | display: inline-block; 46 | margin: 2px 0; 47 | border-radius: 5px; 48 | top: 0; 49 | width: 26px; 50 | height: 26px; 51 | } 52 | 53 | #icn-lock { 54 | background-size: 100%; 55 | } 56 | 57 | #security-label.label-unknown .icn { 58 | background-image: url('img/unknown.png'); 59 | } 60 | 61 | #security-label.label-insecure .icn { 62 | background-image: url('img/insecure.png'); 63 | } 64 | 65 | #security-label.label-insecure span { 66 | color: rgb(192, 0, 0); 67 | } 68 | 69 | #security-label.label-neutral .icn { 70 | background-image: url('img/neutral.png'); 71 | } 72 | 73 | #security-label.label-secure .icn { 74 | background-image: url('img/secure.png'); 75 | } 76 | 77 | #security-label.label-secure span, #security-label.label-neutral span { 78 | display: none; 79 | } 80 | 81 | #icn-favicon { 82 | background-size: 100%; 83 | } 84 | 85 | #img-favicon { 86 | width: 18px; 87 | height: 18px; 88 | padding: 4px; 89 | } 90 | 91 | #address-form { 92 | margin: 0; 93 | } 94 | 95 | #address-field { 96 | flex: 1; 97 | padding: 0; 98 | border: none; 99 | border-radius: 5px; 100 | margin: 0; 101 | 102 | line-height: 30px; 103 | width: 100%; 104 | } 105 | 106 | #address-field:focus { 107 | outline: none; 108 | } 109 | 110 | #btn-fav { 111 | margin: 2px 5px; 112 | background-size: 100%; 113 | background-image: url('img/favorite.png'); 114 | } 115 | 116 | #btn-fav:hover, #btn-clear:hover { 117 | background-color: rgb(230, 230, 230); 118 | } 119 | 120 | #btn-fav.favorited { 121 | background-image: url('img/favorited.png'); 122 | } 123 | 124 | #btn-clear { 125 | display: none; 126 | width: 16px; 127 | height: 16px; 128 | border: none; 129 | align-self: center; 130 | background-color: transparent; 131 | background-image: url(img/cancel.png); 132 | background-size: 100%; 133 | border: none; 134 | border-radius: 8px; 135 | } 136 | -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/controls.css: -------------------------------------------------------------------------------- 1 | #controls-bar { 2 | display: flex; 3 | justify-content: space-between; 4 | flex-direction: row; 5 | height: 40px; 6 | background-color: rgb(230, 230, 230); 7 | } 8 | 9 | .btn, .btn-disabled, .btn-cancel, .btn-active { 10 | display: inline-block; 11 | border: none; 12 | margin: 5px 0; 13 | border-radius: 5px; 14 | outline: none; 15 | height: 30px; 16 | width: 30px; 17 | 18 | background-size: 100%; 19 | } 20 | 21 | #btn-forward { 22 | background-image: url('img/goForward.png'); 23 | } 24 | 25 | .btn-disabled#btn-forward { 26 | background-image: url('img/goForward_disabled.png'); 27 | } 28 | 29 | #btn-back { 30 | background-image: url('img/goBack.png'); 31 | } 32 | 33 | .btn-disabled#btn-back { 34 | background-image: url('img/goBack_disabled.png'); 35 | } 36 | 37 | #btn-reload { 38 | background-image: url('img/reload.png'); 39 | } 40 | 41 | .btn-cancel#btn-reload { 42 | background-image: url('img/cancel.png'); 43 | } 44 | 45 | #btn-options { 46 | background-image: url('img/options.png'); 47 | } 48 | 49 | .controls-group { 50 | display: inline-block; 51 | height: 40px; 52 | } 53 | 54 | #nav-controls-container { 55 | align-self: flex-start; 56 | padding-left: 10px; 57 | } 58 | 59 | #manage-controls-container { 60 | align-self: flex-end; 61 | padding-right: 10px; 62 | } 63 | 64 | .btn:hover, .btn-cancel:hover, .btn-active { 65 | background-color: rgb(200, 200, 200); 66 | } 67 | -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/default.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 70px; 3 | } 4 | -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/favorites.js: -------------------------------------------------------------------------------- 1 | function isFavorite(uri, callback) { 2 | queryDB((db) => { 3 | let transaction = db.transaction(['favorites']); 4 | let favoritesStore = transaction.objectStore('favorites'); 5 | let favoriteStatusRequest = favoritesStore.get(uri); 6 | 7 | favoriteStatusRequest.onerror = function(event) { 8 | console.log(`Could not query for ${uri}: ${event.target.error.message}`); 9 | }; 10 | 11 | favoriteStatusRequest.onsuccess = function() { 12 | callback(favoriteStatusRequest.result); 13 | }; 14 | }); 15 | } 16 | 17 | function addFavorite(favorite, callback) { 18 | queryDB((db) => { 19 | let transaction = db.transaction(['favorites'], 'readwrite'); 20 | let favoritesStore = transaction.objectStore('favorites'); 21 | let addFavoriteRequest = favoritesStore.add(favorite); 22 | 23 | addFavoriteRequest.onerror = function(event) { 24 | console.log(`Could not add favorite with key: ${favorite.uri}`); 25 | console.log(event.target.error.message); 26 | }; 27 | 28 | addFavoriteRequest.onsuccess = function(event) { 29 | if (callback) { 30 | callback(); 31 | } 32 | }; 33 | }); 34 | } 35 | 36 | function removeFavorite(key, callback) { 37 | queryDB((db) => { 38 | let transaction = db.transaction(['favorites'], 'readwrite'); 39 | let favoritesStore = transaction.objectStore('favorites'); 40 | let removeFavoriteRequest = favoritesStore.delete(key); 41 | 42 | removeFavoriteRequest.onerror = function(event) { 43 | console.log(`Could not remove favorite with key: ${key}`); 44 | console.log(event.target.error.message); 45 | }; 46 | 47 | removeFavoriteRequest.onsuccess = function(event) { 48 | if (callback) { 49 | callback(); 50 | } 51 | }; 52 | }); 53 | } 54 | 55 | function getFavoritesAsJson(callback) { 56 | queryDB((db) => { 57 | let transaction = db.transaction(['favorites']); 58 | let favoritesStore = transaction.objectStore('favorites'); 59 | let getFavoritesRequest = favoritesStore.getAll(); 60 | 61 | getFavoritesRequest.onerror = function(event) { 62 | console.log(`Could retrieve favorites`); 63 | console.log(event.target.error.message); 64 | }; 65 | 66 | getFavoritesRequest.onsuccess = function(event) { 67 | if (callback) { 68 | callback(getFavoritesRequest.result); 69 | } 70 | }; 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/cancel.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/favicon.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/favorite.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/favorited.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/favorited.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/goBack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/goBack.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/goBack_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/goBack_disabled.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/goForward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/goForward.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/goForward_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/goForward_disabled.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/insecure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/insecure.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/neutral.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/options.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/reload.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/secure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/secure.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/img/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deweizhu/bookget/6d4fac3f245fb3b5fd98160843604d8a34842c79/bookget-gui/gui/controls_ui/img/unknown.png -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/options.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 200px; 3 | height: 107px; 4 | } 5 | 6 | #dropdown-wrapper { 7 | width: calc(100% - 2px); 8 | height: calc(100% - 2px); 9 | border: 1px solid gray; 10 | } 11 | 12 | .dropdown-item { 13 | background-color: rgb(240, 240, 240); 14 | } 15 | 16 | .dropdown-item:hover { 17 | background-color: rgb(220, 220, 220); 18 | } 19 | 20 | .item-label { 21 | display: flex; 22 | height: 35px; 23 | text-align: center; 24 | vertical-align: middle; 25 | 26 | white-space: nowrap; 27 | overflow: hidden; 28 | } 29 | 30 | .item-label span { 31 | font-family: Arial; 32 | font-size: 0.8em; 33 | vertical-align: middle; 34 | flex: 1; 35 | align-self: center; 36 | text-align: left; 37 | padding-left: 10px; 38 | } 39 | -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Settings 11 | 12 | 13 | 14 | 15 | History 16 | 17 | 18 | 19 | 20 | Favorites 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/options.js: -------------------------------------------------------------------------------- 1 | function navigateToBrowserPage(path) { 2 | const navMessage = { 3 | message: commands.MG_NAVIGATE, 4 | args: { 5 | uri: `browser://${path}` 6 | } 7 | }; 8 | 9 | window.chrome.webview.postMessage(navMessage); 10 | } 11 | 12 | // Add listener for the options menu entries 13 | function addItemsListeners() { 14 | 15 | // Close dropdown when item is selected 16 | (() => { 17 | const dropdownItems = Array.from(document.getElementsByClassName('dropdown-item')); 18 | dropdownItems.map(item => { 19 | item.addEventListener('click', function(e) { 20 | const closeMessage = { 21 | message: commands.MG_OPTION_SELECTED, 22 | args: {} 23 | }; 24 | window.chrome.webview.postMessage(closeMessage); 25 | }); 26 | 27 | // Navigate to browser page 28 | let entry = item.id.split('-')[1]; 29 | switch (entry) { 30 | case 'settings': 31 | case 'history': 32 | case 'favorites': 33 | item.addEventListener('click', function(e) { 34 | navigateToBrowserPage(entry); 35 | }); 36 | break; 37 | } 38 | }); 39 | })(); 40 | } 41 | 42 | function init() { 43 | addItemsListeners(); 44 | } 45 | 46 | init(); 47 | -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/storage.js: -------------------------------------------------------------------------------- 1 | function handleUpgradeEvent(event) { 2 | console.log('Creating DB'); 3 | let newDB = event.target.result; 4 | 5 | newDB.onerror = function(event) { 6 | console.log('Something went wrong'); 7 | console.log(event); 8 | }; 9 | 10 | let newFavoritesStore = newDB.createObjectStore('favorites', { 11 | keyPath: 'uri' 12 | }); 13 | 14 | newFavoritesStore.transaction.oncomplete = function(event) { 15 | console.log('Object stores created'); 16 | }; 17 | 18 | let newHistoryStore = newDB.createObjectStore('history', { 19 | autoIncrement: true 20 | }); 21 | 22 | newHistoryStore.createIndex('timestamp', 'timestamp', { 23 | unique: false 24 | }); 25 | 26 | newHistoryStore.createIndex('stampedURI', ['uri', 'timestamp'], { 27 | unique: false 28 | }); 29 | } 30 | 31 | function queryDB(query) { 32 | let request = window.indexedDB.open('WVBrowser'); 33 | 34 | request.onerror = function(event) { 35 | console.log('Failed to open database'); 36 | }; 37 | 38 | request.onsuccess = function(event) { 39 | let db = event.target.result; 40 | query(db); 41 | }; 42 | 43 | request.onupgradeneeded = handleUpgradeEvent; 44 | } 45 | -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/strip.css: -------------------------------------------------------------------------------- 1 | #tabs-strip { 2 | display: flex; 3 | height: 28px; 4 | 5 | border-top: 1px solid rgb(200, 200, 200); 6 | border-bottom: 1px solid rgb(200, 200, 200); 7 | background-color: rgb(230, 230, 230); 8 | } 9 | 10 | .tab, .tab-active { 11 | display: flex; 12 | flex: 1; 13 | height: 100%; 14 | border-right: 1px solid rgb(200, 200, 200); 15 | overflow: hidden; 16 | max-width: 250px; 17 | } 18 | 19 | .tab-active { 20 | background-color: rgb(240, 240, 240); 21 | } 22 | 23 | #btn-new-tab { 24 | display: flex; 25 | height: 100%; 26 | width: 30px; 27 | } 28 | 29 | #btn-new-tab:hover, .btn-tab-close:hover { 30 | background-color: rgb(200, 200, 200); 31 | } 32 | 33 | #plus-label { 34 | display: block; 35 | flex: 1; 36 | align-self: center; 37 | line-height: 30px; 38 | width: 30px; 39 | text-align: center; 40 | } 41 | 42 | .btn-tab-close { 43 | display: inline-block; 44 | align-self: center; 45 | min-width: 16px; 46 | width: 16px; 47 | height: 16px; 48 | margin: 0 5px; 49 | border-radius: 4px; 50 | 51 | background-size: 100%; 52 | background-image: url('img/cancel.png'); 53 | } 54 | 55 | .tab-label { 56 | display: flex; 57 | flex: 1; 58 | padding: 5px; 59 | padding-left: 10px; 60 | height: calc(100% - 10px); 61 | vertical-align: middle; 62 | 63 | white-space:nowrap; 64 | overflow:hidden; 65 | } 66 | 67 | .tab-label span { 68 | font-family: Arial; 69 | font-size: 0.8em; 70 | flex: 1; 71 | align-self: center; 72 | } 73 | -------------------------------------------------------------------------------- /bookget-gui/gui/controls_ui/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | border: none; 4 | padding: 0; 5 | } 6 | 7 | p { 8 | margin: 0; 9 | } 10 | 11 | * { 12 | user-select: none; 13 | } 14 | -------------------------------------------------------------------------------- /bookget-gui/scripts/guji.nlc.cn.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | const div = document.querySelector('.container_read_body_image_bottom_num'); 4 | if (!div) { 5 | return 'MAX_PAGE'; 6 | } 7 | const text = div.textContent; 8 | const match = text.match(/\/(\d+)/); // 匹配 "*/数字" 9 | if (!match) { 10 | return 'MAX_PAGE'; 11 | } 12 | // 获取最大页数 13 | const currentIndex = parseInt(match[0]); 14 | const maxPages = parseInt(match[1]); 15 | console.log(maxPages) 16 | if (currentIndex >= maxPages) { 17 | return 'MAX_REACHED'; 18 | } 19 | //自动点击:下一页 20 | const nextBtn = document.querySelector('.container_read_body_image_bottom_arrow_rit'); 21 | console.log(nextBtn) 22 | if (nextBtn) { 23 | nextBtn.click(); 24 | console.log('点击成功') 25 | return 'OK'; 26 | } 27 | console.log('失击失败') 28 | return ''; 29 | })(); -------------------------------------------------------------------------------- /bookget-gui/scripts/rbook.ncl.edu.tw.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // 获取最大页数 3 | const maxPages = document.getElementById('sel-content-no') ? 4 | document.getElementById('sel-content-no').options.length : 0; 5 | 6 | // 从localStorage获取当前执行次数,默认为0 7 | let executionCount = parseInt(localStorage.getItem('pageExecutionCount') || '0'); 8 | 9 | // 如果已经达到或超过最大页数,直接返回 10 | if (executionCount >= maxPages) { 11 | localStorage.clear(); 12 | return 'MAX_REACHED'; 13 | } 14 | 15 | //自动点击:下一页 16 | if (document.getElementById('AftT')) { 17 | document.getElementById('AftT').click(); 18 | 19 | // 增加执行计数并保存到localStorage 20 | executionCount++; 21 | localStorage.setItem('pageExecutionCount', executionCount.toString()); 22 | 23 | return 'OK'; 24 | } 25 | 26 | return ''; 27 | })(); -------------------------------------------------------------------------------- /bookget-gui/scripts/szlib.clcn.net.cn.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | const pages = document.querySelectorAll('.container_read_head_body_image_footer_paging_font'); 4 | const currentPage = pages[0].value || pages[0].textContent; 5 | const totalPages = pages[1].value || pages[1].textContent; 6 | // 获取最大页数 7 | const currentIndex = parseInt(currentPage); 8 | const maxPages = parseInt(totalPages); 9 | if (currentIndex >= maxPages) { 10 | return 'MAX_REACHED'; 11 | } 12 | //自动点击:下一页 13 | const arrow = document.querySelectorAll('.container_read_head_body_image_footer_paging_arrow'); 14 | const nextBtn = arrow[1]; 15 | console.log(nextBtn) 16 | if (nextBtn) { 17 | nextBtn.click(); 18 | console.log('点击成功') 19 | return 'OK'; 20 | } 21 | console.log('失击失败') 22 | return ''; 23 | })(); -------------------------------------------------------------------------------- /bookget-gui/targetver.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #pragma once 6 | 7 | // Including SDKDDKVer.h defines the highest available Windows platform. 8 | // If you wish to build your application for a previous Windows platform, include WinSDKVer.h and 9 | // set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. 10 | #include 11 | 12 | -------------------------------------------------------------------------------- /config/conf.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/spf13/pflag" 7 | "os" 8 | "path" 9 | "regexp" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type Input struct { 15 | DownloaderMode int //自动检测下载URL。可选值[0|1|2],;0=默认;1=通用批量下载(类似IDM、迅雷);2= IIIF manifest.json 自动检测下载图片 16 | UseDzi bool //启用Dezoomify下载IIIF 17 | 18 | DUrl string 19 | UrlsFile string //已废弃 20 | CookieFile string //输入 chttp.txt 21 | HeaderFile string //输入 header.txt 22 | 23 | Seq string //页面范围 4:434 24 | SeqStart int 25 | SeqEnd int 26 | Volume string //册范围 4:434 27 | VolStart int 28 | VolEnd int 29 | 30 | Sleep int //限速 31 | Directory string //下载文件存放目录,默认为当前文件夹下 Downloads 目录下 32 | Format string //;全高清图下载时,指定宽度像素(16开纸185mm*260mm,像素2185*3071) 33 | UserAgent string //自定义UserAgent 34 | 35 | Threads int 36 | MaxConcurrent int 37 | Timeout time.Duration //超时秒数 38 | Retries int //重试次数 39 | 40 | FileExt string //指定下载的扩展名 41 | Quality int //JPG品质 42 | 43 | Help bool 44 | Version bool 45 | } 46 | 47 | func Init(ctx context.Context) bool { 48 | 49 | dir, _ := os.Getwd() 50 | 51 | //你们为什么没有良好的电脑使用习惯?中文虽好,但不适用于计算机。 52 | if os.PathSeparator == '\\' { 53 | matched, _ := regexp.MatchString(`([^A-z0-9_\\/\-:.]+)`, dir) 54 | if matched { 55 | fmt.Println("本软件存放目录,不能包含空格、中文等特殊符号。推荐:D:\\bookget") 56 | fmt.Println("按回车键终止程序。Press Enter to exit ...") 57 | endKey := make([]byte, 1) 58 | os.Stdin.Read(endKey) 59 | os.Exit(0) 60 | } 61 | } 62 | 63 | pflag.StringVarP(&Conf.DUrl, "input", "i", "", "下载 URL") 64 | pflag.StringVarP(&Conf.UrlsFile, "input-file", "I", "", "下载 URLs") 65 | pflag.StringVarP(&Conf.Directory, "dir", "O", path.Join(dir, "downloads"), "保存文件到目录") 66 | 67 | pflag.StringVarP(&Conf.Seq, "sequence", "p", "", "页面范围,如4:434") 68 | pflag.StringVarP(&Conf.Volume, "volume", "v", "", "多册图书,如10:20册,只下载10至20册") 69 | 70 | pflag.StringVar(&Conf.Format, "format", "full/full/0/default.jpg", "IIIF 图像请求URI") 71 | 72 | pflag.StringVarP(&Conf.UserAgent, "user-agent", "U", defaultUserAgent, "http头信息 user-agent") 73 | 74 | pflag.BoolVarP(&Conf.UseDzi, "dzi", "d", true, "使用 IIIF/DeepZoom 拼图下载") 75 | 76 | pflag.StringVarP(&Conf.CookieFile, "cookies", "C", path.Join(dir, "cookie.txt"), "cookie 文件") 77 | pflag.StringVarP(&Conf.HeaderFile, "headers", "H", path.Join(dir, "header.txt"), "header 文件") 78 | 79 | pflag.IntVarP(&Conf.Threads, "threads", "n", 1, "每任务最大线程数") 80 | pflag.IntVarP(&Conf.MaxConcurrent, "concurrent", "c", 16, "最大并发任务数") 81 | 82 | pflag.IntVar(&Conf.Quality, "quality", 80, "JPG品质,默认80") 83 | pflag.StringVar(&Conf.FileExt, "ext", ".jpg", "指定文件扩展名[.jpg|.tif|.png]等") 84 | 85 | pflag.IntVar(&Conf.Retries, "retries", 3, "下载重试次数") 86 | 87 | pflag.DurationVarP(&Conf.Timeout, "timeout", "T", 300, "网络超时(秒)") 88 | pflag.IntVar(&Conf.Sleep, "sleep", 3, "间隔睡眠几秒,一般情况 3-20") 89 | 90 | pflag.IntVarP(&Conf.DownloaderMode, "downloader_mode", "m", 0, "下载模式。可选值[0|1|2],;0=默认;\n1=通用批量下载(类似IDM、迅雷);\n2= IIIF manifest.json 自动检测下载图片") 91 | 92 | pflag.BoolVarP(&Conf.Help, "help", "h", false, "显示帮助") 93 | pflag.BoolVarP(&Conf.Version, "version", "V", false, "显示版本 -v") 94 | pflag.Parse() 95 | 96 | k := len(os.Args) 97 | if k == 2 { 98 | if Conf.Version { 99 | printVersion() 100 | return false 101 | } 102 | if Conf.Help { 103 | printHelp() 104 | return false 105 | } 106 | } 107 | v := pflag.Arg(0) 108 | if strings.HasPrefix(v, "http") { 109 | Conf.DUrl = v 110 | } 111 | if Conf.UrlsFile != "" && !strings.Contains(Conf.UrlsFile, string(os.PathSeparator)) { 112 | Conf.UrlsFile = path.Join(dir, Conf.UrlsFile) 113 | } 114 | initSeqRange() 115 | initVolumeRange() 116 | //保存目录处理 117 | _ = os.Mkdir(Conf.Directory, os.ModePerm) 118 | //_ = os.Mkdir(CacheDir(), os.ModePerm) 119 | return true 120 | } 121 | 122 | func printHelp() { 123 | printVersion() 124 | fmt.Println(`Usage: bookget [OPTION]... [URL]...`) 125 | pflag.PrintDefaults() 126 | fmt.Println() 127 | fmt.Println("Originally written by zhudw .") 128 | fmt.Println("https://github.com/deweizhu/bookget/") 129 | } 130 | 131 | func printVersion() { 132 | fmt.Printf("bookget v%s\n", Version) 133 | } 134 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | const configContent = ` 10 | ` 11 | 12 | // CreateConfigIfNotExists 检查并创建配置文件 13 | func CreateConfigIfNotExists(configPath string) error { 14 | // 检查文件是否存在 15 | if _, err := os.Stat(configPath); os.IsNotExist(err) { 16 | dir := filepath.Dir(configPath) 17 | if dir != "" { 18 | if err := os.MkdirAll(dir, 0755); err != nil { 19 | return fmt.Errorf("创建目录失败: %w", err) 20 | } 21 | } 22 | 23 | // 文件不存在,创建并写入内容 24 | if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 25 | return fmt.Errorf("创建配置文件失败: %w", err) 26 | } 27 | fmt.Printf("配置文件已创建: %s\n", configPath) 28 | } else if err != nil { 29 | // 其他错误 30 | return fmt.Errorf("检查配置文件失败: %w", err) 31 | } else { 32 | fmt.Printf("配置文件在: %s\n", configPath) 33 | } 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /config/constant.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "time" 7 | ) 8 | 9 | const ( 10 | Version = "25.0601" 11 | CatalogVersionInfo = "#版本=1.0" // 书签目录版本TXT 12 | defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36" 13 | defaultFileExtension = ".jpg" 14 | 15 | defaultRetry = 3 16 | defaultTimeout = 300 * time.Second 17 | defaultQuality = 80 18 | defaultFormat = "full/full/0/default.jpg" 19 | ) 20 | 21 | func UserHomeDir() string { 22 | if os.PathSeparator == '\\' { 23 | home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 24 | if home == "" { 25 | home = os.Getenv("USERPROFILE") 26 | } 27 | return home 28 | } 29 | return os.Getenv("HOME") 30 | } 31 | 32 | func BookgetHomeDir() string { 33 | home, err := os.UserHomeDir() 34 | if err == nil { 35 | // Unix-like: ~/bookget/path 36 | // Windows: ~\bookget\path 37 | configDir := filepath.Join(home, "bookget") 38 | if os.PathSeparator == '\\' { // Windows 39 | configDir = filepath.Join(home, "bookget") 40 | } 41 | homeDir := filepath.Join(configDir) 42 | if err := os.Mkdir(homeDir, 0755); err != nil && !os.IsExist(err) { 43 | return "" 44 | } 45 | return homeDir 46 | } 47 | return home 48 | } 49 | 50 | func CacheDir() string { 51 | return filepath.Join(BookgetHomeDir(), "cache") 52 | } 53 | -------------------------------------------------------------------------------- /config/init.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | var Conf Input 9 | 10 | // initSeq false = 最小值 <= 当前页码 <= 最大值 11 | func initSeqRange() { 12 | if Conf.Seq == "" || !strings.Contains(Conf.Seq, ":") { 13 | return 14 | } 15 | m := strings.Split(Conf.Seq, ":") 16 | if len(m) == 1 { 17 | Conf.SeqStart, _ = strconv.Atoi(m[0]) 18 | Conf.SeqEnd = Conf.SeqStart 19 | } else { 20 | Conf.SeqStart, _ = strconv.Atoi(m[0]) 21 | Conf.SeqEnd, _ = strconv.Atoi(m[1]) 22 | } 23 | return 24 | } 25 | 26 | // initVolumeRange false = 最小值 <= 当前页码 <= 最大值 27 | func initVolumeRange() { 28 | m := strings.Split(Conf.Volume, ":") 29 | if len(m) == 1 { 30 | Conf.VolStart, _ = strconv.Atoi(m[0]) 31 | Conf.VolEnd = Conf.VolStart 32 | } else { 33 | Conf.VolStart, _ = strconv.Atoi(m[0]) 34 | Conf.VolEnd, _ = strconv.Atoi(m[1]) 35 | } 36 | return 37 | } 38 | 39 | // PageRange return true (最小值 <= 当前页码 <= 最大值) 40 | func PageRange(index, size int) bool { 41 | //未设置 42 | if Conf.SeqStart <= 0 { 43 | return true 44 | } 45 | //结束页负数 46 | if Conf.SeqEnd < 0 && (index-size >= Conf.SeqEnd) { 47 | return false 48 | } 49 | //结束页 50 | if Conf.SeqEnd > 0 { 51 | //结束了 52 | if index >= Conf.SeqEnd { 53 | return false 54 | } 55 | //起始页 56 | if index+1 >= Conf.SeqStart { 57 | return true 58 | } 59 | } else if index+1 >= Conf.SeqStart { //在起始页后 60 | return true 61 | } 62 | return false 63 | } 64 | 65 | // VolumeRange return true (最小值 <= 当前页码 <= 最大值) 66 | func VolumeRange(index int) bool { 67 | //未设置 68 | if Conf.VolStart <= 0 { 69 | return true 70 | } 71 | //结束页负数 72 | if Conf.VolEnd < 0 && index > Conf.VolStart { 73 | return false 74 | } 75 | //结束页 76 | if Conf.VolEnd > 0 { 77 | //结束了 78 | if index >= Conf.VolEnd { 79 | return false 80 | } 81 | //起始页 82 | if index+1 >= Conf.VolStart { 83 | return true 84 | } 85 | } else if index+1 >= Conf.VolStart { //在起始页后 86 | return true 87 | } 88 | return false 89 | } 90 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module bookget 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.3 6 | 7 | require ( 8 | github.com/andreburgaud/crypt2go v1.8.0 9 | github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 10 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db 11 | github.com/rivo/uniseg v0.4.7 12 | github.com/spf13/pflag v1.0.5 13 | github.com/stretchr/testify v1.9.0 14 | golang.org/x/term v0.31.0 15 | golang.org/x/text v0.18.0 16 | gopkg.in/ini.v1 v1.67.0 17 | ) 18 | 19 | require ( 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | golang.org/x/sys v0.32.0 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andreburgaud/crypt2go v1.8.0 h1:J73vGTb1P6XL69SSuumbKs0DWn3ulbl9L92ZXBjw6pc= 2 | github.com/andreburgaud/crypt2go v1.8.0/go.mod h1:L5nfShQ91W78hOWhUH2tlGRPO+POAPJAF5fKOLB9SXg= 3 | github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= 4 | github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 h1:G+9t9cEtnC9jFiTxyptEKuNIAbiN5ZCQzX2a74lj3xg= 8 | github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004/go.mod h1:KmHnJWQrgEvbuy0vcvj00gtMqbvNn1L+3YUZLK/B92c= 9 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 10 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 11 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 12 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 16 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 17 | github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= 18 | github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= 19 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 20 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 21 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 22 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 23 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 24 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 25 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 26 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 27 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 28 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 29 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 30 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 34 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /model/cuhk/cuhk.go: -------------------------------------------------------------------------------- 1 | package cuhk 2 | 3 | type ResponsePage struct { 4 | ImagePage []ImagePage `json:"pages"` 5 | } 6 | 7 | type ImagePage struct { 8 | Pid string `json:"pid"` 9 | Page string `json:"page"` 10 | Label string `json:"label"` 11 | Width string `json:"width"` 12 | Height string `json:"height"` 13 | Dsid string `json:"dsid"` 14 | Token string `json:"token"` 15 | Identifier string `json:"identifier"` 16 | } 17 | -------------------------------------------------------------------------------- /model/family/family.go: -------------------------------------------------------------------------------- 1 | package family 2 | 3 | type ImageData struct { 4 | DgsNum string 5 | WaypointURL string 6 | ImageURL string 7 | } 8 | type ResultError struct { 9 | Error struct { 10 | Message string `json:"message"` 11 | FailedRoles []string `json:"failedRoles"` 12 | StatusCode int `json:"statusCode"` 13 | } `json:"error"` 14 | } 15 | 16 | type ReqData struct { 17 | Type string `json:"type"` 18 | Args struct { 19 | ImageURL string `json:"imageURL"` 20 | State struct { 21 | ImageOrFilmUrl string `json:"imageOrFilmUrl"` 22 | ViewMode string `json:"viewMode"` 23 | SelectedImageIndex int `json:"selectedImageIndex"` 24 | } `json:"state"` 25 | Locale string `json:"locale"` 26 | } `json:"args"` 27 | } 28 | 29 | type Response struct { 30 | ImageURL string `json:"imageURL"` 31 | ArkId string `json:"arkId"` 32 | DgsNum string `json:"dgsNum"` 33 | Meta struct { 34 | SourceDescriptions []struct { 35 | Id string `json:"id"` 36 | About string `json:"about"` 37 | Titles []struct { 38 | Value string `json:"value"` 39 | Lang string `json:"lang,omitempty"` 40 | } `json:"titles"` 41 | Identifiers struct { 42 | HttpGedcomxOrgPrimary []string `json:"http://gedcomx.org/Primary"` 43 | } `json:"identifiers"` 44 | Descriptor struct { 45 | Resource string `json:"resource"` 46 | } `json:"descriptor,omitempty"` 47 | } `json:"sourceDescriptions"` 48 | } `json:"meta"` 49 | } 50 | 51 | type FilmDataReqData struct { 52 | Type string `json:"type"` 53 | Args struct { 54 | DgsNum string `json:"dgsNum"` 55 | State struct { 56 | I string `json:"i"` 57 | Cat string `json:"cat"` 58 | ImageOrFilmUrl string `json:"imageOrFilmUrl"` 59 | CatalogContext string `json:"catalogContext"` 60 | ViewMode string `json:"viewMode"` 61 | SelectedImageIndex int `json:"selectedImageIndex"` 62 | } `json:"state"` 63 | Locale string `json:"locale"` 64 | SessionId string `json:"sessionId"` 65 | LoggedIn bool `json:"loggedIn"` 66 | } `json:"args"` 67 | } 68 | 69 | type FilmDataResponse struct { 70 | DgsNum string `json:"dgsNum"` 71 | Images []string `json:"images"` 72 | PreferredCatalogId string `json:"preferredCatalogId"` 73 | Type string `json:"type"` 74 | WaypointCrumbs interface{} `json:"waypointCrumbs"` 75 | WaypointURL interface{} `json:"waypointURL"` 76 | Templates struct { 77 | DasTemplate string `json:"dasTemplate"` 78 | DzTemplate string `json:"dzTemplate"` 79 | } `json:"templates"` 80 | } 81 | -------------------------------------------------------------------------------- /model/iiif/dzi.go: -------------------------------------------------------------------------------- 1 | package iiif 2 | 3 | type ServerResponse struct { 4 | Code int `json:"code"` 5 | Message string `json:"message"` 6 | Data struct { 7 | Title string `json:"title"` 8 | ServerBase string `json:"serverBase"` 9 | Images []string `json:"images"` 10 | } `json:"data"` 11 | } 12 | 13 | type BaseResponse struct { 14 | Tiles map[string]Item `json:"tiles"` 15 | } 16 | 17 | type Item struct { 18 | Extension string `json:"extension"` 19 | Height int `json:"height"` 20 | Resolutions int `json:"resolutions"` 21 | TileSize struct { 22 | H int `json:"h"` 23 | W int `json:"w"` 24 | } `json:"tile_size"` 25 | TileSize2 struct { 26 | Height int `json:"height"` 27 | Width int `json:"width"` 28 | } `json:"tileSize"` 29 | Width int `json:"width"` 30 | } 31 | 32 | type DziFormat struct { 33 | Xmlns string `json:"xmlns"` 34 | Url string `json:"Url"` 35 | Overlap int `json:"Overlap"` 36 | TileSize int `json:"TileSize"` 37 | Format string `json:"Format"` 38 | Size struct { 39 | Width int `json:"Width"` 40 | Height int `json:"Height"` 41 | } `json:"Size"` 42 | } 43 | -------------------------------------------------------------------------------- /model/iiif/iiif.go: -------------------------------------------------------------------------------- 1 | package iiif 2 | 3 | // ManifestResponse by view-source:https://iiif.lib.harvard.edu/manifests/drs:53262215 4 | type ManifestResponse struct { 5 | Sequences []struct { 6 | Canvases []struct { 7 | Id string `json:"@id"` 8 | Type string `json:"@type"` 9 | //兼容某些不正规的网站竟然用了string类型,见https://digitalarchive.npm.gov.tw/Antique/setJsonU?uid=58102&Dept=U 10 | //Height int `json:"height"` 11 | Images []struct { 12 | Id string `json:"@id"` 13 | Type string `json:"@type"` 14 | On string `json:"on"` 15 | Resource struct { 16 | Id string `json:"@id"` 17 | Type string `json:"@type"` 18 | Format string `json:"format"` 19 | //兼容digitalarchive.npm.gov.tw 20 | //Height int `json:"height"` 21 | Service struct { 22 | Id string `json:"@id"` 23 | } `json:"service"` 24 | Width int `json:"width"` 25 | } `json:"resource"` 26 | } `json:"images"` 27 | Label string `json:"label"` 28 | //Width int `json:"width"` 29 | } `json:"canvases"` 30 | } `json:"sequences"` 31 | } 32 | 33 | // ManifestV3Response https://iiif.io/api/presentation/3.0/#52-manifest 34 | type ManifestV3Response struct { 35 | Id string `json:"id"` 36 | Type string `json:"type"` 37 | Label struct { 38 | None []string `json:"none"` 39 | } `json:"label"` 40 | Height int `json:"height"` 41 | Width int `json:"width"` 42 | Canvases []struct { 43 | Id string `json:"id"` 44 | Type string `json:"type"` 45 | Height int `json:"height"` 46 | Width int `json:"width"` 47 | Items []struct { 48 | Id string `json:"id"` 49 | Type string `json:"type"` 50 | Items []struct { 51 | Id string `json:"id"` 52 | Type string `json:"type"` 53 | Motivation string `json:"motivation"` 54 | Body struct { 55 | Id string `json:"id"` 56 | Type string `json:"type"` 57 | Format string `json:"format"` 58 | Service []struct { 59 | Id string `json:"id"` 60 | Type string `json:"type"` 61 | //![ See https://da.library.pref.osaka.jp/api/items/03-0000183/manifest.json 62 | Id_ string `json:"@id"` 63 | Type_ string `json:"@type"` 64 | //]! 65 | Profile string `json:"profile"` 66 | } `json:"service"` 67 | Height int `json:"height"` 68 | Width int `json:"width"` 69 | } `json:"body"` 70 | Target string `json:"target"` 71 | } `json:"items"` 72 | } `json:"items"` 73 | } `json:"items"` 74 | Annotations []struct { 75 | Id string `json:"id"` 76 | Type string `json:"type"` 77 | Items []interface{} `json:"items"` 78 | } `json:"annotations"` 79 | } 80 | 81 | type ManifestPresentation struct { 82 | Context string `json:"@context"` 83 | Id string `json:"id"` 84 | } 85 | -------------------------------------------------------------------------------- /model/korea/korea.go: -------------------------------------------------------------------------------- 1 | package korea 2 | 3 | type Response struct { 4 | ImgInfos []struct { 5 | BookNum string `json:"bookNum"` 6 | Num string `json:"num"` 7 | BookPath string `json:"bookPath"` 8 | ImgNum string `json:"imgNum"` 9 | Fname string `json:"fname"` 10 | } `json:"imgInfos"` 11 | BookNum string `json:"bookNum"` 12 | } 13 | 14 | type PartialCanvases struct { 15 | Directory string 16 | Title string 17 | Canvases []string 18 | } 19 | -------------------------------------------------------------------------------- /model/loc/loc.go: -------------------------------------------------------------------------------- 1 | package loc 2 | 3 | type ManifestsJson struct { 4 | Resources []struct { 5 | Caption string `json:"caption"` 6 | Files [][]ImageFile `json:"files"` 7 | Image string `json:"image"` 8 | Url string `json:"url"` 9 | } `json:"resources"` 10 | } 11 | type ImageFile struct { 12 | Height *int `json:"height"` 13 | Levels int `json:"levels"` 14 | Mimetype string `json:"mimetype"` 15 | Url string `json:"url"` 16 | Width *int `json:"width"` 17 | Info string `json:"info,omitempty"` 18 | Size int `json:"size,omitempty"` 19 | } 20 | -------------------------------------------------------------------------------- /model/njuedu/njuedu.go: -------------------------------------------------------------------------------- 1 | package njuedu 2 | 3 | type Catalog struct { 4 | Code int `json:"code"` 5 | Message string `json:"message"` 6 | Data []struct { 7 | BookId string `json:"bookId"` 8 | BookName string `json:"bookName"` 9 | VolumeNum string `json:"volumeNum"` 10 | ImgDescription interface{} `json:"imgDescription"` 11 | Catalogues []interface{} `json:"catalogues"` 12 | } `json:"data"` 13 | } 14 | type Response struct { 15 | Code int `json:"code"` 16 | Message string `json:"message"` 17 | Data struct { 18 | Title string `json:"title"` 19 | ServerBase string `json:"serverBase"` 20 | Images []string `json:"images"` 21 | } `json:"data"` 22 | } 23 | 24 | type Detail struct { 25 | Code int `json:"code"` 26 | Message string `json:"message"` 27 | Data []struct { 28 | Id int `json:"id"` 29 | BookId string `json:"bookId"` 30 | Num int `json:"num"` 31 | AttributeId int `json:"attributeId"` 32 | AttributeValue string `json:"attributeValue"` 33 | Operator int `json:"operator"` 34 | OperatorName string `json:"operatorName"` 35 | CreateTime int `json:"createTime"` 36 | UpdateTime int `json:"updateTime"` 37 | TypeId int `json:"typeId"` 38 | Captions string `json:"captions"` 39 | } `json:"data"` 40 | } 41 | 42 | type Item struct { 43 | Extension string `json:"extension"` 44 | Height int `json:"height"` 45 | Resolutions int `json:"resolutions"` 46 | TileSize struct { 47 | H int `json:"h"` 48 | W int `json:"w"` 49 | } `json:"tile_size"` 50 | TileSize2 struct { 51 | Height int `json:"height"` 52 | Width int `json:"width"` 53 | } `json:"tileSize"` 54 | Width int `json:"width"` 55 | } 56 | 57 | type ResponseTiles struct { 58 | Tiles map[string]Item `json:"tiles"` 59 | } 60 | -------------------------------------------------------------------------------- /model/nlc/nlc.go: -------------------------------------------------------------------------------- 1 | package nlc 2 | 3 | // 基础响应结构 4 | type BaseResponse struct { 5 | Msg string `json:"msg"` 6 | Code int `json:"code"` 7 | Data Node `json:"data"` 8 | } 9 | 10 | // 数据项结构 11 | type DataItem struct { 12 | OrderSeq string `json:"orderSeq"` 13 | ImageId string `json:"imageId"` 14 | StructureId int `json:"structureId"` 15 | PageNum int `json:"pageNum"` 16 | } 17 | 18 | // 通用节点结构(用于多级嵌套) 19 | type Node struct { 20 | ImageIdList []DataItem `json:"imageIdList"` 21 | Total int `json:"total"` 22 | } 23 | 24 | type ImageData struct { 25 | Msg string `json:"msg"` 26 | Code int `json:"code"` 27 | Data struct { 28 | FileName string `json:"fileName"` 29 | ImageId int `json:"imageId"` 30 | FilePath string `json:"filePath"` 31 | StructureId int `json:"structureId"` 32 | UpdateTime string `json:"updateTime"` 33 | CreateTime string `json:"createTime"` 34 | FileType string `json:"fileType"` 35 | } `json:"data"` 36 | } 37 | 38 | // 目录 39 | type StructureResponse struct { 40 | Code int `json:"code"` 41 | Data []Volume `json:"data"` 42 | } 43 | 44 | type Volume struct { 45 | Children []CatalogItem `json:"children"` 46 | } 47 | 48 | type CatalogItem struct { 49 | Title string `json:"volumeTitleAndArticleTitle"` 50 | ImageIDs []interface{} `json:"imageIdList"` 51 | Children []CatalogItem `json:"children"` 52 | } 53 | 54 | type PageResponse struct { 55 | Data struct { 56 | ImageIDList []PageItem `json:"imageIdList"` 57 | } `json:"data"` 58 | } 59 | 60 | type PageItem struct { 61 | ImageID interface{} `json:"imageId"` 62 | PageNum interface{} `json:"pageNum"` 63 | } 64 | 65 | type GroupedVolume struct { 66 | VolID int 67 | Items []DataItem 68 | } 69 | -------------------------------------------------------------------------------- /model/onbdigital/onbdigital.go: -------------------------------------------------------------------------------- 1 | package onbdigital 2 | 3 | type Response struct { 4 | ImageData []struct { 5 | ImageID string `json:"imageID"` 6 | OrderNumber string `json:"orderNumber"` 7 | QueryArgs string `json:"queryArgs"` 8 | } `json:"imageData"` 9 | } 10 | -------------------------------------------------------------------------------- /model/ouroots/ouroots.go: -------------------------------------------------------------------------------- 1 | package ouroots 2 | 3 | type ResponseLoginAnonymousUser struct { 4 | StatusCode string `json:"statusCode"` 5 | Msg string `json:"msg"` 6 | Token string `json:"token"` 7 | } 8 | 9 | type ResponseCatalogImage struct { 10 | StatusCode string `json:"statusCode"` 11 | Msg string `json:"msg"` 12 | ImagePath string `json:"imagePath"` 13 | ImageSize int `json:"imageSize"` 14 | DocPath string `json:"docPath"` 15 | } 16 | 17 | type Volume struct { 18 | Name string `json:"name"` 19 | Pages int `json:"pages"` 20 | VolumeId int `json:"volumeId"` 21 | } 22 | 23 | type Catalogue struct { 24 | Key string `json:"_key"` 25 | Id string `json:"_id"` 26 | Rev string `json:"_rev"` 27 | BatchID string `json:"batchID"` 28 | PageProp string `json:"page_prop"` 29 | BookId string `json:"book_id"` 30 | ChapterName string `json:"chapter_name"` 31 | SerialNum string `json:"serial_num"` 32 | AdminId string `json:"adminId"` 33 | CreateTime int64 `json:"createTime"` 34 | IsLike bool `json:"isLike"` 35 | IsCollect bool `json:"isCollect"` 36 | ViewNum int `json:"viewNum"` 37 | LikeNum int `json:"likeNum"` 38 | CollectionNum int `json:"collectionNum"` 39 | ShareNum int `json:"shareNum"` 40 | VolumeID int `json:"volumeID"` 41 | EndNum *int `json:"end_num"` 42 | VolumeNum string `json:"volume_num,omitempty"` 43 | PageNum string `json:"page_num,omitempty"` 44 | } 45 | type ResponseVolume struct { 46 | StatusCode string `json:"statusCode"` 47 | Msg string `json:"msg"` 48 | Volume []Volume `json:"volume"` 49 | Catalogue []Catalogue `json:"catalogue"` 50 | } 51 | -------------------------------------------------------------------------------- /model/princeton/princeton.go: -------------------------------------------------------------------------------- 1 | package princeton 2 | 3 | // Graphql 查manifestUrl 4 | type Graphql struct { 5 | Data struct { 6 | ResourcesByOrangelightIds []struct { 7 | Id string `json:"id"` 8 | Url string `json:"url"` 9 | ManifestUrl string `json:"manifestUrl"` 10 | } `json:"resourcesByOrangelightIds"` 11 | } `json:"data"` 12 | } 13 | 14 | type ResponseManifest struct { 15 | Manifests []struct { 16 | Id string `json:"@id"` 17 | } `json:"manifests"` 18 | } 19 | 20 | // Manifest 查info.json 21 | type ResponseManifest2 struct { 22 | Sequences []struct { 23 | Canvases []struct { 24 | Images []struct { 25 | Resource struct { 26 | Service struct { 27 | Id string `json:"@id"` 28 | } `json:"service"` 29 | } `json:"resource"` 30 | } `json:"images"` 31 | } `json:"canvases"` 32 | } `json:"sequences"` 33 | } 34 | -------------------------------------------------------------------------------- /model/rslru/rslru.go: -------------------------------------------------------------------------------- 1 | package rslru 2 | 3 | type Response struct { 4 | IsAvailable bool `json:"isAvailable"` 5 | IsAuthorizationRequired bool `json:"isAuthorizationRequired"` 6 | IsGosuslugiVerificationRequired bool `json:"isGosuslugiVerificationRequired"` 7 | Formats []string `json:"formats"` 8 | PageCount int `json:"pageCount"` 9 | IsSearchable bool `json:"isSearchable"` 10 | HasTextLayer bool `json:"hasTextLayer"` 11 | OwnershipSystem string `json:"ownershipSystem"` 12 | AccessLevel string `json:"accessLevel"` 13 | AccessInformationMessage interface{} `json:"accessInformationMessage"` 14 | Description struct { 15 | Author interface{} `json:"author"` 16 | Title string `json:"title"` 17 | Imprint string `json:"imprint"` 18 | } `json:"description"` 19 | PrintAccess struct { 20 | IsPrintable bool `json:"isPrintable"` 21 | IsPrintableWhenLoggedIn bool `json:"isPrintableWhenLoggedIn"` 22 | } `json:"printAccess"` 23 | ViewAccess struct { 24 | AvailablePdfPages []struct { 25 | Min int `json:"min"` 26 | Max int `json:"max"` 27 | } `json:"availablePdfPages"` 28 | AvailableEpubPercent interface{} `json:"availableEpubPercent"` 29 | PreviewPdfPages interface{} `json:"previewPdfPages"` 30 | OutOfPreviewRangeAction interface{} `json:"outOfPreviewRangeAction"` 31 | } `json:"viewAccess"` 32 | DownloadAccess struct { 33 | IsDownloadable bool `json:"isDownloadable"` 34 | DownloadableFormats []interface{} `json:"downloadableFormats"` 35 | ForbiddenReasonText interface{} `json:"forbiddenReasonText"` 36 | } `json:"downloadAccess"` 37 | HasAudio bool `json:"hasAudio"` 38 | HasWordCoordinates bool `json:"hasWordCoordinates"` 39 | ReadingSessionId string `json:"readingSessionId"` 40 | AllowedAccessTokens struct { 41 | Pdf bool `json:"pdf"` 42 | } `json:"allowedAccessTokens"` 43 | } 44 | -------------------------------------------------------------------------------- /model/sdutcm/sdutcm.go: -------------------------------------------------------------------------------- 1 | package sdutcm 2 | 3 | type PagePicTxt struct { 4 | Url string `json:"url"` 5 | Text string `json:"text"` 6 | Charmax int `json:"charmax"` 7 | ColNum int `json:"colNum"` 8 | PageNum string `json:"pageNum"` 9 | ImageList struct { 10 | } `json:"imageList"` 11 | } 12 | type VolumeList struct { 13 | List []struct { 14 | ShortTitle string `json:"short_title"` 15 | ContentId string `json:"content_id"` 16 | Lshh string `json:"lshh"` 17 | Title string `json:"title"` 18 | } `json:"list"` 19 | } 20 | -------------------------------------------------------------------------------- /model/sillokgokr/sillokgokr.go: -------------------------------------------------------------------------------- 1 | package sillokgokr 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type Canvases struct { 9 | KingCode string `json:"kingCode"` 10 | ImageId string `json:"imageId"` 11 | Previous string `json:"previous"` 12 | Firstchild string `json:"firstchild"` 13 | Title string `json:"title"` 14 | PageId string `json:"pageId"` 15 | Parent string `json:"parent"` 16 | Type string `json:"type"` 17 | Level string `json:"level"` 18 | Next string `json:"next"` 19 | Seq string `json:"seq"` 20 | } 21 | 22 | type Response struct { 23 | TreeList struct { 24 | List []Canvases `json:"list"` 25 | ListCount int `json:"listCount"` 26 | } `json:"treeList"` 27 | } 28 | 29 | type Book struct { 30 | Title string 31 | Seq int 32 | Id string 33 | KingCode string 34 | Level int 35 | Type string 36 | Next string 37 | Firstchild string 38 | } 39 | 40 | type ByImageIdSort []Canvases 41 | 42 | func (ni ByImageIdSort) Len() int { return len(ni) } 43 | func (ni ByImageIdSort) Swap(i, j int) { ni[i], ni[j] = ni[j], ni[i] } 44 | func (ni ByImageIdSort) Less(i, j int) bool { 45 | idA := ni[i].ImageId 46 | idB := ni[j].ImageId 47 | a, err1 := strconv.ParseInt(idA[strings.LastIndex(idA, "_"):], 10, 64) 48 | b, err2 := strconv.ParseInt(idB[strings.LastIndex(idB, "_"):], 10, 64) 49 | if err1 != nil || err2 != nil { 50 | return idA < idB 51 | } 52 | return a < b 53 | } 54 | -------------------------------------------------------------------------------- /model/szLib/szLib.go: -------------------------------------------------------------------------------- 1 | package szLib 2 | 3 | type ResultVolumes struct { 4 | Meta struct { 5 | Topic string `json:"topic"` 6 | Uri856 struct { 7 | Image struct { 8 | Brief []interface{} `json:"brief"` 9 | Publish []interface{} `json:"publish"` 10 | IndexPage []interface{} `json:"indexPage"` 11 | } `json:"image"` 12 | Pdf struct { 13 | FullText []interface{} `json:"fullText"` 14 | } `json:"pdf"` 15 | } `json:"uri_856"` 16 | UAbstract string `json:"U_Abstract"` 17 | USubject string `json:"U_Subject"` 18 | UContributor string `json:"U_Contributor"` 19 | UAuthor string `json:"U_Author"` 20 | UTitle string `json:"U_Title"` 21 | UPublishYear string `json:"U_PublishYear"` 22 | UZuozheFangshi string `json:"U_Zuozhe_Fangshi"` 23 | ULibrary string `json:"U_Library"` 24 | UZhuangzhenxinshi string `json:"U_Zhuangzhenxinshi"` 25 | UPublisher string `json:"U_Publisher"` 26 | UPlace string `json:"U_Place"` 27 | UCunjuan string `json:"U_Cunjuan"` 28 | UZuozheShijian string `json:"U_Zuozhe_Shijian"` 29 | USeries string `json:"U_Series"` 30 | UZiliaojibie string `json:"U_Ziliaojibie"` 31 | UGuest string `json:"U_Guest"` 32 | UTimingJianti string `json:"U_Timing_Jianti"` 33 | UKeywords string `json:"U_Keywords"` 34 | ULibCallno string `json:"U_LibCallno"` 35 | UVersionLeixin string `json:"U_Version_Leixin"` 36 | USubjectRegular2 string `json:"U_Subject_Regular2"` 37 | USubjectRegular string `json:"U_Subject_Regular"` 38 | UExpectDate string `json:"U_ExpectDate"` 39 | UPage string `json:"U_Page"` 40 | UVersion string `json:"U_Version"` 41 | UPuchabianhao string `json:"U_Puchabianhao"` 42 | UCallno string `json:"U_Callno"` 43 | UPublish string `json:"U_Publish"` 44 | UFence string `json:"U_Fence"` 45 | Volume string `json:"volume"` 46 | } `json:"meta"` 47 | Volumes []Directory `json:"directory"` 48 | } 49 | 50 | type Directory struct { 51 | Name string `json:"name"` 52 | Volume string `json:"volume"` 53 | Page string `json:"page"` 54 | HasText string `json:"has_text"` 55 | Children []struct { 56 | Volume string `json:"volume"` 57 | Children []interface{} `json:"children"` 58 | Page string `json:"page"` 59 | } `json:"children"` 60 | } 61 | 62 | type ResultPage struct { 63 | TextInfo struct { 64 | } `json:"text_info"` 65 | PicInfo struct { 66 | Description string `json:"description"` 67 | Tolvol string `json:"tolvol"` 68 | Period string `json:"period"` 69 | Topic string `json:"topic"` 70 | Title string `json:"title"` 71 | Path string `json:"path"` 72 | } `json:"pic_info"` 73 | BookImageUrl string `json:"book_image_url"` 74 | } 75 | -------------------------------------------------------------------------------- /model/tianyige/tianyige.go: -------------------------------------------------------------------------------- 1 | package tianyige 2 | 3 | type ResponseVolume struct { 4 | Code int `json:"code"` 5 | Msg string `json:"msg"` 6 | Data []Volume `json:"data"` 7 | } 8 | type PageImage struct { 9 | Records []ImageRecord `json:"records"` 10 | Total int `json:"total"` 11 | Size int `json:"size"` 12 | Current int `json:"current"` 13 | SearchCount bool `json:"searchCount"` 14 | Pages int `json:"pages"` 15 | } 16 | type ImageRecord struct { 17 | ImageId string `json:"imageId"` 18 | ImageName string `json:"imageName"` 19 | DirectoryId string `json:"directoryId"` 20 | FascicleId string `json:"fascicleId"` 21 | CatalogId string `json:"catalogId"` 22 | Sort int `json:"sort"` 23 | Type int `json:"type"` 24 | IsParse interface{} `json:"isParse"` 25 | Description interface{} `json:"description"` 26 | Creator string `json:"creator"` 27 | CreateTime string `json:"createTime"` 28 | Updator string `json:"updator"` 29 | UpdateTime string `json:"updateTime"` 30 | IsDeleted int `json:"isDeleted"` 31 | OcrInfo interface{} `json:"ocrInfo"` 32 | File interface{} `json:"file"` 33 | } 34 | 35 | // 页面 36 | type ResponsePage struct { 37 | Code int `json:"code"` 38 | Msg string `json:"msg"` 39 | Data PageImage `json:"data"` 40 | } 41 | 42 | type ResponseFile struct { 43 | Code int `json:"code"` 44 | Msg string `json:"msg"` 45 | Data struct { 46 | File []struct { 47 | FileName string `json:"fileName"` 48 | FileSuffix string `json:"fileSuffix"` 49 | FilePath string `json:"filePath"` 50 | UpdateTime string `json:"updateTime"` 51 | Sort string `json:"sort"` 52 | CreateTime string `json:"createTime"` 53 | FileSize int `json:"fileSize"` 54 | FileOldname string `json:"fileOldname"` 55 | FileInfoId string `json:"fileInfoId"` 56 | } `json:"file"` 57 | } `json:"data"` 58 | } 59 | 60 | type Volume struct { 61 | FascicleId string `json:"fascicleId"` 62 | CatalogId string `json:"catalogId"` 63 | Name string `json:"name"` 64 | Introduction interface{} `json:"introduction"` 65 | GradeId string `json:"gradeId"` 66 | Sort int `json:"sort"` 67 | Creator interface{} `json:"creator"` 68 | CreateTime string `json:"createTime"` 69 | Updator interface{} `json:"updator"` 70 | UpdateTime string `json:"updateTime"` 71 | IsDeleted int `json:"isDeleted"` 72 | FilePath interface{} `json:"filePath"` 73 | ImageCount interface{} `json:"imageCount"` 74 | } 75 | 76 | type Catalog struct { 77 | Code int `json:"code"` 78 | Msg string `json:"msg"` 79 | Data struct { 80 | Records []struct { 81 | DirectoryId string `json:"directoryId"` 82 | FascicleId string `json:"fascicleId"` 83 | CatalogId string `json:"catalogId"` 84 | Name string `json:"name"` 85 | Description interface{} `json:"description"` 86 | PageId string `json:"pageId"` 87 | GradeId string `json:"gradeId"` 88 | Region string `json:"region"` 89 | Sort int `json:"sort"` 90 | Creator interface{} `json:"creator"` 91 | CreateTime string `json:"createTime"` 92 | Updator interface{} `json:"updator"` 93 | UpdateTime *string `json:"updateTime"` 94 | IsDeleted int `json:"isDeleted"` 95 | } `json:"records"` 96 | Total int `json:"total"` 97 | Size int `json:"size"` 98 | Current int `json:"current"` 99 | SearchCount bool `json:"searchCount"` 100 | Pages int `json:"pages"` 101 | } `json:"data"` 102 | } 103 | 104 | type Parts map[string][]ImageRecord 105 | -------------------------------------------------------------------------------- /model/usthk/usthk.go: -------------------------------------------------------------------------------- 1 | package usthk 2 | 3 | type Response struct { 4 | FileList []string `json:"file_list"` 5 | } 6 | -------------------------------------------------------------------------------- /model/wzlib/wzlib.go: -------------------------------------------------------------------------------- 1 | package wzlib 2 | 3 | type Digital struct { 4 | ID int `json:"ID"` 5 | SiteID int `json:"SiteID"` 6 | Title string `json:"Title"` 7 | Author string `json:"author"` 8 | Source string `json:"source"` 9 | Txt string `json:"txt"` 10 | PdfUrl string `json:"pdf_url"` 11 | DigitalResourceData []struct { 12 | Title string `json:"Title"` 13 | Url string `json:"Url"` 14 | } `json:"DigitalResourceData"` 15 | } 16 | 17 | type Result []Item 18 | 19 | type Item struct { 20 | Items []struct { 21 | Id string `json:"_id"` 22 | DcPublisher string `json:"dc_publisher"` 23 | DcTitle string `json:"dc_title"` 24 | WzlPdfUrl string `json:"wzl_pdf_url"` 25 | } `json:"items"` 26 | Title string `json:"title"` 27 | } 28 | 29 | type PdfUrls []PdfUrl 30 | type PdfUrl struct { 31 | Url string 32 | Name string 33 | } 34 | 35 | type ResultPdf struct { 36 | Data struct { 37 | Id string `json:"_id"` 38 | DcTitle string `json:"dc_title"` 39 | ModelId string `json:"model_id"` 40 | RelateName string `json:"relate_name"` 41 | WzlPdfUrl string `json:"wzl_pdf_url"` 42 | } `json:"Data"` 43 | 44 | RelateList []interface{} `json:"RelateList"` 45 | } 46 | -------------------------------------------------------------------------------- /model/yndfz/yndfz.go: -------------------------------------------------------------------------------- 1 | package yndfz 2 | 3 | type Response struct { 4 | Url string `json:"url"` 5 | } 6 | -------------------------------------------------------------------------------- /pkg/chttp/cookie.go: -------------------------------------------------------------------------------- 1 | package chttp 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-") 13 | 14 | func sanitizeCookieName(n string) string { 15 | return cookieNameSanitizer.Replace(n) 16 | } 17 | func sanitizeCookieValue(v string, quoted bool) string { 18 | v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v) 19 | if len(v) == 0 { 20 | return v 21 | } 22 | if strings.ContainsAny(v, " ,") || quoted { 23 | return `"` + v + `"` 24 | } 25 | return v 26 | } 27 | 28 | func validCookieValueByte(b byte) bool { 29 | return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\' 30 | } 31 | 32 | func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string { 33 | ok := true 34 | for i := 0; i < len(v); i++ { 35 | if valid(v[i]) { 36 | continue 37 | } 38 | log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName) 39 | ok = false 40 | break 41 | } 42 | if ok { 43 | return v 44 | } 45 | buf := make([]byte, 0, len(v)) 46 | for i := 0; i < len(v); i++ { 47 | if b := v[i]; valid(b) { 48 | buf = append(buf, b) 49 | } 50 | } 51 | return string(buf) 52 | } 53 | 54 | func ReadHttpCookiesFromFile(cookieFile string) ([]http.Cookie, error) { 55 | fp, err := os.Open(cookieFile) 56 | if err != nil { 57 | return nil, err 58 | } 59 | defer fp.Close() 60 | 61 | bsHeader, err := io.ReadAll(fp) 62 | if err != nil { 63 | return nil, err 64 | } 65 | mHeader := strings.Split(string(bsHeader), "\n") 66 | cookies := make([]http.Cookie, 0, len(mHeader)+1) 67 | for _, line := range mHeader { 68 | if strings.HasPrefix(line, "#") { 69 | continue 70 | } 71 | text := regexp.MustCompile(`\\"`).ReplaceAllString(line, "\"") 72 | row := strings.Split(text, "\t") 73 | if len(row) < 8 { 74 | continue 75 | } 76 | name := strings.ReplaceAll(row[5], "\"", "") 77 | value := strings.ReplaceAll(row[6], "\"", "") 78 | //expires := strings.ReplaceAll(row[4], "#HttpOnly_", "") 79 | cookies = append(cookies, http.Cookie{Name: name, Value: value}) 80 | } 81 | return cookies, nil 82 | } 83 | 84 | func ReadCookiesFromFile(cfile string) (cookies string, err error) { 85 | bs, err := os.ReadFile(cfile) 86 | if err != nil { 87 | return "", err 88 | } 89 | mCookie := strings.Split(string(bs), "\n") 90 | 91 | for _, line := range mCookie { 92 | if strings.HasPrefix(line, "#") { 93 | continue 94 | } 95 | text := regexp.MustCompile(`\\"`).ReplaceAllString(line, "\"") 96 | row := strings.Split(text, "\t") 97 | if len(row) < 8 { 98 | continue 99 | } 100 | name := strings.ReplaceAll(row[5], "\"", "") 101 | value := strings.ReplaceAll(row[6], "\"", "") 102 | s := name + "=" + value + "; " 103 | cookies += s 104 | } 105 | return cookies, nil 106 | } 107 | -------------------------------------------------------------------------------- /pkg/chttp/header.go: -------------------------------------------------------------------------------- 1 | package chttp 2 | 3 | import ( 4 | "bufio" 5 | "net/http" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func ReadHttpHeadersFromFile(filename string) (http.Header, error) { 11 | file, err := os.Open(filename) 12 | if err != nil { 13 | return nil, err 14 | } 15 | defer file.Close() 16 | 17 | headers := make(http.Header) 18 | scanner := bufio.NewScanner(file) 19 | 20 | for scanner.Scan() { 21 | line := strings.TrimSpace(scanner.Text()) 22 | if line == "" { 23 | continue 24 | } 25 | 26 | parts := strings.SplitN(line, ":", 2) 27 | if len(parts) == 2 { 28 | key := strings.TrimSpace(parts[0]) 29 | value := strings.TrimSpace(parts[1]) 30 | headers.Add(key, value) 31 | } 32 | } 33 | 34 | if err := scanner.Err(); err != nil { 35 | return nil, err 36 | } 37 | 38 | return headers, nil 39 | } 40 | 41 | // 从文件读取HTTP头信息 42 | func ReadHeadersFromFile(filename string) (map[string]string, error) { 43 | file, err := os.Open(filename) 44 | if err != nil { 45 | return nil, err 46 | } 47 | defer file.Close() 48 | 49 | headers := make(map[string]string) 50 | scanner := bufio.NewScanner(file) 51 | 52 | for scanner.Scan() { 53 | line := strings.TrimSpace(scanner.Text()) 54 | if line == "" { 55 | continue 56 | } 57 | 58 | parts := strings.SplitN(line, ":", 2) 59 | if len(parts) == 2 { 60 | key := strings.TrimSpace(parts[0]) 61 | value := strings.TrimSpace(parts[1]) 62 | headers[key] = value 63 | } 64 | } 65 | 66 | if err := scanner.Err(); err != nil { 67 | return nil, err 68 | } 69 | 70 | return headers, nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/crypt/aes.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "encoding/base64" 8 | "errors" 9 | ) 10 | 11 | func pkcs7Padding(data []byte, blockSize int) []byte { 12 | padding := blockSize - len(data)%blockSize 13 | padText := bytes.Repeat([]byte{byte(padding)}, padding) 14 | return append(data, padText...) 15 | } 16 | 17 | func pkcs7UnPadding(data []byte) ([]byte, error) { 18 | length := len(data) 19 | if length == 0 { 20 | return nil, errors.New("加密字符串错误!") 21 | } 22 | unPadding := int(data[length-1]) 23 | return data[:(length - unPadding)], nil 24 | } 25 | 26 | func AesEncrypt(data []byte, key, iv []byte) ([]byte, error) { 27 | block, err := aes.NewCipher(key) 28 | if err != nil { 29 | return nil, err 30 | } 31 | blockSize := block.BlockSize() 32 | encryptBytes := pkcs7Padding(data, blockSize) 33 | crypted := make([]byte, len(encryptBytes)) 34 | //blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) 35 | blockMode := cipher.NewCBCEncrypter(block, iv[:blockSize]) 36 | blockMode.CryptBlocks(crypted, encryptBytes) 37 | return crypted, nil 38 | } 39 | 40 | func AesDecrypt(data []byte, key []byte, iv []byte) ([]byte, error) { 41 | 42 | block, err := aes.NewCipher(key) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | blockSize := block.BlockSize() 48 | blockMode := cipher.NewCBCDecrypter(block, iv[:blockSize]) 49 | //blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) 50 | crypted := make([]byte, len(data)) 51 | blockMode.CryptBlocks(crypted, data) 52 | crypted, err = pkcs7UnPadding(crypted) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return crypted, nil 57 | } 58 | 59 | // EncryptByAes Aes加密 后 base64 再加 60 | func EncryptByAes(data []byte, key []byte, iv []byte) (string, error) { 61 | res, err := AesEncrypt(data, key, iv) 62 | if err != nil { 63 | return "", err 64 | } 65 | return base64.StdEncoding.EncodeToString(res), nil 66 | } 67 | 68 | // DecryptByAes Aes 解密 69 | func DecryptByAes(data string, key []byte, iv []byte) ([]byte, error) { 70 | dataByte, err := base64.StdEncoding.DecodeString(data) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return AesDecrypt(dataByte, key, iv) 75 | } 76 | -------------------------------------------------------------------------------- /pkg/crypt/escape.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | // Javascript encodeURI 效率很低,应该从底层修改 9 | func EncodeURI(path string) string { 10 | // var set1 = ";,/?:@&=+$" // 保留字符 11 | // var set2 = "-_.!~*'()" // 不转义字符 12 | // var set3 = "#" // 数字标志 13 | // var set4 = "ABC abc 123" // 字母数字字符和空格 14 | s := url.PathEscape(path) 15 | s = strings.ReplaceAll(s, "%3B", ";") 16 | s = strings.ReplaceAll(s, "%2C", ",") 17 | s = strings.ReplaceAll(s, "%2F", "/") 18 | s = strings.ReplaceAll(s, "%3F", "?") 19 | s = strings.ReplaceAll(s, "%21", "!") 20 | s = strings.ReplaceAll(s, "%2A", "*") 21 | s = strings.ReplaceAll(s, "%27", "'") 22 | s = strings.ReplaceAll(s, "%28", "(") 23 | s = strings.ReplaceAll(s, "%29", ")") 24 | s = strings.ReplaceAll(s, "%23", "#") 25 | return s 26 | } 27 | -------------------------------------------------------------------------------- /pkg/downloader/base.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | const ( 4 | maxConcurrent = 16 // 最大并发下载数 5 | userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0" 6 | minFileSize = 1024 // 最小文件大小(1KB) 7 | 8 | maxRetries = 3 9 | JPGQuality = 90 10 | ) 11 | 12 | // 在 downloader.go 或相关文件中添加 13 | type Vec2d struct { 14 | x int // 或 float64 根据需求 15 | y int // 或 float64 16 | } 17 | 18 | // 可选:添加构造函数 19 | func NewVec2d(x, y int) Vec2d { 20 | return Vec2d{x: x, y: y} 21 | } 22 | 23 | // 可选:添加常用方法 24 | func (v Vec2d) Width() int { return v.x } 25 | func (v Vec2d) Height() int { return v.y } 26 | 27 | type TileSizeFormat int 28 | 29 | const ( 30 | WidthHeight TileSizeFormat = iota // "width,height" 31 | Width // "width," 32 | ) 33 | 34 | // Quality preference order (least to most preferred) 35 | var qualityOrder = []string{"default", "native"} 36 | 37 | // Format preference order (least to most preferred) 38 | var formatOrder = []string{"jpg", "png"} 39 | -------------------------------------------------------------------------------- /pkg/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bookget/config" 5 | "strings" 6 | ) 7 | 8 | func Ext(uri string) string { 9 | if config.Conf.FileExt != "" && config.Conf.FileExt[0] == '.' { 10 | return config.Conf.FileExt 11 | } 12 | return Extention(uri) 13 | } 14 | func Extention(uri string) string { 15 | ext := "" 16 | k := len(uri) 17 | for i := k - 1; i >= 0; i-- { 18 | if uri[i] == '?' { 19 | k = i 20 | continue 21 | } 22 | if uri[i] == '.' { 23 | ext = uri[i:k] 24 | break 25 | } 26 | } 27 | return ext 28 | } 29 | 30 | func Name(uri string) string { 31 | if strings.Contains(uri, "?") { 32 | pos := strings.Index(uri, "?") 33 | uri = uri[:pos] 34 | } 35 | if strings.Contains(uri, "&") { 36 | pos := strings.Index(uri, "&") 37 | uri = uri[:pos] 38 | } 39 | name := "" 40 | for i := len(uri) - 1; i >= 0; i-- { 41 | if uri[i] == '/' { 42 | name = uri[i+1:] 43 | break 44 | } 45 | } 46 | return name 47 | } 48 | -------------------------------------------------------------------------------- /pkg/gohttp/chunk.go: -------------------------------------------------------------------------------- 1 | package gohttp 2 | 3 | import ( 4 | "io" 5 | "path/filepath" 6 | "runtime" 7 | ) 8 | 9 | type OffsetWriter struct { 10 | io.WriterAt 11 | offset int64 12 | } 13 | 14 | func (dst *OffsetWriter) Write(b []byte) (n int, err error) { 15 | n, err = dst.WriteAt(b, dst.offset) 16 | dst.offset += int64(n) 17 | return 18 | } 19 | 20 | // Chunk represents the partial content range 21 | type Chunk struct { 22 | Start, End uint64 23 | } 24 | 25 | // Return constant path which will not change once the download starts 26 | func (d *Download) Path() string { 27 | 28 | // Set the default path 29 | if d.path == "" { 30 | 31 | if d.Dest != "" { 32 | d.path = d.Dest 33 | } else if d.unsafeName != "" { 34 | d.path = getNameFromHeader(d.unsafeName) 35 | } else { 36 | d.path = getFilename(d.URL) 37 | } 38 | d.path = filepath.Join(d.Dir, d.path) 39 | } 40 | 41 | return d.path 42 | } 43 | 44 | func getDefaultConcurrency() int { 45 | c := runtime.NumCPU() * 2 46 | return c 47 | } 48 | 49 | func getDefaultChunkSize(totalSize, min, max, concurrency uint64) uint64 { 50 | 51 | cs := totalSize / concurrency 52 | 53 | // if chunk size >= 102400000 bytes set default to (ChunkSize / 2) 54 | if cs >= 102400000 { 55 | cs = cs / 2 56 | } 57 | 58 | // Set default min chunk size to 2m, or file size / 2 59 | if min == 0 { 60 | 61 | min = 2097152 62 | 63 | if min >= totalSize { 64 | min = totalSize / 2 65 | } 66 | } 67 | 68 | // if Chunk size < Min size set chunk size to min. 69 | if cs < min { 70 | cs = min 71 | } 72 | 73 | // Change ChunkSize if MaxChunkSize are set and ChunkSize > Max size 74 | if max > 0 && cs > max { 75 | cs = max 76 | } 77 | 78 | // When chunk size > total file size, divide chunk / 2 79 | if cs >= totalSize { 80 | cs = totalSize / 2 81 | } 82 | 83 | return cs 84 | } 85 | -------------------------------------------------------------------------------- /pkg/gohttp/filename.go: -------------------------------------------------------------------------------- 1 | package gohttp 2 | 3 | import ( 4 | "mime" 5 | "net/url" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | // DefaultFileName is the fallback name for GetFilename. 11 | var DefaultFileName = "gohttp.output" 12 | 13 | // GetFilename it returns default file name from a URL. 14 | func getFilename(URL string) string { 15 | 16 | if u, err := url.Parse(URL); err == nil && filepath.Ext(u.Path) != "" { 17 | 18 | return filepath.Base(u.Path) 19 | } 20 | 21 | return DefaultFileName 22 | } 23 | 24 | func getNameFromHeader(val string) string { 25 | 26 | _, params, err := mime.ParseMediaType(val) 27 | 28 | // Prevent path traversal 29 | if err != nil || strings.Contains(params["filename"], "..") || strings.Contains(params["filename"], "/") || strings.Contains(params["filename"], "\\") { 30 | return "" 31 | } 32 | 33 | return params["filename"] 34 | } 35 | -------------------------------------------------------------------------------- /pkg/gohttp/gohttp.go: -------------------------------------------------------------------------------- 1 | package gohttp 2 | 3 | import "context" 4 | 5 | // NewClient new request object 6 | func NewClient(c context.Context, opts ...Options) *Request { 7 | req := &Request{ctx: c} 8 | if len(opts) > 0 { 9 | req.opts = opts[0] 10 | } else { 11 | req.opts = Options{} 12 | } 13 | return req 14 | } 15 | 16 | // Get send get request 17 | func Get(c context.Context, uri string, opts ...Options) (*Response, error) { 18 | r := NewClient(c) 19 | return r.Get(uri, opts...) 20 | } 21 | 22 | // Post send post request 23 | func Post(c context.Context, uri string, opts ...Options) (*Response, error) { 24 | r := NewClient(c) 25 | return r.Post(uri, opts...) 26 | } 27 | 28 | // Put send put request 29 | func Put(c context.Context, uri string, opts ...Options) (*Response, error) { 30 | r := NewClient(c) 31 | return r.Post(uri, opts...) 32 | } 33 | 34 | // Patch send patch request 35 | func Patch(c context.Context, uri string, opts ...Options) (*Response, error) { 36 | r := NewClient(c) 37 | return r.Patch(uri, opts...) 38 | } 39 | 40 | // Delete send delete request 41 | func Delete(c context.Context, uri string, opts ...Options) (*Response, error) { 42 | r := NewClient(c) 43 | return r.Delete(uri, opts...) 44 | } 45 | 46 | // Download file 47 | func FastGet(c context.Context, uri string, opts ...Options) (*Response, error) { 48 | r := NewClient(c) 49 | return r.FastGet(uri, opts...) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/gohttp/options.go: -------------------------------------------------------------------------------- 1 | package gohttp 2 | 3 | import ( 4 | "net/http/cookiejar" 5 | "time" 6 | ) 7 | 8 | // Options object 9 | type Options struct { 10 | Debug bool 11 | Concurrency int //CPU核数 12 | BaseURI string 13 | Timeout float32 14 | timeout time.Duration 15 | Retry int 16 | Query interface{} 17 | Headers map[string]interface{} 18 | HeaderFile string 19 | 20 | Cookies interface{} 21 | CookieFile string 22 | CookieJar *cookiejar.Jar 23 | FormParams map[string]interface{} 24 | JSON interface{} 25 | Body []byte 26 | XML interface{} 27 | Proxy string 28 | DestFile string //保存到本地文件 29 | Overwrite bool //覆蓋文件 30 | } 31 | -------------------------------------------------------------------------------- /pkg/gohttp/response.go: -------------------------------------------------------------------------------- 1 | package gohttp 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | // Response response object 11 | type Response struct { 12 | resp *http.Response 13 | req *http.Request 14 | body []byte 15 | err error 16 | } 17 | 18 | // ResponseBody response body 19 | type ResponseBody []byte 20 | 21 | // String fmt outout 22 | func (r ResponseBody) String() string { 23 | return string(r) 24 | } 25 | 26 | // Read get slice of response body 27 | func (r ResponseBody) Read(length int) []byte { 28 | if length > len(r) { 29 | length = len(r) 30 | } 31 | 32 | return r[:length] 33 | } 34 | 35 | // GetContents format response body as string 36 | func (r ResponseBody) GetContents() string { 37 | return string(r) 38 | } 39 | 40 | // GetRequest get request object 41 | func (r *Response) GetRequest() *http.Request { 42 | return r.req 43 | } 44 | 45 | // GetBody parse response body 46 | func (r *Response) GetBody() (ResponseBody, error) { 47 | return ResponseBody(r.body), r.err 48 | } 49 | 50 | // GetParsedBody parse response body 51 | func (r *Response) GetJsonDecodeBody(body interface{}) (err error) { 52 | return json.Unmarshal(r.body, body) 53 | } 54 | 55 | // GetStatusCode get response status code 56 | func (r *Response) GetStatusCode() int { 57 | return r.resp.StatusCode 58 | } 59 | 60 | // GetReasonPhrase get response reason phrase 61 | func (r *Response) GetReasonPhrase() string { 62 | status := r.resp.Status 63 | arr := strings.Split(status, " ") 64 | 65 | return arr[1] 66 | } 67 | 68 | // IsTimeout get if request is timeout 69 | func (r *Response) IsTimeout() bool { 70 | if r.err == nil { 71 | return false 72 | } 73 | netErr, ok := r.err.(net.Error) 74 | if !ok { 75 | return false 76 | } 77 | if netErr.Timeout() { 78 | return true 79 | } 80 | 81 | return false 82 | } 83 | 84 | // GetHeaders get response headers 85 | func (r *Response) GetHeaders() map[string][]string { 86 | return r.resp.Header 87 | } 88 | 89 | // GetHeader get response header 90 | func (r *Response) GetHeader(name string) []string { 91 | headers := r.GetHeaders() 92 | for k, v := range headers { 93 | if strings.ToLower(name) == strings.ToLower(k) { 94 | return v 95 | } 96 | } 97 | 98 | return nil 99 | } 100 | 101 | // GetHeaderLine get a single response header 102 | func (r *Response) GetHeaderLine(name string) string { 103 | header := r.GetHeader(name) 104 | if len(header) > 0 { 105 | return header[0] 106 | } 107 | 108 | return "" 109 | } 110 | 111 | // HasHeader get if header exsits in response headers 112 | func (r *Response) HasHeader(name string) bool { 113 | headers := r.GetHeaders() 114 | for k := range headers { 115 | if strings.ToLower(name) == strings.ToLower(k) { 116 | return true 117 | } 118 | } 119 | 120 | return false 121 | } 122 | 123 | // Cookies get if header exsits in response headers 124 | func (r *Response) GetCookies() []*http.Cookie { 125 | return r.resp.Cookies() 126 | } 127 | -------------------------------------------------------------------------------- /pkg/queue/queue.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | ) 7 | 8 | // ConcurrentQueue 提供带有限制的并发执行队列 9 | type ConcurrentQueue struct { 10 | capacity int // 最大并发数 11 | sem chan struct{} // 信号量通道,用于控制并发 12 | wg sync.WaitGroup // 等待所有任务完成 13 | } 14 | 15 | // NewConcurrentQueue 创建一个新的并发队列 16 | // capacity: 最大并发数量,必须大于0 17 | func NewConcurrentQueue(capacity int) *ConcurrentQueue { 18 | if capacity <= 0 { 19 | panic("queue capacity must be greater than 0") 20 | } 21 | return &ConcurrentQueue{ 22 | capacity: capacity, 23 | sem: make(chan struct{}, capacity), 24 | } 25 | } 26 | 27 | // Go 提交一个任务到队列中异步执行 28 | // 如果队列已满,会阻塞直到有可用槽位 29 | func (q *ConcurrentQueue) Go(task func()) { 30 | q.wg.Add(1) 31 | go func() { 32 | q.sem <- struct{}{} // 获取信号量 33 | defer func() { 34 | <-q.sem // 释放信号量 35 | q.wg.Done() 36 | }() 37 | 38 | // 执行任务并处理可能的panic 39 | defer func() { 40 | if r := recover(); r != nil { 41 | log.Printf("task panic recovered: %v", r) 42 | } 43 | }() 44 | 45 | task() 46 | }() 47 | } 48 | 49 | // Wait 等待所有已提交的任务完成 50 | func (q *ConcurrentQueue) Wait() { 51 | q.wg.Wait() 52 | } 53 | 54 | // TryGo 尝试提交任务,如果队列已满则立即返回false 55 | func (q *ConcurrentQueue) TryGo(task func()) bool { 56 | select { 57 | case q.sem <- struct{}{}: // 尝试获取信号量 58 | q.wg.Add(1) 59 | go func() { 60 | defer func() { 61 | <-q.sem 62 | q.wg.Done() 63 | if r := recover(); r != nil { 64 | log.Printf("task panic recovered: %v", r) 65 | } 66 | }() 67 | task() 68 | }() 69 | return true 70 | default: 71 | return false 72 | } 73 | } 74 | 75 | // CurrentCount 返回当前正在执行的任务数量 76 | func (q *ConcurrentQueue) CurrentCount() int { 77 | return len(q.sem) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/quickxorhash/xor.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.20 2 | 3 | package quickxorhash 4 | 5 | func xorBytes(dst, src []byte) int { 6 | n := len(dst) 7 | if len(src) < n { 8 | n = len(src) 9 | } 10 | if n == 0 { 11 | return 0 12 | } 13 | dst = dst[:n] 14 | //src = src[:n] 15 | src = src[:len(dst)] // remove bounds check in loop 16 | for i := range dst { 17 | dst[i] ^= src[i] 18 | } 19 | return n 20 | } 21 | -------------------------------------------------------------------------------- /pkg/quickxorhash/xor_1.20.go: -------------------------------------------------------------------------------- 1 | //go:build go1.20 2 | 3 | package quickxorhash 4 | 5 | import "crypto/subtle" 6 | 7 | func xorBytes(dst, src []byte) int { 8 | return subtle.XORBytes(dst, src, dst) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/sharedmemory/sharedmemory_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package sharedmemory 4 | 5 | import ( 6 | "unsafe" 7 | ) 8 | 9 | const ( 10 | MEM_NAME = "Local\\WebView2SharedMemory" 11 | MUTEX_NAME = "Local\\WebView2SharedMemoryMutex" 12 | ) 13 | 14 | // 确保与C++结构体完全一致的内存布局 15 | type SharedMemoryData struct { 16 | URLReady uint32 // Windows BOOL实际上是32位整数 17 | HTMLReady uint32 18 | CookiesReady uint32 19 | ImagePathReady uint32 20 | PID uint32 // 进程ID 21 | URL [1024]uint16 // wchar_t[1024] 22 | ImagePath [1024]uint16 // wchar_t[1024] 23 | Cookies [4096]uint16 // 4KB 24 | HTML [1024 * 1024 * 8]uint16 // 8MB 25 | } 26 | 27 | // 计算共享内存大小(转换为uint32) 28 | func getSharedMemorySize() uint32 { 29 | // 计算结构体大小并确保不超过uint32最大值 30 | size := unsafe.Sizeof(SharedMemoryData{}) 31 | if size > 0xFFFFFFFF { 32 | panic("Shared memory size exceeds maximum limit") 33 | } 34 | return uint32(size) 35 | } 36 | 37 | func WriteURLToSharedMemory(url string) error { 38 | 39 | return nil 40 | } 41 | 42 | func WriteURLImagePathToSharedMemory(url, imagePath string) error { 43 | 44 | return nil 45 | } 46 | 47 | func ReadHTMLFromSharedMemory() (string, error) { 48 | 49 | return "", nil 50 | } 51 | 52 | func ReadCookiesFromSharedMemory() (string, error) { 53 | 54 | return "", nil 55 | } 56 | 57 | func ReadImageReadyFromSharedMemory() (bool, error) { 58 | 59 | return false, nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/sharedmemory/windowsmem_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package sharedmemory 4 | -------------------------------------------------------------------------------- /pkg/sharedmemory/windowsmem_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package sharedmemory 4 | 5 | import ( 6 | "golang.org/x/sys/windows" 7 | "unsafe" 8 | ) 9 | 10 | var ( 11 | modkernel32 = windows.NewLazySystemDLL("kernel32.dll") 12 | procOpenFileMappingW = modkernel32.NewProc("OpenFileMappingW") 13 | ) 14 | 15 | func openFileMapping(desiredAccess uint32, inheritHandle bool, name *uint16) (handle windows.Handle, err error) { 16 | inherit := 0 17 | if inheritHandle { 18 | inherit = 1 19 | } 20 | r1, _, e1 := procOpenFileMappingW.Call( 21 | uintptr(desiredAccess), 22 | uintptr(inherit), 23 | uintptr(unsafe.Pointer(name)), 24 | ) 25 | if r1 == 0 { 26 | err = error(e1) 27 | } else { 28 | handle = windows.Handle(r1) 29 | } 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /pkg/util/cmd_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package util 4 | 5 | import ( 6 | "bufio" 7 | "context" 8 | "fmt" 9 | "io" 10 | "os" 11 | "os/exec" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | const ( 17 | TH32CS_SNAPPROCESS = 0x00000002 18 | MAX_PATH = 260 19 | ) 20 | 21 | type ProcessEntry32 struct { 22 | Size uint32 23 | CntUsage uint32 24 | ProcessID uint32 25 | DefaultHeapID uintptr 26 | ModuleID uint32 27 | CntThreads uint32 28 | ParentProcessID uint32 29 | PriClassBase int32 30 | Flags uint32 31 | ExeFile [MAX_PATH]uint16 // 使用 uint16 而不是 byte 32 | } 33 | 34 | func RunCommand(ctx context.Context, text string) error { 35 | fmt.Println(text) 36 | var cmd *exec.Cmd 37 | if os.PathSeparator == '\\' { 38 | cmd = exec.CommandContext(ctx, "cmd", "/c", text) 39 | } else { 40 | cmd = exec.CommandContext(ctx, "bash", "-c", text) 41 | } 42 | //捕获标准输出 43 | stdout, err := cmd.StdoutPipe() 44 | cmd.Stderr = cmd.Stdout 45 | if err != nil { 46 | return err 47 | } 48 | // 执行命令cmd.CombinedOutput(),且捕获输出 49 | //output, err = cmd.CombinedOutput() 50 | if err = cmd.Start(); err != nil { 51 | return err 52 | } 53 | readout := bufio.NewReader(stdout) 54 | GetOutput(readout) 55 | if err = cmd.Wait(); err != nil { 56 | return err 57 | } 58 | return nil 59 | } 60 | 61 | func GetOutput(reader *bufio.Reader) { 62 | var sumOutput string //统计屏幕的全部输出内容 63 | outputBytes := make([]byte, 200) 64 | for { 65 | n, err := reader.Read(outputBytes) //获取屏幕的实时输出(并不是按照回车分割,所以要结合sumOutput) 66 | if err != nil { 67 | if err == io.EOF { 68 | break 69 | } 70 | fmt.Println(err) 71 | sumOutput += err.Error() 72 | } 73 | output := string(outputBytes[:n]) 74 | fmt.Print(output) //输出屏幕内容 75 | sumOutput += output 76 | } 77 | return 78 | } 79 | 80 | // PrintSleepTime 打印0-60秒等待 81 | func PrintSleepTime(sec int) { 82 | if sec <= 0 || sec > 60 { 83 | return 84 | } 85 | fmt.Println() 86 | for t := sec; t > 0; t-- { 87 | seconds := strconv.Itoa(int(t)) 88 | if t < 10 { 89 | seconds = fmt.Sprintf("0%d", t) 90 | } 91 | fmt.Printf("\rplease wait.... [00:%s of appr. Max %d sec]", seconds, sec) 92 | time.Sleep(time.Second) 93 | } 94 | fmt.Println() 95 | } 96 | 97 | func StartProcess(inputUri string, outfile string, args []string) bool { 98 | // procAttr := &os.ProcAttr{ 99 | // Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, 100 | // } 101 | // userArgs := strings.Split(config.Conf.DezoomifyRs, " ") 102 | // argv := []string{""} 103 | // if userArgs != nil { 104 | // argv = append(argv, userArgs...) 105 | // } 106 | // if args != nil { 107 | // argv = append(argv, args...) 108 | // } 109 | // argv = append(argv, inputUri, outfile) 110 | // process, err := os.StartProcess(config.Conf.DezoomifyPath, argv, procAttr) 111 | // if err != nil { 112 | // fmt.Println("start process error:", err) 113 | // return false 114 | // } 115 | // _, err = process.Wait() 116 | // if err != nil { 117 | // fmt.Println("wait error:", err) 118 | // return false 119 | // } 120 | // fmt.Println() 121 | return false 122 | } 123 | 124 | func OpenWebBrowser(args []string) bool { 125 | 126 | return true 127 | } 128 | 129 | func isProcessRunning(processName string) (bool, error) { 130 | 131 | return false, nil 132 | } 133 | 134 | func IsBookgetGuiRunning() (ok bool, err error) { 135 | return false, nil 136 | } 137 | -------------------------------------------------------------------------------- /pkg/util/cn2number.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strings" 4 | 5 | // 数字 6 | var chnNumChar = [10]string{"零", "一", "二", "三", "四", "五", "六", "七", "八", "九"} 7 | 8 | // 权位 9 | var chnUnitSection = [4]string{"", "万", "亿", "万亿"} 10 | 11 | // 数字权位 12 | var chnUnitChar = [4]string{"", "十", "百", "千"} 13 | 14 | type chnNameValue struct { 15 | name string 16 | value int 17 | secUnit bool 18 | } 19 | 20 | // 权位于结点的关系 21 | var chnValuePair = []chnNameValue{{"十", 10, false}, {"百", 100, false}, {"千", 1000, false}, {"万", 10000, true}, {"亿", 100000000, true}} 22 | 23 | //func main() { 24 | // for { 25 | // var typeStr string 26 | // var scanStr string 27 | // 28 | // fmt.Println("1 阿拉伯转中文数字 2 中文数字转阿拉伯数字") 29 | // fmt.Println("请输入") 30 | // 31 | // //fmt.Scanf("%s", &a) 32 | // fmt.Scan(&typeStr) 33 | // 34 | // fmt.Println("请输入要转换的内容") 35 | // fmt.Scan(&scanStr) 36 | // if typeStr == "1" { 37 | // num, _ := strconv.ParseInt(scanStr, 10, 64) 38 | // var chnStr = numberToChinese(num) 39 | // fmt.Println(chnStr) 40 | // } else { 41 | // var numInt = chineseToNumber(scanStr) 42 | // fmt.Println(numInt) 43 | // } 44 | // } 45 | //} 46 | 47 | // 阿拉伯数字转汉字 48 | func NumberToChinese(num int64) (numStr string) { 49 | var unitPos = 0 50 | var needZero = false 51 | 52 | for num > 0 { //小于零特殊处理 53 | section := num % 10000 // 已万为小结处理 54 | if needZero { 55 | numStr = chnNumChar[0] + numStr 56 | } 57 | strIns := sectionToChinese(section) 58 | if section != 0 { 59 | strIns += chnUnitSection[unitPos] 60 | } else { 61 | strIns += chnUnitSection[0] 62 | } 63 | numStr = strIns + numStr 64 | /*千位是 0 需要在下一个 section 补零*/ 65 | needZero = (section < 1000) && (section > 0) 66 | num = num / 10000 67 | unitPos++ 68 | } 69 | return 70 | } 71 | func sectionToChinese(section int64) (chnStr string) { 72 | var strIns string 73 | var unitPos = 0 74 | var zero = true 75 | for section > 0 { 76 | var v = section % 10 77 | if v == 0 { 78 | if !zero { 79 | zero = true /*需要补,zero 的作用是确保对连续的多个,只补一个中文零*/ 80 | chnStr = chnNumChar[v] + chnStr 81 | } 82 | } else { 83 | zero = false //至少有一个数字不是 84 | strIns = chnNumChar[v] //此位对应的中文数字 85 | strIns += chnUnitChar[unitPos] //此位对应的中文权位 86 | chnStr = strIns + chnStr 87 | } 88 | unitPos++ //移位 89 | section = section / 10 90 | } 91 | return 92 | } 93 | 94 | // 汉字转阿拉伯数字 95 | func ChineseToNumber(chnStr string) (rtnInt int) { 96 | var section = 0 97 | var number = 0 98 | //十一、十二、一百十一、一百十二 这样的单独处理。 99 | if len(chnStr) == 6 || strings.Contains(chnStr, "百十") { 100 | chnStr = strings.Replace(chnStr, "十", "一十", -1) 101 | } 102 | for index, value := range chnStr { 103 | var num = chineseToValue(string(value)) 104 | if num > 0 { 105 | number = num 106 | if index == len(chnStr)-3 { 107 | section += number 108 | rtnInt += section 109 | break 110 | } 111 | } else { 112 | unit, secUnit := chineseToUnit(string(value)) 113 | if secUnit { 114 | section = (section + number) * unit 115 | rtnInt += section 116 | section = 0 117 | 118 | } else { 119 | section += (number * unit) 120 | 121 | } 122 | number = 0 123 | if index == len(chnStr)-3 { 124 | rtnInt += section 125 | break 126 | } 127 | } 128 | } 129 | 130 | return 131 | } 132 | func chineseToUnit(chnStr string) (unit int, secUnit bool) { 133 | 134 | for i := 0; i < len(chnValuePair); i++ { 135 | if chnValuePair[i].name == chnStr { 136 | unit = chnValuePair[i].value 137 | secUnit = chnValuePair[i].secUnit 138 | } 139 | } 140 | return 141 | } 142 | func chineseToValue(chnStr string) (num int) { 143 | switch chnStr { 144 | case "零": 145 | num = 0 146 | break 147 | case "一": 148 | num = 1 149 | break 150 | case "二": 151 | num = 2 152 | break 153 | case "三": 154 | num = 3 155 | break 156 | case "四": 157 | num = 4 158 | break 159 | case "五": 160 | num = 5 161 | break 162 | case "六": 163 | num = 6 164 | break 165 | case "七": 166 | num = 7 167 | break 168 | case "八": 169 | num = 8 170 | break 171 | case "九": 172 | num = 9 173 | break 174 | default: 175 | num = -1 176 | } 177 | return 178 | } 179 | -------------------------------------------------------------------------------- /pkg/util/file.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | var byteUnits = []string{"B", "KB", "MB", "GB", "TB", "PB"} 15 | 16 | func ByteUnitString(n int64) string { 17 | var unit string 18 | size := float64(n) 19 | for i := 1; i < len(byteUnits); i++ { 20 | if size < 1000 { 21 | unit = byteUnits[i-1] 22 | break 23 | } 24 | 25 | size = size / 1000 26 | } 27 | 28 | return fmt.Sprintf("%.3g %s", size, unit) 29 | } 30 | 31 | func FileExist(path string) bool { 32 | fi, err := os.Stat(path) 33 | if err == nil && fi.Size() > 0 { 34 | return true 35 | } 36 | return false 37 | } 38 | 39 | func FileWrite(b []byte, filename string) (err error) { 40 | if len(b) <= 0 { 41 | return nil 42 | } 43 | fp, err := os.Create(filename) 44 | if err != nil { 45 | return err 46 | } 47 | defer fp.Close() 48 | 49 | buf := new(bytes.Buffer) 50 | binary.Write(buf, binary.LittleEndian, b) 51 | fp.Write(buf.Bytes()) 52 | //log.Printf("save as %s (%s)\n", filename, ByteUnitString(int64(len(buf.Bytes())))) 53 | return nil 54 | } 55 | 56 | func FileExt(uri string) string { 57 | ext := "" 58 | k := len(uri) 59 | for i := k - 1; i >= 0; i-- { 60 | if uri[i] == '?' { 61 | k = i 62 | continue 63 | } 64 | if uri[i] == '.' { 65 | ext = uri[i:k] 66 | break 67 | } 68 | } 69 | return ext 70 | } 71 | 72 | func FileName(uri string) string { 73 | if strings.Contains(uri, "?") { 74 | pos := strings.Index(uri, "?") 75 | uri = uri[:pos] 76 | } 77 | if strings.Contains(uri, "&") { 78 | pos := strings.Index(uri, "&") 79 | uri = uri[:pos] 80 | } 81 | name := "" 82 | for i := len(uri) - 1; i >= 0; i-- { 83 | if uri[i] == '/' { 84 | name = uri[i+1:] 85 | break 86 | } 87 | } 88 | return name 89 | } 90 | 91 | // 压缩文件 92 | func Zip(srcFile string, destZip string) error { 93 | zipFile, err := os.Create(destZip) 94 | if err != nil { 95 | return err 96 | } 97 | defer zipFile.Close() 98 | archive := zip.NewWriter(zipFile) 99 | defer archive.Close() 100 | filepath.Walk(srcFile, func(path string, info os.FileInfo, err error) error { 101 | if err != nil { 102 | return err 103 | } 104 | 105 | header, err := zip.FileInfoHeader(info) 106 | if err != nil { 107 | return err 108 | } 109 | header.Name = path 110 | if info.IsDir() { 111 | header.Name += "/" 112 | } else { 113 | header.Method = zip.Deflate 114 | } 115 | writer, err := archive.CreateHeader(header) 116 | if err != nil { 117 | return err 118 | } 119 | if !info.IsDir() { 120 | file, err := os.Open(path) 121 | if err != nil { 122 | return err 123 | } 124 | defer file.Close() 125 | 126 | _, err = io.Copy(writer, file) 127 | } 128 | return err 129 | }) 130 | return err 131 | } 132 | 133 | // 解压缩 134 | func Unzip(zipFile string, destDir string, sortId string) error { 135 | zipReader, err := zip.OpenReader(zipFile) 136 | if err != nil { 137 | return err 138 | } 139 | defer zipReader.Close() 140 | for _, f := range zipReader.File { 141 | newName := f.Name 142 | if sortId != "" { 143 | newName = fmt.Sprintf("%s.%s", sortId, f.Name) 144 | } 145 | fpath := filepath.Join(destDir, newName) 146 | if f.FileInfo().IsDir() { 147 | os.MkdirAll(fpath, os.ModePerm) 148 | } else { 149 | if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { 150 | return err 151 | } 152 | inFile, err := f.Open() //这个是从压缩文件读取出来的 153 | if err != nil { 154 | return err 155 | } 156 | defer inFile.Close() 157 | 158 | outFile, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode()) //创建的新文件 159 | if err != nil { 160 | return err 161 | } 162 | defer outFile.Close() 163 | 164 | _, err = io.Copy(outFile, inFile) 165 | } 166 | } 167 | return err 168 | } 169 | -------------------------------------------------------------------------------- /pkg/util/file_type.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bookget/config" 5 | "crypto/tls" 6 | "log" 7 | "net/http" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | var ( 16 | contentTypeCache = sync.Map{} 17 | jsonExtensions = map[string]bool{ 18 | ".json": true, 19 | } 20 | contentTypeMappings = map[string]string{ 21 | "application/ld+json": "json", 22 | "application/json": "json", 23 | "text/html": "html", 24 | "image/jpeg": "bookget", 25 | "image/png": "bookget", 26 | "application/pdf": "bookget", 27 | } 28 | rangeRegex = regexp.MustCompile(`$(\d+)-(\d+)$`) 29 | ) 30 | 31 | func GetHeaderContentType(sUrl string) string { 32 | // 检查缓存 33 | if cached, ok := contentTypeCache.Load(sUrl); ok { 34 | return cached.(string) 35 | } 36 | 37 | // 1. 首先检查文件扩展名 38 | if hasJSONExtension(sUrl) { 39 | return cacheAndReturn(sUrl, "json") 40 | } 41 | 42 | // 2. 检查是否有范围格式 43 | if isRangeFormat(sUrl) { 44 | return cacheAndReturn(sUrl, "octet-stream") 45 | } 46 | 47 | // 3. 最后通过HTTP请求获取Content-Type 48 | return determineContentTypeByRequest(sUrl) 49 | } 50 | 51 | func hasJSONExtension(url string) bool { 52 | ext := filepath.Ext(url) 53 | return jsonExtensions[ext] 54 | } 55 | 56 | func isRangeFormat(url string) bool { 57 | return rangeRegex.MatchString(url) 58 | } 59 | 60 | func determineContentTypeByRequest(url string) string { 61 | // 创建一次性使用的HTTP客户端 62 | client := &http.Client{ 63 | Timeout: config.Conf.Timeout * time.Second, 64 | Transport: &http.Transport{ 65 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 66 | DisableKeepAlives: true, 67 | }, 68 | } 69 | 70 | req, err := http.NewRequest("GET", url, nil) 71 | if err != nil { 72 | log.Printf("创建请求失败: %v", err) 73 | return "bookget" 74 | } 75 | 76 | req.Header.Set("User-Agent", config.Conf.UserAgent) 77 | req.Header.Set("Range", "bytes=0-0") // 只请求头信息 78 | 79 | resp, err := client.Do(req) 80 | if err != nil { 81 | log.Printf("请求失败: %v", err) 82 | return "bookget" 83 | } 84 | defer resp.Body.Close() 85 | 86 | contentType := resp.Header.Get("Content-Type") 87 | if contentType == "" { 88 | return cacheAndReturn(url, "bookget") 89 | } 90 | 91 | // 去除可能的参数部分(如charset=utf-8) 92 | contentType = strings.Split(contentType, ";")[0] 93 | contentType = strings.TrimSpace(contentType) 94 | 95 | if result, ok := contentTypeMappings[contentType]; ok { 96 | return cacheAndReturn(url, result) 97 | } 98 | 99 | // 默认返回bookget 100 | return cacheAndReturn(url, "bookget") 101 | } 102 | 103 | func cacheAndReturn(url, result string) string { 104 | contentTypeCache.Store(url, result) 105 | return result 106 | } 107 | -------------------------------------------------------------------------------- /pkg/util/number.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | func LetterNumberEscape(s string) string { 9 | m := regexp.MustCompile(`([A-Za-z0-9-_]+)`).FindAllString(s, -1) 10 | if m != nil { 11 | s = strings.Join(m, "") 12 | } 13 | return s 14 | } 15 | -------------------------------------------------------------------------------- /pkg/util/sort.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | type SortByStr []string 4 | 5 | func (s SortByStr) Len() int { return len(s) } 6 | func (s SortByStr) Less(i, j int) bool { return s[i] < s[j] } 7 | func (s SortByStr) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 8 | -------------------------------------------------------------------------------- /pkg/util/text.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type UriMatch struct { 12 | Min string 13 | Max string 14 | IMin int 15 | IMax int 16 | } 17 | 18 | func SubText(text, from, to string) string { 19 | iPos := strings.Index(text, from) 20 | if iPos == -1 { 21 | return "" 22 | } 23 | subText := text[iPos:] 24 | iPos2 := strings.Index(subText, to) 25 | if iPos2 == -1 { 26 | return "" 27 | } 28 | return subText[:iPos2] 29 | } 30 | 31 | func GetUriMatch(uri string) (u UriMatch, ok bool) { 32 | m := regexp.MustCompile(`\((\d+)-(\d+)\)`).FindStringSubmatch(uri) 33 | if m == nil { 34 | return u, false 35 | } 36 | 37 | u.Min = m[1] 38 | u.Max = m[2] 39 | i, _ := strconv.Atoi(u.Min) 40 | u.IMin = i 41 | iMax, _ := strconv.Atoi(u.Max) 42 | u.IMax = iMax 43 | 44 | return u, true 45 | } 46 | 47 | func GetHostUrl(uri string) string { 48 | u, err := url.Parse(uri) 49 | if err != nil { 50 | return "" 51 | } 52 | var hostUrl = fmt.Sprintf("%s://%s/", u.Scheme, u.Host) 53 | return hostUrl 54 | } 55 | 56 | func RemoveDuplicate(source []string) []string { 57 | mTmp := map[string]string{} 58 | newArray := make([]string, len(source), 0) 59 | for e := range source { 60 | if value, ok := mTmp[source[e]]; !ok { 61 | newArray = append(newArray, value) 62 | } 63 | } 64 | return newArray 65 | } 66 | 67 | func ToInt(val interface{}) (int, error) { 68 | switch v := val.(type) { 69 | case int: 70 | return v, nil 71 | case float64: 72 | return int(v), nil 73 | case string: 74 | return strconv.Atoi(v) 75 | default: 76 | return 0, fmt.Errorf("unsupported type") 77 | } 78 | } 79 | 80 | func ToString(val interface{}) (string, error) { 81 | switch v := val.(type) { 82 | case string: 83 | return v, nil 84 | case int: 85 | return strconv.Itoa(v), nil 86 | case float64: 87 | return strconv.Itoa(int(v)), nil 88 | default: 89 | return "", fmt.Errorf("unsupported type") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pkg/version/version_check.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "bookget/config" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | CacheFileName = ".bookget_version_cache" 17 | DefaultCheckInterval = 24 * time.Hour 18 | ) 19 | 20 | type Checker struct { 21 | CurrentVersion string 22 | RepoOwner string 23 | RepoName string 24 | CachePath string 25 | LastChecked time.Time 26 | } 27 | 28 | type cache struct { 29 | Version string `json:"version"` 30 | LastChecked time.Time `json:"last_checked"` 31 | } 32 | 33 | type githubRelease struct { 34 | TagName string `json:"tag_name"` 35 | } 36 | 37 | func NewChecker(currentVersion, repoOwner, repoName string) *Checker { 38 | cachePath, _ := getCachePath() 39 | return &Checker{ 40 | CurrentVersion: currentVersion, 41 | RepoOwner: repoOwner, 42 | RepoName: repoName, 43 | CachePath: cachePath, 44 | } 45 | } 46 | 47 | func (c *Checker) CheckForUpdate() (string, bool, error) { 48 | // 检查是否需要跳过本次检查 49 | if time.Since(c.LastChecked) < DefaultCheckInterval { 50 | return "", false, nil 51 | } 52 | 53 | // 获取最新版本 54 | latestVersion, err := c.getLatestVersion() 55 | if err != nil { 56 | return "", false, fmt.Errorf("获取最新版本失败: %w", err) 57 | } 58 | 59 | c.LastChecked = time.Now() 60 | 61 | // 比较版本 62 | if !c.compareVersions(latestVersion) { 63 | return latestVersion, true, nil 64 | } 65 | 66 | return latestVersion, false, nil 67 | } 68 | 69 | func (c *Checker) getLatestVersion() (string, error) { 70 | // 先尝试从缓存读取 71 | if cached, err := c.readCache(); err == nil && cached != nil { 72 | return cached.Version, nil 73 | } 74 | 75 | // 从GitHub API获取最新版本 76 | version, err := c.fetchFromGitHub() 77 | if err != nil { 78 | // 如果API失败但缓存存在,返回缓存版本 79 | if cached, err := c.readCache(); err == nil && cached != nil { 80 | return cached.Version, nil 81 | } 82 | return "", err 83 | } 84 | 85 | // 更新缓存 86 | if err := c.writeCache(version); err != nil { 87 | return "", fmt.Errorf("更新缓存失败: %w", err) 88 | } 89 | 90 | return version, nil 91 | } 92 | 93 | func (c *Checker) fetchFromGitHub() (string, error) { 94 | url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", c.RepoOwner, c.RepoName) 95 | resp, err := http.Get(url) 96 | if err != nil { 97 | return "", fmt.Errorf("GitHub API请求失败: %w", err) 98 | } 99 | defer resp.Body.Close() 100 | 101 | if resp.StatusCode != http.StatusOK { 102 | return "", fmt.Errorf("GitHub API返回非200状态码: %d", resp.StatusCode) 103 | } 104 | 105 | body, err := io.ReadAll(resp.Body) 106 | if err != nil { 107 | return "", fmt.Errorf("读取响应体失败: %w", err) 108 | } 109 | 110 | var release githubRelease 111 | if err := json.Unmarshal(body, &release); err != nil { 112 | return "", fmt.Errorf("解析JSON失败: %w", err) 113 | } 114 | 115 | return strings.TrimPrefix(release.TagName, "v"), nil 116 | } 117 | 118 | func (c *Checker) compareVersions(latest string) bool { 119 | return c.CurrentVersion == latest 120 | } 121 | 122 | func (c *Checker) readCache() (*cache, error) { 123 | if c.CachePath == "" { 124 | return nil, nil 125 | } 126 | 127 | file, err := os.ReadFile(c.CachePath) 128 | if err != nil { 129 | if os.IsNotExist(err) { 130 | return nil, nil 131 | } 132 | return nil, err 133 | } 134 | 135 | var data cache 136 | if err := json.Unmarshal(file, &data); err != nil { 137 | return nil, err 138 | } 139 | 140 | return &data, nil 141 | } 142 | 143 | func (c *Checker) writeCache(version string) error { 144 | if c.CachePath == "" { 145 | return nil 146 | } 147 | 148 | data := cache{ 149 | Version: version, 150 | LastChecked: time.Now(), 151 | } 152 | 153 | jsonData, err := json.Marshal(data) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | return os.WriteFile(c.CachePath, jsonData, 0644) 159 | } 160 | 161 | func getCachePath() (string, error) { 162 | homeDir := config.UserHomeDir() 163 | return filepath.Join(homeDir, CacheFileName), nil 164 | } 165 | --------------------------------------------------------------------------------