├── .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 | --------------------------------------------------------------------------------