├── .github
└── workflows
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── cmd
└── rss2cloud.go
├── go.mod
├── go.sum
├── main.go
├── node-site-config.json
├── p115
├── login_darwin.go
├── login_linux.go
├── login_windows.go
└── p115.go
├── request
├── request.go
└── request_test.go
├── rss.json
├── rsssite
├── acgnx.go
├── dmhy.go
├── mikanani.go
├── nyaa.go
├── rssconfig.go
├── rsshub.go
├── rsssite.go
├── rsssite_test.go
└── utils.go
├── server
└── server.go
└── store
├── store.go
└── store_test.go
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Dispatch Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | release_tag:
7 | type: string
8 | description: 'Release Tag'
9 | required: true
10 | jobType:
11 | type: choice
12 | description: 'Job Type'
13 | default: 'all'
14 | required: true
15 | options:
16 | - all
17 | - musl
18 | - darwin
19 | - windows
20 |
21 | permissions:
22 | contents: write
23 | packages: write
24 |
25 | jobs:
26 | releases-musl:
27 | if: ${{ github.event.inputs.jobType == 'musl' || github.event.inputs.jobType == 'all' }}
28 | name: Release Go Binary
29 | runs-on: ubuntu-latest
30 | strategy:
31 | matrix:
32 | goos: [linux]
33 | goarch: [amd64]
34 | steps:
35 | - uses: actions/checkout@v4
36 | - uses: wangyoucao577/go-release-action@v1
37 | with:
38 | pre_command: 'export CGO_ENABLED=1'
39 | github_token: ${{ secrets.GITHUB_TOKEN }}
40 | goos: ${{ matrix.goos }}
41 | goarch: ${{ matrix.goarch }}
42 | ldflags: '-s -w -extldflags -static'
43 | asset_name: rss2cloud-${{ inputs.release_tag }}-${{ matrix.goos }}-${{ matrix.goarch }}-musl
44 | release_tag: ${{ github.event.inputs.release_tag }}
45 | releases-darwin:
46 | if: ${{ github.event.inputs.jobType == 'darwin' || github.event.inputs.jobType == 'all' }}
47 | name: Release Go Binary
48 | runs-on: macos-latest
49 | steps:
50 | - uses: actions/checkout@v4
51 | - name: Setup Go
52 | uses: actions/setup-go@v4
53 | with:
54 | go-version: '1.20'
55 | check-latest: true
56 | - name: Install dependencies
57 | run: |
58 | export CGO_ENABLED=1
59 | mkdir -p ./release-tmp
60 | go get .
61 | - name: Build
62 | run: go build -ldflags "-s -w" -o ./release-tmp/
63 | - name: Generate tar
64 | run: |
65 | mkdir release-ready
66 | cd ./release-tmp
67 | tar -zcvf ../release-ready/rss2cloud-${{ inputs.release_tag }}-darwin-arm64.tar.gz *
68 | - name: Upload Release
69 | uses: svenstaro/upload-release-action@v2
70 | with:
71 | repo_token: ${{ secrets.GITHUB_TOKEN }}
72 | file: ./release-ready/rss2cloud-${{ inputs.release_tag }}-darwin-arm64.tar.gz
73 | tag: ${{ inputs.release_tag }}
74 | overwrite: true
75 |
76 | releases-windows:
77 | if: ${{ github.event.inputs.jobType == 'windows' || github.event.inputs.jobType == 'all' }}
78 | name: Release Go Binary
79 | runs-on: ubuntu-latest
80 | strategy:
81 | matrix:
82 | goos: [windows]
83 | goarch: [amd64]
84 | steps:
85 | - uses: actions/checkout@v4
86 | - uses: wangyoucao577/go-release-action@v1
87 | with:
88 | pre_command: 'apt-get update && apt-get install --no-install-recommends -y gcc-mingw-w64-x86-64 && export CGO_ENABLED=1 && export CC=x86_64-w64-mingw32-gcc'
89 | github_token: ${{ secrets.GITHUB_TOKEN }}
90 | goos: ${{ matrix.goos }}
91 | goarch: ${{ matrix.goarch }}
92 | release_tag: ${{ github.event.inputs.release_tag }}
93 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish Release
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 |
7 | permissions:
8 | contents: write
9 | packages: write
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - id: create_release
17 | uses: actions/create-release@v1
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 | with:
21 | tag_name: ${{ github.ref }}
22 | release_name: Release ${{ github.ref }}
23 | body_path: ./CHANGELOG.md
24 |
25 | releases-musl:
26 | name: Release musl Go Binary
27 | runs-on: ubuntu-latest
28 | strategy:
29 | matrix:
30 | goos: [linux]
31 | goarch: [amd64]
32 | steps:
33 | - uses: actions/checkout@v4
34 | - uses: wangyoucao577/go-release-action@v1
35 | with:
36 | pre_command: 'export CGO_ENABLED=1'
37 | github_token: ${{ secrets.GITHUB_TOKEN }}
38 | goos: ${{ matrix.goos }}
39 | goarch: ${{ matrix.goarch }}
40 | ldflags: '-s -w -extldflags -static'
41 | asset_name: rss2cloud-${{ github.ref_name }}-${{ matrix.goos }}-${{ matrix.goarch }}-musl
42 | release_tag: ${{ github.ref_name }}
43 | releases-darwin:
44 | name: Release darwin Go Binary
45 | runs-on: macos-latest
46 | steps:
47 | - uses: actions/checkout@v4
48 | - name: Setup Go
49 | uses: actions/setup-go@v4
50 | with:
51 | go-version: '1.23'
52 | check-latest: true
53 | - name: Install dependencies
54 | run: |
55 | export CGO_ENABLED=1
56 | mkdir -p ./release-tmp
57 | go get .
58 | - name: Build
59 | run: go build -ldflags "-s -w" -o ./release-tmp/
60 | - name: Generate tar
61 | run: |
62 | mkdir release-ready
63 | cd ./release-tmp
64 | tar -zcvf ../release-ready/rss2cloud-${{ github.ref_name }}-darwin-arm64.tar.gz *
65 | - name: Upload Release
66 | uses: svenstaro/upload-release-action@v2
67 | with:
68 | repo_token: ${{ secrets.GITHUB_TOKEN }}
69 | file: ./release-ready/rss2cloud-${{ github.ref_name }}-darwin-arm64.tar.gz
70 | tag: ${{ github.ref_name }}
71 | overwrite: true
72 |
73 | releases-windows:
74 | name: Release windows Go Binary
75 | runs-on: ubuntu-latest
76 | strategy:
77 | matrix:
78 | goos: [windows]
79 | goarch: [amd64]
80 | steps:
81 | - uses: actions/checkout@v4
82 | - uses: wangyoucao577/go-release-action@v1
83 | with:
84 | pre_command: 'apt-get update && apt-get install --no-install-recommends -y gcc-mingw-w64-x86-64 && export CGO_ENABLED=1 && export CC=x86_64-w64-mingw32-gcc'
85 | github_token: ${{ secrets.GITHUB_TOKEN }}
86 | goos: ${{ matrix.goos }}
87 | goarch: ${{ matrix.goarch }}
88 | ldflags: '-s -w'
89 | release_tag: ${{ github.ref_name }}
90 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /.idea
3 | /.vscode
4 | t.txt
5 | db.sqlite
6 | .cookies
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # v0.1.12
2 |
3 | ## 变动
4 |
5 | - 使用dlclark/regexp2 替代 regexp
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Alan Yang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rss2cloud
2 |
3 | 将 RSS 订阅离线下载到 115 网盘。
4 |
5 | 支持批量添加 magnet, ed2k, http 链接到 115 离线任务
6 |
7 | ## 关于
8 |
9 | 基于 [deadblue/elevengo](https://github.com/deadblue/elevengo)
10 |
11 | 支持 RSS 源: nyaa, dmhy, mikanni, share.acgnx.net
12 |
13 | 已添加的 RSS 任务记录保存在本地的同一目录下面的 db.sqlite 文件里
14 |
15 | Rust 版本 [rss2pan](https://github.com/zhifengle/rss2pan) 使用的 Web API 添加离线任务。
16 |
17 | 移除读取浏览器 cookies 的功能。需要此功能使用 [gcookie](https://github.com/zhifengle/gcookie)
18 |
19 | ```bat
20 | REM 使用 gcookie 读取浏览器的 cookie
21 | gcookie.exe 115.com > .cookies
22 | REM rss2cloud 会读取 .cookies 文件
23 | rss2cloud.exe
24 | ```
25 |
26 | ## 用法
27 |
28 | 在同一目录下面,配置好 `rss.json` 和 `node-site-config.json`
29 |
30 | 在命令行运行 `rss2cloud`
31 |
32 | ```bash
33 | # 查看帮助
34 | rss2cloud -h
35 | # 直接运行。读取 rss.json,依次添加离线任务
36 | rss2cloud
37 | # 使用二维码登录
38 | rss2cloud -q
39 | # 使用cookies
40 | rss2cloud --cookies "yourcookies"
41 |
42 | # 指定 rss URL 离线下载
43 | # 如果 rss.json 存在这条url 的配置,会读取配置。没有配置,默认离线到 115 的默认目录
44 | rss2cloud -u "https://mikanani.me/RSS/Bangumi?bangumiId=2739&subgroupid=12"
45 | # --no-cache 跳过检查 db.sqlite 里面缓存的
46 | rss2cloud --no-cache -u "https://mikanani.me/RSS/Bangumi?bangumiId=2739&subgroupid=12"
47 | # --clear-task-type 清除离线任务。 1: 已完成的 2: 所有任务 3: 失败任务 4: 运行的任务 5: 完成并删除的任务 6: 所有的任务
48 | # 清除115任务列表里面已经完成的任务
49 | rss2cloud --clear-task-type 1
50 |
51 | # 查看 magnet 子命令帮助
52 | rss2cloud magnet -h
53 | rss2cloud magnet --link "magnet:?xt=urn:btih:12345" --cid "12345"
54 | # 离线包含 magnet 的 txt 文件; 按行分割
55 | rss2cloud magnet --txt magnet.txt --cid "12345"
56 | ```
57 |
58 | ### 服务模式
59 |
60 | ```bash
61 | # 查看 server 子命令帮助
62 | rss2cloud server -h
63 | # 运行服务
64 | rss2cloud server
65 | # 添加任务
66 | curl -d '{"tasks": ["magnet:?xt=urn:btih:xx"], "cid":"12345"}' -X POST http://localhost:8115/add
67 | ```
68 |
69 | POST `http://localhost:8115/add`
70 |
71 | body 示例:
72 |
73 | ```json
74 | {
75 | "tasks": ["magnet:?xt=urn:btih:xxx"],
76 | "cid": "12345"
77 | }
78 | ```
79 |
80 | ## 配置
81 |
82 |
83 | 「 点击查看 配置文件 rss.json 」
84 |
85 | ```json
86 | {
87 | "mikanani.me": [
88 | {
89 | "name": "test",
90 | "filter": "/简体|1080p/",
91 | "url": "https://mikanani.me/RSS/Bangumi?bangumiId=2739&subgroupid=12"
92 | }
93 | ],
94 | "nyaa.si": [
95 | {
96 | "name": "VCB-Studio",
97 | "cid": "2479224057885794455",
98 | "url": "https://nyaa.si/?page=rss&u=VCB-Studio"
99 | }
100 | ],
101 | "sukebei.nyaa.si": [
102 | {
103 | "name": "name",
104 | "cid": "2479224057885794455",
105 | "url": "https://sukebei.nyaa.si/?page=rss"
106 | }
107 | ],
108 | "share.dmhy.org": [
109 | {
110 | "name": "水星的魔女",
111 | "filter": "简日双语",
112 | "cid": "2479224057885794455",
113 | "url": "https://share.dmhy.org/topics/rss/rss.xml?keyword=%E6%B0%B4%E6%98%9F%E7%9A%84%E9%AD%94%E5%A5%B3&sort_id=2&team_id=0&order=date-desc"
114 | }
115 | ]
116 | }
117 | ```
118 |
119 |
120 |
121 | 配置了 `filter` 后,标题包含该文字的会被离线。不设置 `filter` 默认离线全部
122 |
123 | `/简体|\\d{3-4}[pP]/` 使用斜线包裹的正则规则。注意转义规则
124 |
125 | cid 是离线到指定的文件夹的 id 。
126 |
127 | 获取方法: 浏览器打开 115 的文件,地址栏像 `https://115.com/?cid=2479224057885794455&offset=0&tab=&mode=wangpan`
128 |
129 | > 其中 2479224057885794455 就是 cid
130 |
131 |
132 | 「 点击查看 node-site-config.json 配置 」
133 |
134 | 配置示例。 设置 【httpsAgent】 表示使用代理连接对应网站。不想使用代理删除对应的配置。
135 |
136 | ```json
137 | {
138 | "share.dmhy.org": {
139 | "httpsAgent": "httpsAgent"
140 | },
141 | "nyaa.si": {
142 | "httpsAgent": "httpsAgent"
143 | },
144 | "sukebei.nyaa.si": {
145 | "httpsAgent": "httpsAgent"
146 | },
147 | "mikanime.tv": {
148 | "headers": {
149 | "Referer": "https://mikanime.tv/"
150 | }
151 | },
152 | "mikanani.me": {
153 | "httpsAgent": "httpsAgent"
154 | }
155 | }
156 | ```
157 |
158 |
159 |
160 | ### proxy 配置
161 |
162 | 设置【httpsAgent】会使用代理。默认使用的地址 `http://127.0.0.1:10809`。
163 |
164 | > 【httpsAgent】沿用的 node 版的配置。
165 |
166 | 需要自定义代理时,在命令行设置 Windows: set HTTPS_PROXY=http://youraddr:port
167 |
168 | > Linux: export HTTPS_PROXY=http://youraddr:port
169 |
170 |
171 | 「 点击查看 批处理脚本 」
172 |
173 | ```batch
174 | @ECHO off
175 | SETLOCAL
176 | CALL :find_dp0
177 | REM set HTTPS_PROXY=http://youraddr:port
178 | rss2cloud.exe %*
179 | ENDLOCAL
180 | EXIT /b %errorlevel%
181 | :find_dp0
182 | SET dp0=%~dp0
183 | EXIT /b
184 | ```
185 |
186 |
187 |
188 | 把上面的 batch 例子改成自己的代理地址。另存为 rss2cloud.cmd 和 rss2cloud.exe 放在一个目录下面。
189 |
190 | 在命令行运行 rss2cloud.cmd 就能够使用自己的代理的了。
191 |
192 |
193 | 「 点击查看 配置 Linux 定时任务 」
194 | 假设 rss2cloud 目录在 `$HOME` 下面
195 |
196 | 新建一个 rss2cloud.sh 的文件
197 |
198 | ```bash
199 | #!/bin/bash
200 | cd "$(dirname "$0")"
201 | #export HTTPS_PROXY=http://youraddr:port
202 | $HOME/rss2cloud/rss2cloud >> $HOME/rss2cloud/logfile.log 2>&1
203 | ```
204 |
205 | 配置定时任务 `10 8 * * * $HOME/rss2cloud/rss2cloud.sh`
206 |
207 | 不使用 shell 脚本,定时任务这样写 `10 8 * * * cd $HOME/rss2cloud && ./rss2cloud`
208 |
209 |
210 |
--------------------------------------------------------------------------------
/cmd/rss2cloud.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 |
8 | "github.com/spf13/cobra"
9 | "github.com/zhifengle/rss2cloud/p115"
10 | "github.com/zhifengle/rss2cloud/rsssite"
11 | "github.com/zhifengle/rss2cloud/server"
12 | )
13 |
14 | var (
15 | pAgent *p115.Agent
16 | rssUrl string
17 | cookies string
18 | rssJsonPath string
19 | qrLogin bool
20 | disableCache bool
21 | chunkDelay int
22 | chunkSize int
23 | clearTaskNum int
24 | rootCmd = &cobra.Command{
25 | Use: "rss2cloud",
26 | Short: `Add offline tasks to 115`,
27 | Run: func(_cmd *cobra.Command, _args []string) {
28 | initAgent()
29 | if rssJsonPath != "" {
30 | rsssite.SetRssJsonPath(rssJsonPath)
31 | }
32 | if rssUrl != "" {
33 | pAgent.AddRssUrlTask(rssUrl)
34 | return
35 | }
36 | if clearTaskNum > 0 {
37 | err := pAgent.OfflineClear(clearTaskNum - 1)
38 | if err != nil {
39 | log.Fatalln(err)
40 | }
41 | return
42 | }
43 | pAgent.ExecuteAllRssTask()
44 | },
45 | }
46 | // magnet link
47 | linkUrl string
48 | cid string
49 | textFile string
50 | magnetCmd = &cobra.Command{
51 | Use: "magnet",
52 | Short: `Add magnet tasks to 115`,
53 | Run: func(_cmd *cobra.Command, _args []string) {
54 | initAgent()
55 | magnets := []string{}
56 | if textFile != "" {
57 | var err error
58 | magnets, err = rsssite.GetMagnetsFromText(textFile)
59 | if err != nil {
60 | log.Fatalln(err)
61 | }
62 | } else if linkUrl != "" {
63 | magnets = append(magnets, linkUrl)
64 | }
65 | if len(magnets) == 0 {
66 | log.Fatalln("magnets is empty")
67 | }
68 | pAgent.AddMagnetTask(magnets, cid)
69 | },
70 | }
71 | // server subcommand
72 | port int
73 |
74 | serverCmd = &cobra.Command{
75 | Use: "server",
76 | Short: `Start server`,
77 | Run: func(_cmd *cobra.Command, _args []string) {
78 | initAgent()
79 | server.New(pAgent, port).StartServer()
80 | },
81 | }
82 | )
83 |
84 | func Execute() {
85 | if err := rootCmd.Execute(); err != nil {
86 | fmt.Fprintln(os.Stderr, err)
87 | os.Exit(1)
88 | }
89 | }
90 |
91 | func init() {
92 | rootCmd.Flags().StringVarP(&rssUrl, "url", "u", "", "rss url")
93 | rootCmd.Flags().StringVar(&cookies, "cookies", "", "115 cookies")
94 | rootCmd.Flags().StringVarP(&rssJsonPath, "rss", "r", "", "rss json path")
95 | rootCmd.Flags().BoolVarP(&qrLogin, "qrcode", "q", false, "login 115 by qrcode")
96 | magnetCmd.Flags().StringVarP(&linkUrl, "link", "l", "", "magnet link")
97 | magnetCmd.Flags().StringVar(&cid, "cid", "", "cid")
98 | magnetCmd.Flags().StringVar(&textFile, "text", "", "text file")
99 | rootCmd.Flags().BoolVar(&disableCache, "no-cache", false, "skip checking cache in db.sqlite")
100 | rootCmd.Flags().IntVar(&chunkDelay, "chunk-delay", 0, "chunk delay. default 2")
101 | rootCmd.Flags().IntVar(&chunkSize, "chunk-size", 0, "chunk size. default 200")
102 | rootCmd.Flags().IntVar(&clearTaskNum, "clear-task-type", 0, "clear offline task type: 1-6.\n 1: OfflineClearDone\n 2: OfflineClearAll\n 3: OfflineClearFailed\n 4: OfflineClearRunning\n 5: OfflineClearDoneAndDelete\n 6: OfflineClearAllAndDelete")
103 | rootCmd.AddCommand(magnetCmd)
104 | // server subcommand
105 | serverCmd.Flags().IntVarP(&port, "port", "p", 8115, "server port")
106 | rootCmd.AddCommand(serverCmd)
107 | }
108 |
109 | func initAgent() {
110 | p115.SetOption(p115.Option{DisableCache: disableCache, ChunkDelay: chunkDelay, ChunkSize: chunkSize})
111 | var err error
112 | if cookies != "" {
113 | pAgent, err = p115.NewAgent(cookies)
114 | } else if qrLogin {
115 | pAgent, err = p115.NewAgentByQrcode()
116 | } else {
117 | pAgent, err = p115.New()
118 | }
119 | if err != nil {
120 | log.Fatalln(err)
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/zhifengle/rss2cloud
2 |
3 | go 1.23
4 |
5 | toolchain go1.23.2
6 |
7 | require github.com/spf13/cobra v1.8.0
8 |
9 | require (
10 | filippo.io/nistec v0.0.3 // indirect
11 | github.com/PuerkitoBio/goquery v1.8.1 // indirect
12 | github.com/andybalholm/cascadia v1.3.2 // indirect
13 | github.com/json-iterator/go v1.1.12 // indirect
14 | github.com/mmcdole/goxpp v1.1.0 // indirect
15 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
16 | github.com/modern-go/reflect2 v1.0.2 // indirect
17 | golang.org/x/net v0.20.0 // indirect
18 | golang.org/x/text v0.14.0 // indirect
19 | )
20 |
21 | require (
22 | github.com/deadblue/elevengo v0.7.8
23 | github.com/dlclark/regexp2 v1.11.5
24 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
25 | github.com/mattn/go-sqlite3 v1.14.21
26 | github.com/mmcdole/gofeed v1.2.1
27 | github.com/spf13/pflag v1.0.5 // indirect
28 | )
29 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | filippo.io/nistec v0.0.3 h1:h336Je2jRDZdBCLy2fLDUd9E2unG32JLwcJi0JQE9Cw=
2 | filippo.io/nistec v0.0.3/go.mod h1:84fxC9mi+MhC2AERXI4LSa8cmSVOzrFikg6hZ4IfCyw=
3 | github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
4 | github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
5 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
6 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
7 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
8 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 | github.com/deadblue/elevengo v0.4.6-0.20240807113021-601131b0ac51 h1:FLmzviqU+9U+W+ydAdhm3fw9bZC7qHC/lSwC88UTNj4=
13 | github.com/deadblue/elevengo v0.4.6-0.20240807113021-601131b0ac51/go.mod h1:E/4UJxjtZECiITsAkSVv4tR9TQa1P42s8qyTp7ghxK0=
14 | github.com/deadblue/elevengo v0.6.2 h1:Q1e6REkSX5/bIEB1wt6CrCyBVbWwr5gTF5Z8a8ys5vc=
15 | github.com/deadblue/elevengo v0.6.2/go.mod h1:E/4UJxjtZECiITsAkSVv4tR9TQa1P42s8qyTp7ghxK0=
16 | github.com/deadblue/elevengo v0.6.5 h1:qJ/ffqoyjLW/tihEViCopOaN2oJSbz//cDC3nnzh/8o=
17 | github.com/deadblue/elevengo v0.6.5/go.mod h1:E/4UJxjtZECiITsAkSVv4tR9TQa1P42s8qyTp7ghxK0=
18 | github.com/deadblue/elevengo v0.7.1 h1:VyjBBiBa0gcyB9La+MDc+sXTngOei3+qCyMkxZsFtNE=
19 | github.com/deadblue/elevengo v0.7.1/go.mod h1:rTKHkJuPwdRjRbDpfeARXxfvDHbPf7PLUvIfeTqQ9vE=
20 | github.com/deadblue/elevengo v0.7.4 h1:oVjnBLrmKQGdvoNeTw1oNhD3NBlT01v41dC6Ls460kM=
21 | github.com/deadblue/elevengo v0.7.4/go.mod h1:rTKHkJuPwdRjRbDpfeARXxfvDHbPf7PLUvIfeTqQ9vE=
22 | github.com/deadblue/elevengo v0.7.8 h1:7bQDYSDBcGCllBdwfqpTL9gcOVvIS5778Yo4Xhz8hM0=
23 | github.com/deadblue/elevengo v0.7.8/go.mod h1:rTKHkJuPwdRjRbDpfeARXxfvDHbPf7PLUvIfeTqQ9vE=
24 | github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
25 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
26 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
27 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
28 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
29 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
30 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
31 | github.com/mattn/go-sqlite3 v1.14.21 h1:IXocQLOykluc3xPE0Lvy8FtggMz1G+U3mEjg+0zGizc=
32 | github.com/mattn/go-sqlite3 v1.14.21/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
33 | github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s=
34 | github.com/mmcdole/gofeed v1.2.1/go.mod h1:2wVInNpgmC85q16QTTuwbuKxtKkHLCDDtf0dCmnrNr4=
35 | github.com/mmcdole/goxpp v1.1.0 h1:WwslZNF7KNAXTFuzRtn/OKZxFLJAAyOA9w82mDz2ZGI=
36 | github.com/mmcdole/goxpp v1.1.0/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
37 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
39 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
40 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
41 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
42 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
43 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
44 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
45 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
46 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
47 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
48 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
50 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
51 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
52 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
53 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
54 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
55 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
56 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
57 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
58 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
59 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
60 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
61 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
62 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
63 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
64 | golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
65 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
66 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
67 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
68 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
69 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
70 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
71 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
72 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
73 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
74 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
75 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
76 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
77 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
78 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
79 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
80 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
81 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
82 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
83 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
84 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
85 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
86 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
87 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
88 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
89 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
90 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
91 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
92 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
93 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
95 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
96 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
97 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/zhifengle/rss2cloud/cmd"
5 | )
6 |
7 | func main() {
8 | cmd.Execute()
9 | }
10 |
--------------------------------------------------------------------------------
/node-site-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "share.dmhy.org": {
3 | "httpsAgent": "httpsAgent"
4 | },
5 | "nyaa.si": {
6 | "httpsAgent": "httpsAgent"
7 | },
8 | "sukebei.nyaa.si": {
9 | "httpsAgent": "httpsAgent"
10 | },
11 | "mikanani.me": {
12 | "httpsAgent": "httpsAgent"
13 | }
14 | }
--------------------------------------------------------------------------------
/p115/login_darwin.go:
--------------------------------------------------------------------------------
1 | package p115
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 | )
7 |
8 | func DisplayQrcode(img []byte) error {
9 | imageName := "qrcode115.png"
10 | // save image
11 | err := os.WriteFile(imageName, img, 0644)
12 | if err != nil {
13 | return err
14 | }
15 |
16 | cmd := exec.Command("open", imageName)
17 | err = cmd.Start()
18 | if err != nil {
19 | return err
20 | }
21 | return nil
22 | }
23 |
24 | func DisposeQrcode() {
25 | _ = os.Remove("qrcode115.png")
26 | }
27 |
--------------------------------------------------------------------------------
/p115/login_linux.go:
--------------------------------------------------------------------------------
1 | package p115
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "os/exec"
7 | )
8 |
9 | func getValidOpenCommand() string {
10 | commnads := []string{"xdg-open", "gnome-open", "kde-open"}
11 | for _, c := range commnads {
12 | _, err := exec.LookPath(c)
13 | if err == nil {
14 | return c
15 | }
16 | }
17 | return ""
18 | }
19 |
20 | func DisplayQrcode(img []byte) error {
21 | imageName := "qrcode115.png"
22 | // save image
23 | err := os.WriteFile(imageName, img, 0644)
24 | if err != nil {
25 | return err
26 | }
27 | openCommand := getValidOpenCommand()
28 | if openCommand == "" {
29 | return errors.New("no open command found")
30 | }
31 | cmd := exec.Command(openCommand, imageName)
32 | err = cmd.Start()
33 | if err != nil {
34 | return err
35 | }
36 | return nil
37 | }
38 |
39 | func DisposeQrcode() {
40 | _ = os.Remove("qrcode115.png")
41 | }
42 |
--------------------------------------------------------------------------------
/p115/login_windows.go:
--------------------------------------------------------------------------------
1 | package p115
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 | )
7 |
8 | func DisplayQrcode(img []byte) error {
9 | imageName := "qrcode115.png"
10 | // save image
11 | err := os.WriteFile(imageName, img, 0644)
12 | if err != nil {
13 | return err
14 | }
15 |
16 | cmd := exec.Command("cmd", "/c", "start", imageName)
17 | err = cmd.Start()
18 | if err != nil {
19 | return err
20 | }
21 | return nil
22 | }
23 |
24 | func DisposeQrcode() {
25 | _ = os.Remove("qrcode115.png")
26 | }
27 |
--------------------------------------------------------------------------------
/p115/p115.go:
--------------------------------------------------------------------------------
1 | package p115
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "log"
7 | "os"
8 | "strings"
9 | "time"
10 |
11 | "github.com/deadblue/elevengo"
12 | "github.com/deadblue/elevengo/option"
13 | "github.com/zhifengle/rss2cloud/request"
14 | "github.com/zhifengle/rss2cloud/rsssite"
15 | "github.com/zhifengle/rss2cloud/store"
16 | )
17 |
18 | var disableCache = false
19 | var defaultChunkSize = 200
20 | var chunkDelay = 2
21 |
22 | type Option struct {
23 | DisableCache bool
24 | ChunkDelay int
25 | ChunkSize int
26 | }
27 |
28 | func SetOption(opt Option) {
29 | disableCache = opt.DisableCache
30 | if opt.ChunkDelay > 0 {
31 | chunkDelay = opt.ChunkDelay
32 | }
33 | if opt.ChunkSize > 0 {
34 | defaultChunkSize = opt.ChunkSize
35 | }
36 | }
37 |
38 | type Agent struct {
39 | Agent *elevengo.Agent
40 | StoreInstance *store.Store
41 | }
42 |
43 | func parseCookies(cookiesString string) map[string]string {
44 | cookies := make(map[string]string)
45 |
46 | // Split the cookies string into individual cookies
47 | cookiePairs := strings.Split(cookiesString, ";")
48 |
49 | // Parse each cookie into key-value pair
50 | for _, cookiePair := range cookiePairs {
51 | cookie := strings.TrimSpace(cookiePair)
52 | cookieParts := strings.SplitN(cookie, "=", 2)
53 | if len(cookieParts) == 2 {
54 | key := cookieParts[0]
55 | value := cookieParts[1]
56 | cookies[key] = value
57 | }
58 | }
59 |
60 | return cookies
61 | }
62 |
63 | func New() (*Agent, error) {
64 | cookies := LoadCookies()
65 | if cookies != "" {
66 | agent, err := NewAgent(cookies)
67 | // cookies is invalid
68 | if err != nil {
69 | return nil, err
70 | }
71 | return agent, nil
72 | }
73 | return nil, errors.New(".cookies is empty or not exist")
74 | }
75 |
76 | func NewAgentByQrcode() (*Agent, error) {
77 | cookies := LoadCookies()
78 | if cookies != "" {
79 | agent, err := NewAgent(cookies)
80 | // cookies is invalid
81 | if err != nil {
82 | return QrcodeLogin()
83 | }
84 | return agent, nil
85 | }
86 | return QrcodeLogin()
87 | }
88 | func NewAgentByConfig() (*Agent, error) {
89 | config := request.ReadNodeSiteConfig()
90 | if p115Config, ok := config["115.com"]; ok {
91 | cookies, ok := p115Config.Headers["cookie"]
92 | if !ok {
93 | cookies = p115Config.Headers["Cookie"]
94 | }
95 | if cookies == "" {
96 | return nil, errors.New("115 cookie is empty")
97 | }
98 | return NewAgent(cookies)
99 | }
100 | return nil, errors.New("no 115.com config in node-site-config.json")
101 | }
102 |
103 | func NewAgent(cookies string) (*Agent, error) {
104 | agent := elevengo.Default()
105 | cookiesMap := parseCookies(cookies)
106 | err := agent.CredentialImport(&elevengo.Credential{
107 | UID: cookiesMap["UID"], CID: cookiesMap["CID"], SEID: cookiesMap["SEID"],
108 | KID: cookiesMap["KID"],
109 | })
110 | if err != nil {
111 | return nil, err
112 | }
113 | return &Agent{
114 | Agent: agent,
115 | StoreInstance: store.New(nil),
116 | }, nil
117 | }
118 |
119 | func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) {
120 | for chunkSize < len(items) {
121 | items, chunks = items[chunkSize:], append(chunks, items[0:chunkSize:chunkSize])
122 | }
123 | return append(chunks, items)
124 | }
125 |
126 | func (ag *Agent) addCloudTasks(magnetItems []rsssite.MagnetItem, config *rsssite.RssConfig) {
127 | emptyNum := 0
128 | filterdItems := make([]rsssite.MagnetItem, 0)
129 | for _, item := range magnetItems {
130 | if item.Magnet == "" {
131 | emptyNum += 1
132 | continue
133 | }
134 | if disableCache || !ag.StoreInstance.HasItem(item.Magnet) {
135 | filterdItems = append(filterdItems, item)
136 | }
137 | }
138 | if emptyNum != 0 {
139 | log.Printf("[warning] [%s] has %d empty task\n", config.Name, emptyNum)
140 | }
141 | if len(filterdItems) == 0 {
142 | log.Printf("[%s] has 0 task\n", config.Name)
143 | return
144 | }
145 | for _, items := range chunkBy(filterdItems, defaultChunkSize) {
146 | urls := make([]string, 0)
147 | for _, item := range items {
148 | urls = append(urls, item.Magnet)
149 | }
150 | _, err := ag.Agent.OfflineAddUrl(urls, &option.OfflineAddOptions{SaveDirId: config.Cid})
151 | if err != nil {
152 | log.Printf("Add offline error: %s\n", err)
153 | return
154 | }
155 | log.Printf("[%s] [%s] add %d tasks\n", config.Name, config.Url, len(urls))
156 | ag.StoreInstance.SaveMagnetItems(filterdItems)
157 | time.Sleep(time.Second * time.Duration(chunkDelay))
158 | }
159 | }
160 |
161 | func (ag *Agent) AddRssUrlTask(url string) {
162 | config := rsssite.GetRssConfigByURL(url)
163 | if config == nil {
164 | pwd := os.Getenv("PWD")
165 | log.Printf("config not found: %s for url: %s\n", pwd, url)
166 | return
167 | }
168 | magnetItems := rsssite.GetMagnetItemList(config)
169 | ag.addCloudTasks(magnetItems, config)
170 | }
171 |
172 | func (ag *Agent) ExecuteAllRssTask() {
173 | rssDict := rsssite.ReadRssConfigDict()
174 | if rssDict == nil {
175 | pwd := os.Getenv("PWD")
176 | log.Printf("rss config not found: %s\n", pwd)
177 | return
178 | }
179 | for _, configs := range *rssDict {
180 | for i, config := range configs {
181 | magnetItems := rsssite.GetMagnetItemList(&config)
182 | ag.addCloudTasks(magnetItems, &config)
183 | if i != len(configs)-1 {
184 | time.Sleep(time.Second * time.Duration(chunkDelay))
185 | }
186 | }
187 | }
188 | }
189 |
190 | func (ag *Agent) AddMagnetTask(magnets []string, cid string) {
191 | for _, urls := range chunkBy(magnets, defaultChunkSize) {
192 | _, err := ag.Agent.OfflineAddUrl(urls, &option.OfflineAddOptions{SaveDirId: cid})
193 | if err != nil {
194 | log.Printf("Add offline error: %s\n", err)
195 | return
196 | }
197 | log.Printf("[magnet] add %d tasks\n", len(urls))
198 | time.Sleep(time.Second * time.Duration(chunkDelay))
199 | }
200 | }
201 | func (ag *Agent) OfflineClear(num int) (err error) {
202 | flag := elevengo.OfflineClearFlag(num)
203 | return ag.Agent.OfflineClear(flag)
204 | }
205 |
206 | func SaveCookies(agent *elevengo.Agent) {
207 | cr := &elevengo.Credential{}
208 | agent.CredentialExport(cr)
209 | cookies := fmt.Sprintf("UID=%s; CID=%s; SEID=%s; KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
210 | os.WriteFile(".cookies", []byte(cookies), 0644)
211 | }
212 |
213 | func LoadCookies() string {
214 | // check if .cookies exists
215 | if _, err := os.Stat(".cookies"); err != nil {
216 | return ""
217 | }
218 | cookies, err := os.ReadFile(".cookies")
219 | if err != nil {
220 | return ""
221 | }
222 | return string(cookies)
223 | }
224 |
225 | func QrcodeLogin() (*Agent, error) {
226 | agent := elevengo.Default()
227 | session := &elevengo.QrcodeSession{}
228 | // @TODO: add option; default is tv
229 | err := agent.QrcodeStart(session, option.Qrcode().LoginTv())
230 | if err != nil {
231 | return nil, err
232 | }
233 | err = DisplayQrcode(session.Image)
234 | if err != nil {
235 | return nil, err
236 | }
237 | now := time.Now()
238 | after := now.Add(2 * time.Minute)
239 | for {
240 | time.Sleep(200 * time.Millisecond)
241 | success, err := agent.QrcodePoll(session)
242 | if success {
243 | SaveCookies(agent)
244 | DisposeQrcode()
245 |
246 | return &Agent{
247 | Agent: agent,
248 | StoreInstance: store.New(nil),
249 | }, nil
250 | }
251 | if err != nil && err == elevengo.ErrQrcodeCancelled {
252 | return nil, errors.New("login cancelled")
253 | }
254 | if now.After(after) {
255 | return nil, errors.New("login timed out")
256 | }
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/request/request.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "bytes"
5 | "compress/flate"
6 | "compress/gzip"
7 | "encoding/json"
8 | "io"
9 | "net/http"
10 | "net/http/cookiejar"
11 | urlPkg "net/url"
12 | "os"
13 | "path"
14 | "time"
15 | )
16 |
17 | var (
18 | ReqSiteConfig = ReadNodeSiteConfig()
19 | httpProxy = "http://127.0.0.1:10809"
20 | ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
21 | clientMap = make(map[string]*http.Client)
22 | )
23 |
24 | type SiteConfig struct {
25 | HttpsAgent string `json:"httpsAgent,omitempty"`
26 | Headers map[string]string `json:"headers,omitempty"`
27 | }
28 |
29 | type NodeSiteConfig = map[string]SiteConfig
30 |
31 | func ReadNodeSiteConfig() NodeSiteConfig {
32 | filename := "node-site-config.json"
33 | config := make(NodeSiteConfig)
34 |
35 | if _, err := os.Stat(filename); err != nil {
36 | home, _ := os.UserHomeDir()
37 | filename = path.Join(home, filename)
38 | if _, err := os.Stat(filename); err != nil {
39 | return config
40 | }
41 | }
42 | file, _ := os.ReadFile(filename)
43 | json.Unmarshal(file, &config)
44 | return config
45 | }
46 |
47 | func getClientByReq(req *http.Request) *http.Client {
48 | host := req.URL.Host
49 | if clientMap[host] == nil {
50 | var p func(*http.Request) (*urlPkg.URL, error)
51 | curConfig, ok := ReqSiteConfig[host]
52 | if ok && curConfig.HttpsAgent != "" {
53 | u, _ := http.ProxyFromEnvironment(req)
54 | if u == nil {
55 | proxy, _ := urlPkg.Parse(httpProxy)
56 | p = http.ProxyURL(proxy)
57 | } else {
58 | p = http.ProxyFromEnvironment
59 | }
60 | }
61 | transport := &http.Transport{
62 | Proxy: p,
63 | DisableCompression: true,
64 | TLSHandshakeTimeout: 10 * time.Second,
65 | // TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
66 | }
67 | jar, _ := cookiejar.New(nil)
68 | client := &http.Client{
69 | Transport: transport,
70 | Timeout: 20 * time.Second,
71 | Jar: jar,
72 | }
73 | clientMap[host] = client
74 | }
75 | return clientMap[host]
76 | }
77 |
78 | func Request(method, url string, body io.Reader, headers map[string]string) (*http.Response, error) {
79 | req, err := http.NewRequest(method, url, body)
80 | if err != nil {
81 | return nil, err
82 | }
83 | req.Header.Set("User-Agent", ua)
84 |
85 | curConfig, ok := ReqSiteConfig[req.URL.Host]
86 | if ok {
87 | if curConfig.Headers != nil {
88 | for k, v := range curConfig.Headers {
89 | req.Header.Set(k, v)
90 | }
91 | }
92 | }
93 | client := getClientByReq(req)
94 |
95 | for k, v := range headers {
96 | req.Header.Set(k, v)
97 | }
98 | return client.Do(req)
99 | }
100 |
101 | func GetByte(url string, headers map[string]string) ([]byte, error) {
102 | if headers == nil {
103 | headers = make(map[string]string)
104 | }
105 | res, err := Request("GET", url, nil, headers)
106 | if err != nil {
107 | return nil, err
108 | }
109 | defer res.Body.Close()
110 |
111 | var reader io.ReadCloser
112 | switch res.Header.Get("Content-Encoding") {
113 | case "gzip":
114 | reader, _ = gzip.NewReader(res.Body)
115 | case "deflate":
116 | reader = flate.NewReader(res.Body)
117 | default:
118 | reader = res.Body
119 | }
120 | defer reader.Close()
121 |
122 | body, err := io.ReadAll(reader)
123 | if err != nil && err != io.EOF {
124 | return nil, err
125 | }
126 | return body, nil
127 | }
128 |
129 | func Get(url string, headers map[string]string) (string, error) {
130 | if headers == nil {
131 | headers = make(map[string]string)
132 | }
133 | body, err := GetByte(url, headers)
134 | if err != nil {
135 | return "", err
136 | }
137 | return string(body), nil
138 | }
139 |
140 | func PostJson(url string, body []byte, headers map[string]string) ([]byte, error) {
141 | if headers == nil {
142 | headers = make(map[string]string)
143 | }
144 | headers["Content-Type"] = "application/json; charset=UTF-8"
145 | res, err := Request("POST", url, bytes.NewBuffer(body), headers)
146 | if err != nil {
147 | return nil, err
148 | }
149 | defer res.Body.Close()
150 |
151 | body, err = io.ReadAll(res.Body)
152 | if err != nil && err != io.EOF {
153 | return nil, err
154 | }
155 | return body, nil
156 | }
157 |
158 | func PostForm(url string, data urlPkg.Values, headers map[string]string) ([]byte, error) {
159 | if headers == nil {
160 | headers = make(map[string]string)
161 | }
162 | headers["Content-Type"] = "application/x-www-form-urlencoded"
163 | res, err := Request("POST", url, bytes.NewBufferString(data.Encode()), headers)
164 | if err != nil {
165 | return nil, err
166 | }
167 | defer res.Body.Close()
168 |
169 | body, err := io.ReadAll(res.Body)
170 | if err != nil && err != io.EOF {
171 | return nil, err
172 | }
173 | return body, nil
174 | }
175 |
--------------------------------------------------------------------------------
/request/request_test.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "encoding/json"
5 | "io"
6 | "net/url"
7 | "os"
8 | "testing"
9 | )
10 |
11 | func TestReadNodeSiteConfig(t *testing.T) {
12 | config := ReadNodeSiteConfig()
13 | site := "share.dmhy.org"
14 | siteConfig, ok := config[site]
15 | if !ok {
16 | return
17 | }
18 | t.Log(siteConfig.HttpsAgent)
19 | }
20 |
21 | func TestGet(t *testing.T) {
22 | url := "https://httpbin.org/ip"
23 | ReqSiteConfig["httpbin.org"] = SiteConfig{
24 | HttpsAgent: "yes",
25 | }
26 | res, err := Get(url, nil)
27 | if err != nil {
28 | t.Error()
29 | }
30 | t.Log(res)
31 | }
32 |
33 | type CookiesResponse struct {
34 | Cookies map[string]string `json:"cookies"`
35 | }
36 |
37 | func TestSetCookie(t *testing.T) {
38 | url := "https://httpbin.org/cookies/set?foo=bar"
39 | ReqSiteConfig["httpbin.org"] = SiteConfig{
40 | HttpsAgent: "yes",
41 | }
42 | res, err := Get(url, nil)
43 | if err != nil {
44 | t.Error()
45 | }
46 | var result CookiesResponse
47 | err = json.Unmarshal([]byte(res), &result)
48 | if err != nil {
49 | t.Fatalf("Failed to unmarshal response: %v", err)
50 | }
51 | if result.Cookies["foo"] != "bar" {
52 | t.Errorf("Expected 'foo' to be 'bar', got %v", result.Cookies["foo"])
53 | }
54 | // Test setting the second cookie
55 | url = "https://httpbin.org/cookies/set?baz=qux"
56 | res, err = Get(url, nil)
57 | if err != nil {
58 | t.Fatalf("Failed to set cookie 'baz': %v", err)
59 | }
60 |
61 | result = CookiesResponse{}
62 | err = json.Unmarshal([]byte(res), &result)
63 | if err != nil {
64 | t.Fatalf("Failed to unmarshal response: %v", err)
65 | }
66 | if result.Cookies["baz"] != "qux" || result.Cookies["foo"] != "bar" {
67 | t.Errorf("Expected 'baz' to be 'qux' and 'foo' to be 'bar', got %v and %v", result.Cookies["baz"], result.Cookies["foo"])
68 | }
69 | }
70 |
71 | func TestGetWithProxy(t *testing.T) {
72 | url := "https://httpbin.org/ip"
73 | ReqSiteConfig["httpbin.org"] = SiteConfig{
74 | HttpsAgent: "yes",
75 | }
76 | os.Setenv("http_proxy", "socks5://127.0.0.1:7890")
77 | os.Setenv("https_proxy", "socks5://127.0.0.1:7890")
78 | res, err := Get(url, nil)
79 | if err != nil {
80 | t.Error()
81 | }
82 | t.Log(res)
83 | }
84 |
85 | func TestPostForm(t *testing.T) {
86 | targetUrl := "https://httpbin.org/post"
87 | values := url.Values{}
88 | values.Add("custname", "testpost")
89 | res, err := PostForm(targetUrl, values, nil)
90 | if err != nil {
91 | t.Error()
92 | }
93 | t.Log(string(res))
94 | }
95 |
96 | func TestPostJson(t *testing.T) {
97 | targetUrl := "https://httpbin.org/post"
98 | post_body_struct := struct {
99 | Custname string `json:"custname"`
100 | }{
101 | Custname: "testpost",
102 | }
103 | // convert struct to json bytes
104 | values, _ := json.Marshal(post_body_struct)
105 | res, err := PostJson(targetUrl, values, nil)
106 | if err != nil {
107 | t.Error()
108 | }
109 | t.Log(string(res))
110 | }
111 |
112 | func TestDownloadFile(t *testing.T) {
113 | targetUrl := "https://cachefly.cachefly.net/10mb.test"
114 | res, _ := Request("GET", targetUrl, nil, make(map[string]string))
115 | defer res.Body.Close()
116 | file, _ := os.Create("file.test")
117 | _, err := io.Copy(file, res.Body)
118 | if err != nil {
119 | t.Errorf("Error: %v", err)
120 | }
121 |
122 | // Close the file.
123 | file.Close()
124 | }
125 |
--------------------------------------------------------------------------------
/rss.json:
--------------------------------------------------------------------------------
1 | {
2 | "mikanani.me": [
3 | {
4 | "name": "test",
5 | "filter": "简体内嵌",
6 | "url": "https://mikanani.me/RSS/Bangumi?bangumiId=2739&subgroupid=12"
7 | },
8 | {
9 | "name": "test07",
10 | "filter": "07",
11 | "url": "https://mikanani.me/RSS/Bangumi?bangumiId=2739"
12 | }
13 | ],
14 | "share.dmhy.org": [
15 | {
16 | "name": "水星的魔女",
17 | "filter": "简日双语",
18 | "url": "https://share.dmhy.org/topics/rss/rss.xml?keyword=%E6%B0%B4%E6%98%9F%E7%9A%84%E9%AD%94%E5%A5%B3&sort_id=2&team_id=0&order=date-desc"
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/rsssite/acgnx.go:
--------------------------------------------------------------------------------
1 | package rsssite
2 |
3 | import "github.com/mmcdole/gofeed"
4 |
5 | type Acgnx struct {
6 | }
7 |
8 | func (a *Acgnx) GetMagnet(item *gofeed.Item) string {
9 | if item.Enclosures == nil || len(item.Enclosures) == 0 {
10 | return ""
11 | }
12 | return item.Enclosures[0].URL
13 | }
14 |
15 | func (a *Acgnx) GetMagnetItem(item *gofeed.Item) MagnetItem {
16 | return MagnetItem{
17 | Title: item.Title,
18 | Link: item.Link,
19 | Magnet: a.GetMagnet(item),
20 | Description: item.Description,
21 | Content: item.Content,
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/rsssite/dmhy.go:
--------------------------------------------------------------------------------
1 | package rsssite
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/mmcdole/gofeed"
7 | )
8 |
9 | type Dmhy struct {
10 | }
11 |
12 | func (d *Dmhy) GetMagnet(item *gofeed.Item) string {
13 | if item.Enclosures == nil || len(item.Enclosures) == 0 {
14 | return ""
15 | }
16 | lst := strings.Split(item.Enclosures[0].URL, "&dn=")
17 | if len(lst) != 2 {
18 | return item.Enclosures[0].URL
19 | }
20 | return lst[0]
21 | }
22 |
23 | func (d *Dmhy) GetMagnetItem(item *gofeed.Item) MagnetItem {
24 | return MagnetItem{
25 | Title: item.Title,
26 | Link: item.Link,
27 | Magnet: d.GetMagnet(item),
28 | Description: item.Description,
29 | Content: item.Content,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/rsssite/mikanani.go:
--------------------------------------------------------------------------------
1 | package rsssite
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/mmcdole/gofeed"
8 | )
9 |
10 | type Mikanani struct{}
11 |
12 | func (m *Mikanani) GetMagnet(item *gofeed.Item) string {
13 | lst := strings.Split(item.Link, "Episode/")
14 | if len(lst) != 2 {
15 | return ""
16 | }
17 | return fmt.Sprintf("magnet:?xt=urn:btih:%s", lst[1])
18 | }
19 |
20 | func (m *Mikanani) GetMagnetItem(item *gofeed.Item) MagnetItem {
21 | return MagnetItem{
22 | Title: item.Title,
23 | Link: item.Link,
24 | Magnet: m.GetMagnet(item),
25 | Description: item.Description,
26 | Content: item.Content,
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/rsssite/nyaa.go:
--------------------------------------------------------------------------------
1 | package rsssite
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/mmcdole/gofeed"
7 | )
8 |
9 | type Nyaa struct {
10 | }
11 |
12 | func (n *Nyaa) GetMagnet(item *gofeed.Item) string {
13 | if item.Extensions["nyaa"] == nil {
14 | return ""
15 | }
16 | if item.Extensions["nyaa"]["infoHash"] == nil {
17 | return ""
18 | }
19 | if len(item.Extensions["nyaa"]["infoHash"]) == 0 {
20 | return ""
21 | }
22 | return fmt.Sprintf("magnet:?xt=urn:btih:%s", item.Extensions["nyaa"]["infoHash"][0].Value)
23 | }
24 |
25 | func (n *Nyaa) GetMagnetItem(item *gofeed.Item) MagnetItem {
26 | return MagnetItem{
27 | Title: item.Title,
28 | Link: item.Link,
29 | Magnet: n.GetMagnet(item),
30 | Description: item.Description,
31 | Content: item.Content,
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/rsssite/rssconfig.go:
--------------------------------------------------------------------------------
1 | package rsssite
2 |
3 | import (
4 | "encoding/json"
5 | urlPkg "net/url"
6 | "os"
7 | )
8 |
9 | var (
10 | RssConfigDict map[string][]RssConfig
11 | rssJsonPath string
12 | )
13 |
14 | type RssConfig struct {
15 | Name string `json:"name"`
16 | Url string `json:"url"`
17 | Cid string `json:"cid,omitempty"`
18 | Filter string `json:"filter,omitempty"`
19 | Expiration uint `json:"expiration,omitempty"`
20 | }
21 |
22 | func SetRssJsonPath(p string) {
23 | rssJsonPath = p
24 | }
25 |
26 | func ReadRssConfigDict() *map[string][]RssConfig {
27 | if rssJsonPath == "" {
28 | rssJsonPath = "rss.json"
29 | }
30 | // read config
31 | file, err := os.ReadFile(rssJsonPath)
32 | if err != nil {
33 | return nil
34 | }
35 | config := make(map[string][]RssConfig)
36 | json.Unmarshal(file, &config)
37 | RssConfigDict = config
38 | return &config
39 | }
40 |
41 | func GetRssConfigByURL(url string) *RssConfig {
42 | urlObj, err := urlPkg.Parse(url)
43 | if err != nil {
44 | return nil
45 | }
46 | ReadRssConfigDict()
47 | configs, ok := RssConfigDict[urlObj.Host]
48 | if !ok {
49 | return &RssConfig{
50 | Url: url,
51 | }
52 | }
53 | for _, config := range configs {
54 | if config.Url == url {
55 | return &config
56 | }
57 | }
58 | return &RssConfig{
59 | Url: url,
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/rsssite/rsshub.go:
--------------------------------------------------------------------------------
1 | package rsssite
2 |
3 | import (
4 | "github.com/mmcdole/gofeed"
5 | )
6 |
7 | type Rsshub struct {
8 | }
9 |
10 | func (r *Rsshub) GetMagnet(item *gofeed.Item) string {
11 | return GetMagnetByEnclosure(item)
12 | }
13 |
14 | func (r *Rsshub) GetMagnetItem(item *gofeed.Item) MagnetItem {
15 | return MagnetItem{
16 | Title: item.Title,
17 | Link: item.Link,
18 | Magnet: r.GetMagnet(item),
19 | Description: item.Description,
20 | Content: item.Content,
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/rsssite/rsssite.go:
--------------------------------------------------------------------------------
1 | package rsssite
2 |
3 | import (
4 | "log"
5 | urlPkg "net/url"
6 | "strings"
7 |
8 | "github.com/dlclark/regexp2"
9 |
10 | "github.com/mmcdole/gofeed"
11 | "github.com/zhifengle/rss2cloud/request"
12 | )
13 |
14 | type MagnetSite interface {
15 | GetMagnet(item *gofeed.Item) string
16 | GetMagnetItem(item *gofeed.Item) MagnetItem
17 | }
18 |
19 | type MagnetItem struct {
20 | Title string `json:"title"`
21 | Link string `json:"link"`
22 | Magnet string `json:"magnet"`
23 | Description string `json:"description"`
24 | Content string `json:"content"`
25 | }
26 |
27 | func getSite(url string) MagnetSite {
28 | name := url
29 | if strings.HasPrefix(url, "http") {
30 | urlObj, _ := urlPkg.Parse(url)
31 | name = urlObj.Host
32 | }
33 | switch name {
34 | case "mikanani.me", "mikanime.tv":
35 | return &Mikanani{}
36 | case "nyaa.si", "sukebei.nyaa.si":
37 | return &Nyaa{}
38 | case "share.dmhy.org":
39 | return &Dmhy{}
40 | case "share.acgnx.se", "share.acgnx.net", "www.acgnx.se":
41 | return &Acgnx{}
42 | case "rsshub.app":
43 | return &Rsshub{}
44 | default:
45 | log.Printf("[error] not support site: [%s]. rss URL: %s\n", name, url)
46 | return nil
47 | }
48 | }
49 |
50 | func GetFeed(url string) *gofeed.Feed {
51 | res, err := request.Get(url, nil)
52 | if err != nil {
53 | log.Printf("[error] get rss from %s error: %s\n", url, err)
54 | return nil
55 | }
56 | feed, err := gofeed.NewParser().ParseString(res)
57 | if err != nil {
58 | log.Printf("[error] parse rss error: %s\n", err)
59 | return nil
60 | }
61 | return feed
62 | }
63 |
64 | func GetMagnetItemList(config *RssConfig) []MagnetItem {
65 | site := getSite(config.Url)
66 | if site == nil {
67 | return nil
68 | }
69 | feed := GetFeed(config.Url)
70 | if feed == nil {
71 | return nil
72 | }
73 | var itemList []MagnetItem
74 | var re *regexp2.Regexp
75 | if strings.HasPrefix(config.Filter, "/") && strings.HasSuffix(config.Filter, "/") {
76 | re = regexp2.MustCompile(config.Filter[1:len(config.Filter)-1], 0)
77 | }
78 | for _, item := range feed.Items {
79 | flag := true
80 | if config.Filter != "" {
81 | if re != nil {
82 | flag, _ = re.MatchString(item.Title)
83 | } else {
84 | flag = strings.Contains(item.Title, config.Filter)
85 | }
86 | }
87 | if !flag {
88 | continue
89 | }
90 | itemList = append(itemList, site.GetMagnetItem(item))
91 | }
92 | return itemList
93 | }
94 |
95 | func GetMagnetByEnclosure(item *gofeed.Item) string {
96 | if len(item.Enclosures) == 0 {
97 | return ""
98 | }
99 | // find enclosure by type == "application/x-bittorrent" or url has prefix magnet:?
100 | for _, enclosure := range item.Enclosures {
101 | if enclosure.Type == "application/x-bittorrent" || strings.HasPrefix(enclosure.URL, "magnet:?") {
102 | lst := strings.Split(item.Enclosures[0].URL, "&dn=")
103 | if len(lst) != 2 {
104 | return enclosure.URL
105 | }
106 | return lst[0]
107 | }
108 | }
109 | return ""
110 | }
111 |
--------------------------------------------------------------------------------
/rsssite/rsssite_test.go:
--------------------------------------------------------------------------------
1 | package rsssite
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/mmcdole/gofeed"
7 | )
8 |
9 | func TestDmhy(t *testing.T) {
10 | dmhy := &Dmhy{}
11 | fp := gofeed.NewParser()
12 | feed, _ := fp.ParseURL("https://share.dmhy.org/topics/rss/rss.xml")
13 | t.Log(feed)
14 | for _, item := range feed.Items {
15 | t.Log(dmhy.GetMagnetItem(item))
16 | }
17 | }
18 |
19 | func TestAcgnx(t *testing.T) {
20 | acgnx := &Acgnx{}
21 | fp := gofeed.NewParser()
22 | feed, _ := fp.ParseURL("https://share.acgnx.net/rss.xml")
23 | for _, item := range feed.Items[:1] {
24 | t.Log(acgnx.GetMagnetItem(item))
25 | }
26 | }
27 |
28 | func TestGetRssConfigByURL(t *testing.T) {
29 | rssConfig := GetRssConfigByURL("http://share.dmhy.org/topics/rss/rss.xml")
30 | t.Log(rssConfig)
31 | }
32 |
--------------------------------------------------------------------------------
/rsssite/utils.go:
--------------------------------------------------------------------------------
1 | package rsssite
2 |
3 | import (
4 | "bufio"
5 | "os"
6 | "strings"
7 | )
8 |
9 | func HasPrefix(str string, prefixArr []string) bool {
10 | for _, prefix := range prefixArr {
11 | if strings.HasPrefix(str, prefix) {
12 | return true
13 | }
14 | }
15 | return false
16 | }
17 |
18 | func GetMagnetsFromText(textFile string) ([]string, error) {
19 | file, err := os.Open(textFile)
20 | if err != nil {
21 | return nil, err
22 | }
23 | defer file.Close()
24 |
25 | var lines []string
26 | prefixArr := []string{"magnet:", "ed2k://", "https://", "http://", "ftp://"}
27 | scanner := bufio.NewScanner(file)
28 | for scanner.Scan() {
29 | text := scanner.Text()
30 | if HasPrefix(text, prefixArr) {
31 | lines = append(lines, text)
32 | }
33 | }
34 | return lines, scanner.Err()
35 | }
36 |
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "net/http"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | "time"
12 |
13 | "github.com/zhifengle/rss2cloud/p115"
14 | )
15 |
16 | type Server struct {
17 | Agent *p115.Agent
18 | Port int
19 | }
20 |
21 | type OfflineTask struct {
22 | Tasks []string `json:"tasks"`
23 | Cid string `json:"cid"`
24 | }
25 |
26 | var mux = http.NewServeMux()
27 | var srv *http.Server
28 |
29 | func New(agent *p115.Agent, port int) *Server {
30 | return &Server{
31 | Agent: agent,
32 | Port: port,
33 | }
34 | }
35 |
36 | func (s *Server) Start(ctx context.Context) error {
37 | mux.Handle("/add", http.HandlerFunc(s.handleAddTask))
38 | srv = &http.Server{
39 | Addr: fmt.Sprintf(":%d", s.Port),
40 | Handler: mux,
41 | }
42 | fmt.Printf("server started on port %d\n", s.Port)
43 | return srv.ListenAndServe()
44 | }
45 |
46 | func (s *Server) Shutdown(ctx context.Context) error {
47 | ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
48 | defer cancel()
49 |
50 | if err := srv.Shutdown(ctx); err != nil {
51 | return err
52 | }
53 |
54 | // Close database connection
55 | if err := s.Agent.StoreInstance.Close(); err != nil {
56 | fmt.Printf("failed to close database, error: %v\n", err)
57 | }
58 |
59 | fmt.Printf("server stopped properly\n")
60 | return nil
61 | }
62 |
63 | func (s *Server) handleAddTask(w http.ResponseWriter, r *http.Request) {
64 | if r.Method != http.MethodPost {
65 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
66 | return
67 | }
68 |
69 | // Decode the JSON data from the request body
70 | var task OfflineTask
71 | err := json.NewDecoder(r.Body).Decode(&task)
72 | if err != nil {
73 | http.Error(w, err.Error(), http.StatusBadRequest)
74 | return
75 | }
76 | s.Agent.AddMagnetTask(task.Tasks, task.Cid)
77 |
78 | // Send a response back
79 | w.WriteHeader(http.StatusOK)
80 | w.Write([]byte("message success"))
81 | }
82 |
83 | func (s *Server) StartServer() {
84 | ctx, cancel := context.WithCancel(context.Background())
85 |
86 | c := make(chan os.Signal, 1)
87 | // Trigger graceful shutdown on SIGINT or SIGTERM.
88 | // The default signal sent by the `kill` command is SIGTERM,
89 | // which is taken as the graceful shutdown signal for many systems, eg., Kubernetes, Gunicorn.
90 | signal.Notify(c, os.Interrupt, syscall.SIGTERM)
91 | go func() {
92 | sig := <-c
93 | fmt.Printf("%s received.\n", sig.String())
94 | err := s.Shutdown(ctx)
95 | if err != nil {
96 | fmt.Printf("failed to shutdown server, error: %+v\n", err)
97 | }
98 | cancel()
99 | }()
100 |
101 | if err := s.Start(ctx); err != nil {
102 | if err != http.ErrServerClosed {
103 | fmt.Printf("failed to start server, error: %+v\n", err)
104 | cancel()
105 | }
106 | }
107 |
108 | // Wait for CTRL-C.
109 | <-ctx.Done()
110 | }
111 |
--------------------------------------------------------------------------------
/store/store.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "database/sql"
5 | "net/url"
6 | "time"
7 |
8 | _ "github.com/mattn/go-sqlite3"
9 | "github.com/zhifengle/rss2cloud/rsssite"
10 | )
11 |
12 | type Store struct {
13 | DBInstance *sql.DB
14 | }
15 |
16 | func New(db *sql.DB) *Store {
17 | if db == nil {
18 | db, _ = sql.Open("sqlite3", "db.sqlite")
19 | }
20 | db.Exec("CREATE TABLE if not exists `rss_items` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `link` VARCHAR(255), `title` VARCHAR(255), `guid` VARCHAR(255), `pubDate` DATETIME, `creator` VARCHAR(255), `summary` TEXT, `content` VARCHAR(255), `isoDate` DATETIME, `categories` VARCHAR(255), `contentSnippet` VARCHAR(255), `done` TINYINT(1) DEFAULT 0, `magnet` VARCHAR(255) NOT NULL, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL)")
21 | db.Exec("CREATE TABLE if not exists `sites_status` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(255), `needLogin` TINYINT(1), `abnormalOp` TINYINT(1), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL)")
22 | return &Store{
23 | DBInstance: db,
24 | }
25 | }
26 | func (s *Store) SaveMagnetItems(items []rsssite.MagnetItem) error {
27 | now := time.Now()
28 | for _, item := range items {
29 | sql := "INSERT INTO rss_items (`link`,`title`,`content`,`magnet`,`done`,`createdAt`,`updatedAt`) VALUES (?,?,?,?,?,?,?)"
30 | _, err := s.DBInstance.Exec(sql, item.Link, item.Title, item.Content, item.Magnet, 0, now, now)
31 | if err != nil {
32 | return err
33 | }
34 | }
35 | return nil
36 | }
37 |
38 | func (s *Store) HasItem(magnet string) bool {
39 | var count int
40 | s.DBInstance.QueryRow("SELECT count(*) AS num FROM rss_items WHERE magnet = ?", magnet).Scan(&count)
41 | return count > 0
42 | }
43 |
44 | // @TODO 替换 HasItem. 注意目前 magnet 存的长度是 VARCHAR(255)。有tracker的长URI会存不了.
45 | func (s *Store) HasMagnetByXt(magnet string) bool {
46 | var count int
47 | u, err := url.Parse(magnet)
48 | if err != nil {
49 | return false
50 | }
51 | params := u.Query()
52 | xt := params.Get("xt")
53 | s.DBInstance.QueryRow("SELECT count(*) AS num FROM rss_items WHERE magnet LIKE ?", "%"+xt+"%").Scan(&count)
54 | return count > 0
55 | }
56 |
57 | func (s *Store) Close() error {
58 | return s.DBInstance.Close()
59 | }
60 |
--------------------------------------------------------------------------------
/store/store_test.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "database/sql"
5 | "testing"
6 |
7 | _ "github.com/mattn/go-sqlite3"
8 | "github.com/zhifengle/rss2cloud/rsssite"
9 | )
10 |
11 | func TestStore(t *testing.T) {
12 | db, _ := sql.Open("sqlite3", ":memory:")
13 | s := New(db)
14 | err := s.SaveMagnetItems([]rsssite.MagnetItem{
15 | {
16 | Title: "test",
17 | Link: "test",
18 | Magnet: "magnet:?xt=urn:btih:aa",
19 | Description: "test",
20 | Content: "test",
21 | },
22 | {
23 | Title: "test",
24 | Link: "test",
25 | Magnet: "magnet:?xt=urn:btih:uniquehash&dn=test",
26 | Description: "test",
27 | Content: "test",
28 | },
29 | })
30 | if err != nil {
31 | t.Errorf("Error: %v", err)
32 | }
33 | exists := s.HasItem("magnet:?xt=urn:btih:aa")
34 | if !exists {
35 | t.Errorf("Error: %v", err)
36 | }
37 | exists = s.HasMagnetByXt("magnet:?xt=urn:btih:uniquehash&dn=test2")
38 | if !exists {
39 | t.Errorf("Error: %v", err)
40 | }
41 | notExists := s.HasItem("magnet:?xt=urn:btih:bb")
42 | if notExists {
43 | t.Errorf("Error: %v", err)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------