Compare commits

..

41 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 80b0980eac Apply remaining changes 2026-06-30 08:37:46 +00:00
copilot-swe-agent[bot] 75632de2e6 fix(media): return 404 on missing item_id and fix unscraped item query
- Validate GetMediaItemByID result before spawning goroutine in
  StartMediaScrape; return 404 when item_id > 0 but item is not found
- Replace `description` check with `plot` check in GetUnscrappedItems
  since scrapers populate Plot, not Description, to avoid re-selecting
  already-scraped items on every run
2026-06-30 08:35:06 +00:00
Pikachu Ren b85f82c76d Merge branch 'main' into dev-media 2026-06-30 16:32:07 +08:00
pikachuim 3ce3dc4109 feat(media): 刮削修复写入作品名问题 2026-05-15 19:59:05 +08:00
pikachuim 6a46f0e809 feat(media): 刮削支持自定义刮削数量,避免太占性能 2026-05-15 19:34:26 +08:00
pikachuim 609d0a7b84 feat(media): 刮削支持自定义刮削数量,避免太占性能 2026-05-15 19:24:15 +08:00
pikachuim 9a28f7c09c Merge branch 'dev-media' of https://github.com/OpenListTeam/OpenList into dev-media 2026-05-15 18:36:09 +08:00
pikachuim 6646ffe891 feat(media): 提升刮削精准度,支持模糊刮削 2026-05-15 18:32:15 +08:00
j2rong4cn 20d9d39f83 fix(media): clean up imports and improve FetchFileReader logic 2026-05-15 17:12:20 +08:00
pikachuim ccb74025ea feat(media): 提升刮削精准度,支持模糊刮削 2026-05-15 17:05:09 +08:00
pikachuim 94002a2118 Merge branch 'main' into dev-media 2026-05-15 16:31:09 +08:00
pikachuim e750ff502a feat(media): support cloud codec via ffmpeg 2026-05-15 16:26:24 +08:00
pikachuim 97e885fee6 feat(media): support cloud codec via ffmpeg 2026-05-15 16:01:38 +08:00
pikachuim f891d3df1f Merge branch 'dev-media' of https://github.com/OpenListTeam/OpenList into dev-media 2026-05-15 15:48:52 +08:00
pikachuim 6e9878fe61 feat(media): support cloud codec via ffmpeg 2026-05-15 15:48:23 +08:00
pikachuim 08d9e2eb70 Merge branch 'dev-codec' into dev-media
# Conflicts:
#	internal/bootstrap/data/setting.go   resolved by dev-codec version
#	internal/conf/const.go   resolved by dev-codec version
#	internal/model/setting.go   resolved by dev-codec version
#	server/router.go   resolved by dev-codec version
2026-05-15 15:20:09 +08:00
pikachuim 0386f87110 feat(media): support cloud codec via ffmpeg 2026-05-15 15:16:01 +08:00
Pikachu Ren 4c9e63611f Merge branch 'main' into dev-media 2026-05-14 20:36:45 +08:00
pikachuim b00e694f68 fix(media): fix media scan sub path error 2026-05-14 18:25:57 +08:00
pikachuim ae6f05af11 fix(media): fix lrc file display & music id3 2026-05-14 17:07:35 +08:00
pikachuim 8dbcd0fd88 fix(media): fix api proxy path 2026-05-14 16:28:09 +08:00
pikachuim 2182ccff96 feat(media): 清空刮削、删除已失效 2026-05-14 14:27:54 +08:00
pikachuim b5f72ecdcc feat(media): 并发刮削(默认 5)+ 单条落库
增强 TMDB 搜索:模糊匹配 + 多候选关键词
管理页面新增导入/导出媒体库(全部 / 按扫描路径)
缩略图路径默认值改为 imgs
2026-05-14 14:14:58 +08:00
pikachuim 5a27d5fa6d feat(func): media library support change api url 2026-05-14 12:41:52 +08:00
pikachuim ce6bff2ad8 feat(func): media library support change api url 2026-05-14 12:37:07 +08:00
pikachuim cc1fec59ac feat(func): media library support change api url 2026-05-14 12:25:28 +08:00
Pikachu Ren 29f0636833 Merge branch 'main' into dev-media 2026-04-17 14:25:04 +08:00
pikachuim 8a72c78075 feat(func): media library support select 2026-04-08 21:21:14 +08:00
pikachuim 3527cab26b feat(func): media library support select 2026-04-08 20:32:40 +08:00
pikachuim dc92fccf52 feat(func): media library support select 2026-04-07 23:28:55 +08:00
pikachuim e33ff8d9f7 feat(func): media library support select 2026-04-07 23:07:33 +08:00
pikachuim 7571afcd41 Merge branch 'main' into dev-media
# Conflicts:
#	go.sum   resolved by main version
2026-04-07 23:07:07 +08:00
pikachuim 85c7f85b18 Merge branch 'main' into dev-media 2026-04-07 21:26:33 +08:00
pikachuim 9775971eb7 feat(func): media library fix remote fetch error 2026-04-01 19:31:50 +08:00
pikachuim 9096eb60c2 feat(func): media library fix remote fetch error 2026-04-01 19:30:56 +08:00
pikachuim 51c703c0a8 feat(func): media library fix remote fetch error 2026-04-01 19:23:25 +08:00
pikachuim 0e33727675 feat(func): media library build test 2026-03-25 18:07:25 +08:00
pikachuim 879870ef63 feat(func): media library build test 2026-03-25 17:55:07 +08:00
pikachuim bd84607537 feat(func): media library 2026-03-09 20:56:21 +08:00
pikachuim 0f340111da feat(func): media library 2026-03-09 20:52:55 +08:00
pikachuim e776d2ca43 feat(func): media library 2026-03-09 20:43:16 +08:00
59 changed files with 10599 additions and 1530 deletions
+153
View File
@@ -0,0 +1,153 @@
name: Beta Media Release builds
on:
push:
branches: ["dev-media"]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
changelog:
name: Beta Media Release Changelog
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create or update ref
id: create-or-update-ref
uses: ovsds/create-or-update-ref-action@v1
with:
ref: tags/beta-media
sha: ${{ github.sha }}
- name: Delete beta-media tag
run: git tag -d beta-media
continue-on-error: true
- name: changelog
id: changelog
run: |
git tag -l
npx changelogithub --output CHANGELOG.md
- name: Upload assets to beta-media release
uses: softprops/action-gh-release@v2
with:
body_path: CHANGELOG.md
files: CHANGELOG.md
prerelease: true
tag_name: beta-media
- name: Upload assets to github artifact
uses: actions/upload-artifact@v4
with:
name: beta-media changelog
path: ${{ github.workspace }}/CHANGELOG.md
compression-level: 0
if-no-files-found: error
release:
needs:
- changelog
strategy:
matrix:
include:
- target: "!(*musl*|*windows-arm64*|*windows7-*|*android*|*freebsd*)" # xgo and loongarch
hash: "md5"
- target: "linux-!(arm*)-musl*" #musl-not-arm
hash: "md5-linux-musl"
- target: "linux-arm*-musl*" #musl-arm
hash: "md5-linux-musl-arm"
- target: "windows-arm64" #win-arm64
hash: "md5-windows-arm64"
- target: "windows7-*" #win7
hash: "md5-windows7"
- target: "android-*" #android
hash: "md5-android"
- target: "freebsd-*" #freebsd
hash: "md5-freebsd"
name: Beta Media Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.25.0"
- name: Setup web
run: |
frontendRepo="${FRONTEND_REPO:-OpenListTeam/OpenList-Frontend}"
release_json=$(curl -fsSL --max-time 10 \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/$frontendRepo/releases/tags/beta-media")
tar_url=$(echo "$release_json" | jq -r '.assets[].browser_download_url' | grep "openlist-frontend-dist" | grep -v "lite" | grep "\.tar\.gz$")
echo "Downloading frontend from: $tar_url"
curl -fsSL "$tar_url" -o dist.tar.gz
rm -rf public/dist && mkdir -p public/dist
tar -zxvf dist.tar.gz -C public/dist
rm -rf dist.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FRONTEND_REPO: ${{ vars.FRONTEND_REPO }}
- name: Build
uses: OpenListTeam/cgo-actions@v1.2.2
with:
targets: ${{ matrix.target }}
musl-target-format: $os-$musl-$arch
github-token: ${{ secrets.GITHUB_TOKEN }}
out-dir: build
output: openlist-$target$ext
musl-base-url: "https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
x-flags: |
github.com/OpenListTeam/OpenList/v4/internal/conf.BuiltAt=$built_at
github.com/OpenListTeam/OpenList/v4/internal/conf.GitAuthor=The OpenList Projects Contributors <noreply@openlist.team>
github.com/OpenListTeam/OpenList/v4/internal/conf.GitCommit=$git_commit
github.com/OpenListTeam/OpenList/v4/internal/conf.Version=$tag
github.com/OpenListTeam/OpenList/v4/internal/conf.WebVersion=rolling
- name: Compress
run: |
bash build.sh zip ${{ matrix.hash }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets to beta-media release
uses: softprops/action-gh-release@v2
with:
files: build/compress/*
prerelease: true
tag_name: beta-media
- name: Clean illegal characters from matrix.target
id: clean_target_name
run: |
ILLEGAL_CHARS_REGEX='[":<>|*?\\/\r\n]'
CLEANED_TARGET=$(echo "${{ matrix.target }}" | sed -E "s/$ILLEGAL_CHARS_REGEX//g")
echo "Original target: ${{ matrix.target }}"
echo "Cleaned target: $CLEANED_TARGET"
echo "cleaned_target=$CLEANED_TARGET" >> $GITHUB_ENV
- name: Upload assets to github artifact
uses: actions/upload-artifact@v4
with:
name: beta-media builds for ${{ env.cleaned_target }}
path: ${{ github.workspace }}/build/compress/*
compression-level: 0
if-no-files-found: error
+1 -1
View File
@@ -115,7 +115,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: "1.26.4"
go-version: "1.25.0"
- name: Setup web
run: bash build.sh dev web
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: "1.26.4"
go-version: "1.25.0"
- name: Setup web
run: bash build.sh dev web
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
if: github.event_name == 'issues'
steps:
- name: Check issue for unchecked tasks and reply
uses: actions/github-script@v9
uses: actions/github-script@v7
with:
script: |
let comment = "";
@@ -81,7 +81,7 @@ jobs:
if: github.event_name == 'pull_request'
steps:
- name: Check PR title for required prefix and comment
uses: actions/github-script@v9
uses: actions/github-script@v7
with:
script: |
const title = context.payload.pull_request.title || "";
+2 -3
View File
@@ -46,13 +46,12 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: '1.26.4'
go-version: '1.25.0'
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
fetch-tags: true
fetch-depth: 0
- name: Install dependencies
if: matrix.target-platform == ''
+2 -2
View File
@@ -47,7 +47,7 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: '1.26'
go-version: '1.25.0'
- name: Cache Musl
id: cache-musl
@@ -87,7 +87,7 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: '1.26.4'
go-version: '1.25.0'
- name: Cache Musl
id: cache-musl
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: '1.26.4'
go-version: '1.25.0'
- name: Cache Musl
id: cache-musl
+13 -26
View File
@@ -16,33 +16,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Trigger Makefile hash update
uses: actions/github-script@v9
uses: peter-evans/repository-dispatch@v3
with:
github-token: ${{ secrets.EXTERNAL_REPO_TOKEN_LUCI_APP_OPENLIST }}
script: |
const repository = process.env.REPOSITORY;
const [owner, repo] = repository.split('/');
await github.rest.repos.createDispatchEvent({
owner: owner,
repo: repo,
event_type: 'update-hashes',
client_payload: {
source_repository: process.env.SOURCE_REPOSITORY,
release_tag: process.env.RELEASE_TAG,
release_name: process.env.RELEASE_TAG,
release_url: process.env.RELEASE_URL,
triggered_by: process.env.ACTOR,
trigger_reason: process.env.EVENT_NAME
}
});
env:
REPOSITORY: ${{ vars.HOOK_REPO || 'OpenListTeam/OpenList-OpenWRT' }}
SOURCE_REPOSITORY: ${{ github.repository }}
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
RELEASE_URL: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.tag || github.ref_name }}
ACTOR: ${{ github.actor }}
EVENT_NAME: ${{ github.event_name }}
token: ${{ secrets.EXTERNAL_REPO_TOKEN_LUCI_APP_OPENLIST }}
repository: ${{ vars.HOOK_REPO || 'OpenListTeam/OpenList-OpenWRT' }}
event-type: update-hashes
client-payload: |
{
"source_repository": "${{ github.repository }}",
"release_tag": "${{ inputs.tag || github.ref_name }}",
"release_name": "${{ inputs.tag || github.ref_name }}",
"release_url": "${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.tag || github.ref_name }}",
"triggered_by": "${{ github.actor }}",
"trigger_reason": "${{ github.event_name }}"
}
- name: Log trigger information
run: |
+9 -3
View File
@@ -178,6 +178,7 @@ BuildWin7() {
}
BuildDev() {
rm -rf .git/
mkdir -p "dist"
muslflags="$(GetMuslStaticLdflags)"
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
@@ -268,6 +269,7 @@ BuildDockerMultiplatform() {
}
BuildRelease() {
rm -rf .git/
mkdir -p "build"
BuildWinArm64 ./build/"$appName"-windows-arm64.exe
BuildWin7 ./build/"$appName"-windows7
@@ -287,7 +289,7 @@ BuildRelease() {
BuildLoongGLIBC() {
local target_abi="$2"
local output_file="$1"
local oldWorldGoVersion="1.26.4"
local oldWorldGoVersion="1.25.0"
local loong_tags
loong_tags=$(GetBuildTagsForTarget "linux-loong64")
@@ -307,13 +309,13 @@ BuildLoongGLIBC() {
# Download and setup patched Go compiler for old-world
if ! curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://github.com/loong64-abi1-0/golang/releases/download/go${oldWorldGoVersion}/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250821/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
-o go-loong64-abi1.0.tar.gz; then
echo "Error: Failed to download patched Go compiler for old-world ABI1.0"
if [ -n "$GITHUB_TOKEN" ]; then
echo "Error output from curl:"
curl -fsSL --retry 3 -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://github.com/loong64-abi1-0/golang/releases/download/go${oldWorldGoVersion}/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
"https://github.com/loong64/loong64-abi1.0-toolchains/releases/download/20250821/go${oldWorldGoVersion}.linux-amd64.tar.gz" \
-o go-loong64-abi1.0.tar.gz || true
fi
return 1
@@ -440,6 +442,7 @@ BuildLoongGLIBC() {
}
BuildReleaseLinuxMusl() {
rm -rf .git/
mkdir -p "build"
muslflags="$(GetMuslStaticLdflags)"
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
@@ -468,6 +471,7 @@ BuildReleaseLinuxMusl() {
}
BuildReleaseLinuxMuslArm() {
rm -rf .git/
mkdir -p "build"
muslflags="$(GetMuslStaticLdflags)"
BASE="https://github.com/OpenListTeam/musl-compilers/releases/latest/download/"
@@ -498,6 +502,7 @@ BuildReleaseLinuxMuslArm() {
BuildReleaseAndroid() {
rm -rf .git/
mkdir -p "build"
wget https://dl.google.com/android/repository/android-ndk-r26b-linux.zip
unzip android-ndk-r26b-linux.zip
@@ -518,6 +523,7 @@ BuildReleaseAndroid() {
}
BuildReleaseFreeBSD() {
rm -rf .git/
mkdir -p "build/freebsd"
# Get latest FreeBSD 14.x release version from GitHub
+565
View File
File diff suppressed because it is too large Load Diff
+167
View File
@@ -0,0 +1,167 @@
# OpenList 转码协议 v1(自研)
> 用于 OpenList Master 与远程 FFmpeg 转码 Worker 之间的通信。
> 协议本身、所有依赖、推荐编码 (H.264/AV1) 均为开源免费,可商用。
## 总览
```
Player ──► Master(/api/transcode/play) ──► Scheduler.Submit
Worker ──► Master(/api/transcode/worker/register) # 启动一次
Worker ──► Master(/api/transcode/worker/heartbeat) # 周期 10s
Worker ──► Master(/api/transcode/worker/claim) # 长轮询拉任务
Worker ──► Master(/api/transcode/worker/segment) # 流式回推切片
Worker ──► Master(/api/transcode/worker/job/finish) # 上报完成
Player ──► Master(/tc/{job}/{token}/master.m3u8) # 拉取播放列表
Player ──► Master(/tc/{job}/{token}/{profile}/seg-N.ts) # 拉取切片
```
## 通用约定
- 所有请求体均为 JSON。
- Worker 与 Master 间通过两类凭据鉴权:
- **shared_secret**(管理端 `transcode_worker_secret` 设置项):用于 register / heartbeat / claim
- **callback_token**(Master 在 job 中下发,一次性):用于 segment / job/finish
- Header 形式:`Authorization: Bearer <token>`
- 返回包统一使用 OpenList 通用响应:`{"code":200,"message":"success","data":{...}}`
## 端点详细
### POST /api/transcode/worker/register
> 鉴权:Bearer = shared_secret
请求:
```json
{
"name": "gpu-node-01",
"version": "1.0.0",
"capacity": 4,
"hwaccel": ["nvenc"],
"codecs_decode": ["h264","hevc","av1","vp9"],
"codecs_encode": ["h264","hevc"],
"max_resolution": "3840x2160",
"tags": ["linux","amd64"]
}
```
响应:
```json
{
"code":200,"message":"success",
"data": {
"worker_id":"wk_xxx",
"heartbeat_interval":10,
"claim_strategy":"pull",
"protocol_version":"v1"
}
}
```
### POST /api/transcode/worker/heartbeat
> 鉴权:Bearer = shared_secret
> 间隔:register 返回的 `heartbeat_interval`;服务端 30s 未收到则剔除 worker
请求:
```json
{ "worker_id":"wk_xxx", "load":0.4, "running":["job_a"], "free_slots":3 }
```
响应:
```json
{ "code":200,"message":"success","data":{"ok":true,"kick":false,"cancel":["job_x"]}}
```
- `kick=true`:服务端要求 Worker 主动退出
- `cancel`:需要 Worker 立刻终止的 job_ids(异步取消通道)
### POST /api/transcode/worker/claim
> 鉴权:Bearer = shared_secret
> 长轮询:当无任务时,服务端最多挂起 `wait` 秒(最大 30)
请求:
```json
{ "worker_id":"wk_xxx", "slots":1, "wait":20 }
```
响应:
```json
{ "code":200,"message":"success","data":{"jobs":[{
"id":"job_xxx",
"path":"/movies/x.mkv",
"source_url":"https://example.com/d/movies/x.mkv?sign=...",
"profiles":[{
"name":"1080p","video_codec":"h264","video_bitrate":"4000k",
"scale":"1920:-2","audio_codec":"aac","audio_bitrate":"160k",
"hwaccel":"nvenc"
}],
"output":{"format":"hls","segment_duration":6},
"callback_token":"tk_xxx",
"deadline":1731601234
}]}}
```
### PUT /api/transcode/worker/segment
> 鉴权:Bearer = callback_token
> Query 参数:
> - `job` 任务 ID
> - `profile` profile 名(如 `1080p`)
> - `seq` 切片序号(从 0 开始)
> - `duration` 该切片秒数
> - `final` 是否为最后一片(true/false)
>
> Body:原始 MPEG-TS 二进制(`Content-Type: video/mp2t`)
响应:`204 No Content`
### POST /api/transcode/worker/job/finish
> 鉴权:Bearer = callback_token
请求:
```json
{
"job_id":"job_xxx",
"status":"finished",
"error":"",
"stats":{"elapsed":312.4,"fps":120,"speed":2.1,"bytes_out":890123456}
}
```
status 取值:`finished` / `failed` / `cancelled`
## 播放端
### POST /api/fs/transcode/play
> 由前端播放器调用,用户登录态可见
> 请求:`{"path":"/movies/x.mkv"}`
>
> 响应(无需转码):
> `{"transcode":false,"reason":"size below threshold"}`
>
> 响应(已下发转码任务):
> `{"transcode":true,"job_id":"job_xx","master_url":"https://.../tc/.../master.m3u8","profile":"1080p"}`
### GET /tc/:job/:token/master.m3u8
返回多档位主 m3u8。
### GET /tc/:job/:token/:profile/playlist.m3u8
阻塞等待首切片就绪(最多 30s)后返回 HLS 播放列表,未结束时不带 `#EXT-X-ENDLIST`。
### GET /tc/:job/:token/:profile/seg-N.ts
返回 N 号切片;如果尚未生成,最多等待 60s。
## 启动远程 Worker
```bash
# 用 Master 配置中的 transcode_worker_secret 作为密钥
go build -o openlist-worker ./cmd/worker
./openlist-worker \
-master http://openlist.example.com:5244 \
-secret <transcode_worker_secret> \
-capacity 2 \
-hwaccel nvenc \
-ffmpeg /usr/bin/ffmpeg \
-workdir /tmp/openlist-worker
```
## License 说明
- 协议:自研,可自由使用
- FFmpeg:使用系统包提供的 LGPL 版本即可(含 libx264/libx265,libx264 是 GPL,建议在 Worker 镜像中遵循 GPL 公开/动态链接合规)
- 推荐编码:H.264 互联网分发免授权 / AV1 永久免版税
+1 -30
View File
@@ -262,16 +262,6 @@ func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.
if err = y.WaitBatchTask("MOVE", resp.TaskID, time.Millisecond*400); err != nil {
return nil, err
}
// 跟随移动 torrent 文件
if !srcObj.IsDir() {
var srcFolderId string
if f, ok := srcObj.(*Cloud189File); ok {
srcFolderId = f.ParentID
}
y.torrentFollowMove(srcFolderId, srcObj.GetName(), dstDir)
}
return srcObj, nil
}
@@ -308,12 +298,6 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
}
return nil, err
}
// 跟随重命名 torrent 文件
if f, ok := srcObj.(*Cloud189File); ok {
y.torrentFollowRename(f.ParentID, srcObj.GetName(), newName)
}
switch f := srcObj.(type) {
case *Cloud189File:
return resp.toFile(f), nil
@@ -335,20 +319,7 @@ func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
if err != nil {
return err
}
if err = y.WaitBatchTask("COPY", resp.TaskID, time.Second); err != nil {
return err
}
// 跟随复制 torrent 文件
if !srcObj.IsDir() {
var srcFolderId string
if f, ok := srcObj.(*Cloud189File); ok {
srcFolderId = f.ParentID
}
y.torrentFollowCopy(srcFolderId, srcObj.GetName(), dstDir)
}
return nil
return y.WaitBatchTask("COPY", resp.TaskID, time.Second)
}
func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
-158
View File
@@ -1,7 +1,6 @@
package _189pc
import (
"bytes"
"context"
"crypto/sha1"
"encoding/hex"
@@ -9,13 +8,10 @@ import (
"io"
"net/url"
"strings"
"time"
"github.com/go-resty/resty/v2"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/torrent"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
)
@@ -166,36 +162,6 @@ func (y *Cloud189PC) RapidUploadFromTorrent(ctx context.Context, dstDir model.Ob
return nil, fmt.Errorf("提交上传失败: %w", err)
}
// 秒传成功后,将 torrent 文件上传到目标目录(异步,不影响秒传结果)
if y.Addition.GenerateTorrent {
capturedDstDir := dstDir
capturedIsFamily := isFamily
go func() {
torrentName := fileName + ".cas.torrent"
infoHash, _ := GetInfoHashHex(torrentData)
utils.Log.Infof("秒传成功,上传 torrent: %s (info_hash: %s, size: %d bytes)",
torrentName, infoHash, len(torrentData))
torrentFileStream := &stream.FileStream{
Ctx: context.Background(),
Obj: &model.Object{
Name: torrentName,
Size: int64(len(torrentData)),
IsFolder: false,
},
Reader: bytes.NewReader(torrentData),
Mimetype: "application/x-bittorrent",
}
_, uploadErr := y.fastUpload(context.Background(), capturedDstDir, torrentFileStream, func(p float64) {}, capturedIsFamily, false, false)
if uploadErr != nil {
utils.Log.Warnf("上传 torrent 文件失败: %v", uploadErr)
} else {
utils.Log.Infof("torrent 文件已上传: %s", torrentName)
op.Cache.DeleteDirectory(y, capturedDstDir.GetPath())
}
}()
}
return resp.toFile(), nil
}
@@ -292,10 +258,6 @@ func GetInfoHashHex(torrentData []byte) (string, error) {
return hex.EncodeToString(t.InfoHash), nil
}
func isCASTorrentFile(fileName string) bool {
return strings.HasSuffix(fileName, ".cas.torrent")
}
// ComputeSliceMD5sFromReader 从 reader 中计算每个 10MB 分片的 MD5
// 返回:整文件 MD5、分片 MD5 列表
func ComputeSliceMD5sFromReader(reader io.Reader, sliceSize int64) (string, []string, error) {
@@ -327,123 +289,3 @@ func ComputeSliceMD5sFromReader(reader io.Reader, sliceSize int64) (string, []st
fileMD5Hex := strings.ToUpper(hex.EncodeToString(fileMD5Hash.Sum(nil)))
return fileMD5Hex, sliceMD5s, nil
}
// torrentFollowCopy 跟随复制 torrent 文件(异步,不影响主操作)
// srcFolderId: 源文件所在目录 ID
// srcFileName: 源文件名
// dstDir: 目标目录
func (y *Cloud189PC) torrentFollowCopy(srcFolderId string, srcFileName string, dstDir model.Obj) {
if !y.Addition.GenerateTorrent {
return
}
if srcFolderId == "" {
return
}
torrentName := srcFileName + ".cas.torrent"
isFamily := y.isFamily()
go func() {
torrentFile, err := y.findFileByName(context.Background(), torrentName, srcFolderId, isFamily)
if err != nil {
utils.Log.Debugf("查找 torrent 文件失败(跟随复制): %v", err)
return
}
// 复制 torrent 文件到目标目录
resp, copyErr := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(),
map[string]string{"targetFileName": dstDir.GetName()},
BatchTaskInfo{
FileId: torrentFile.GetID(),
FileName: torrentFile.GetName(),
IsFolder: 0,
})
if copyErr != nil {
utils.Log.Warnf("跟随复制 torrent 文件失败: %v", copyErr)
return
}
if err = y.WaitBatchTask("COPY", resp.TaskID, time.Second); err != nil {
utils.Log.Warnf("等待跟随复制 torrent 文件失败: %v", err)
}
}()
}
// torrentFollowMove 跟随移动 torrent 文件(异步,不影响主操作)
// srcFolderId: 源文件所在目录 ID
// srcFileName: 源文件名
// dstDir: 目标目录
func (y *Cloud189PC) torrentFollowMove(srcFolderId string, srcFileName string, dstDir model.Obj) {
if !y.Addition.GenerateTorrent {
return
}
if srcFolderId == "" {
return
}
torrentName := srcFileName + ".cas.torrent"
isFamily := y.isFamily()
go func() {
torrentFile, err := y.findFileByName(context.Background(), torrentName, srcFolderId, isFamily)
if err != nil {
utils.Log.Debugf("查找 torrent 文件失败(跟随移动): %v", err)
return
}
// 移动 torrent 文件到目标目录
resp, moveErr := y.CreateBatchTask("MOVE", IF(isFamily, y.FamilyID, ""), dstDir.GetID(),
map[string]string{"targetFileName": dstDir.GetName()},
BatchTaskInfo{
FileId: torrentFile.GetID(),
FileName: torrentFile.GetName(),
IsFolder: 0,
})
if moveErr != nil {
utils.Log.Warnf("跟随移动 torrent 文件失败: %v", moveErr)
return
}
if err = y.WaitBatchTask("MOVE", resp.TaskID, time.Millisecond*400); err != nil {
utils.Log.Warnf("等待跟随移动 torrent 文件失败: %v", err)
}
}()
}
// torrentFollowRename 跟随重命名 torrent 文件(异步,不影响主操作)
// folderId: 文件所在目录 ID
// oldFileName: 原文件名
// newFileName: 新文件名
func (y *Cloud189PC) torrentFollowRename(folderId string, oldFileName string, newFileName string) {
if !y.Addition.GenerateTorrent {
return
}
if folderId == "" {
return
}
oldTorrentName := oldFileName + ".cas.torrent"
newTorrentName := newFileName + ".cas.torrent"
isFamily := y.isFamily()
go func() {
torrentFile, err := y.findFileByName(context.Background(), oldTorrentName, folderId, isFamily)
if err != nil {
utils.Log.Debugf("查找 torrent 文件失败(跟随重命名): %v", err)
return
}
// 重命名 torrent 文件
queryParam := make(map[string]string)
fullUrl := API_URL
method := "POST"
if isFamily {
fullUrl += "/family/file"
method = "GET"
queryParam["familyId"] = y.FamilyID
}
fullUrl += "/renameFile.action"
queryParam["fileId"] = torrentFile.GetID()
queryParam["destFileName"] = newTorrentName
_, renameErr := y.request(fullUrl, method, func(req *resty.Request) {
req.SetContext(context.Background()).SetQueryParams(queryParam)
}, nil, &RenameResp{}, isFamily)
if renameErr != nil {
utils.Log.Warnf("跟随重命名 torrent 文件失败: %v", renameErr)
}
}()
}
+4 -5
View File
@@ -166,11 +166,10 @@ type FamilyInfoResp struct {
/*文件部分*/
// 文件
type Cloud189File struct {
ID String `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
Md5 string `json:"md5"`
ParentID string `json:"-"` // 由 getFiles 设置,不从 JSON 解析
ID String `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
Md5 string `json:"md5"`
LastOpTime Time `json:"lastOpTime"`
CreateDate Time `json:"createDate"`
+1 -63
View File
@@ -210,7 +210,6 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool)
res = append(res, &resp.FileListAO.FolderList[i])
}
for i := 0; i < len(resp.FileListAO.FileList); i++ {
resp.FileListAO.FileList[i].ParentID = fileId
res = append(res, &resp.FileListAO.FileList[i])
}
}
@@ -911,11 +910,6 @@ func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream m
// 快传
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
generateTorrent := y.Addition.GenerateTorrent && !isCASTorrentFile(file.GetName())
return y.fastUpload(ctx, dstDir, file, up, isFamily, overwrite, generateTorrent)
}
func (y *Cloud189PC) fastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool, generateTorrent bool) (model.Obj, error) {
var (
cache = file.GetFile()
tmpF *os.File
@@ -953,9 +947,6 @@ func (y *Cloud189PC) fastUpload(ctx context.Context, dstDir model.Obj, file mode
if tmpF != nil {
writers = append(writers, tmpF)
}
pieceSHA1Hashes := make([]byte, 0, count*20)
written := int64(0)
for i := 1; i <= count; i++ {
if utils.IsCanceled(ctx) {
@@ -966,17 +957,7 @@ func (y *Cloud189PC) fastUpload(ctx context.Context, dstDir model.Obj, file mode
byteSize = lastSliceSize
}
// 如果需要生成 torrent,同时计算 SHA-1
var sha1Writer hash.Hash
var multiWriter io.Writer
if generateTorrent {
sha1Writer = sha1Pkg.New()
multiWriter = io.MultiWriter(append(writers, sha1Writer)...)
} else {
multiWriter = io.MultiWriter(writers...)
}
n, err := utils.CopyWithBufferN(multiWriter, file, byteSize)
n, err := utils.CopyWithBufferN(io.MultiWriter(writers...), file, byteSize)
written += n
if err != nil && err != io.EOF {
return nil, err
@@ -985,11 +966,6 @@ func (y *Cloud189PC) fastUpload(ctx context.Context, dstDir model.Obj, file mode
sliceMd5Hexs = append(sliceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte)))
partInfos = append(partInfos, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte)))
sliceMd5.Reset()
// 收集 SHA-1 piece hash(仅在本次分片实际写入了数据时追加)
if generateTorrent && n > 0 {
pieceSHA1Hashes = append(pieceSHA1Hashes, sha1Writer.Sum(nil)...)
}
}
if tmpF != nil {
@@ -1105,44 +1081,6 @@ func (y *Cloud189PC) fastUpload(ctx context.Context, dstDir model.Obj, file mode
if err != nil {
return nil, err
}
// 生成 torrent 文件(异步,不影响上传结果)
if generateTorrent && size > 0 && len(pieceSHA1Hashes) > 0 {
capturedDstDir := dstDir
capturedIsFamily := isFamily
capturedFileName := file.GetName()
go func() {
torrentData, err := GenerateTorrent(capturedFileName, size, fileMd5Hex, sliceMd5Hexs, sliceSize, pieceSHA1Hashes)
if err != nil {
utils.Log.Warnf("生成 torrent 失败: %v", err)
return
}
infoHash, _ := GetInfoHashHex(torrentData)
torrentName := capturedFileName + ".cas.torrent"
utils.Log.Infof("已生成 torrent: %s (info_hash: %s, size: %d bytes)",
torrentName, infoHash, len(torrentData))
// 将 torrent 文件上传到同一目录
torrentFileStream := &stream.FileStream{
Ctx: context.Background(),
Obj: &model.Object{
Name: torrentName,
Size: int64(len(torrentData)),
IsFolder: false,
},
Reader: bytes.NewReader(torrentData),
Mimetype: "application/x-bittorrent",
}
_, uploadErr := y.fastUpload(context.Background(), capturedDstDir, torrentFileStream, func(p float64) {}, capturedIsFamily, false, false)
if uploadErr != nil {
utils.Log.Warnf("上传 torrent 文件失败: %v", uploadErr)
} else {
utils.Log.Infof("torrent 文件已上传: %s", torrentName)
op.Cache.DeleteDirectory(y, capturedDstDir.GetPath())
}
}()
}
return resp.toFile(), nil
}
+3
View File
@@ -46,6 +46,9 @@ func (d *Wps) Init(ctx context.Context) error {
if !resp.IsSuccess() {
return fmt.Errorf("failed to check login status, status code: %d, body: %s", resp.StatusCode(), resp.String())
}
if d.login.CompanyID == 0 {
return fmt.Errorf("wps company id is empty, please check business account login")
}
return nil
}
+52 -56
View File
File diff suppressed because it is too large Load Diff
+106 -114
View File
File diff suppressed because it is too large Load Diff
+73
View File
@@ -249,6 +249,79 @@ func InitialSettings() []model.SettingItem {
{Key: conf.StreamMaxClientUploadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
{Key: conf.StreamMaxServerDownloadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
{Key: conf.StreamMaxServerUploadSpeed, Value: "-1", Type: conf.TypeNumber, Group: model.TRAFFIC, Flag: model.PRIVATE},
// media settings
{Key: conf.MediaTMDBKey, Value: "", Type: conf.TypeString, Group: model.MEDIA, Flag: model.PRIVATE},
{Key: conf.MediaTMDBAPIURL, Value: "api.themoviedb.org", Type: conf.TypeString, Group: model.MEDIA, Flag: model.PRIVATE},
{Key: conf.MediaDiscogsToken, Value: "", Type: conf.TypeString, Group: model.MEDIA, Flag: model.PRIVATE},
{Key: conf.MediaDiscogsAPIURL, Value: "api.discogs.com", Type: conf.TypeString, Group: model.MEDIA, Flag: model.PRIVATE},
{Key: conf.MediaStoreThumbnail, Value: "false", Type: conf.TypeBool, Group: model.MEDIA, Flag: model.PRIVATE},
{Key: conf.MediaThumbnailMode, Value: "base64", Type: conf.TypeSelect, Options: "base64,local", Group: model.MEDIA, Flag: model.PRIVATE},
{Key: conf.MediaThumbnailPath, Value: "/imgs", Type: conf.TypeString, Group: model.MEDIA, Flag: model.PRIVATE},
{Key: conf.MediaScrapeConcurrency, Value: "5", Type: conf.TypeNumber, Group: model.MEDIA, Flag: model.PRIVATE},
// transcode settings (FFmpeg 云端/本地转码) - 默认全部关闭
{Key: conf.TranscodeEnabled, Value: "false", Type: conf.TypeBool, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `开启后,超过阈值的媒体文件将通过 FFmpeg 转码后再播放;默认关闭`},
{Key: conf.TranscodeRunMode, Value: "local", Type: conf.TypeSelect, Options: "local,remote,hybrid", Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `local=仅使用本机内置 worker;remote=只使用远程 Worker 节点;hybrid=本地优先,超载后派发到远程`},
{Key: conf.TranscodeMinSizeGB, Value: "5", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `文件大于该 GB 数才走转码(小于则直链播放),0=任意大小都转码`},
{Key: conf.TranscodeMinBitrateMbps, Value: "20", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `视频码率超过该 Mbps 才转码,0=不限制`},
{Key: conf.TranscodeSourceCodecs, Value: "hevc,h265,av1,vvc,vp9", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `仅对这些源视频编码进行转码(逗号分隔)。常见高码率/兼容性差的编码:hevc,av1,vvc,vp9`},
{Key: conf.TranscodeSourceExtensions, Value: "mkv,ts,m2ts,mov,avi,wmv,flv,rmvb,webm", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `仅对这些后缀进行转码(逗号分隔,不带点),mp4 默认不转码可直接播放`},
{Key: conf.TranscodeOutputFormat, Value: "hls", Type: conf.TypeSelect, Options: "hls,dash,mp4", Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `输出封装格式,HLS 兼容性最好`},
{Key: conf.TranscodeOutputCodec, Value: "h264", Type: conf.TypeSelect, Options: "h264,hevc,av1", Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `重新编码后的视频编码,推荐 h264(最广兼容、互联网分发免授权费)`},
{Key: conf.TranscodeOutputBitrate, Value: "4000k", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `输出视频码率,例如 4000k / 6M。建议 1080p:4000k、720p:2500k`},
{Key: conf.TranscodeOutputAudioCodec, Value: "aac", Type: conf.TypeSelect, Options: "aac,mp3,opus,copy", Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `输出音频编码,aac 兼容性最好;copy=直接复制源音频流`},
{Key: conf.TranscodeOutputAudioBitrate, Value: "160k", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `输出音频码率`},
{Key: conf.TranscodeOutputResolution, Value: "1920x1080", Type: conf.TypeSelect, Options: "source,3840x2160,2560x1440,1920x1080,1280x720,854x480", Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `输出分辨率上限,超过该分辨率会下采样;source=保持源分辨率`},
{Key: conf.TranscodeSegmentDuration, Value: "6", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `HLS/DASH 切片时长(秒),越小首帧越快但请求数变多,推荐 4-10`},
{Key: conf.TranscodeHWAccel, Value: "none", Type: conf.TypeSelect,
Options: "none,auto,nvenc,qsv,vaapi,amf,videotoolbox",
Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `GPU 硬件加速:
none = 纯 CPU(libx264)
auto = 自动探测可用加速器
nvenc = NVIDIA GPU(GeForce/Tesla/Quadro/RTX,需 NVIDIA 驱动)
qsv = Intel 集显/独显 QuickSync(免费,功耗低)
vaapi = Linux 通用 VA-API(支持 Intel/AMD)
amf = AMD GPU(Windows AMF)
videotoolbox = macOS 硬件加速`},
{Key: conf.TranscodeFFmpegPath, Value: "ffmpeg", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `FFmpeg 可执行路径,留空则使用 PATH 中的 ffmpeg`},
{Key: conf.TranscodeFFprobePath, Value: "ffprobe", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `FFprobe 可执行路径`},
{Key: conf.TranscodeWorkerSecret, Value: "", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `远程 Worker 注册时使用的共享密钥,留空则禁用远程 Worker 注册`},
{Key: conf.TranscodeCachePath, Value: "data/transcode_cache", Type: conf.TypeString, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `转码切片缓存目录`},
{Key: conf.TranscodeCacheMaxGB, Value: "20", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `切片缓存最大容量(GB),超过后按 LRU 清理`},
{Key: conf.TranscodeJobTimeoutMin, Value: "120", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `单个转码任务超时分钟数,超时自动失败`},
{Key: conf.TranscodeLocalConcurrency, Value: "1", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `本机内置 worker 同时执行的转码任务数(local/hybrid 模式生效)`},
{Key: conf.TranscodeIdleTimeoutSec, Value: "90", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `播放端无请求多少秒后自动停止转码(默认90秒,0=禁用)`},
{Key: conf.TranscodeChunkDurationSec, Value: "60", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `每个 chunk 时长(秒),越大每段缓冲越长但 ffmpeg 进程切换越少(默认60秒)`},
{Key: conf.TranscodeMaxChunkConcurrency, Value: "2", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `单个视频任务最多同时运行的 chunk 数(默认2,越大并发越高但 CPU/GPU 占用越多)`},
{Key: conf.TranscodeChunkIdleSec, Value: "60", Type: conf.TypeNumber, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `chunk 多久没被访问就 kill ffmpeg 释放资源(默认60秒)`},
{Key: conf.TranscodeChunkPrefetch, Value: "true", Type: conf.TypeBool, Group: model.TRANSCODE, Flag: model.PRIVATE,
Help: `当前 chunk 转码进度过半时,是否预启动下一个 chunk(默认开启,可让顺序播放无缝衔接)`},
}
additionalSettingItems := tool.Tools.Items()
// 固定顺序
+2 -2
View File
@@ -20,12 +20,12 @@ import (
func InitDB() {
logLevel := logger.Silent
if flags.Debug || flags.Dev {
logLevel = logger.Info
logLevel = logger.Warn // Warn 级别:只输出慢查询和错误,不输出每条 SQL
}
newLogger := logger.New(
stdlog.New(log.StandardLogger().Out, "\r\n", stdlog.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
SlowThreshold: 200 * time.Millisecond, // 超过 200ms 才记录慢查询
LogLevel: logLevel,
IgnoreRecordNotFoundError: true,
Colorful: true,

Some files were not shown because too many files have changed in this diff Show More