├── .gitattributes
├── .github
└── workflows
│ ├── main.yml
│ └── pyinstall-win.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── console.py
├── console.spec
├── cron.py
├── dockerfile
├── frontend
├── assets
│ ├── index-BsMP4CtQ.css
│ └── index-yGtPj0fc.js
└── index.html
├── job.py
├── lib.py
├── log.py
├── main.py
├── poetry.lock
├── pyproject.toml
├── requirements.txt
├── server.py
└── watch.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: docker images cicd
2 | # 触发器设置
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | # 项目任务,任务之间可以并行调度
10 | jobs:
11 | build:
12 | # 选择云端运行的环境
13 | runs-on: ubuntu-latest
14 | steps:
15 | # uses代表使用一个模块,此处使用的是checkout模块,将github项目文件导入到当前环境中
16 | - uses: actions/checkout@v3
17 | # 使用with跟在后面来为前面的模块输入参数
18 | with:
19 | submodules: 'true'
20 | - name: Set up QEMU
21 | uses: docker/setup-qemu-action@v2
22 | - name: Set up Docker Buildx
23 | uses: docker/setup-buildx-action@v2
24 | - name: Login to DockerHub
25 | uses: docker/login-action@v2
26 | with:
27 | # 这里用到了github的secrets功能,避免账户和密码随仓库泄露
28 | username: ${{ secrets.DOCKER_USERNAME }}
29 | password: ${{ secrets.DOCKER_TOKEN }}
30 | # 开始构建镜像
31 | - name: Build and push
32 | uses: docker/build-push-action@v2
33 | with:
34 | context: .
35 | file: dockerfile
36 | build-args: |
37 | GITHUB_TOKEN=${{ secrets.RELEASE_TOKEN }}
38 | platforms: |
39 | linux/amd64
40 | linux/arm64
41 | push: true
42 | # 指定用户/仓库名
43 | tags: |
44 | ${{ secrets.DOCKER_USERNAME }}/115strm:latest
45 | # 这里是通过md文件自动生成dockerhub描述的模块,也可以不需要
46 | - name: Docker Hub Description
47 | uses: peter-evans/dockerhub-description@v3
48 | with:
49 | username: ${{ secrets.DOCKER_USERNAME }}
50 | password: ${{ secrets.DOCKER_TOKEN }}
51 | repository: ${{ secrets.DOCKER_USERNAME }}/115strm
52 | readme-filepath: ./README.md
--------------------------------------------------------------------------------
/.github/workflows/pyinstall-win.yml:
--------------------------------------------------------------------------------
1 | name: PyInstaller
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | runs-on: ${{ matrix.os }}
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | os: ['windows-latest', 'ubuntu-latest']
15 |
16 | env:
17 | MAIN_PY_FILE: 'console.spec' # Define the path to your main.py file here
18 |
19 | steps:
20 | - name: Checkout code
21 | uses: actions/checkout@v3
22 |
23 | - name: Set up Python
24 | uses: actions/setup-python@v4
25 | with:
26 | python-version: 3.12
27 |
28 | - name: Install Python dependencies
29 | run: |
30 | pip install -r requirements.txt
31 | working-directory: ./
32 |
33 | - name: Install PyInstaller
34 | run: |
35 | pip install pyinstaller
36 | working-directory: ./
37 |
38 | - name: Build executable
39 | run: |
40 | pyinstaller ${{ env.MAIN_PY_FILE }}
41 | working-directory: ./
42 |
43 | - name: Zip the app (Windows)
44 | if: matrix.os == 'windows-latest'
45 | uses: vimtor/action-zip@v1.2
46 | with:
47 | files: dist/q115strm.exe
48 | dest: dist/windows-x86.zip
49 |
50 | - uses: actions/upload-artifact@v4
51 | if: matrix.os == 'windows-latest'
52 | with:
53 | name: windows app
54 | path: dist/windows-x86.zip
55 |
56 | - name: Zip the app (Linux)
57 | if: matrix.os == 'ubuntu-latest'
58 | uses: vimtor/action-zip@v1.2
59 | with:
60 | files: dist/q115strm
61 | dest: dist/linux-x86.zip
62 |
63 | - uses: actions/upload-artifact@v4
64 | if: matrix.os == 'ubuntu-latest'
65 | with:
66 | name: linux app
67 | path: dist/linux-x86.zip
68 |
69 | # - name: create release
70 | # id: create_release
71 | # uses: actions/create-release@v1
72 | # env:
73 | # GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
74 | # with:
75 | # tag_name: ${{ github.run_number }}
76 | # release_name: Release ${{ github.run_number }}
77 | # body: |
78 | # Test Release
79 | # draft: false
80 | # prerelease: false
81 |
82 | # - name: Upload release asset
83 | # id: upload-release-asset
84 | # uses: actions/upload-release-asset@v1
85 | # env:
86 | # GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
87 | # with:
88 | # upload_url: ${{ steps.create_release.outputs.upload_url }}
89 | # asset_path: dist/windowx-x86.zip
90 | # asset_name: windowx-x86.zip
91 | # asset_content_type: application/zip
92 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | /build
3 | /dist
4 | 115-cookies.txt
5 | *.exe
6 | *.log
7 | poetry.lock
8 | /data/config/*
9 | __pycache__
10 | .input
11 | a.json
12 | rmstrm.py
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.REPL.enableREPLSmartSend": false
3 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 作者已经放弃网盘,此项目已归档
2 |
3 | ## 介绍
4 | ##### 基于[p115client](https://github.com/ChenyangGao/p115client)开发,通过生成115目录树来快速完成STRM文件创建,由于只有一次请求所以不会触发风控
5 | ##### 默认用户名密码都是admin
6 |
7 | ## 注意事项
8 | 1. 同一个账号同一时间只能有一个生成目录树的操作,请不要添加多个相同账号的cookie
9 | 1. 115网盘中的目录名不能包含媒体文件扩展名,否则会被识别为文件而不是目录
10 | > 比如战狼电影:Media/Movie/战狼.FLAC.MP4/战狼.FLAC.MP4,这个目录会被识别为两个MP4文件
11 | - Media/Movie/战狼.FLAC.MP4
12 | - Media/Movie/战狼.FLAC.MP4/战狼.FLAC.MP4
13 | > 这是由于115目录树不包含文件元数据,只能通过是否有媒体文件扩展名来确定到底是文件还是目录
14 | 1. 如果文件很多,建议添加多个同步目录,这样处理速度更快
15 | 1. 如果同一账号的多个目录都使用定时同步方式,那么执行时间需要错开,间隔5分钟为佳
16 | - 目录1每天0点0分执行:0 0 * * *
17 | - 目录2每天0点5分执行:5 0 * * *
18 | - 目录3每天0点10分执行:10 0 * * *
19 | 1. 监控变更依赖于CD2的会员功能,请确保使用CD2并且开通了会员
20 | 1. alist302方式要求emby/jellyfin + emby2alist配合,否则无法直接播放
21 | 1. 如果配置电报通知并且服务器在国内,需要配置代理,docke添加环境变量PROXY_HOST=http://ip:port
22 | 1. 如果需要编程触发任务执行,请调用:http://ip:port/api/job/{path},path参数指添加同步目录时的同步路径字段的值
23 |
24 | ## TODO
25 | - [x] STRM生成
26 | - [x] 元数据复制
27 | - [x] 支持源文件不存在时删除目标文件
28 | - [x] 支持CD2本地挂载,STRM内存放媒体文件的本地路径
29 | - [x] 支持WebDAV,STRM内存放WebDAV Url,可供播放器直接播放
30 | - [x] 支持Alist 302,STRM内存放Alist链接(http://ip:port/d/115/xxxxx.mkv) ,配合emby2alist插件,客户端可播放115真实链接节省服务器流量(v0.3.2版本)
31 | - [x] 元数据增加软链接处理方式
32 | - [x] docker支持 + 简单的web ui (v0.2版本)
33 | - [x] docker版本增加监控文件变更,自动生成STRM,CD2 only (v0.2版本)
34 | - [x] docker版本定时同步 (v0.2版本)
35 | - [x] docker版本支持添加多个同步目录,每个同步目录都可以单独设置类型(local,webdav),strm_ext, meta_ext,以及使用不同的115账号(v0.2版本)
36 | - [x] docker版本监控服务使用队列来进行精细化操作,减少对115目录树的生成请求(v0.3版本)
37 | - [x] 可执行文件采用交互式命令行来创建配置文件(v0.3.1版本)
38 | - [x] 支持其他网盘的STRM生成,但是需要本地挂载软件如CD或RClone支持(v0.3.4版本)
39 | - [x] Web UI支持简易HTTP AUTH (v0.4版本)
40 | - [x] 支持发送电报通知 (v0.4版本)
41 |
42 |
43 | ## 一、可执行文件运行:
44 | 1. 下载对应平台的压缩包,并解压
45 | 2. 打开终端切换到项目目录执行命令,比如解压到了D盘q115-strm目录:
46 | ```console
47 | cd D:\q115-strm
48 | // 查看同步目录列表
49 | q115strm.exe list
50 | // 添加115账号
51 | q115strm.exe add115
52 | // 添加同步目录
53 | q115strm.exe create
54 | // 执行全部同步任务
55 | q115strm.exe run
56 | // 执行单个同步任务
57 | q115strm.exe run -k=xxx
58 | ```
59 |
60 | ## 二、DOCKER
61 | ```bash
62 | docker run -d \
63 | --name q115strm \
64 | -e "TZ=Asia/Shanghai" \
65 | -v /vol1/1000/docker/q115strm/data:/app/data \
66 | -v /vol1/1000/docker/clouddrive2/shared/115:/vol1/1000/docker/clouddrive2/shared/115:shared \
67 | -v /vol1/1000/视频/网盘/115:/115 \
68 | -p 12123:12123 \
69 | --restart unless-stopped \
70 | qicfan/115strm:latest
71 | ```
72 |
73 | 或者compose
74 |
75 | ```
76 | services:
77 | 115strm:
78 | image: qicfan/115strm
79 | container_name: q115strm
80 | environment:
81 | - TZ=Asia/Shanghai
82 | ports:
83 | - target: 12123
84 | published: 12123
85 | protocol: tcp
86 | volumes:
87 | - /vol1/1000/docker/q115strm/data:/app/data # 运行日志和数据
88 | - /vol1/1000/docker/clouddrive2/shared/115:/vol1/1000/docker/clouddrive2/shared/115:shared # CD2挂载115的的绝对路径,必须完整映射到容器中,如果使用WebDAV则不需要这个映射
89 | - /vol1/1000/视频/网盘/115:/115 # 存放STRM文件的根目录
90 |
91 | restart: unless-stopped
92 | ```
93 |
94 | #### Docker 配置解释
95 | - `-v /vol1/1000/docker/q115strm/data:/app/data`: 该目录用来存放程序运行的日志和数据,建议映射,后续重装可以直接恢复数据
96 | - `-v /vol1/1000/docker/clouddrive2/shared/115:/vol1/1000/docker/clouddrive2/shared/115:shared`: CD2挂载115的的绝对路径,必须完整映射到容器中,如果使用WebDAV则不需要这个映射。
97 | - `-v /vol1/1000/视频/网盘/115:/115` 存放STRM文件的根目录,必须存在这个映射
98 | - `-p 12123:12123`: 映射12123端口,一个简易的web ui。
99 | - `--restart unless-stopped` 设置容器在退出时自动重启。
100 | - `-e "TZ=Asia/Shanghai"` 时区变量,可以根据所在地设置;会影响记录的任务执行时间,定时执行任务的时间
101 | - `-e "PROXY_HOST=http://192.168.1.1:10808"`
102 |
103 | ## 关键词解释:
104 | - 同步路径:115网盘中的目录,跟alist无关,请到115网盘app或者浏览器中查看实际的目录,多个目录用 / 分隔,比如:Media/电影/华语电影
105 | - AList根文件夹:Alist -> 管理 -> 存储 -> 115网盘 -> 编辑 -> 拉倒最下面找到根文件夹ID
106 | - 如果是0,则该字段留空
107 | - 如果不为0则输入该ID对应的文件夹路径,多个目录用 / 分隔,如:Media/电影
108 | - 115挂载路径:Alist -> 管理 -> 存储 -> 115网盘 -> 编辑 -> 挂载路径
109 | > 挂载路径是什么就填什么,去掉开头和结尾的/(不去也行,程序已经做了处理)
110 | - 元数据选项:如果网盘中存放了字幕文件、封面、nfo文件等,可以通过选择的操作来讲元数据同步到strm跟路径的对应文件夹内
111 |
--------------------------------------------------------------------------------
/console.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import json
3 | import os
4 | import sys
5 |
6 | if not os.path.exists('./data/logs'):
7 | os.makedirs('./data/logs')
8 | if not os.path.exists('./data/config'):
9 | os.makedirs('./data/config')
10 |
11 | from job import StartJob
12 | from lib import OO5, Lib, Libs, OO5List
13 | from rich import print as rprint
14 | from rich.prompt import Prompt, Confirm, FloatPrompt
15 | from rich.console import Console
16 | from rich.table import Table
17 |
18 | LIBS = Libs()
19 | o5List = OO5List()
20 |
21 | def listLib():
22 | libList = LIBS.list()
23 | if len(libList) == 0:
24 | rprint('[bold red]还没有添加任何同步目录[/]')
25 | return
26 | table = Table(title="同步目录列表")
27 |
28 | table.add_column("KEY", justify="left", style="cyan", no_wrap=True)
29 | table.add_column("网盘类型",)
30 | table.add_column("名称", style="magenta")
31 | table.add_column("目录树路径", justify="right", style="green")
32 | table.add_column("方式", justify="right", style="red")
33 | for lib in libList:
34 | table.add_row(lib.key, lib.cloud_type, lib.name, lib.path, lib.type)
35 | console = Console()
36 | console.print(table)
37 |
38 | def run(key: str | None = None):
39 | if key != None:
40 | StartJob(key, logStream=True)
41 | return
42 | # 循环执行所有目录
43 | libs = LIBS.list()
44 | for lib in libs:
45 | StartJob(lib.key, logStream=True)
46 |
47 | def add115():
48 | # 添加115账号
49 | oo5 = {}
50 | oo5['cookie'] = Prompt.ask("[green]cookie[/] 请输入115的cookie,您可以通过其他途径获取")
51 | if oo5['cookie'] == '':
52 | rprint("[bold red]cookie必须输入/]")
53 | return
54 | oo5['name'] = Prompt.ask("[green]name[/] 请输入该cookie的名字,好记就行,如:账号1")
55 | if oo5['name'] == '':
56 | rprint("[bold red]名字必须输入/]")
57 | return
58 | rs, msg = o5List.add(oo5)
59 | if not rs:
60 | rprint("添加失败: [bold red]{0}[/]".format(msg))
61 | return
62 | rprint("115账号{0}已添加".format(oo5['name']))
63 | rprint('如果cookie失效,您可以在[bold]./data/config/115.json[/]文件中修改对应的cookie')
64 | return
65 |
66 | def create():
67 | tmpFile = './.input'
68 | def saveTmp():
69 | with open(tmpFile, mode='w', encoding='utf-8') as f:
70 | json.dump(lib.getJson(), f)
71 | pass
72 | def readTmp():
73 | if not os.path.exists(tmpFile):
74 | return {}
75 | with open(tmpFile, mode='r', encoding='utf-8') as f:
76 | dict = json.load(f)
77 | return dict
78 | isWin = sys.platform.startswith('win')
79 | tmp = readTmp()
80 | if tmp.get('path') is not None:
81 | rprint("已经将上一次输入的值设置为每一项的默认值,[bold]如果没有改动可以直接回车[/],直到未完成的输入项")
82 | lib = Lib(tmp)
83 | # 选择网盘类型
84 | o5s: list[OO5] = o5List.getList()
85 | if len(o5s) == 0:
86 | rprint("[bold red]请先添加115账号,执行:q115strm.exe add115[/]")
87 | return
88 | # 生成选择项
89 | oo5Choices = []
90 | oo5Default = o5s[0].name
91 | for o in o5s:
92 | if lib.id_of_115 != '' and lib.id_of_115 == o.key:
93 | oo5Default = o.name
94 | oo5Choices.append(o.name)
95 | oo5Name = Prompt.ask("[green]id_of_115[/] 请选择要使用的115账号", choices=oo5Choices, default=oo5Default)
96 | for o in o5s:
97 | if oo5Name == o.name:
98 | lib.id_of_115 = o.key
99 | lib.path = Prompt.ask("[green]path[/] 请输入要生成目录树的115路径,如:media/movie", default=lib.path)
100 | if lib.path == '':
101 | rprint("[bold red]路径必须输入[/]")
102 | return
103 | lib.path = lib.path.strip('/')
104 | saveTmp()
105 | lib.name = Prompt.ask("[green]name[/] 请输入该路径的名称,如:电影", default=lib.name if lib.name != '' else "默认目录")
106 | saveTmp()
107 | strm_root_path_example = '/115'
108 | if isWin:
109 | strm_root_path_example = 'F:\\115'
110 | lib.strm_root_path = Prompt.ask("[green]strm_root_path[/] 请输入存放STRM文件的根目录,如:%s" % strm_root_path_example, default=lib.strm_root_path)
111 | if lib.strm_root_path == '':
112 | rprint("[bold red]STRM文件的根目录必须输入/]")
113 | return
114 | lib.strm_root_path = lib.strm_root_path.rstrip(os.sep)
115 | if not os.path.exists(lib.strm_root_path):
116 | mk_strm_root_path = Confirm.ask("[bold red]{0}不存在[/],是否创建该目录?".format(lib.strm_root_path), default=True)
117 | if mk_strm_root_path:
118 | os.makedirs(lib.strm_root_path)
119 | else:
120 | rprint("[bold red]请输入正确的strm根目录[/]")
121 | return
122 | saveTmp()
123 | lib.type = Prompt.ask("[green]type[/] 请选择STRM类型", choices=["本地路径", "WebDAV", "alist302"], default=lib.type)
124 | saveTmp()
125 | lib.mount_path = Prompt.ask("[green]mount_path[/] 如果使用Alist请输入Alist创建存储时输入的根文件夹ID对应的路径", default=lib.mount_path)
126 | lib.mount_path = lib.mount_path.strip('/')
127 | saveTmp()
128 | if lib.type == '本地路径':
129 | lib.path_of_115 = Prompt.ask("[green]path_of_115[/] 请输入挂载115的目录,例如CD2的/CloudNAS/115", default=lib.path_of_115)
130 | if (lib.path_of_115 == ''):
131 | rprint("[bold red]115挂载目录必须输入[/]")
132 | return
133 | if not os.path.exists(lib.path_of_115):
134 | rprint("[bold red]{0}不存在,请检查CD2或其他挂载服务是否正常启动,挂载目录是否输入正确[/]".format(lib.path_of_115))
135 | return
136 | lib.path_of_115 = lib.path_of_115.rstrip(os.sep)
137 | saveTmp()
138 | lib.copy_meta_file= Prompt.ask("[green]copy_meta_file[/] 是否复制元数据?", default=lib.copy_meta_file, choices=["关闭", "复制", "软连接"])
139 | if lib.copy_meta_file == '复制':
140 | lib.copy_delay = FloatPrompt.ask("[green]copy_delay[/] 每个元数据复制的间隔秒数,支持两位小数如:0.01, 默认1秒?", default=float(lib.copy_delay))
141 | saveTmp()
142 | if lib.type == 'WebDAV':
143 | lib.webdav_url = Prompt.ask("[green]webdav_url[/] 请输入webdav服务中的115挂载路径, 格式:http[s]//ip:port/[dav/115]", default=lib.webdav_url)
144 | if (lib.webdav_url == ''):
145 | rprint("[bold red]webdav服务的url必须输入[/]")
146 | return
147 | lib.webdav_url = lib.webdav_url.rstrip('/')
148 | saveTmp()
149 | lib.webdav_username = Prompt.ask("[green]webdav_username[/] 请输入webdav服务的登录用户名,只是用字母和数字不要包含特殊字符", default=lib.webdav_url)
150 | if (lib.webdav_username == ''):
151 | rprint("[bold red]webdav服务的登录用户名必须输入[/]")
152 | return
153 | saveTmp()
154 | lib.webdav_password = Prompt.ask("[green]webdav_password[/] 请输入webdav服务的登录密码,只是用字母和数字不要包含特殊字符", default=lib.webdav_url)
155 | if (lib.webdav_password == ''):
156 | rprint("[bold red]webdav服务的登录密码必须输入[/]")
157 | return
158 | saveTmp()
159 | if lib.type == 'alist302':
160 | lib.alist_server = Prompt.ask("[green]alist_server[/] 请输入alist地址, 格式:http[s]//ip:port", default=lib.alist_server)
161 | if (lib.alist_server == ''):
162 | rprint("[bold red]alist地址l必须输入[/]")
163 | return
164 | lib.alist_server = lib.alist_server.rstrip('/')
165 | saveTmp()
166 | lib.alist_115_path = Prompt.ask("[green]alist_115_path[/] 请输入alist存储中115的挂载路径", default=lib.alist_115_path)
167 | if (lib.alist_115_path == ''):
168 | rprint("[bold red]webdav服务的登录用户名必须输入[/]")
169 | return
170 | lib.alist_115_path = lib.alist_115_path.strip('/')
171 | saveTmp()
172 | strmExtStr = ';'.join(lib.strm_ext)
173 | newStrmExtStr = Prompt.ask("[green]strm_ext[/] 请输入要生成STRM的文件扩展名,分号分隔,可以直接复制默认值来修改", default=strmExtStr)
174 | strmExtList = newStrmExtStr.split(';')
175 | i = 0
176 | for ext in strmExtList:
177 | if not ext.startswith('.'):
178 | strmExtList[i] = ".{0}".format(ext).strip()
179 | i += 1
180 | lib.strm_ext = strmExtList
181 | saveTmp()
182 | if lib.copy_meta_file != '关闭':
183 | metaExtStr = ';'.join(lib.meta_ext)
184 | newMetaExtStr = Prompt.ask("[green]strm_ext[/] 请输入元数据的文件扩展名,分号分隔,可以直接复制默认值来修改", default=metaExtStr)
185 | metaExtList = newMetaExtStr.split(';')
186 | i = 0
187 | for ext in metaExtList:
188 | if not ext.startswith('.'):
189 | metaExtList[i] = ".{0}".format(ext).strip()
190 | i += 1
191 | lib.meta_ext = metaExtList
192 | saveTmp()
193 | lib.makeKey()
194 | rs, msg = LIBS.add(lib.getJson())
195 | if not rs:
196 | rprint("添加失败:[bold red]{0}[/]".format(msg))
197 | return
198 | rprint("已添加同步目录: %s" % lib.key)
199 | rprint("您也可以在 [bold]data/config/libs.json[/] 中手动修改需要的参数")
200 | if isWin:
201 | rprint("稍后可执行 .\\q115strm.exe run -k={0} 执行单个同步任务 或者 .\\q115strm.exe run 执行全部同步任务".format(lib.key))
202 | else:
203 | rprint("稍后可执行 ./q115strm run -k={0} 执行单个同步任务 或者 ./q115strm run 执行全部同步任务".format(lib.key))
204 | os.unlink('.input')
205 |
206 |
207 | if __name__ == '__main__':
208 | action: str | None = None
209 | key: str | None = None
210 | parser = argparse.ArgumentParser(prog='115-STRM', description='将挂载的115网盘目录生成STRM', formatter_class=argparse.RawTextHelpFormatter)
211 | parser.add_argument('action', help='要执行的操作\nlist 列出所有已添加的同步目录\nadd115 添加115账号的cookie \ncreate 添加同步目录\nrun 执行同步任务')
212 | parser.add_argument('-k', '--key', help='要处理的同步目录')
213 | args, unknown = parser.parse_known_args()
214 | if args.action != None:
215 | action = args.action
216 | if args.key != None:
217 | key = args.key
218 | if action == '':
219 | sys.exit(0)
220 | if action == 'list':
221 | listLib()
222 | if action == 'create':
223 | create()
224 | if action == 'run':
225 | run(key)
226 | if action == 'add115':
227 | add115()
--------------------------------------------------------------------------------
/console.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python ; coding: utf-8 -*-
2 |
3 |
4 | a = Analysis(
5 | ['console.py'],
6 | pathex=[],
7 | binaries=[],
8 | datas=[],
9 | hiddenimports=[],
10 | hookspath=[],
11 | hooksconfig={},
12 | runtime_hooks=[],
13 | excludes=[],
14 | noarchive=False,
15 | optimize=0,
16 | )
17 | pyz = PYZ(a.pure)
18 |
19 | exe = EXE(
20 | pyz,
21 | a.scripts,
22 | a.binaries,
23 | a.datas,
24 | [],
25 | name='q115strm',
26 | debug=False,
27 | bootloader_ignore_signals=False,
28 | strip=False,
29 | upx=True,
30 | upx_exclude=[],
31 | runtime_tmpdir=None,
32 | console=True,
33 | disable_windowed_traceback=False,
34 | argv_emulation=False,
35 | target_arch=None,
36 | codesign_identity=None,
37 | entitlements_file=None,
38 | )
--------------------------------------------------------------------------------
/cron.py:
--------------------------------------------------------------------------------
1 |
2 | import hashlib
3 | from multiprocessing import Process
4 | import os
5 | import time
6 | from crontab import CronTab
7 |
8 | from lib import TABFILE, Libs
9 | from log import getLogger
10 |
11 | cronSubProc: Process | None = None
12 | logger = getLogger(name='cron', rotating=True, stream=True)
13 |
14 | def get_file_md5(file_path):
15 | """
16 | 获取文件md5值
17 | :param file_path: 文件路径名
18 | :return: 文件md5值
19 | """
20 | with open(file_path, 'rb') as f:
21 | md5obj = hashlib.md5()
22 | md5obj.update(f.read())
23 | _hash = md5obj.hexdigest()
24 | return str(_hash).upper()
25 |
26 | def startCronSub():
27 | logger.info('启动Crontab守护进程')
28 | tab = CronTab(tabfile=TABFILE)
29 | try:
30 | for result in tab.run_scheduler():
31 | logger.info("Return code: {0}".format(result.returncode))
32 | logger.info("Standard Out: {0}".format(result.stdout))
33 | logger.info("Standard Err: {0}".format(result.stderr))
34 | except:
35 | pass
36 |
37 | def StartCron():
38 | if not os.path.exists(TABFILE):
39 | with open(TABFILE, mode='w', encoding='utf-8') as f:
40 | f.write('')
41 | LIBS = Libs()
42 | # 启动定时任务服务
43 | LIBS.initCron()
44 | logger.info("启动定时任务监控进程")
45 | cronSubProc = Process(target=startCronSub)
46 | cronSubProc.start()
47 | md5 = get_file_md5(TABFILE)
48 | logger.info("记录cron文件的指纹:{0}".format(md5))
49 | while(True):
50 | newmd5 = get_file_md5(TABFILE)
51 | if md5 != newmd5:
52 | logger.info("cron文件有变化,重新加载定时任务{0} : {1}".format(md5, newmd5))
53 | # 有变化,重启进程
54 | cronSubProc.terminate()
55 | cronSubProc = Process(target=startCronSub)
56 | cronSubProc.start()
57 | md5 = newmd5
58 | # else:
59 | # print("cron文件没有变化,等待10秒重试")
60 | try:
61 | # logger.info('已启动所有定时任务,开始10s一次检测任务执行状态')
62 | time.sleep(10)
63 | except:
64 | break
65 |
66 | if __name__ == '__main__':
67 | StartCron()
68 |
--------------------------------------------------------------------------------
/dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.12-slim
2 | EXPOSE 12123
3 | WORKDIR /app
4 |
5 | ENV PATH=/app:$PATH
6 | ENV TZ="Asia/Shanghai"
7 |
8 | # RUN cp /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list.d/debian.sources.bak \
9 | # && sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources
10 | RUN apt update && apt install -y git cron
11 |
12 | COPY . .
13 | RUN chmod -R 0755 /app/frontend/*
14 |
15 | RUN pip install -r requirements.txt
16 |
17 | VOLUME ["/app/data"]
18 |
19 | ENTRYPOINT ["python", "main.py"]
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |