Compare commits
295 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c1f9a6e7e | |||
| ec7be38266 | |||
| ebf791c436 | |||
| 83c0b65a0b | |||
| 1153fe4191 | |||
| 93866d169b | |||
| d283d39008 | |||
| fc89d23867 | |||
| 1f14a1e85b | |||
| f0a6b630c6 | |||
| c723664e46 | |||
| 5faa6975d0 | |||
| cbfae0252e | |||
| 57ee482146 | |||
| a9b3110b2c | |||
| ff86c2f597 | |||
| a5731fa963 | |||
| 531e45f677 | |||
| cd3f028f0b | |||
| bd7b7a722b | |||
| 83deffcf45 | |||
| c74f6be250 | |||
| c911a57467 | |||
| 1dbc3f8fe7 | |||
| a563ee11d6 | |||
| 215ebf6942 | |||
| e4c0e85eaa | |||
| 60f05129c9 | |||
| fa4c90d47a | |||
| 1ec8303fec | |||
| b28c0d9247 | |||
| 91c6d9dfd9 | |||
| 95d8126d65 | |||
| 3aa8662c45 | |||
| 5c85478883 | |||
| fd54d42db7 | |||
| 4595b7c5be | |||
| ee3927cdf1 | |||
| 4b34fc4616 | |||
| b7f1cd85f8 | |||
| be9d3a6d9e | |||
| b8e48a1f45 | |||
| 670dd90dbe | |||
| 9a68ef701f | |||
| 386fc358d1 | |||
| 2989649445 | |||
| 5ddca9a1f6 | |||
| e337d9932b | |||
| df654ea47b | |||
| 41fe75e136 | |||
| e65accf2cd | |||
| f54ef2228c | |||
| 3c2bfb14ca | |||
| 8f64f961ac | |||
| 6797394c5b | |||
| e71d9d61d1 | |||
| 1ff60d5641 | |||
| 3f361631d0 | |||
| cfe0c0cf28 | |||
| d6355d5582 | |||
| 0f06bdf929 | |||
| e840cbd2c9 | |||
| 2bb3610da7 | |||
| 9ca1b28cb4 | |||
| 9effb1422b | |||
| b855e00345 | |||
| 8f2551a1b6 | |||
| 18e6cb40fd | |||
| 92e693ccac | |||
| 62206aa05a | |||
| f9dd082159 | |||
| d489c1608e | |||
| 1ea29cca9e | |||
| ec5f81e6cb | |||
| 8eb017fedc | |||
| fb4e762026 | |||
| 4239474100 | |||
| 8225b4e319 | |||
| c086b01381 | |||
| f196e5621f | |||
| 13d60b3bd9 | |||
| 3d30f6f27c | |||
| bd02e194ff | |||
| 1c9b742c8d | |||
| 20e7d7fa99 | |||
| 394ea0382e | |||
| 4d698d2757 | |||
| fdfb2ee987 | |||
| b23b1d728e | |||
| ad427fc0d2 | |||
| c489374b61 | |||
| 5a47e6f52d | |||
| c4b8114115 | |||
| 0e592105c4 | |||
| f20c9c3234 | |||
| 38b85bb487 | |||
| 6bf8a6878a | |||
| 2e636ce43f | |||
| 28aa466107 | |||
| f1b8010496 | |||
| 394d1a3cda | |||
| 465e98a792 | |||
| 292ae22dcd | |||
| bc7e5fc79d | |||
| f7fec93d44 | |||
| b4a1dc38bb | |||
| 3150971f10 | |||
| a71a18ce82 | |||
| 5a189a44fc | |||
| fb84c62e9a | |||
| 355fd06036 | |||
| 4173caac5d | |||
| 2aeedcc182 | |||
| d96782622d | |||
| ebe425036b | |||
| edbaa1c84b | |||
| 0179332013 | |||
| 7fb3db0f4c | |||
| e4a101b502 | |||
| 28551c7883 | |||
| a826ed9613 | |||
| 030ed5f325 | |||
| 60308800fb | |||
| 582495dece | |||
| 2a4545eb44 | |||
| 2bde71099e | |||
| 1b06a01603 | |||
| fe407caf9e | |||
| ae02c14d27 | |||
| 5404e7773f | |||
| a368110e60 | |||
| 0e9fb2c937 | |||
| d00b14df55 | |||
| 1407b9b5c4 | |||
| 7fa05391e1 | |||
| 4790f7753e | |||
| a311438aa1 | |||
| 203abd106a | |||
| cc40169616 | |||
| cd720ae879 | |||
| fc84291346 | |||
| c37e936f48 | |||
| 389cb99bcf | |||
| 1b611a6181 | |||
| 25c03f2b0d | |||
| a56b1801f9 | |||
| 99c755dc29 | |||
| 086c0307c3 | |||
| 51d0f5010e | |||
| b9dd09a5d2 | |||
| e79f393969 | |||
| e422ecce53 | |||
| d0e29a0d8d | |||
| a7eeb8fbeb | |||
| 64906d659d | |||
| c702a68cfb | |||
| 4150d03852 | |||
| a79bbb80a8 | |||
| 205c71b0ad | |||
| 5474784107 | |||
| fb92373f0d | |||
| 2b99f5f108 | |||
| 9ab32c10ca | |||
| e872223b4e | |||
| c7441e06d4 | |||
| 936be24328 | |||
| b7f2760728 | |||
| ea732e5bd7 | |||
| 4e02692aeb | |||
| e8c710c261 | |||
| 27d2b79e1f | |||
| 1e291df647 | |||
| 1025e3c693 | |||
| ba27d48d10 | |||
| 6d20dc65de | |||
| d4c82ac07e | |||
| acdb7b2ead | |||
| 32ae1aeba9 | |||
| 55badfa6ad | |||
| 8754ee615f | |||
| 4943d28743 | |||
| ee2272f743 | |||
| e4f41c0b3d | |||
| a02583ee7d | |||
| e079a3cbb2 | |||
| 5cd1aa3e91 | |||
| e923c9d7a1 | |||
| bbb512760a | |||
| da99182222 | |||
| c43d1ca637 | |||
| b51eee3d94 | |||
| b0fe40142a | |||
| a50ceaf1a7 | |||
| bf49323789 | |||
| 47118c8a53 | |||
| 67b85840dc | |||
| fbc33862fb | |||
| 04fc50a4b0 | |||
| dab9a1073a | |||
| bd3d4ad357 | |||
| bd5bfdcc10 | |||
| 73dc37fef8 | |||
| f1b7d99a33 | |||
| 638dae0c91 | |||
| 791722aec6 | |||
| 1c1da494b7 | |||
| 97b3a7bd7f | |||
| 543615a2a8 | |||
| 2b7828e1fe | |||
| 0b1fe52e24 | |||
| 2fc2c17b05 | |||
| be96acb035 | |||
| 6a72e7ab82 | |||
| 4ebd477ca2 | |||
| 0320d956ed | |||
| 95395c479a | |||
| eba7904c99 | |||
| 1171fe9464 | |||
| 06b96102c8 | |||
| 077458df31 | |||
| 9124c9be9c | |||
| 3ae3c6540d | |||
| 313ebdd402 | |||
| db52792ada | |||
| a8065680bb | |||
| 3304f09b1d | |||
| ba621cfc4a | |||
| deca7735a7 | |||
| b503fdee7a | |||
| 3fe37e050b | |||
| d3233a62c0 | |||
| 165eee7b36 | |||
| 78526afc9c | |||
| f534e0607a | |||
| db64a6c093 | |||
| 5c9e7fcaa6 | |||
| af67f4cd6e | |||
| 73de139d1f | |||
| e0cc481b96 | |||
| 7ac75ec69a | |||
| 0e73e43cbd | |||
| bd41ab3fb4 | |||
| 73b95a8e38 | |||
| 5a5555d28a | |||
| 4dc9cbe2e7 | |||
| 6af0e5fdcf | |||
| f0055b76fe | |||
| 0eb3f104f4 | |||
| c171b38155 | |||
| 89d7a6cbb2 | |||
| 85918d1196 | |||
| fa9307e2e1 | |||
| 5ab18c2725 | |||
| a4a2bde1a4 | |||
| 28113f5fdc | |||
| 42b8805e94 | |||
| 51a64f41b9 | |||
| a7bd9a942c | |||
| 97491a6175 | |||
| ea5cb3a130 | |||
| 921010926b | |||
| 7425c3d57b | |||
| 1b7d4eef46 | |||
| c30dab56a3 | |||
| e365250440 | |||
| 47d10e1f58 | |||
| e6473300a7 | |||
| 994f64f753 | |||
| 90064a5480 | |||
| 6e8eac9887 | |||
| d3051eaffe | |||
| 496a897782 | |||
| 22b7290ee1 | |||
| 14037d5dea | |||
| 152db3fb9f | |||
| 0413d123da | |||
| 5f6bd7b5cd | |||
| 3c25bb5d61 | |||
| 659b91b000 | |||
| fe5b3bd56a | |||
| 82bbb116ae | |||
| 9f5ff7e6f0 | |||
| a00504080a | |||
| fc6e2e6f10 | |||
| 65c5f3e4bf | |||
| 07e340251b | |||
| 9b956b928b | |||
| c3187f6e3f | |||
| 60c815a8b3 | |||
| 41b155ea31 | |||
| 2888ae8bf7 | |||
| 7363064d89 | |||
| 1d53bf2ae1 | |||
| 618165ec31 | |||
| 87c66a9b8c |
@@ -0,0 +1,2 @@
|
||||
'Get-Content' is not recognized as an internal or external command,
|
||||
operable program or batch file.
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"ssh": {
|
||||
"command": "cmd.exe",
|
||||
"args": [
|
||||
"/c",
|
||||
"npx",
|
||||
"-y",
|
||||
"@aiondadotcom/mcp-ssh"
|
||||
],
|
||||
"env": {
|
||||
"ProgramData": "C:\\ProgramData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,12 @@ verify-cache/
|
||||
verify-media/
|
||||
verify-downloads/
|
||||
.codex-*
|
||||
.codex/
|
||||
.tmp/
|
||||
.tmp_*
|
||||
.tmp-deploy-*
|
||||
.tmp-deploy-data/
|
||||
.mmtl.pid
|
||||
.mebox.pid
|
||||
*.db
|
||||
*.db-journal
|
||||
*.db-shm
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Telegram MMTL 交流群
|
||||
url: https://t.me/MMTL
|
||||
- name: Telegram MeBox 交流群
|
||||
url: https://t.me/MeBox
|
||||
about: 适合快速交流部署经验、使用问题和排查线索。
|
||||
|
||||
@@ -12,7 +12,7 @@ assignees: ""
|
||||
|
||||
## 期望方案
|
||||
|
||||
请描述你希望 MMTL 如何工作。
|
||||
请描述你希望 MeBox 如何工作。
|
||||
|
||||
## 使用场景
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: MMTL CodeQL
|
||||
name: MeBox CodeQL
|
||||
|
||||
paths-ignore:
|
||||
- internal/service/fileid_other.go
|
||||
|
||||
@@ -1,121 +1,78 @@
|
||||
name: AuTo Docker Image
|
||||
|
||||
name: Build & Publish
|
||||
|
||||
# 版本策略(version 分支托管,main 零污染):
|
||||
# - VERSION 文件单独存放在 version 分支,CI 构建时读取并自增写回 version 分支,
|
||||
# main 分支不再出现任何 CI 提交,本地推送永不与远程冲突。
|
||||
# - push 到 main:版本号自动 patch+1,发布 latest + 版本镜像、GitHub Release、
|
||||
# 多平台单文件二进制,并部署服务器。
|
||||
# - push tag v*:正式发版,版本号取 tag 名(不 bump version 分支),其余同上。
|
||||
# - 手动触发:版本号在 version 分支当前值上自增,等同 push main 全量发布。
|
||||
# 查看当前版本号:git show origin/version:VERSION
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
# 保留手动触发作为备选
|
||||
tags: ['v*']
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_type:
|
||||
description: '版本递增类型'
|
||||
required: true
|
||||
default: 'patch'
|
||||
type: choice
|
||||
options:
|
||||
- patch # 0.0.x
|
||||
- minor # 0.x.0
|
||||
- major # x.0.0
|
||||
|
||||
permissions:
|
||||
contents: write # 需要写入权限来更新版本文件
|
||||
contents: write # 读写 version 分支、发布 Release 与上传二进制需要
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
version-and-publish:
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
release_tag: ${{ steps.version.outputs.release_tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # 获取完整历史以便版本计算
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# 1. 获取或初始化版本号
|
||||
- name: Get current version
|
||||
id: get_version
|
||||
# 1. 解析版本号:tag 触发取 tag 名(去掉 v 前缀);其余场景读 version 分支并 patch+1
|
||||
- name: Resolve version
|
||||
id: version
|
||||
run: |
|
||||
# 从文件读取版本号,或使用默认值
|
||||
if [ -f VERSION ]; then
|
||||
CURRENT_VERSION=$(cat VERSION)
|
||||
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
else
|
||||
CURRENT_VERSION="0.0.0"
|
||||
echo $CURRENT_VERSION > VERSION
|
||||
git fetch origin version
|
||||
BASE=$(git show FETCH_HEAD:VERSION 2>/dev/null || echo "0.0.0")
|
||||
MAJOR=$(echo "$BASE" | cut -d. -f1)
|
||||
MINOR=$(echo "$BASE" | cut -d. -f2)
|
||||
PATCH=$(echo "$BASE" | cut -d. -f3)
|
||||
VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
|
||||
fi
|
||||
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# 分离版本组成部分
|
||||
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
|
||||
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
|
||||
PATCH=$(echo $CURRENT_VERSION | cut -d. -f3)
|
||||
echo "major=$MAJOR" >> $GITHUB_OUTPUT
|
||||
echo "minor=$MINOR" >> $GITHUB_OUTPUT
|
||||
echo "patch=$PATCH" >> $GITHUB_OUTPUT
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_tag=mebox-v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "new_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# 2. 计算新版本号
|
||||
- name: Bump version
|
||||
id: bump_version
|
||||
run: |
|
||||
MAJOR=${{ steps.get_version.outputs.major }}
|
||||
MINOR=${{ steps.get_version.outputs.minor }}
|
||||
PATCH=${{ steps.get_version.outputs.patch }}
|
||||
|
||||
# 手动触发时根据选择递增
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
TYPE="${{ github.event.inputs.version_type }}"
|
||||
if [ "$TYPE" = "major" ]; then
|
||||
MAJOR=$((MAJOR + 1))
|
||||
MINOR=0
|
||||
PATCH=0
|
||||
elif [ "$TYPE" = "minor" ]; then
|
||||
MINOR=$((MINOR + 1))
|
||||
PATCH=0
|
||||
else # patch
|
||||
PATCH=$((PATCH + 1))
|
||||
fi
|
||||
else
|
||||
# 自动触发时默认 patch 递增
|
||||
PATCH=$((PATCH + 1))
|
||||
fi
|
||||
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "tag=MMTL-v${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "tag=mmtl-v${NEW_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
# 3. 更新 VERSION 文件
|
||||
- name: Update version file
|
||||
run: |
|
||||
echo "${{ steps.bump_version.outputs.new_version }}" > VERSION
|
||||
|
||||
# 如果存在 go.mod,也更新其中的版本(可选)
|
||||
# if [ -f go.mod ]; then
|
||||
# sed -i "s/^version .*/version ${{ steps.bump_version.outputs.new_version }}/" go.mod
|
||||
# fi
|
||||
|
||||
# 4. 提交版本变更
|
||||
- name: Commit version bump
|
||||
# 2. 把新版本号写回 version 分支(clone 单分支写入,冲突时 rebase 重试)
|
||||
# tag 触发的正式发版版本号来自 tag 本身,跳过自增。
|
||||
- name: Bump version branch
|
||||
if: github.ref_type != 'tag'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
REPO="https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
|
||||
git clone --depth 1 --branch version "$REPO" "$RUNNER_TEMP/version-branch"
|
||||
cd "$RUNNER_TEMP/version-branch"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add VERSION
|
||||
git commit -m "chore: bump version to ${{ steps.bump_version.outputs.new_version }} [skip ci]"
|
||||
git push
|
||||
echo "${{ steps.version.outputs.new_version }}" > VERSION
|
||||
git commit -am "chore: bump version to ${{ steps.version.outputs.new_version }}"
|
||||
ok=0
|
||||
for i in 1 2 3 4 5; do
|
||||
if git push origin version; then ok=1; break; fi
|
||||
git pull --rebase origin version || true
|
||||
sleep 5
|
||||
done
|
||||
[ "$ok" = "1" ] || { echo "::error::version 分支推送冲突,重试 5 次仍失败"; exit 1; }
|
||||
|
||||
# 5. 创建 Git Tag
|
||||
- name: Create and push tag
|
||||
run: |
|
||||
TAG="${{ steps.bump_version.outputs.tag }}"
|
||||
git tag $TAG
|
||||
git push origin $TAG
|
||||
|
||||
# 6. 设置 Docker QEMU 和 Buildx
|
||||
# 3. 设置 Docker QEMU 和 Buildx
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
# 7. 登录 GHCR
|
||||
# 3. 登录 GHCR
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -123,18 +80,17 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# 8. 提取镜像元数据
|
||||
# 4. 提取镜像元数据
|
||||
- name: Extract image metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/mmtl
|
||||
images: ghcr.io/${{ github.repository_owner }}/mebox
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=raw,value=${{ steps.bump_version.outputs.tag }}
|
||||
type=raw,value=${{ steps.bump_version.outputs.new_version }}
|
||||
type=raw,value=${{ steps.version.outputs.version }}
|
||||
|
||||
# 9. 构建并推送
|
||||
# 5. 构建并推送
|
||||
- name: Build & push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
@@ -146,6 +102,167 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
VERSION=${{ steps.bump_version.outputs.new_version }}
|
||||
VERSION=${{ steps.version.outputs.release_tag }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# 单文件可执行构建:把前端打包进二进制(go:embed),交叉编译 Windows /
|
||||
# Linux / macOS 的 amd64 / arm64 产物,作为 GitHub Release 附件发布。
|
||||
build-frontend:
|
||||
needs: [build-image]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Install
|
||||
working-directory: web
|
||||
run: npm ci
|
||||
- name: Build SPA
|
||||
working-directory: web
|
||||
run: npm run build
|
||||
- name: Upload web/dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: web-dist
|
||||
path: web/dist
|
||||
retention-days: 1
|
||||
|
||||
# 先创建(幂等)空的 GitHub Release,供后续 build-binaries 并行上传附件,
|
||||
# 也避免矩阵各 job 并发 upload 时 release 尚不存在而互相竞争。
|
||||
publish-create-release:
|
||||
needs: [build-image]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ needs.build-image.outputs.release_tag }}
|
||||
run: |
|
||||
set -eux
|
||||
# tag push 时 tag 已存在;手动触发时基于当前 main 创建 tag(幂等)
|
||||
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
|
||||
git tag "$RELEASE_TAG"
|
||||
git push origin "$RELEASE_TAG"
|
||||
fi
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--title "MeBox ${{ needs.build-image.outputs.version }}" \
|
||||
--notes "自动化发布 ${{ needs.build-image.outputs.version }}" \
|
||||
--verify-tag --latest || true
|
||||
|
||||
build-binaries:
|
||||
needs: [build-image, build-frontend, publish-create-release]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
ext: ""
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
ext: ""
|
||||
- goos: windows
|
||||
goarch: amd64
|
||||
ext: .exe
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
ext: .exe
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
ext: ""
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
ext: ""
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: true
|
||||
- name: Download web/dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: web-dist
|
||||
path: web/dist
|
||||
- name: Build binary
|
||||
run: |
|
||||
LDFLAGS="-s -w -X main.version=${{ needs.build-image.outputs.release_tag }}"
|
||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||
LDFLAGS="$LDFLAGS -H=windowsgui"
|
||||
fi
|
||||
CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \
|
||||
go build -trimpath -ldflags="$LDFLAGS" \
|
||||
-o "dist/mebox-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}" ./cmd/server
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir -p package/mebox
|
||||
cp "dist/mebox-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}" package/mebox/mebox${{ matrix.ext }}
|
||||
cp README.md package/mebox/ 2>/dev/null || true
|
||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||
(cd package && zip -r "../mebox_${{ matrix.goos }}_${{ matrix.goarch }}.zip" mebox)
|
||||
else
|
||||
tar -czf "mebox_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz" -C package mebox
|
||||
fi
|
||||
- name: Upload to GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ needs.build-image.outputs.release_tag }}
|
||||
run: |
|
||||
set -eux
|
||||
PKG="mebox_${{ matrix.goos }}_${{ matrix.goarch }}.zip"
|
||||
TAR="mebox_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz"
|
||||
# 并发上传到同一 release 各自文件,--clobber 幂等覆盖
|
||||
if [ -f "$PKG" ]; then
|
||||
for i in 1 2 3; do gh release upload "$RELEASE_TAG" "$PKG" --clobber && break || sleep 5; done
|
||||
fi
|
||||
if [ -f "$TAR" ]; then
|
||||
for i in 1 2 3; do gh release upload "$RELEASE_TAG" "$TAR" --clobber && break || sleep 5; done
|
||||
fi
|
||||
|
||||
deploy:
|
||||
name: Deploy to Server
|
||||
needs: [build-image]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy via SSH
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
password: ${{ secrets.SERVER_PASSWORD }}
|
||||
port: ${{ secrets.SERVER_PORT }}
|
||||
script: |
|
||||
set -e
|
||||
echo "==== 开始部署 MeBox ===="
|
||||
cd /root/dockerData/mebox
|
||||
|
||||
# 判断 compose 命令版本兼容性(docker compose 或 docker-compose)
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="docker compose"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
echo "错误: 未找到 docker compose 或 docker-compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "正在拉取最新镜像..."
|
||||
$COMPOSE_CMD pull
|
||||
|
||||
echo "正在重启服务..."
|
||||
$COMPOSE_CMD up -d
|
||||
|
||||
echo "清理旧的无用镜像..."
|
||||
docker image prune -f
|
||||
|
||||
echo "==== 部署完成并已启动 ===="
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# Beta 分支自动构建流水线
|
||||
#
|
||||
# 触发:push 到 beta 分支 / PR 到 beta / 手动触发。
|
||||
# 产出:
|
||||
# 1. 前端 + 后端编译验证(go vet / go test / go build)
|
||||
# 2. 多平台可执行二进制 artifact(linux/amd64、linux/arm64、windows/amd64)
|
||||
# 3. ghcr.io/{owner}/mebox:beta 多架构 Docker 镜像(linux/amd64 + linux/arm64)
|
||||
#
|
||||
# 与 main 分支的发布流(Auto-docker-publish.yml)隔离:beta 不做版本递增、
|
||||
# 不打 release tag,只构建带 -beta 标识的产物供测试。
|
||||
|
||||
name: Beta Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [beta]
|
||||
pull_request:
|
||||
branches: [beta]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
BETA_VERSION_PREFIX: beta
|
||||
|
||||
jobs:
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 1) 编译验证 + 多平台二进制产物
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
test-and-build:
|
||||
name: Test & build artifacts
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve beta version
|
||||
id: version
|
||||
run: |
|
||||
BASE_VERSION=$(cat VERSION 2>/dev/null || echo "0.0.0")
|
||||
SHA_SHORT=${GITHUB_SHA:0:7}
|
||||
echo "full_version=${BASE_VERSION}-beta.${SHA_SHORT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The binary embeds the SPA (web/dist) via go:embed, so dist must exist
|
||||
# before the Go toolchain touches the web package.
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Build SPA
|
||||
working-directory: web
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: true
|
||||
|
||||
- name: go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: go test
|
||||
run: go test ./...
|
||||
|
||||
- name: go build (host)
|
||||
run: go build ./...
|
||||
|
||||
# 多平台可执行文件(嵌入刚构建的 web/dist)
|
||||
- name: Build linux/amd64
|
||||
run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w -X main.version=${{ steps.version.outputs.full_version }}" -o dist/mebox-beta-linux-amd64 ./cmd/server
|
||||
- name: Build linux/arm64
|
||||
run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags="-s -w -X main.version=${{ steps.version.outputs.full_version }}" -o dist/mebox-beta-linux-arm64 ./cmd/server
|
||||
- name: Build windows/amd64
|
||||
run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w -H=windowsgui -X main.version=${{ steps.version.outputs.full_version }}" -o dist/mebox-beta-windows-amd64.exe ./cmd/server
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mebox-beta-binaries
|
||||
path: dist/*
|
||||
if-no-files-found: error
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 2) Beta Docker 镜像(ghcr.io/{owner}/mebox:beta)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
docker-beta:
|
||||
name: Build & push beta Docker image
|
||||
needs: test-and-build
|
||||
runs-on: ubuntu-latest
|
||||
# PR 事件不推送镜像,仅 push beta / 手动触发时推送
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve beta version
|
||||
id: version
|
||||
run: |
|
||||
BASE_VERSION=$(cat VERSION 2>/dev/null || echo "0.0.0")
|
||||
SHA_SHORT=${GITHUB_SHA:0:7}
|
||||
echo "full_version=${BASE_VERSION}-beta.${SHA_SHORT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build & push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
provenance: false
|
||||
sbom: false
|
||||
tags: ghcr.io/${{ github.repository_owner }}/mebox:beta
|
||||
labels: |
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.source=${{ github.repository }}
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.full_version }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -18,6 +18,19 @@ jobs:
|
||||
go-version: '1.25'
|
||||
cache: true
|
||||
|
||||
# The binary embeds the SPA (web/dist) via go:embed, so the dist must exist
|
||||
# before the Go toolchain touches the `web` package.
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- name: Build SPA
|
||||
working-directory: web
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: go vet
|
||||
run: go vet ./...
|
||||
|
||||
@@ -38,7 +51,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Image tag to publish, for example MMTL-v0.0.32'
|
||||
description: 'Image tag to publish, for example MeBox-v0.0.32'
|
||||
required: true
|
||||
type: string
|
||||
ref:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# 自动使用当前仓库所有者
|
||||
images: ghcr.io/${{ github.repository_owner }}/mmtl
|
||||
images: ghcr.io/${{ github.repository_owner }}/mebox
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=raw,value=${{ env.RELEASE_VERSION }}
|
||||
@@ -61,3 +61,41 @@ jobs:
|
||||
VERSION=${{ env.RELEASE_VERSION }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
deploy:
|
||||
name: Deploy to Server
|
||||
needs: [docker]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy via SSH
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
password: ${{ secrets.SERVER_PASSWORD }}
|
||||
port: ${{ secrets.SERVER_PORT }}
|
||||
script: |
|
||||
set -e
|
||||
echo "==== 开始部署 MeBox ===="
|
||||
cd /root/dockerData/mebox
|
||||
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="docker compose"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
echo "错误: 未找到 docker compose 或 docker-compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "正在拉取最新镜像..."
|
||||
$COMPOSE_CMD pull
|
||||
|
||||
echo "正在重启服务..."
|
||||
$COMPOSE_CMD up -d
|
||||
|
||||
echo "清理旧的无用镜像..."
|
||||
docker image prune -f
|
||||
|
||||
echo "==== 部署完成并已启动 ===="
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ logs/
|
||||
.tmp-deploy-cache/
|
||||
.tmp-deploy-server.*
|
||||
.tmp-live-server.*
|
||||
.mmtl.pid
|
||||
.mebox.pid
|
||||
*.log
|
||||
*.db
|
||||
*.db-journal
|
||||
@@ -66,6 +66,7 @@ config.yaml
|
||||
.tmp-live-backups/
|
||||
.tmp-*
|
||||
.codex-*
|
||||
.codex/
|
||||
downloads/
|
||||
media/
|
||||
*.pid
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
## 删除用户云下载任务
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:----------------------------------|
|
||||
| 接口名称 | 删除用户云下载任务 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /del_task |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
删除当前授权用户的指定云下载任务,可选择同时删除对应源文件。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/offline/del_task
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----------------|:-------|:-----|:-------|:---------------------------|:-------------|
|
||||
| info_hash | string | 是 | - | 需删除的任务Hash | `<info_hash>` |
|
||||
| del_source_file | int | 否 | 0 | 是否删除源文件,1=删除 0=不删除 | 0 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/offline/del_task' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
--form-string 'info_hash=<info_hash>' \
|
||||
--form-string 'del_source_file=0'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------|:--------|:----------------------|
|
||||
| state | boolean | 操作结果状态 |
|
||||
| message | string | 返回信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | object[] | 返回数据,成功时为空数组 |
|
||||
|
||||
#### 响应的 code 字段(错误码)说明
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|:-------|:---------|:-------------------------|
|
||||
| 990002 | 参数错误 | 检查 `info_hash` 参数 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- 未传 `del_source_file` 时默认不删除源文件。
|
||||
- `del_source_file=1` 会删除任务对应的源文件,操作前需确认影响范围。
|
||||
|
||||
### 性能与安全说明
|
||||
|
||||
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||
- 接口按应用、用户和接口维度执行频率限制。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 删除结果以响应中的 `state`、`code` 和 `message` 字段为准。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,111 @@
|
||||
## 添加云下载BT任务
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:----------------------------------|
|
||||
| 接口名称 | 添加云下载BT任务 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /add_task_bt |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
根据已解析的BT种子信息添加云下载BT任务。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/offline/add_task_bt
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-------------|:-------|:-----|:-------|:----------------------------------|:----------------|
|
||||
| info_hash | string | 是 | - | BT任务Hash | `<info_hash>` |
|
||||
| wanted | string | 是 | - | 选中下载的文件索引,使用半角逗号分隔 | `<file_indexes>` |
|
||||
| save_path | string | 是 | - | BT任务文件保存路径 | `A/B` |
|
||||
| torrent_sha1 | string | 是 | - | BT种子SHA1 | `<torrent_sha1>` |
|
||||
| pick_code | string | 是 | - | BT种子文件提取码 | `<pick_code>` |
|
||||
| wp_path_id | string | 否 | 0 | 保存目标文件夹ID | 0 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/offline/add_task_bt' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
--form-string 'info_hash=<info_hash>' \
|
||||
--form-string 'wanted=<file_indexes>' \
|
||||
--form-string 'save_path=A/B' \
|
||||
--form-string 'torrent_sha1=<torrent_sha1>' \
|
||||
--form-string 'pick_code=<pick_code>' \
|
||||
--form-string 'wp_path_id=0'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------|:--------|:----------------------|
|
||||
| state | boolean | 操作结果状态 |
|
||||
| message | string | 返回信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | object[] | 返回数据,成功时为空数组 |
|
||||
|
||||
#### 响应的 code 字段(错误码)说明
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|:--------|:---------------------|:-------------------------------|
|
||||
| 20018 | 文件不存在或已删除 | 检查提取码、文件归属和种子SHA1 |
|
||||
| 91006 | 存储空间不足 | 扩充存储空间后重试 |
|
||||
| 990002 | 参数错误 | 检查必填参数 |
|
||||
| 1000012 | 云下载配额已用完 | 购买配额或获得更多配额后重试 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- `wanted` 可以为 `0`,但不能是空字符串。
|
||||
- 种子文件需属于当前授权用户、位于指定存储区域,并且文件SHA1与 `torrent_sha1` 一致。
|
||||
- 不传 `wp_path_id` 时默认保存到根目录;`save_path` 是相对于 `wp_path_id` 所在文件夹的路径。例如,`wp_path_id` 不传或传云下载文件夹ID且 `save_path=A/B` 时,最终路径为根目录下的 `A/B/`。
|
||||
- 添加任务前会检查当前授权账号的剩余存储空间;空间不足时不扣减云下载配额。
|
||||
- 云下载服务返回状态码 `10007` 或 `10010` 时,接口统一返回错误码 `1000012`。
|
||||
|
||||
### 性能与安全说明
|
||||
|
||||
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||
- 接口按应用、用户和接口维度执行频率限制。
|
||||
- 种子文件必须属于当前授权账号,并且文件SHA1与 `torrent_sha1` 一致。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 任务处理结果以响应中的 `data` 字段为准。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,115 @@
|
||||
## 添加云下载链接任务
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:----------------------------------|
|
||||
| 接口名称 | 添加云下载链接任务 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /add_task_urls |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
批量添加云下载链接任务。多个链接使用换行符分隔,支持HTTP(S)、FTP、磁力链和电驴链接。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/offline/add_task_urls
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-----------|:-------|:-----|:-------|:----------------------------------------|:----------------|
|
||||
| urls | string | 是 | - | 云下载链接,多个链接使用换行符分隔 | `<download_url>` |
|
||||
| wp_path_id | string | 否 | 0 | 保存目标文件夹ID;不传或传0时保存到根目录 | 0 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/offline/add_task_urls' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
--form-string 'urls=<download_url>' \
|
||||
--form-string 'wp_path_id=0'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:-----------------|:---------|:-------------------------------|
|
||||
| state | boolean | 操作结果状态 |
|
||||
| message | string | 返回信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | object[] | 各链接任务的添加结果 |
|
||||
| data[].state | boolean | 链接任务添加状态 |
|
||||
| data[].code | int | 链接任务状态码 |
|
||||
| data[].message | string | 链接任务状态描述 |
|
||||
| data[].info_hash | string | 链接任务SHA1,仅任务成功时返回 |
|
||||
| data[].url | string | 链接任务URL |
|
||||
|
||||
#### 响应的 code 字段(错误码)说明
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|:--------|:---------------------------|:-----------------------------|
|
||||
| 91006 | 存储空间不足 | 扩充存储空间后重试 |
|
||||
| 980004 | 操作失败 | 稍后重试 |
|
||||
| 990002 | 参数错误 | 检查必填参数 |
|
||||
| 1000011 | 链接数量超过115个 | 将链接拆分为每批不超过115个 |
|
||||
| 1000012 | 云下载配额已用完 | 购买配额或获得更多配额后重试 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": [
|
||||
{
|
||||
"state": true,
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"info_hash": "",
|
||||
"url": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- 服务端按换行符拆分并清理链接,单次最多提交115个链接。
|
||||
- 添加任务前会检查当前授权账号的剩余存储空间;空间不足时不扣减云下载配额。
|
||||
- 云下载服务返回状态码 `10007` 或 `10010` 时,接口统一返回错误码 `1000012`。
|
||||
|
||||
### 性能与安全说明
|
||||
|
||||
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||
- 接口按应用、用户和接口维度执行频率限制。
|
||||
- 本接口限制单批链接数量,避免不受控的批量请求。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 任务处理结果以响应中的 `data` 字段为准。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,104 @@
|
||||
## 清空云下载任务
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:----------------------------------|
|
||||
| 接口名称 | 清空云下载任务 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /clear_task |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
按指定类型清空当前授权用户的云下载任务。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/offline/clear_task
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-------|:-----|:-----|:-------|:-----------------------|:----------|
|
||||
| flag | int | 是 | - | 清空任务类型,见下方枚举表格 | 1 |
|
||||
|
||||
#### 请求参数中的 flag 参数枚举
|
||||
|
||||
| 值 | 说明 | 备注 |
|
||||
|:---|:-----------------------------|:-----|
|
||||
| 0 | 清空已完成任务 | - |
|
||||
| 1 | 清空全部任务 | - |
|
||||
| 2 | 清空失败任务 | - |
|
||||
| 3 | 清空进行中任务 | - |
|
||||
| 4 | 清空已完成任务并删除对应源文件 | - |
|
||||
| 5 | 清空全部任务并删除对应源文件 | - |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/offline/clear_task' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
--form-string 'flag=1'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------|:--------|:-----------------|
|
||||
| state | boolean | 操作结果状态 |
|
||||
| message | string | 返回信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | object[] | 返回数据,成功时为空数组 |
|
||||
|
||||
#### 响应的 code 字段(错误码)说明
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|:-------|:---------|:--------------------------|
|
||||
| 990002 | 参数错误 | 检查 `flag` 是否为0至5的整数 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- `flag=4` 或 `flag=5` 会同时删除对应源文件,操作前需确认清理范围。
|
||||
|
||||
### 性能与安全说明
|
||||
|
||||
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||
- 接口按应用、用户和接口维度执行频率限制。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 清空结果以响应中的 `state`、`code` 和 `message` 字段为准。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,101 @@
|
||||
## 获取云下载配额信息
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:----------------------------------|
|
||||
| 接口名称 | 获取云下载配额信息 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /get_quota_info |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
获取当前授权用户各类云下载配额的使用情况和过期明细。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/offline/get_quota_info
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
无。
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/offline/get_quota_info' \
|
||||
-H 'Authorization: Bearer <access_token>'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------------------------------------------|:---------|:---------------------|
|
||||
| state | boolean | 状态,true表示成功 |
|
||||
| message | string | 返回信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | object | 云下载配额数据 |
|
||||
| data.package | object[] | 配额类型列表 |
|
||||
| data.package[].surplus | int | 该类型剩余配额 |
|
||||
| data.package[].used | int | 该类型已用配额 |
|
||||
| data.package[].count | int | 该类型总配额 |
|
||||
| data.package[].name | string | 该类型配额名称 |
|
||||
| data.package[].expire_info | object[] | 该类型配额过期明细 |
|
||||
| data.package[].expire_info[].surplus | int | 明细项剩余配额 |
|
||||
| data.package[].expire_info[].expire_time | int | 明细项过期时间 |
|
||||
| data.count | int | 用户总配额数量 |
|
||||
| data.surplus | int | 用户总剩余配额数量 |
|
||||
| data.used | int | 用户总已用配额数量 |
|
||||
|
||||
#### 响应的 code 字段(错误码)说明
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|:-------|:---------|:-------------|
|
||||
| 990002 | 参数错误 | 检查授权信息 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"package": [],
|
||||
"count": 0,
|
||||
"surplus": 0,
|
||||
"used": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 性能与安全说明
|
||||
|
||||
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||
- 接口按应用、用户和接口维度执行频率限制。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 云下载配额以响应中的 `data` 字段为准。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,136 @@
|
||||
## 获取用户云下载任务列表
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:----------------------------------|
|
||||
| 接口名称 | 获取用户云下载任务列表 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /get_task_list |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
分页获取当前授权用户的云下载任务列表。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/offline/get_task_list
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-------|:-----|:-----|:-------|:---------|:----------|
|
||||
| page | int | 否 | 1 | 页码 | 1 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl -G 'https://proapi.115.com/open/offline/get_task_list' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
--data-urlencode 'page=1'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:----------------------------|:---------|:--------------------------------------|
|
||||
| state | boolean | 状态,true表示成功 |
|
||||
| message | string | 返回信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | object | 分页任务数据 |
|
||||
| data.page | int | 当前页码 |
|
||||
| data.page_count | int | 总页数 |
|
||||
| data.count | int | 任务总数 |
|
||||
| data.tasks | object[] | 云下载任务列表 |
|
||||
| data.tasks[].info_hash | string | 任务SHA1 |
|
||||
| data.tasks[].add_time | int | 任务添加时间戳 |
|
||||
| data.tasks[].percentDone | int | 任务下载进度 |
|
||||
| data.tasks[].size | int | 任务总大小,单位为字节 |
|
||||
| data.tasks[].name | string | 任务名称 |
|
||||
| data.tasks[].last_update | int | 任务最后更新时间戳 |
|
||||
| data.tasks[].file_id | string | 任务源文件或文件夹ID |
|
||||
| data.tasks[].delete_file_id | string | 删除任务并删除源文件时需传递的文件或文件夹ID |
|
||||
| data.tasks[].status | int | 任务状态,见下方枚举表格 |
|
||||
| data.tasks[].url | string | 链接任务URL |
|
||||
| data.tasks[].wp_path_id | string | 任务源文件所在父文件夹ID |
|
||||
| data.tasks[].def2 | int | 视频清晰度,见下方枚举表格 |
|
||||
| data.tasks[].play_long | int | 视频时长 |
|
||||
| data.tasks[].can_appeal | int | 是否可以申诉 |
|
||||
|
||||
#### 响应的 data.tasks[].status 字段枚举
|
||||
|
||||
| 值 | 说明 | 备注 |
|
||||
|:---|:---------|:-----|
|
||||
| -1 | 下载失败 | - |
|
||||
| 0 | 分配中 | - |
|
||||
| 1 | 下载中 | - |
|
||||
| 2 | 下载成功 | - |
|
||||
|
||||
#### 响应的 data.tasks[].def2 字段枚举
|
||||
|
||||
| 值 | 说明 | 备注 |
|
||||
|:----|:------|:-----|
|
||||
| 1 | 标清 | - |
|
||||
| 2 | 高清 | - |
|
||||
| 3 | 超清 | - |
|
||||
| 4 | 1080P | - |
|
||||
| 5 | 4K | - |
|
||||
| 100 | 原画 | - |
|
||||
|
||||
#### 响应的 code 字段(错误码)说明
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|:-------|:---------|:---------------|
|
||||
| 990002 | 参数错误 | 检查授权信息 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"page": 1,
|
||||
"page_count": 0,
|
||||
"count": 0,
|
||||
"tasks": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- 未传 `page` 或参数值为空时,默认查询第1页。
|
||||
|
||||
### 性能与安全说明
|
||||
|
||||
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||
- 接口按应用、用户和接口维度执行频率限制。
|
||||
- 任务列表按 `page` 分页,实际返回数量以响应为准。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 云下载任务状态和字段以响应中的 `data` 字段为准。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,118 @@
|
||||
## 解析BT种子
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:----------------------------------|
|
||||
| 接口名称 | 解析BT种子 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /torrent |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
解析已上传的BT种子文件,返回种子任务信息和文件列表。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/offline/torrent
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-------------|:-------|:-----|:-------|:---------------|:----------------|
|
||||
| torrent_sha1 | string | 是 | - | BT种子文件SHA1 | `<torrent_sha1>` |
|
||||
| pick_code | string | 是 | - | BT种子文件提取码 | `<pick_code>` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/offline/torrent' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
--form-string 'torrent_sha1=<torrent_sha1>' \
|
||||
--form-string 'pick_code=<pick_code>'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:-------------------------------|:---------|:---------------------|
|
||||
| state | boolean | 状态,true表示成功 |
|
||||
| message | string | 返回信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | object | 种子解析结果 |
|
||||
| data.file_size | int | 任务大小 |
|
||||
| data.torrent_name | string | 任务名称 |
|
||||
| data.file_count | int | 文件数量 |
|
||||
| data.info_hash | string | 任务SHA1 |
|
||||
| data.torrent_filelist | object[] | 文件列表 |
|
||||
| data.torrent_filelist[].size | int | 文件大小 |
|
||||
| data.torrent_filelist[].path | string | 文件路径 |
|
||||
| data.torrent_filelist[].wanted | int | 文件是否默认选中 |
|
||||
|
||||
#### 响应的 code 字段(错误码)说明
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|:-------|:---------------------|:---------------------------------|
|
||||
| 20018 | 文件不存在或已删除 | 检查提取码、文件归属和种子SHA1 |
|
||||
| 990002 | 参数错误 | 检查必填参数 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"file_size": 0,
|
||||
"torrent_name": "",
|
||||
"file_count": 0,
|
||||
"info_hash": "",
|
||||
"torrent_filelist": [
|
||||
{
|
||||
"size": 0,
|
||||
"path": "",
|
||||
"wanted": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- 种子文件需属于当前授权用户、位于指定存储区域,并且文件SHA1与 `torrent_sha1` 一致。
|
||||
- 现有开放平台文档建议先将种子文件上传至“云下载/种子文件”文件夹,但该目录不是硬性要求。
|
||||
|
||||
### 性能与安全说明
|
||||
|
||||
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||
- 接口按应用、用户和接口维度执行频率限制。
|
||||
- 种子文件必须属于当前授权账号,并且文件SHA1与 `torrent_sha1` 一致。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 种子解析结果以响应中的 `data` 字段为准。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,172 @@
|
||||
## 开发者商业价值转化:推广产品得收益
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------|:--------------------|
|
||||
| 文档名称 | 开发者商业价值转化:推广产品得收益 |
|
||||
| 文档版本 | v1.0 |
|
||||
|
||||
## 一、简介
|
||||
|
||||
为帮助开发者实现商业价值转化,115生活开放平台推出“推广产品得收益”计划,通过“推广有奖、收益共享”的方式,与开发者共同构建可持续发展的开放生态。
|
||||
|
||||
开发者接入标准化服务接口后,可以在用户需要升级使用权限、扩充长期存储空间或增加云下载配额时,引导用户购买对应的 115 增值服务,并基于用户实际购买的产品获取相应推广收益。
|
||||
|
||||
## 二、服务形式说明
|
||||
|
||||
### 1. 标准化服务接口
|
||||
|
||||
标准化服务接口覆盖以下核心场景。
|
||||
|
||||
#### 1.1. VIP 服务
|
||||
|
||||
适用于用户对应功能使用权限不足的场景:
|
||||
|
||||
- 视频播放权限升级:解决非 VIP 用户不支持在线预览视频、年费VIP以下用户不支持视频 4K 超轻转码等权限限制。
|
||||
- 大文件上传权限升级:解决月费VIP以下用户不支持上传 115GB 大文件等权限限制。
|
||||
|
||||
开发者可以引导用户升级至更高的 VIP 类型,以获取对应使用权限。
|
||||
|
||||
#### 1.2. 长期存储空间扩容服务
|
||||
|
||||
适用于用户存储空间不足的场景,可解决因空间容量不足导致文件上传、复制及添加云下载失败等问题。
|
||||
|
||||
开发者可以引导用户购买 VIP 服务以获取更多长期存储空间,或单独购买长期存储空间进行扩容。
|
||||
|
||||
#### 1.3. 云下载配额
|
||||
|
||||
适用于用户云下载配额不足的场景,可解决因服务配额不足导致添加云下载失败等问题。
|
||||
|
||||
开发者可以引导用户升级至更高的 VIP 类型以获取更多云下载配额,或单独购买云下载配额。
|
||||
|
||||
### 2. 收益获取模式
|
||||
|
||||
用户通过开发者应用进入购买页面并成功购买以下任一服务产品后,开发者可以获得对应推广收益:
|
||||
|
||||
- 115生活 VIP 服务,包括月费VIP、年费VIP等。
|
||||
- 长期存储空间扩容。
|
||||
- 云下载配额。
|
||||
|
||||
VIP 服务购买页面示例:
|
||||
|
||||

|
||||
|
||||
长期存储空间扩容购买页面示例:
|
||||
|
||||

|
||||
|
||||
## 三、接入流程
|
||||
|
||||
### 1. 成为开发者
|
||||
|
||||
已成为 115 生活开发者的用户可以直接进行接口接入;未注册的开发者,请先参考[接入流程](../接入指南/接入流程.md)完成注册。
|
||||
|
||||
### 2. 接口接入
|
||||
|
||||
开发者可以调用“获取产品列表地址”接口取得购买页面地址,并在对应业务场景中引导用户访问。
|
||||
|
||||
#### 接口信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------|:------------|
|
||||
| 接口名称 | 获取产品列表地址 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /vip/qr_url |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
该接口用于获取 115 生活开放平台增值服务产品列表地址。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/vip/qr_url
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-------------------|:-------|:---|:-----|:---------------------------|:--------------------------------------------------------|
|
||||
| default_product_id | int | 否 | null | 打开产品列表时默认选中的产品 ID | 月费:`5`;年费:`1`;尝鲜 1 天:`101`;长期VIP(至尊版):`24072401` |
|
||||
| open_device | string | 是 | - | 设备号 | `DEVICE_ID_PLACEHOLDER` |
|
||||
| hide_title | int | 否 | 0 | 是否隐藏购买页推荐人信息:0-不隐藏;1-隐藏 | `0` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl -G 'https://proapi.115.com/open/vip/qr_url' \
|
||||
-H 'Authorization: Bearer ACCESS_TOKEN_PLACEHOLDER' \
|
||||
--data-urlencode 'default_product_id=1' \
|
||||
--data-urlencode 'open_device=DEVICE_ID_PLACEHOLDER' \
|
||||
--data-urlencode 'hide_title=0'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:----------------|:--------|:--------------|
|
||||
| state | boolean | 状态码,true 表示成功 |
|
||||
| message | string | 响应信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | object | 响应数据 |
|
||||
| data.qrcode_url | string | 开放平台产品列表地址 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"qrcode_url": "PRODUCT_LIST_URL_PLACEHOLDER"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 接口根据 `access_token` 识别授权信息,调用方无需额外传入 115 账号和 AppID。
|
||||
- `open_device` 不能为空;未传或传入空值时,接口返回参数错误。
|
||||
- `default_product_id` 未传或为空时,服务端不指定默认产品。
|
||||
- `hide_title` 大于 0 时,服务端会将该参数传递给产品列表服务。
|
||||
- `access_token` 属于敏感凭证,不得写入公开仓库、客户端日志或公开沟通内容。
|
||||
|
||||
### 3. 场景触发与收益
|
||||
|
||||
完成接口接入后,开发者可以在用户使用权限不足、长期存储空间不足或云下载配额不足时触发对应引导。用户完成购买后,开发者可以获得对应推广收益。
|
||||
|
||||
## 四、收益管理与结算
|
||||
|
||||
### 1. 收益查看与提现
|
||||
|
||||
开发者可以登录“115生活-生活-联盟”,或直接访问[115联盟](https://union.115.com/),查看收益明细等推广收益情况并进行提现操作。
|
||||
|
||||

|
||||
|
||||
### 2. 规则说明
|
||||
|
||||
收益计算方式、结算方式和提现流程等详情,参见[联盟规则](https://union.115.com/?ac=help&i=10)。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----------------|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
| 2026年08月05日(周三) 17:30:01 | 恢复业务说明、接入流程及示例图片 |
|
||||
| 2026年08月19日(周三) 11:00:08 | 长期VIP默认产品名称改为至尊版 |
|
||||
@@ -0,0 +1,79 @@
|
||||
## 删除或清空回收站
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:--------------------------------|
|
||||
| 接口名称 | 删除或清空回收站 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /del |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
批量彻底删除回收站中的文件(夹),或在不传 `tid` 时清空回收站。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/rb/del
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----|:-------|:---|:----|:--------------------------------|:------|
|
||||
| tid | string | 否 | "" | 需要删除的回收站ID,多个ID用半角逗号分隔,最多 1150 个;不传时清空回收站 | 1,2,3,4 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/rb/del' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--form-string 'tid=1,2,3,4'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------|:---------|:--------------|
|
||||
| state | boolean | 状态码,true 表示成功 |
|
||||
| message | string | 错误信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | string[] | 响应数据 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||
- 不传 `tid` 会清空当前授权用户的整个回收站,操作后无法恢复。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,80 @@
|
||||
## 删除文件
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:--------------------------------|
|
||||
| 接口名称 | 删除文件 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /delete |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
批量删除文件(夹),删除操作异步执行并将目标移入回收站。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/ufile/delete
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----------|:-------|:---|:----|:-----------------------|:-------------------------------------------|
|
||||
| file_ids | string | 是 | - | 需要删除的文件(夹)ID,多个ID用半角逗号分隔 | 3073323042143855813,3073323042143855822 |
|
||||
| parent_id | string | 否 | 0 | 待删除文件(夹)所在的父目录ID | 3073311192547189943 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/ufile/delete' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--form-string 'file_ids=3073323042143855813,3073323042143855822' \
|
||||
--form-string 'parent_id=3073311192547189943'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------|:---------|:-----------------|
|
||||
| state | boolean | 状态码,true 表示请求已受理 |
|
||||
| message | string | 错误信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | string[] | 响应数据 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,126 @@
|
||||
## 回收站列表
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:--------------------------------|
|
||||
| 接口名称 | 回收站列表 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /list |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
分页获取当前授权用户的回收站文件(夹)列表。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/rb/list
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-------|:----|:---|:----|:--------|:------|
|
||||
| limit | int | 否 | 30 | 单页记录数 | 最大 200 |
|
||||
| offset | int | 否 | 0 | 数据显示偏移量 | 0 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl -G 'https://proapi.115.com/open/rb/list' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--data-urlencode 'limit=30' \
|
||||
--data-urlencode 'offset=0'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:----------------------|:--------|:-------------------------------|
|
||||
| state | boolean | 状态码,true 表示成功 |
|
||||
| message | string | 错误信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | object | 响应数据 |
|
||||
| data.offset | int | 数据显示偏移量 |
|
||||
| data.limit | int | 单页记录数 |
|
||||
| data.count | string | 回收站文件(夹)总数 |
|
||||
| data.rb_pass | int | 是否设置回收站密码:1-是,0-否 |
|
||||
| data.{回收站ID} | object | 以回收站ID为键的文件(夹)信息 |
|
||||
| data.{回收站ID}.id | string | 回收站ID |
|
||||
| data.{回收站ID}.file_name | string | 文件(夹)名称 |
|
||||
| data.{回收站ID}.type | string | 类型:1-文件,2-文件夹 |
|
||||
| data.{回收站ID}.file_size | string | 文件大小,单位为字节 |
|
||||
| data.{回收站ID}.dtime | string | 删除时间 |
|
||||
| data.{回收站ID}.thumb_url | string | 缩略图地址 |
|
||||
| data.{回收站ID}.status | string | 还原状态:-1-还原中,0-正常 |
|
||||
| data.{回收站ID}.cid | int | 原文件(夹)的父目录ID |
|
||||
| data.{回收站ID}.parent_name | string | 原文件(夹)的父目录名称 |
|
||||
| data.{回收站ID}.pick_code | string | 文件提取码 |
|
||||
| data.{回收站ID}.isv | int | 是否为视频文件,按文件类型返回 |
|
||||
| data.{回收站ID}.def2 | int | 视频清晰度,按文件类型返回 |
|
||||
| data.{回收站ID}.ico | string | 文件扩展名,按文件类型返回 |
|
||||
| data.{回收站ID}.muc | string | 音频封面地址,按文件类型返回 |
|
||||
| data.{回收站ID}.d_img | string | 文档缩略图地址,按文件类型返回 |
|
||||
| data.{回收站ID}.play_long | int | 音视频时长,按文件类型返回 |
|
||||
| data.{回收站ID}.sha1 | string | 文件 SHA1 值,按文件类型返回 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"offset": 0,
|
||||
"limit": 30,
|
||||
"count": "0",
|
||||
"rb_pass": 0,
|
||||
"3074054555277845747": {
|
||||
"id": "3074054555277845747",
|
||||
"file_name": "",
|
||||
"type": "1",
|
||||
"file_size": "0",
|
||||
"dtime": "",
|
||||
"thumb_url": "",
|
||||
"status": "0",
|
||||
"cid": 0,
|
||||
"parent_name": "",
|
||||
"pick_code": "",
|
||||
"isv": 0,
|
||||
"def2": 0,
|
||||
"ico": "",
|
||||
"muc": "",
|
||||
"d_img": "",
|
||||
"play_long": 0,
|
||||
"sha1": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
| 2026年08月05日(周三) 17:15:28 | 补充单页记录数上限 |
|
||||
@@ -0,0 +1,78 @@
|
||||
## 回收站还原
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:--------------------------------|
|
||||
| 接口名称 | 回收站还原 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /revert |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
批量还原回收站中的文件(夹)。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/rb/revert
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----|:-------|:---|:----|:---------------------------|:--------------|
|
||||
| tid | string | 是 | - | 需要还原的回收站ID,多个ID用半角逗号分隔,最多 1150 个 | 111,222,333,444 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/rb/revert' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--form-string 'tid=111,222,333,444'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------|:---------|:--------------|
|
||||
| state | boolean | 状态码,true 表示成功 |
|
||||
| message | string | 错误信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | string[] | 响应数据 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,87 @@
|
||||
## 文件(夹)更新
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:--------------------------------|
|
||||
| 接口名称 | 文件(夹)更新 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /update |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
更新文件(夹)名称或星标状态。`file_name` 与 `star` 至少传入一个。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/ufile/update
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:---------|:-------|:---|:----|:----------------------|:------------------|
|
||||
| file_id | string | 是 | - | 需要更新的文件(夹)ID | 3073323042143855813 |
|
||||
| file_name | string | 否 | null | 新的文件(夹)名称,文件夹名称限制 255 字节 | 新的名字 |
|
||||
| star | int | 否 | null | 是否星标:1-星标,0-取消星标 | 1 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/ufile/update' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--form-string 'file_id=3073323042143855813' \
|
||||
--form-string 'file_name=新的名字' \
|
||||
--form-string 'star=1'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:---------------|:--------|:------------------|
|
||||
| state | boolean | 状态码,true 表示成功 |
|
||||
| message | string | 错误信息 |
|
||||
| code | int | 错误码 |
|
||||
| data | object | 响应数据 |
|
||||
| data.file_name | string | 更新后的文件(夹)名称 |
|
||||
| data.star | int | 更新后的星标状态:1-星标,0-取消星标 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"file_name": "新的名字",
|
||||
"star": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,41 @@
|
||||
# 上传流程
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:---------|:------------------------------|
|
||||
| 文档名称 | 上传流程 |
|
||||
| 文档版本 | v1.0 |
|
||||
| 适用场景 | 115开放平台文件上传 |
|
||||
|
||||
### 文档说明
|
||||
|
||||
本文档说明 115 开放平台的文件秒传、普通上传和断点续传流程。
|
||||
|
||||
## 流程概览
|
||||
|
||||
1. 请求「文件上传」接口初始化上传。
|
||||
2. 若响应中 `status=2`,表示秒传成功,上传流程结束。
|
||||
3. 若响应提示需要二次认证,按 `sign_check` 指定的字节范围计算大写 SHA1,然后携带 `sign_key` 和 `sign_val` 重新请求「文件上传」接口。
|
||||
4. 若 `status=1`,携带初始化响应中的 `bucket`、`object`、`callback` 及「获取上传凭证」接口返回的凭证,向对象存储上传文件。
|
||||
5. 需要续传时,携带初始化响应中的 `pick_code` 及待上传文件信息请求「断点续传」接口,获取新的对象存储上传参数。
|
||||
6. 对象存储返回上传成功后,普通上传或断点续传完成。
|
||||
|
||||
## 相关接口
|
||||
|
||||
- [文件上传](文件上传.md)
|
||||
- [获取上传凭证](获取上传凭证.md)
|
||||
- [断点续传](断点续传.md)
|
||||
- [阿里云 OSS 上传文件说明](https://help.aliyun.com/zh/oss/user-guide/upload-objects-to-oss/)
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 对象存储上传不请求 `proapi.115.com`,应使用「获取上传凭证」和上传调度接口返回的域名、对象标识、临时凭证与回调参数发起请求。
|
||||
- `sign_check` 的起止字节均在 SHA1 计算范围内。例如 `0-99` 表示计算共 100 字节的内容。
|
||||
- 调用开放平台接口时必须携带 `Authorization: Bearer access_token`,并妥善保管临时上传凭证。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,141 @@
|
||||
## 文件上传
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:---------|:------------------------------|
|
||||
| 接口名称 | 文件上传 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /upload/init |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
初始化断点续传调度,完成秒传判定、二次认证调度或返回对象存储上传参数。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/upload/init
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----------|:-----|:---|:----|:---------------------------|:--------------------|
|
||||
| file_name | string | 是 | - | 文件名 | 图片.jpg |
|
||||
| file_size | int | 是 | - | 文件大小,单位为字节 | 5335 |
|
||||
| target | string | 是 | - | 文件上传目标 | `U_1_0`,格式为 `U_1_<文件夹ID>` |
|
||||
| fileid | string | 是 | - | 文件 SHA1 值 | `<FILE_SHA1>` |
|
||||
| preid | string | 否 | "" | 文件前 128 KiB 内容的 SHA1 值 | `<PREID_SHA1>` |
|
||||
| path | string | 否 | "" | 上传路径 | "" |
|
||||
| pick_code | string | 否 | "" | 上传任务唯一标识,用于续传 | `<PICK_CODE>` |
|
||||
| topupload | int | 否 | "" | 上传调度文件类型标记,见下方枚举表格 | 0 |
|
||||
| sign_key | string | 否 | "" | 二次认证标识 | `<SIGN_KEY>` |
|
||||
| sign_val | string | 否 | "" | 根据 `sign_check` 计算的大写 SHA1 值 | `<SIGN_VAL>` |
|
||||
|
||||
#### 请求参数中的 topupload 字段枚举
|
||||
|
||||
| 值 | 说明 | 备注 |
|
||||
|:---|:---------------------------|:---|
|
||||
| -1 | 没有上传调度文件类型标记 | - |
|
||||
| 0 | 单文件上传任务,记录一条独立上传记录 | - |
|
||||
| 1 | 文件夹任务的第一个子文件,记录一次文件夹上传 | - |
|
||||
| 2 | 文件夹任务的其他子文件,不单独记录上传记录 | - |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/upload/init' \
|
||||
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
|
||||
-F 'file_name=图片.jpg' \
|
||||
-F 'file_size=5335' \
|
||||
-F 'target=U_1_0' \
|
||||
-F 'fileid=<FILE_SHA1>' \
|
||||
-F 'topupload=0'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------------------------|:------|:----------------------------------------|
|
||||
| state | boolean | 状态码,是表示成功,否表示异常 |
|
||||
| message | string | 异常信息 |
|
||||
| code | int | 异常码 |
|
||||
| data | object | 上传调度数据 |
|
||||
| data.status | int | 上传状态:1-非秒传 2-秒传 |
|
||||
| data.code | int | 上传调度状态码 |
|
||||
| data.pick_code | string | 上传任务唯一标识,用于续传 |
|
||||
| data.target | string | 文件上传目标 |
|
||||
| data.bucket | string | 对象存储 bucket |
|
||||
| data.object | string | OSS 对象标识 |
|
||||
| data.callback | object | 上传完成回调数据 |
|
||||
| data.callback.callback | string | 上传完成回调信息 |
|
||||
| data.callback.callback_var | string | 上传完成回调参数 |
|
||||
| data.sign_key | string | 本次二次认证的 SHA1 标识 |
|
||||
| data.sign_check | string | 二次认证所需本地文件 SHA1 计算的字节范围 |
|
||||
| data.file_id | string | 秒传成功时新增文件 ID |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"status": 1,
|
||||
"code": 0,
|
||||
"pick_code": "",
|
||||
"target": "U_1_0",
|
||||
"bucket": "",
|
||||
"object": "",
|
||||
"callback": {
|
||||
"callback": "",
|
||||
"callback_var": ""
|
||||
},
|
||||
"sign_key": "",
|
||||
"sign_check": "",
|
||||
"file_id": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- `target` 必须匹配 `U_1_<数字文件夹ID>`;`U_1_0` 表示网盘根目录。
|
||||
- 非 VIP 用户单文件大小不得超过 5 GiB;尝鲜VIP、体验VIP用户单文件大小不得超过 15 GiB。
|
||||
- 上传前会检查用户剩余空间和盗播上传封禁状态。
|
||||
- 二次认证调度结果见下表。
|
||||
|
||||
| code | status | 说明 | 后续处理 |
|
||||
|:-----|:-------|:---------|:------|
|
||||
| 700 | 6 | 签名认证后失败 | 按 `sign_check` 截取包含起止字节的文件内容计算大写 SHA1,再传入 `sign_key` 和 `sign_val` |
|
||||
| 701 | 7 | 需要认证签名 | 按 `sign_check` 截取包含起止字节的文件内容计算大写 SHA1,再传入 `sign_key` 和 `sign_val` |
|
||||
| 702 | 8 | 签名认证失败 | 按 `sign_check` 截取包含起止字节的文件内容计算大写 SHA1,再传入 `sign_key` 和 `sign_val` |
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `sign_check` 格式为 `起始字节-结束字节`,起止字节均包含在 SHA1 计算范围内;例如 `2392148-2392298` 需计算该范围内文件内容的 SHA1。
|
||||
- 省略 `topupload` 时,服务端会将其归一化为空字符串;显式传入枚举值时会归一化为整数。
|
||||
- 请妥善保管 `access_token`、上传凭证与回调数据,不要在日志或客户端可见信息中输出。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,110 @@
|
||||
## 断点续传
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:---------|:------------------------------|
|
||||
| 接口名称 | 断点续传 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /upload/resume |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
根据已有上传任务和待上传文件信息,获取断点续传所需的对象存储上传参数。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/upload/resume
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----------|:-----|:---|:----|:------------------|:--------------------|
|
||||
| file_size | int | 是 | - | 文件大小,单位为字节 | 5335 |
|
||||
| target | string | 是 | - | 文件上传目标 | `U_1_0`,格式为 `U_1_<文件夹ID>` |
|
||||
| fileid | string | 是 | - | 文件 SHA1 值 | `<FILE_SHA1>` |
|
||||
| pick_code | string | 是 | - | 上传任务唯一标识 | `<PICK_CODE>` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/upload/resume' \
|
||||
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
|
||||
-F 'file_size=5335' \
|
||||
-F 'target=U_1_0' \
|
||||
-F 'fileid=<FILE_SHA1>' \
|
||||
-F 'pick_code=<PICK_CODE>'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------------------------|:------|:----------------------|
|
||||
| state | boolean | 状态码,是表示成功,否表示异常 |
|
||||
| message | string | 异常信息 |
|
||||
| code | int | 异常码 |
|
||||
| data | object | 续传调度数据 |
|
||||
| data.version | string | 上传接口版本 |
|
||||
| data.target | string | 文件上传目标 |
|
||||
| data.pick_code | string | 上传任务唯一标识 |
|
||||
| data.bucket | string | 对象存储 bucket |
|
||||
| data.object | string | OSS 对象标识 |
|
||||
| data.callback | object | 上传完成回调数据 |
|
||||
| data.callback.callback | string | 上传完成回调信息 |
|
||||
| data.callback.callback_var | string | 上传完成回调参数 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"version": "",
|
||||
"target": "U_1_0",
|
||||
"pick_code": "",
|
||||
"bucket": "",
|
||||
"object": "",
|
||||
"callback": {
|
||||
"callback": "",
|
||||
"callback_var": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- `target` 必须匹配 `U_1_<数字文件夹ID>`;`U_1_0` 表示网盘根目录。
|
||||
- 非 VIP 用户单文件大小不得超过 5 GiB;尝鲜VIP、体验VIP用户单文件大小不得超过 15 GiB。
|
||||
- 续传前会检查用户剩余空间和盗播上传封禁状态。
|
||||
- 上游调度结果的 `status` 只有为 1 或 2 时才按成功响应返回续传参数。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `pick_code` 必须来自原上传初始化调度响应,并与当前文件信息匹配。
|
||||
- 请妥善保管 `access_token`、上传凭证与回调数据,不要在日志或客户端可见信息中输出。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,87 @@
|
||||
## 获取上传凭证
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:---------|:------------------------------|
|
||||
| 接口名称 | 获取上传凭证 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /upload/get_token |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
获取对象存储上传域名和临时上传凭证。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/upload/get_token
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
无
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/upload/get_token' \
|
||||
-H 'Authorization: Bearer <ACCESS_TOKEN>'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:-------------------|:------|:---------------|
|
||||
| state | boolean | 状态码,是表示成功,否表示异常 |
|
||||
| message | string | 异常信息 |
|
||||
| code | int | 异常码 |
|
||||
| data | object | 上传凭证数据 |
|
||||
| data.endpoint | string | 上传域名 |
|
||||
| data.AccessKeySecret | string | 临时上传凭证密钥 |
|
||||
| data.SecurityToken | string | 临时安全令牌 |
|
||||
| data.Expiration | string | 上传凭证过期时间 |
|
||||
| data.AccessKeyId | string | 临时上传凭证 ID |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"endpoint": "",
|
||||
"AccessKeySecret": "",
|
||||
"SecurityToken": "",
|
||||
"Expiration": "",
|
||||
"AccessKeyId": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 网页版文档中的密钥字段名存在拼写误差,服务端实际返回字段为 `AccessKeySecret`。
|
||||
- 上传凭证为敏感信息,仅用于当前上传流程;不要写入日志、持久化存储或对外暴露。
|
||||
- 服务端通过当前 `access_token` 识别用户,不接收用户账号请求参数。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,84 @@
|
||||
## 文件复制
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 文件复制 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /copy |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
将一个或多个文件、文件夹复制到指定目录。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/ufile/copy
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:--------|:-------|:---|:----|:------------------------------------|:--------------------|
|
||||
| pid | string | 否 | "0" | 目标目录ID,根目录ID为 `0` | 1054251402869818368 |
|
||||
| file_id | string | 是 | - | 待复制的文件或文件夹ID,多个ID使用半角逗号分隔 | 2323423573680609857 |
|
||||
| nodupli | int | 否 | 0 | 目标目录是否不允许同名:0-允许,1-不允许 | 1 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/ufile/copy' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--form-string 'pid=1054251402869818368' \
|
||||
--form-string 'file_id=2323423573680609857' \
|
||||
--form-string 'nodupli=1'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------|:---------|:--------------------|
|
||||
| state | boolean | 接口状态,true表示成功 |
|
||||
| message | string | 异常信息 |
|
||||
| code | int | 异常码 |
|
||||
| data | object[] | 响应数据 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `pid` 不传或传 `0` 时复制到根目录。
|
||||
- 接口受文件操作频率限制;触发限制时返回异常。
|
||||
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,121 @@
|
||||
## 文件搜索
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 文件搜索 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /search |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
根据文件名搜索文件或文件夹,支持按目录、文件标签、时间范围和文件类型筛选。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/ufile/search
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:------------|:-------|:---|:----|:----------------------------------------------------------|:---------|
|
||||
| search_value | string | 否 | "" | 搜索关键词;与 `file_label` 至少传一个,最多取前40个字符 | "文件" |
|
||||
| limit | int | 否 | 20 | 单页记录数;`offset + limit` 最大不超过10000 | 20 |
|
||||
| offset | int | 否 | 0 | 数据显示偏移量 | 0 |
|
||||
| file_label | string | 否 | "" | 文件标签;与 `search_value` 至少传一个 | "1" |
|
||||
| cid | int | 否 | 0 | 目标目录ID;`-1` 表示不返回任何列表内容 | 0 |
|
||||
| gte_day | string | 否 | "" | 搜索结果匹配的开始日期 | 2020-11-19 |
|
||||
| lte_day | string | 否 | "" | 搜索结果匹配的结束日期 | 2020-11-20 |
|
||||
| fc | int | 否 | 0 | 显示类型:1-只显示文件夹,2-只显示文件,0-全部 | 0 |
|
||||
| type | int | 否 | 0 | 一级筛选大分类,见下方枚举表格 | 1 |
|
||||
| suffix | string | 否 | "" | 一级筛选选择“其他”时填写的后缀名 | "pdf" |
|
||||
|
||||
#### 请求参数中的 type 字段枚举
|
||||
|
||||
| 值 | 说明 | 备注 |
|
||||
|:--|:----|:---|
|
||||
| 1 | 文档 | - |
|
||||
| 2 | 图片 | - |
|
||||
| 3 | 音频 | - |
|
||||
| 4 | 视频 | - |
|
||||
| 5 | 压缩包 | - |
|
||||
| 6 | 应用 | - |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl -G 'https://proapi.115.com/open/ufile/search' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--data-urlencode 'search_value=文件' \
|
||||
--data-urlencode 'limit=20' \
|
||||
--data-urlencode 'offset=0'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:-------------------|:---------|:----------------------------------------|
|
||||
| count | int | 符合条件的文件或文件夹总数 |
|
||||
| data | object[] | 文件或文件夹列表 |
|
||||
| data[].file_id | string | 文件或文件夹ID |
|
||||
| data[].user_id | string | 115账号 |
|
||||
| data[].sha1 | string | 文件SHA-1值 |
|
||||
| data[].file_name | string | 文件或文件夹名称 |
|
||||
| data[].file_size | string | 文件大小 |
|
||||
| data[].user_ptime | string | 上传时间 |
|
||||
| data[].user_utime | string | 更新时间 |
|
||||
| data[].pick_code | string | 文件提取码 |
|
||||
| data[].parent_id | string | 父目录ID |
|
||||
| data[].area_id | string | 文件状态:1-正常,7-已删除(回收站),120-彻底删除 |
|
||||
| data[].is_private | int | 文件是否隐藏:0-未隐藏,1-已隐藏 |
|
||||
| data[].file_category | string | 文件属性:1-文件,0-文件夹 |
|
||||
| data[].ico | string | 文件后缀 |
|
||||
| limit | int | 分页数量 |
|
||||
| offset | int | 偏移量 |
|
||||
| state | boolean | 接口状态,true表示成功 |
|
||||
| message | string | 异常信息 |
|
||||
| code | int | 异常码 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 0,
|
||||
"data": [],
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `search_value` 与 `file_label` 至少传一个;搜索关键词最多取前40个字符。
|
||||
- 当 `offset + limit` 超过10000时,服务端按 `offset=0`、`limit=115` 查询。
|
||||
- 搜索结果会过滤不属于正常区域的文件;当前账号未开启隐藏文件展示时,也会过滤隐藏文件。
|
||||
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,91 @@
|
||||
## 文件移动
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 文件移动 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /move |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
将一个或多个文件、文件夹移动到指定目录。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/ufile/move
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:---------|:-------|:---|:----|:------------------------------------|:----------------------------------------|
|
||||
| file_ids | string | 是 | - | 待移动的文件或文件夹ID,多个ID使用半角逗号分隔 | 3073323042143855813,3073323042143855822 |
|
||||
| to_cid | string | 否 | "0" | 目标目录ID;`0` 表示根目录,非 `0` 时必须指向正常可用的目录 | 3073311192547189943 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/ufile/move' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--form-string 'file_ids=3073323042143855813,3073323042143855822' \
|
||||
--form-string 'to_cid=3073311192547189943'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------|:---------|:--------------------|
|
||||
| state | boolean | 接口状态,true表示成功 |
|
||||
| message | string | 异常信息 |
|
||||
| code | int | 异常码 |
|
||||
| data | object[] | 响应数据 |
|
||||
|
||||
#### 响应的 code 字段(错误码)说明
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|:-------|:-----------------------------------|:-----------------------------|
|
||||
| 20009 | 目标目录不存在或目标ID不是目录 | 确认 to_cid 指向存在的目录 |
|
||||
| 20018 | 目标目录不在正常区域或已经删除 | 选择正常可用的目标目录 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `to_cid` 不传或传 `0` 时移动到根目录。
|
||||
- `to_cid` 非 `0` 时,目标必须是正常可用的目录;目标不存在、不是目录或已经删除时,移动失败。
|
||||
- 接口受文件操作频率限制;触发限制时返回异常。
|
||||
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:---------------------------------|:---------------------------------------|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
| 2026年08月14日(周五) 15:38:44 | 补充移动目标目录有效性约束及错误码 |
|
||||
@@ -0,0 +1,86 @@
|
||||
## 新建文件夹
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 新建文件夹 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /add |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
在指定父目录下新建文件夹。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/folder/add
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:---------|:-------|:---|:----|:---------------------------|:--------------------|
|
||||
| pid | string | 否 | "0" | 父目录ID,根目录ID为 `0` | 3073323042143855813 |
|
||||
| file_name | string | 是 | - | 文件夹名称,最多255个字符 | 新建文件夹名称 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/folder/add' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--form-string 'pid=3073323042143855813' \
|
||||
--form-string 'file_name=新建文件夹名称'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:---------------|:--------|:----------------------|
|
||||
| state | boolean | 接口状态,true表示成功 |
|
||||
| message | string | 异常信息 |
|
||||
| code | int | 异常码 |
|
||||
| data | object | 响应数据 |
|
||||
| data.file_name | string | 新建的文件夹名称 |
|
||||
| data.file_id | string | 新建的文件夹ID |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"file_name": "",
|
||||
"file_id": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `pid` 不传或传 `0` 时在根目录下新建文件夹。
|
||||
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,114 @@
|
||||
## 按ID获取
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 按ID获取 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /get_info |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
根据文件或文件夹ID获取详情。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/folder/get_info
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:---------|:-------|:---|:----|:----------|:--------------------|
|
||||
| file_id | string | 是 | - | 文件或文件夹ID | 1288444975268439877 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl -G 'https://proapi.115.com/open/folder/get_info' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--data-urlencode 'file_id=1288444975268439877'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:----------------------|:---------|:----------------------------------------|
|
||||
| state | boolean | 接口状态,true表示成功 |
|
||||
| message | string | 异常信息 |
|
||||
| code | int | 异常码 |
|
||||
| data | object | 文件或文件夹详情 |
|
||||
| data.count | int | 包含的文件总数 |
|
||||
| data.size | string | 文件或文件夹总大小 |
|
||||
| data.size_byte | int | 文件或文件夹总大小,单位为字节 |
|
||||
| data.folder_count | int | 包含的文件夹总数 |
|
||||
| data.play_long | int | 视频时长;`-1` 表示正在统计,其他数值单位为秒 |
|
||||
| data.show_play_long | int | 是否开启展示视频时长 |
|
||||
| data.ptime | string | 上传时间 |
|
||||
| data.utime | string | 修改时间 |
|
||||
| data.file_name | string | 文件或文件夹名称 |
|
||||
| data.pick_code | string | 文件提取码 |
|
||||
| data.sha1 | string | 文件SHA-1值 |
|
||||
| data.file_id | string | 文件或文件夹ID |
|
||||
| data.is_mark | string | 是否星标 |
|
||||
| data.open_time | int | 文件或文件夹最近打开时间 |
|
||||
| data.file_category | string | 文件属性:1-文件,0-文件夹 |
|
||||
| data.paths | object[] | 文件或文件夹所在路径 |
|
||||
| data.paths[].file_id | string | 父目录ID |
|
||||
| data.paths[].file_name | string | 父目录名称 |
|
||||
| data.paths[].iss | int | 父目录共享状态标识 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"count": 0,
|
||||
"size": "",
|
||||
"size_byte": 0,
|
||||
"folder_count": 0,
|
||||
"play_long": 0,
|
||||
"show_play_long": 0,
|
||||
"ptime": "",
|
||||
"utime": "",
|
||||
"file_name": "",
|
||||
"pick_code": "",
|
||||
"sha1": "",
|
||||
"file_id": "",
|
||||
"is_mark": "",
|
||||
"open_time": 0,
|
||||
"file_category": "",
|
||||
"paths": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 文件或文件夹已进入回收站或被彻底删除时,接口返回异常。
|
||||
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,115 @@
|
||||
## 按路径获取
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 按路径获取 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /get_info |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
根据文件或文件夹路径获取详情。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/folder/get_info
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----|:-------|:---|:----|:----------------------------------------------------------------|:-------------------|
|
||||
| path | string | 是 | - | 文件路径,支持 `/`、`>` 两种分隔符;路径需以分隔符开头,并用同一分隔符分隔目录层级 | /a/b/c.png 或 >a>b>c |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/folder/get_info' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--form-string 'path=/a/b/c.png'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:----------------------|:---------|:----------------------------------------|
|
||||
| state | boolean | 接口状态,true表示成功 |
|
||||
| message | string | 异常信息 |
|
||||
| code | int | 异常码 |
|
||||
| data | object | 文件或文件夹详情 |
|
||||
| data.count | int | 包含的文件总数 |
|
||||
| data.size | string | 文件或文件夹总大小 |
|
||||
| data.size_byte | int | 文件或文件夹总大小,单位为字节 |
|
||||
| data.folder_count | int | 包含的文件夹总数 |
|
||||
| data.play_long | int | 视频时长;`-1` 表示正在统计,其他数值单位为秒 |
|
||||
| data.show_play_long | int | 是否开启展示视频时长 |
|
||||
| data.ptime | string | 上传时间 |
|
||||
| data.utime | string | 修改时间 |
|
||||
| data.file_name | string | 文件或文件夹名称 |
|
||||
| data.pick_code | string | 文件提取码 |
|
||||
| data.sha1 | string | 文件SHA-1值 |
|
||||
| data.file_id | string | 文件或文件夹ID |
|
||||
| data.is_mark | string | 是否星标 |
|
||||
| data.open_time | int | 文件或文件夹最近打开时间 |
|
||||
| data.file_category | string | 文件属性:1-文件,0-文件夹 |
|
||||
| data.paths | object[] | 文件或文件夹所在路径 |
|
||||
| data.paths[].file_id | string | 父目录ID |
|
||||
| data.paths[].file_name | string | 父目录名称 |
|
||||
| data.paths[].iss | int | 父目录共享状态标识 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"count": 0,
|
||||
"size": "",
|
||||
"size_byte": 0,
|
||||
"folder_count": 0,
|
||||
"play_long": 0,
|
||||
"show_play_long": 0,
|
||||
"ptime": "",
|
||||
"utime": "",
|
||||
"file_name": "",
|
||||
"pick_code": "",
|
||||
"sha1": "",
|
||||
"file_id": "",
|
||||
"is_mark": "",
|
||||
"open_time": 0,
|
||||
"file_category": "",
|
||||
"paths": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 文件或文件夹已进入回收站或被彻底删除时,接口返回异常。
|
||||
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,98 @@
|
||||
## 获取文件下载地址
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:--------------------------------|
|
||||
| 接口名称 | 获取文件下载地址 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /downurl |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
根据文件提取码获取文件下载地址。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/ufile/downurl
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:---------|:-------|:---|:----|:------|:-----------------|
|
||||
| pick_code | string | 是 | - | 文件提取码,多个提取码用半角逗号分隔 | dtctprlmfkl4exiok |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/ufile/downurl' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--form-string 'pick_code=dtctprlmfkl4exiok'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:---------------------------|:--------|:---------------------------|
|
||||
| state | boolean | 状态码,true 表示成功 |
|
||||
| message | string | 错误信息 |
|
||||
| code | int | 错误码 |
|
||||
| errno | int | 下载地址获取失败时返回的错误码 |
|
||||
| data | object | 响应数据 |
|
||||
| data.{文件ID} | object | 以文件ID为键的文件下载信息 |
|
||||
| data.{文件ID}.file_name | string | 文件名 |
|
||||
| data.{文件ID}.file_size | int | 文件大小,单位为字节 |
|
||||
| data.{文件ID}.pick_code | string | 文件提取码 |
|
||||
| data.{文件ID}.sha1 | string | 文件 SHA1 值 |
|
||||
| data.{文件ID}.url | object | 下载地址信息 |
|
||||
| data.{文件ID}.url.url | string | 文件下载地址 |
|
||||
| data.can_appeal | boolean | 文件违规时是否可以申诉,按错误场景返回 |
|
||||
| data.want_appeal_id | string | 文件违规时的申诉标识,按错误场景返回 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"2323423573680609857": {
|
||||
"file_name": "",
|
||||
"file_size": 0,
|
||||
"pick_code": "",
|
||||
"sha1": "",
|
||||
"url": {
|
||||
"url": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,204 @@
|
||||
## 获取文件列表
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 获取文件列表 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /files |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
获取指定目录中的文件和文件夹列表,支持分页、排序以及按文件类型、后缀名和星标状态筛选。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/ufile/files
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-----------|:-------|:---|:----------|:-------------------------------------------------------|:----------|
|
||||
| cid | string | 否 | "0" | 目录ID,对应 `parent_id`;根目录ID为 `0` | "0" |
|
||||
| type | int | 否 | 0 | 文件类型,见下方枚举表格 | 1 |
|
||||
| limit | int | 否 | 20 | 查询数量,最大1150 | 20 |
|
||||
| offset | int | 否 | 0 | 查询起始位置 | 0 |
|
||||
| suffix | string | 否 | "" | 文件后缀名 | "pdf" |
|
||||
| asc | int | 否 | 0 | 排序方向:1-升序,0-降序 | 0 |
|
||||
| o | string | 否 | user_ptime | 排序字段,见下方枚举表格 | file_name |
|
||||
| custom_order | int | 否 | 0 | 排序模式,见下方枚举表格 | 0 |
|
||||
| stdir | int | 否 | 0 | 筛选文件时是否显示文件夹:1-显示,0-不显示 | 1 |
|
||||
| star | int | 否 | 0 | 星标筛选:1-仅显示星标文件,0-全部 | 0 |
|
||||
| cur | int | 否 | 0 | 是否只显示当前文件夹内的文件:1-是,0-否 | 1 |
|
||||
| show_dir | int | 否 | 0 | 是否显示目录:1-是,0-否 | 0 |
|
||||
|
||||
#### 请求参数中的 type 字段枚举
|
||||
|
||||
| 值 | 说明 | 备注 |
|
||||
|:--|:----|:---|
|
||||
| 1 | 文档 | - |
|
||||
| 2 | 图片 | - |
|
||||
| 3 | 音频 | - |
|
||||
| 4 | 视频 | - |
|
||||
| 5 | 压缩包 | - |
|
||||
| 6 | 应用 | - |
|
||||
| 7 | 书籍 | - |
|
||||
|
||||
#### 请求参数中的 o 字段枚举
|
||||
|
||||
| 值 | 说明 | 备注 |
|
||||
|:-----------|:-------|:---|
|
||||
| file_name | 文件名 | - |
|
||||
| file_size | 文件大小 | - |
|
||||
| user_ptime | 上传时间 | 默认值 |
|
||||
| user_utime | 更新时间 | - |
|
||||
| file_type | 文件类型 | - |
|
||||
|
||||
#### 请求参数中的 custom_order 字段枚举
|
||||
|
||||
| 值 | 说明 | 备注 |
|
||||
|:--|:----------------------|:---|
|
||||
| 0 | 使用记忆排序,自定义排序失效 | 默认值 |
|
||||
| 1 | 使用自定义排序,不使用记忆排序 | - |
|
||||
| 2 | 使用自定义排序,非文件夹置顶 | - |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl -G 'https://proapi.115.com/open/ufile/files' \
|
||||
-H 'Authorization: Bearer access_token' \
|
||||
--data-urlencode 'cid=0' \
|
||||
--data-urlencode 'limit=20' \
|
||||
--data-urlencode 'offset=0'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:----------------------|:---------|:----------------------------------------------------------|
|
||||
| data | object[] | 文件和文件夹列表 |
|
||||
| data[].fid | string | 文件或文件夹ID |
|
||||
| data[].aid | string | 文件状态:1-正常,7-已删除(回收站),120-彻底删除 |
|
||||
| data[].pid | string | 父目录ID |
|
||||
| data[].fc | string | 文件分类:0-文件夹,1-文件 |
|
||||
| data[].fn | string | 文件或文件夹名称 |
|
||||
| data[].fco | string | 文件夹封面 |
|
||||
| data[].ism | string | 是否星标,1表示星标 |
|
||||
| data[].isp | int | 是否加密,1表示加密 |
|
||||
| data[].pc | string | 文件提取码 |
|
||||
| data[].upt | int | 修改时间 |
|
||||
| data[].uet | int | 修改时间 |
|
||||
| data[].uppt | int | 上传时间 |
|
||||
| data[].cm | int | 特殊目录标识 |
|
||||
| data[].fdesc | string | 文件备注 |
|
||||
| data[].ispl | int | 是否统计文件夹下视频时长 |
|
||||
| data[].fl | object[] | 文件标签 |
|
||||
| data[].fl[].id | string | 文件标签ID |
|
||||
| data[].fl[].name | string | 文件标签名称 |
|
||||
| data[].fl[].sort | string | 文件标签排序 |
|
||||
| data[].fl[].color | string | 文件标签颜色 |
|
||||
| data[].fl[].is_default | int | 文件标签类型:0-最近使用,1-非最近使用,2-默认标签 |
|
||||
| data[].fl[].update_time | int | 文件标签更新时间 |
|
||||
| data[].fl[].create_time | int | 文件标签创建时间 |
|
||||
| data[].sha1 | string | 文件SHA-1值 |
|
||||
| data[].fs | int | 文件大小,单位为字节 |
|
||||
| data[].fta | string | 文件状态:0或2-未上传完成,1-已上传完成 |
|
||||
| data[].ico | string | 文件后缀名 |
|
||||
| data[].fatr | string | 音频长度 |
|
||||
| data[].isv | int | 是否为视频 |
|
||||
| data[].def | int | 视频清晰度:1-标清,2-高清,3-超清,4-1080P,5-4K,100-原画 |
|
||||
| data[].def2 | int | 视频清晰度:1-标清,2-高清,3-超清,4-1080P,5-4K,100-原画 |
|
||||
| data[].play_long | int | 音视频时长,单位为秒 |
|
||||
| data[].v_img | string | 视频缩略图地址 |
|
||||
| data[].thumb | string | 图片缩略图地址 |
|
||||
| data[].uo | string | 原图地址 |
|
||||
| count | int | 当前目录文件数量 |
|
||||
| sys_count | int | 系统文件夹数量 |
|
||||
| offset | int | 偏移量 |
|
||||
| limit | int | 分页数量 |
|
||||
| aid | string | 文件状态:1-正常,7-已删除(回收站),120-彻底删除 |
|
||||
| cid | int | 父目录ID |
|
||||
| is_asc | int | 排序方向:1-升序,0-降序 |
|
||||
| min_size | int | 最小文件大小筛选值 |
|
||||
| max_size | int | 最大文件大小筛选值 |
|
||||
| sys_dir | string | 系统目录 |
|
||||
| hide_data | string | 是否返回文件数据 |
|
||||
| record_open_time | string | 是否记录文件夹打开时间 |
|
||||
| star | int | 是否星标:1-星标,0-未星标 |
|
||||
| type | int | 一级筛选大分类,见请求参数中的 `type` 字段枚举 |
|
||||
| suffix | string | 一级筛选选择“其他”时填写的后缀名 |
|
||||
| path | object[] | 父目录树 |
|
||||
| path[].name | string | 父目录名称 |
|
||||
| path[].aid | int | 父目录文件状态 |
|
||||
| path[].cid | int | 父目录ID |
|
||||
| path[].pid | int | 上级父目录ID |
|
||||
| path[].isp | int | 父目录是否加密 |
|
||||
| path[].p_cid | string | 父目录路径标识 |
|
||||
| path[].fv | string | 父目录属性 |
|
||||
| cur | int | 是否只显示当前文件夹内的文件 |
|
||||
| stdir | int | 筛选文件时是否显示文件夹 |
|
||||
| fields | string | 指定返回字段 |
|
||||
| order | string | 实际使用的排序字段 |
|
||||
| state | boolean | 接口状态,true表示成功 |
|
||||
| code | int | 异常码 |
|
||||
| message | string | 异常信息 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [],
|
||||
"count": 0,
|
||||
"sys_count": 0,
|
||||
"offset": 0,
|
||||
"limit": 20,
|
||||
"aid": "1",
|
||||
"cid": 0,
|
||||
"is_asc": 0,
|
||||
"min_size": 0,
|
||||
"max_size": 0,
|
||||
"sys_dir": "",
|
||||
"hide_data": "",
|
||||
"record_open_time": "",
|
||||
"star": 0,
|
||||
"type": 0,
|
||||
"suffix": "",
|
||||
"path": [],
|
||||
"cur": 0,
|
||||
"stdir": 0,
|
||||
"fields": "",
|
||||
"order": "user_ptime",
|
||||
"state": true,
|
||||
"code": 0,
|
||||
"message": ""
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `limit` 最大为1150。
|
||||
- 当 `cid` 指向不存在或已删除的目录时,接口返回异常;当目录为加密目录时,不返回目录内容。
|
||||
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,140 @@
|
||||
## 用户信息
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:---------|:------------------------------|
|
||||
| 接口名称 | 用户信息 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /user/info |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
获取当前授权用户的基本信息、网盘空间信息、VIP 等级信息以及第三方畅用权益信息。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/user/info
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
无
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/user/info' \
|
||||
-H 'Authorization: Bearer <ACCESS_TOKEN>'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:----------------------------------------|:------|:----------------------------------------|
|
||||
| state | boolean | 状态码,是表示成功,否表示异常 |
|
||||
| message | string | 异常信息 |
|
||||
| code | int | 异常码 |
|
||||
| data | object | 响应数据 |
|
||||
| data.user_id | int | 用户115账号 |
|
||||
| data.user_name | string | 用户名称 |
|
||||
| data.user_face_s | string | 小尺寸用户头像 |
|
||||
| data.user_face_m | string | 中尺寸用户头像 |
|
||||
| data.user_face_l | string | 大尺寸用户头像 |
|
||||
| data.rt_space_info | object | 用户实时空间信息 |
|
||||
| data.rt_space_info.all_total | object | 用户总空间 |
|
||||
| data.rt_space_info.all_total.size | int | 用户总空间大小,单位为字节 |
|
||||
| data.rt_space_info.all_total.size_format | string | 用户总空间大小,格式化文本 |
|
||||
| data.rt_space_info.all_remain | object | 用户剩余空间 |
|
||||
| data.rt_space_info.all_remain.size | int | 用户剩余空间大小,单位为字节 |
|
||||
| data.rt_space_info.all_remain.size_format | string | 用户剩余空间大小,格式化文本 |
|
||||
| data.rt_space_info.all_use | object | 用户已使用空间 |
|
||||
| data.rt_space_info.all_use.size | int | 用户已使用空间大小,单位为字节 |
|
||||
| data.rt_space_info.all_use.size_format | string | 用户已使用空间大小,格式化文本 |
|
||||
| data.vip_info | object | 用户 VIP 等级信息 |
|
||||
| data.vip_info.level_name | string | VIP 等级名称,见下方枚举表格 |
|
||||
| data.vip_info.expire | int | VIP 过期时间戳,无 VIP 时为 0 |
|
||||
| data.vip_info.tp_rights | object | 第三方畅用权益信息 |
|
||||
| data.vip_info.tp_rights.is_tp_rights | int | 是否具有当前应用的第三方畅用权益:0-否 1-是 |
|
||||
| data.vip_info.tp_rights.tp_rights_time | int | 第三方畅用权益过期时间戳,无有效权益时为 0 |
|
||||
|
||||
#### 响应的 data.vip_info.level_name 字段枚举
|
||||
|
||||
| 值 | 说明 | 备注 |
|
||||
|:----------|:---|:---|
|
||||
| 原石会员 | 原石用户 | 无有效 VIP 等级时的服务端固定返回值 |
|
||||
| 尝鲜VIP | 尝鲜VIP | - |
|
||||
| 体验VIP | 体验VIP | - |
|
||||
| 月费VIP | 月费VIP | - |
|
||||
| 年费VIP | 年费VIP | - |
|
||||
| 长期VIP(高级版) | 长期VIP(高级版) | - |
|
||||
| 长期VIP(特级版) | 长期VIP(特级版) | - |
|
||||
| 长期VIP(超级版) | 长期VIP(超级版) | - |
|
||||
| 长期VIP(至尊版) | 长期VIP(至尊版) | - |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"user_id": 0,
|
||||
"user_name": "",
|
||||
"user_face_s": "",
|
||||
"user_face_m": "",
|
||||
"user_face_l": "",
|
||||
"rt_space_info": {
|
||||
"all_total": {
|
||||
"size": 0,
|
||||
"size_format": ""
|
||||
},
|
||||
"all_remain": {
|
||||
"size": 0,
|
||||
"size_format": ""
|
||||
},
|
||||
"all_use": {
|
||||
"size": 0,
|
||||
"size_format": ""
|
||||
}
|
||||
},
|
||||
"vip_info": {
|
||||
"expire": 0,
|
||||
"level_name": "原石会员",
|
||||
"tp_rights": {
|
||||
"is_tp_rights": 0,
|
||||
"tp_rights_time": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 决定当前用户和开放应用,接口不接收用户账号请求参数。
|
||||
- `rt_space_info` 为实时空间数据,`all_use` 由总空间减去剩余空间计算得出。
|
||||
- 请妥善保管 `access_token`,不要在日志或客户端可见信息中输出。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
| 2026年08月18日(周二) 14:40:25 | 更新四种长期VIP等级名称枚举 |
|
||||
@@ -0,0 +1,88 @@
|
||||
## 提交视频转码
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:--------------------------------|
|
||||
| 接口名称 | 提交视频转码 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /video_push |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
按 VIP 等级或消耗枫币提交视频加速转码。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/video/video_push
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----------|:-------|:---|:----|:----------------------------------------|:-----------------|
|
||||
| pick_code | string | 是 | - | 视频文件提取码 | b53gu6z3hvqji8wrm |
|
||||
| op | string | 是 | - | 加速转码方式,`vip_push`:按 VIP 等级加速;`pay_push`:消耗枫币加速 | vip_push |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/video/video_push' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
-F 'pick_code=b53gu6z3hvqji8wrm' \
|
||||
-F 'op=vip_push'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:------|:---------|:----------------------|
|
||||
| state | boolean | 操作结果状态,true:成功;false:失败 |
|
||||
| message | string | 返回信息,成功时为空字符串 |
|
||||
| code | int | 错误码 |
|
||||
| data | object[] | 响应数据,成功时为空数组 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- 仅支持为当前授权用户所属的视频文件提交加速转码。
|
||||
- 已完成转码的视频无需重复提交。
|
||||
- 两种加速转码方式均要求当前用户具备 VIP 权益;`pay_push` 在枫币余额不足时提交失败。
|
||||
- 提交成功后,系统通知转码服务重新计算排队信息,并在 3 小时内记录已加速状态。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 通过 `Authorization` 请求头传递,请将请求示例中的占位符替换为实际访问令牌。
|
||||
- 请勿对同一视频重复提交加速转码。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,125 @@
|
||||
## 获取视频在线播放地址
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:--------------------------------|
|
||||
| 接口名称 | 获取视频在线播放地址 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /play |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
获取指定视频的在线播放地址、清晰度、音轨及文件基础信息。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/video/play
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----------|:-------|:---|:----|:----------|:-----------------|
|
||||
| pick_code | string | 是 | - | 视频文件提取码 | b53gu6z3hvqji8wrm |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl -G 'https://proapi.115.com/open/video/play' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
--data-urlencode 'pick_code=b53gu6z3hvqji8wrm'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:---------------------------------|:---------|:----------------------------------------|
|
||||
| state | boolean | 操作结果状态,true:成功;false:失败 |
|
||||
| message | string | 返回信息,成功时为空字符串 |
|
||||
| code | int | 错误码 |
|
||||
| data | object | 视频播放及文件数据 |
|
||||
| data.file_id | string | 文件ID |
|
||||
| data.parent_id | string | 文件父目录ID |
|
||||
| data.file_name | string | 文件名称 |
|
||||
| data.file_size | string | 文件大小,单位为字节 |
|
||||
| data.file_sha1 | string | 文件哈希值 |
|
||||
| data.file_type | string | 文件类型 |
|
||||
| data.is_private | string | 文件是否加密隐藏,0:否;1:是 |
|
||||
| data.play_long | string | 视频时长 |
|
||||
| data.user_def | int | 记忆的清晰度,1:标清;2:高清;3:超清;4:1080P;5:4K;100:原画 |
|
||||
| data.user_rotate | int | 记忆的视频旋转角度,取值为 0、90、180、270 |
|
||||
| data.user_turn | int | 视频翻转方向,0:不翻转;1:水平翻转;2:垂直翻转 |
|
||||
| data.multitrack_list | object | 多音轨列表,键为音轨序号 |
|
||||
| data.multitrack_list.*.title | string | 音轨标题 |
|
||||
| data.multitrack_list.*.is_selected | string | 音轨是否为上次选中,1:是 |
|
||||
| data.multitrack_list.*.sync_time | string | 音轨同步时间 |
|
||||
| data.definition_list | object | 清晰度列表,键为清晰度值,值为清晰度名称 |
|
||||
| data.definition_list_new | object | 新版清晰度列表,键为清晰度值,值为清晰度名称 |
|
||||
| data.video_url | object[] | 各清晰度的播放地址信息 |
|
||||
| data.video_url[].url | string | 播放地址 |
|
||||
| data.video_url[].height | int | 视频高度 |
|
||||
| data.video_url[].width | int | 视频宽度 |
|
||||
| data.video_url[].definition | int | 视频清晰度 |
|
||||
| data.video_url[].title | string | 视频清晰度名称 |
|
||||
| data.video_url[].definition_n | int | 新版视频清晰度 |
|
||||
| data.video_push_state | boolean | 视频尚未完成转码时是否已提交加速转码,仅对应失败响应返回 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"file_id": "",
|
||||
"parent_id": "",
|
||||
"file_name": "",
|
||||
"file_size": "0",
|
||||
"file_sha1": "",
|
||||
"file_type": "",
|
||||
"is_private": "0",
|
||||
"play_long": "0",
|
||||
"user_def": 0,
|
||||
"user_rotate": 0,
|
||||
"user_turn": 0,
|
||||
"multitrack_list": {},
|
||||
"definition_list": {},
|
||||
"definition_list_new": {},
|
||||
"video_url": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- 切换音轨时,在返回的播放地址后增加整型参数 `audio_track`,参数值取 `multitrack_list` 对应的键;音轨下标从 `0` 开始。
|
||||
- 年费VIP以下用户不支持播放 4K 视频;选择 4K 清晰度时会返回引导升级的视频地址。
|
||||
- 接口不返回下载地址字段 `down_url`。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 通过 `Authorization` 请求头传递,请将请求示例中的占位符替换为实际访问令牌。
|
||||
- 播放地址具有时效性,请勿缓存或向无关方披露。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,96 @@
|
||||
## 获取视频播放进度
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:--------------------------------|
|
||||
| 接口名称 | 获取视频播放进度 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /history |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
获取指定视频已记录的播放进度。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/video/history
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----------|:-------|:---|:----|:----------|:-----------------|
|
||||
| pick_code | string | 是 | - | 视频文件提取码 | b53gu6z3hvqji8wrm |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl -G 'https://proapi.115.com/open/video/history' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
--data-urlencode 'pick_code=b53gu6z3hvqji8wrm'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:-------------|:--------|:-----------------------------|
|
||||
| state | boolean | 操作结果状态,true:成功;false:失败 |
|
||||
| message | string | 返回信息,成功时为空字符串 |
|
||||
| code | int | 错误码 |
|
||||
| data | object | 播放进度数据;没有记录时为空数组 |
|
||||
| data.add_time | int | 记录添加时间,Unix 时间戳 |
|
||||
| data.file_id | string | 文件ID |
|
||||
| data.file_name | string | 文件名称 |
|
||||
| data.hash | string | 文件哈希值 |
|
||||
| data.pick_code | string | 文件提取码 |
|
||||
| data.time | int | 已播放时长,单位为秒 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"add_time": 0,
|
||||
"file_id": "",
|
||||
"file_name": "",
|
||||
"hash": "",
|
||||
"pick_code": "b53gu6z3hvqji8wrm",
|
||||
"time": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- 接口只返回当前 `pick_code` 对应的单条播放进度记录。
|
||||
- 视频是否播放完毕可通过文件列表中的 `played_end` 字段查看,`1` 表示已播放完毕。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 通过 `Authorization` 请求头传递,请将请求示例中的占位符替换为实际访问令牌。
|
||||
- `pick_code` 对应的文件不存在或不在有效文件区域时,接口返回失败。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,119 @@
|
||||
## 视频字幕列表
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:--------------------------------|
|
||||
| 接口名称 | 视频字幕列表 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /subtitle |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
获取指定视频的自动载入字幕和可用字幕列表。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/video/subtitle
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----------|:-------|:---|:----|:----------|:-----------------|
|
||||
| pick_code | string | 是 | - | 视频文件提取码 | b53gu6z3hvqji8wrm |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl -G 'https://proapi.115.com/open/video/subtitle' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
--data-urlencode 'pick_code=b53gu6z3hvqji8wrm'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:----------------------------|:---------|:----------------------------|
|
||||
| state | boolean | 操作结果状态,true:成功;false:失败 |
|
||||
| message | string | 返回信息,成功时为空字符串 |
|
||||
| code | int | 错误码 |
|
||||
| data | object | 响应数据 |
|
||||
| data.autoload | object | 默认自动载入的字幕;无可用字幕时为空数组 |
|
||||
| data.autoload.sid | string | 字幕标识 |
|
||||
| data.autoload.language | string | 字幕语言 |
|
||||
| data.autoload.title | string | 字幕标题 |
|
||||
| data.autoload.url | string | 字幕文件地址 |
|
||||
| data.autoload.type | string | 字幕文件类型 |
|
||||
| data.autoload.key | string | 内置字幕键,仅内置字幕返回 |
|
||||
| data.autoload.sha1 | string | 字幕文件哈希值 |
|
||||
| data.autoload.file_id | string | 外挂或内嵌字幕文件ID |
|
||||
| data.autoload.file_name | string | 外挂或内嵌字幕文件名 |
|
||||
| data.autoload.pick_code | string | 外挂或内嵌字幕文件提取码 |
|
||||
| data.autoload.caption_map_id | string | 内嵌字幕映射ID |
|
||||
| data.autoload.is_caption_map | int | 是否为内嵌字幕,0:否;1:是 |
|
||||
| data.autoload.sync_time | float | 字幕同步时间 |
|
||||
| data.autoload.from | int | 记忆字幕来源标识 |
|
||||
| data.autoload.user_sub | int | 是否为用户记忆字幕,1:是 |
|
||||
| data.list | object[] | 字幕列表 |
|
||||
| data.list[].sid | string | 字幕标识 |
|
||||
| data.list[].language | string | 字幕语言 |
|
||||
| data.list[].title | string | 字幕标题 |
|
||||
| data.list[].url | string | 字幕文件地址 |
|
||||
| data.list[].type | string | 字幕文件类型 |
|
||||
| data.list[].key | string | 内置字幕键,仅内置字幕返回 |
|
||||
| data.list[].sha1 | string | 字幕文件哈希值 |
|
||||
| data.list[].file_id | string | 外挂或内嵌字幕文件ID |
|
||||
| data.list[].file_name | string | 外挂或内嵌字幕文件名 |
|
||||
| data.list[].pick_code | string | 外挂或内嵌字幕文件提取码 |
|
||||
| data.list[].caption_map_id | string | 内嵌字幕映射ID |
|
||||
| data.list[].is_caption_map | int | 是否为内嵌字幕,0:否;1:是 |
|
||||
| data.list[].sync_time | float | 字幕同步时间 |
|
||||
| data.list[].from | int | 记忆字幕来源标识 |
|
||||
| data.list[].user_sub | int | 是否为用户记忆字幕,1:是 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"autoload": [],
|
||||
"list": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- 用户记忆字幕不在列表中时会被前置;其余字幕按同目录同名外挂字幕、内嵌字幕、内置字幕、其他外挂字幕的顺序组合。
|
||||
- 存在用户记忆字幕时优先将其作为自动载入字幕;否则依次选择内嵌字幕、同名外挂字幕或内置字幕。
|
||||
- 字幕项来源不同,部分来源专属字段可能不返回。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 通过 `Authorization` 请求头传递,请将请求示例中的占位符替换为实际访问令牌。
|
||||
- 字幕文件地址具有时效性,请以本次接口返回值为准。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,89 @@
|
||||
## 记忆视频播放进度
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-------------|:--------------------------------|
|
||||
| 接口名称 | 记忆视频播放进度 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /history |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
记录指定视频的播放进度及是否播放完毕。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://proapi.115.com/open/video/history
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
Authorization: Bearer access_token
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----------|:-------|:---|:----|:-------------------|:-----------------|
|
||||
| pick_code | string | 是 | - | 视频文件提取码 | b53gu6z3hvqji8wrm |
|
||||
| time | int | 否 | 0 | 视频播放进度,单位为秒 | 10 |
|
||||
| watch_end | int | 否 | 0 | 是否播放完毕,0:否;1:是 | 0 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://proapi.115.com/open/video/history' \
|
||||
-H 'Authorization: Bearer <access_token>' \
|
||||
-F 'pick_code=b53gu6z3hvqji8wrm' \
|
||||
-F 'time=10' \
|
||||
-F 'watch_end=0'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:------|:---------|:----------------------|
|
||||
| state | boolean | 操作结果状态,true:成功;false:失败 |
|
||||
| message | string | 返回信息,成功时为空字符串 |
|
||||
| code | int | 错误码 |
|
||||
| data | object[] | 响应数据,成功时为空数组 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": true,
|
||||
"message": "",
|
||||
"code": 0,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
- `time` 和 `watch_end` 均不传时,两个参数均按 `0` 处理,即播放进度为 0 秒且未播放完毕。
|
||||
- 非本人文件不写入播放进度,但接口按操作成功返回。
|
||||
- 同一视频未播放完毕的进度在 5 秒内重复上报时不重复持久化;标记为播放完毕时会立即持久化。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 通过 `Authorization` 请求头传递,请将请求示例中的占位符替换为实际访问令牌。
|
||||
- `pick_code` 必须属于当前授权用户可记录播放进度的视频文件。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,30 @@
|
||||
# 开发须知
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 文档名称 | 开发须知 |
|
||||
| 文档版本 | v1.0 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
为共同建设开放、共赢的合作生态并保障平台服务的可持续发展,开发者在接入平台服务前,须认真阅读并严格遵循《115生活开放平台开发者协议》的相关要求,确保合规运营、维护平台秩序。
|
||||
|
||||
基于平台与用户权益保护原则,平台会持续监测开发者的服务使用行为。如发现违反平台规范的行为,平台将视情节采取包括但不限于服务限流、接口冻结、资质回收等限制措施,并保留依法追责的权利。
|
||||
|
||||
开发者严禁实施包括但不限于以下行为:
|
||||
|
||||
1. **数据隐私违规行为**:侵害用户数据隐私安全,包括未经用户授权或未明确用途,违规收集、下载、存储、传播、加工用户存储数据等。开发者需要确保用户数据的获取与使用全程透明、可追溯。
|
||||
2. **商业利益侵犯行为**:损害 115 科技的商业利益,包括多人共享开发者账号及会员权益、开展竞争关系业务、未经授权获取平台相关服务运营数据等。
|
||||
3. **不当使用行为**:违规或未按要求使用 API 服务,包括违反国家相关政策法规、侵犯第三方合法权益、调用非公开接口、实际用途与申请信息不符等。
|
||||
|
||||
## 限流说明
|
||||
|
||||
为确保系统安全并保障服务稳定运行,115生活开放平台对所有 API 实施频率控制策略。出于安全防护需要,相关策略细则暂不公开,平台会持续动态优化该机制。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,60 @@
|
||||
# 授权错误码
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 文档名称 | 授权错误码 |
|
||||
| 文档版本 | v1.0 |
|
||||
|
||||
| 错误码 | 描述 | 建议 |
|
||||
|:-------|:------------------------------------|:-------------------------------------------------------------|
|
||||
| 40100000 | 参数缺失 | - |
|
||||
| 40101017 | 用户验证失败 | - |
|
||||
| 40110000 | 请求异常,需要重试 | - |
|
||||
| 40140100 | `client_id` 错误 | - |
|
||||
| 40140101 | `code_challenge` 必填 | - |
|
||||
| 40140102 | `code_challenge_method` 必须是 `sha256`、`sha1`、`md5` 之一 | - |
|
||||
| 40140103 | `sign` 必填 | - |
|
||||
| 40140104 | `sign` 签名失败 | - |
|
||||
| 40140105 | 生成二维码失败 | - |
|
||||
| 40140106 | AppID 无效 | - |
|
||||
| 40140107 | 应用不存在 | - |
|
||||
| 40140108 | 应用未审核通过 | - |
|
||||
| 40140109 | 应用已被停用 | - |
|
||||
| 40140110 | 应用已过期 | - |
|
||||
| 40140111 | AppSecret 错误 | - |
|
||||
| 40140112 | `code_verifier` 长度要求为 43 至 128 位 | - |
|
||||
| 40140113 | `code_verifier` 验证失败 | - |
|
||||
| 40140114 | `refresh_token` 格式错误(防篡改) | - |
|
||||
| 40140115 | `refresh_token` 签名校验失败(防篡改) | - |
|
||||
| 40140116 | `refresh_token` 无效(已解除授权) | 重新授权。终态错误,重试不会成功;继续重试将被标记为永久失效,见 `40140137` |
|
||||
| 40140117 | `access_token` 刷新太频繁 | - |
|
||||
| 40140118 | 开发者认证已过期 | - |
|
||||
| 40140119 | `refresh_token` 已过期 | 重新授权。终态错误,重试不会成功;继续重试将被标记为永久失效,见 `40140137` |
|
||||
| 40140120 | `refresh_token` 检验失败(防篡改) | 调用 `/open/refreshToken` 后会重新生成 `refresh_token`,检查本地是否已更新其值。终态错误,持续用旧值重试将被标记为永久失效,见 `40140137` |
|
||||
| 40140121 | `access_token` 刷新失败 | 重试 |
|
||||
| 40140122 | 超出授权应用数量上限 | - |
|
||||
| 40140123 | `access_token` 格式错误(防篡改) | - |
|
||||
| 40140124 | `access_token` 签名校验失败(防篡改) | - |
|
||||
| 40140125 | `access_token` 无效(已过期、已解除授权或授权缓存不存在) | 调用 `/open/refreshToken` 获取新凭证;若 `refresh_token` 无效或已过期,重新授权 |
|
||||
| 40140126 | `access_token` 与当前授权记录不匹配 | 重新读取刷新后保存的最新凭证,禁止使用原 `access_token` 重试;没有可用新凭证时调用 `/open/refreshToken` |
|
||||
| 40140127 | `response_type` 错误 | - |
|
||||
| 40140128 | `redirect_uri` 缺少协议 | - |
|
||||
| 40140129 | `redirect_uri` 缺少域名 | - |
|
||||
| 40140130 | 没有配置重定向域名 | 到应用管理中配置域名 |
|
||||
| 40140131 | `redirect_uri` 域名不合法 | 需要与应用管理中配置的应用域名一致 |
|
||||
| 40140132 | `grant_type` 错误 | - |
|
||||
| 40140133 | `client_secret` 验证失败 | - |
|
||||
| 40140134 | 授权码 `code` 验证失败 | - |
|
||||
| 40140135 | `client_id` 验证失败 | - |
|
||||
| 40140136 | `redirect_uri` 验证失败(防 MITM 攻击) | - |
|
||||
| 40140137 | `refresh_token` 已失效,请停止重试并重新授权(终态) | 同一 `refresh_token` 连续多次以 `40140116`/`40140119`/`40140120` 失败后被服务端标记为永久失效。收到后必须停止用该令牌重试,引导用户重新授权;重新授权产生的新令牌不受影响 |
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
| 2026年08月10日(周一) 10:23:15 | 修正 `40140125`、`40140126` 的原因及处理建议 |
|
||||
| 2026年08月24日(周一) 10:16:52 | 新增终态错误码 `40140137`,补充 `40140116`/`40140119`/`40140120` 的永久失效判定说明 |
|
||||
@@ -0,0 +1,92 @@
|
||||
## 刷新access_token
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 刷新access_token |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /refreshToken |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
该接口用于通过 `refresh_token` 获取新的 `access_token` 和 `refresh_token`。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://passportapi.115.com/open/refreshToken
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
OAuth 2.0 刷新凭证
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-------------|:-------|:---|:----|:---------------------|:----------------------------|
|
||||
| refresh_token | string | 是 | - | 用于刷新访问凭证的刷新凭证 | `REFRESH_TOKEN_PLACEHOLDER` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://passportapi.115.com/open/refreshToken' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'refresh_token=REFRESH_TOKEN_PLACEHOLDER'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:-------------------|:-------|:------------------------------------|
|
||||
| state | int | 状态码 |
|
||||
| code | int | 错误码 |
|
||||
| message | string | 响应信息 |
|
||||
| data | object | 响应数据 |
|
||||
| data.access_token | string | 新的 `access_token`,同时刷新有效期 |
|
||||
| data.refresh_token | string | 新的 `refresh_token`,其有效期不延长、不改变 |
|
||||
| data.expires_in | int | `access_token` 有效期,单位为秒 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": 1,
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"access_token": "ACCESS_TOKEN_PLACEHOLDER",
|
||||
"refresh_token": "REFRESH_TOKEN_PLACEHOLDER",
|
||||
"expires_in": 7200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `access_token` 有效期为 7200 秒,调用方应以响应中的 `expires_in` 计算刷新时间。
|
||||
- 同一授权在 60 秒内重复刷新会触发频率控制;多进程或多节点调用方应确保同一授权同一时间只有一个刷新请求。
|
||||
- 调用后会同时生成新的 `access_token` 和 `refresh_token`。调用方必须将两者作为一组原子保存,并停止使用刷新前的旧凭证;刷新凭证本身的有效期不延长、不改变。
|
||||
- 收到 `40140125` 或 `40140126` 时,不要使用原 `access_token` 重复重试。应先读取已保存的最新凭证;没有可用新凭证时,再调用本接口刷新。
|
||||
- `40140116`、`40140119`、`40140120` 是终态错误,用同一个 `refresh_token` 重试永远不会成功。同一 `refresh_token` 连续多次以这三种原因失败会被服务端标记为永久失效,之后每次刷新都直接返回 `40140137`。客户端收到这三种错误或 `40140137` 时必须停止重试,引导用户重新授权;后台常驻程序(如 NAS 同步任务)尤其要实现该逻辑,避免长期无效轮询。
|
||||
- `access_token` 和 `refresh_token` 属于敏感凭证,不得写入公开仓库、客户端日志或公开沟通内容。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
| 2026年08月10日(周一) 10:23:15 | 修正 `access_token` 有效期示例,补充凭证轮换及并发刷新说明 |
|
||||
| 2026年08月24日(周一) 10:16:52 | 新增 `refresh_token` 永久失效判定规则与终态错误码 `40140137` 说明 |
|
||||
@@ -0,0 +1,88 @@
|
||||
## 获取access_token
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 获取access_token |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /deviceCodeToToken |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
该接口用于在用户确认手机扫码授权后,使用设备码和 PKCE 原始校验值换取 `access_token`。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://passportapi.115.com/open/deviceCodeToToken
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
OAuth 2.0 + PKCE
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-----------|:-------|:---|:----|:--------------------------------|:---------------------------------------------------------------|
|
||||
| uid | string | 是 | - | 二维码 ID/设备码 | `DEVICE_CODE_PLACEHOLDER` |
|
||||
| code_verifier | string | 是 | - | 计算 `code_challenge` 时使用的原始随机字符串 | `IGKN6CJanWxCDPDhHZJrhswQdlcPBGLqExkhyujysXaQ4fJKBk_6dlPJo47s` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://passportapi.115.com/open/deviceCodeToToken' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'uid=DEVICE_CODE_PLACEHOLDER' \
|
||||
--data-urlencode 'code_verifier=IGKN6CJanWxCDPDhHZJrhswQdlcPBGLqExkhyujysXaQ4fJKBk_6dlPJo47s'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:-------------------|:-------|:--------------------------------|
|
||||
| state | int | 状态码 |
|
||||
| code | int | 错误码 |
|
||||
| message | string | 响应信息 |
|
||||
| data | object | 响应数据 |
|
||||
| data.access_token | string | 访问资源接口的凭证 |
|
||||
| data.refresh_token | string | 刷新 `access_token` 的凭证,有效期 1 年 |
|
||||
| data.expires_in | int | `access_token` 有效期,单位为秒 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": 1,
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"access_token": "ACCESS_TOKEN_PLACEHOLDER",
|
||||
"refresh_token": "REFRESH_TOKEN_PLACEHOLDER",
|
||||
"expires_in": 7200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `code_verifier` 必须与生成 `code_challenge` 时使用的原始值一致。
|
||||
- `access_token` 和 `refresh_token` 属于敏感凭证,不得写入公开仓库、客户端日志或公开沟通内容。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,103 @@
|
||||
## 获取设备码和二维码内容
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 获取设备码和二维码内容 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /authDeviceCode |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
该接口用于 OAuth 2.0 + PKCE 手机扫码授权流程的第一步,获取设备码和二维码内容。此模式适用于无后端服务的第三方客户端,无需提供 AppSecret。
|
||||
|
||||
第三方客户端需要根据响应中的 `data.qrcode` 生成二维码,供 115 客户端扫码授权。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://passportapi.115.com/open/authDeviceCode
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
OAuth 2.0 + PKCE
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:---------------------|:-------|:---|:----|:--------------------------------|:-----------------------------------------------------------|
|
||||
| client_id | string | 是 | - | AppID | `YOUR_APP_ID` |
|
||||
| code_challenge | string | 是 | - | PKCE 挑战码 | `THHodGWg-FZfv8XYz7QArNGIK_aVomSHPldlSOTUtkw` |
|
||||
| code_challenge_method | string | 是 | - | `code_challenge` 的哈希算法,见下方枚举表格 | `sha256` |
|
||||
|
||||
#### 请求参数中的 code_challenge_method 字段枚举
|
||||
|
||||
| 值 | 说明 | 备注 |
|
||||
|:-------|:---------------------------|:---|
|
||||
| md5 | 使用 MD5 计算挑战码 | - |
|
||||
| sha1 | 使用 SHA-1 计算挑战码 | - |
|
||||
| sha256 | 使用 SHA-256 计算挑战码,推荐使用 | - |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://passportapi.115.com/open/authDeviceCode' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'client_id=YOUR_APP_ID' \
|
||||
--data-urlencode 'code_challenge=THHodGWg-FZfv8XYz7QArNGIK_aVomSHPldlSOTUtkw' \
|
||||
--data-urlencode 'code_challenge_method=sha256'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:-----------|:-----|:------------------------------------------|
|
||||
| state | int | 状态码 |
|
||||
| code | int | 错误码 |
|
||||
| message | string | 响应信息 |
|
||||
| data | object | 响应数据 |
|
||||
| data.uid | string | 设备码,轮询二维码状态时使用 |
|
||||
| data.time | int | 校验时间戳,轮询二维码状态时使用 |
|
||||
| data.qrcode | string | 二维码内容,第三方客户端需要据此生成设备二维码 |
|
||||
| data.sign | string | 校验签名,轮询二维码状态时使用 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": 1,
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"uid": "DEVICE_CODE_PLACEHOLDER",
|
||||
"time": 0,
|
||||
"qrcode": "QRCODE_CONTENT_PLACEHOLDER",
|
||||
"sign": "SIGN_PLACEHOLDER"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `code_verifier` 为长度 43 至 128 位的随机字符串。
|
||||
- `code_challenge` 的计算方式为 `url_safe(base64_encode(hash(code_verifier)))`。哈希结果按二进制数据参与 Base64 编码,算法需要与 `code_challenge_method` 一致。
|
||||
- AppID 等应用凭证请使用实际应用配置,不要在公开仓库或日志中记录敏感凭证。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,89 @@
|
||||
## 轮询二维码状态
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 轮询二维码状态 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /get/status/ |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
该接口用于长轮询设备二维码的扫码和授权状态。当二维码状态没有变化时,接口不会立即响应,直到请求超时或状态发生变化。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://qrcodeapi.115.com/get/status/
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
设备码参数校验
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:----|:-------|:---|:----|:------------------------------------|:------------------------|
|
||||
| uid | string | 是 | - | 二维码 ID/设备码,从 `/open/authDeviceCode` 的 `data.uid` 获取 | `DEVICE_CODE_PLACEHOLDER` |
|
||||
| time | int | 是 | - | 校验时间戳,从 `/open/authDeviceCode` 的 `data.time` 获取 | `0` |
|
||||
| sign | string | 是 | - | 校验签名,从 `/open/authDeviceCode` 的 `data.sign` 获取 | `SIGN_PLACEHOLDER` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl -G 'https://qrcodeapi.115.com/get/status/' \
|
||||
--data-urlencode 'uid=DEVICE_CODE_PLACEHOLDER' \
|
||||
--data-urlencode 'time=0' \
|
||||
--data-urlencode 'sign=SIGN_PLACEHOLDER'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:------------|:-------|:-------------------------------------------|
|
||||
| state | int | 轮询状态:0-二维码无效,结束轮询;1-继续轮询 |
|
||||
| code | int | 错误码 |
|
||||
| message | string | 响应信息 |
|
||||
| data | object | 响应数据;115 客户端扫码或输入设备码后才有值 |
|
||||
| data.msg | string | 操作提示 |
|
||||
| data.status | int | 二维码状态:1-扫码成功,等待确认;2-确认登录或授权,结束轮询 |
|
||||
| data.version | string | 版本信息 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": 1,
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"msg": "OPERATION_MESSAGE_PLACEHOLDER",
|
||||
"status": 1,
|
||||
"version": "VERSION_PLACEHOLDER"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `state=0` 表示二维码无效,应结束轮询;`state=1` 表示继续轮询。
|
||||
- `data.status=1` 表示扫码成功并等待用户确认;`data.status=2` 表示用户已确认登录或授权,应结束轮询并进入换取访问凭证的步骤。
|
||||
- 长轮询超时不等同于授权失败,客户端可以按照自身网络策略重新发起请求。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,95 @@
|
||||
## 用授权码换取access_token
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 用授权码换取access_token |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /authCodeToToken |
|
||||
| 请求方法 | POST |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
该接口用于通过授权码换取 `access_token`。建议在开发者服务端调用,避免泄露 AppSecret。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://passportapi.115.com/open/authCodeToToken
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
POST
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
OAuth 2.0 授权码模式
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-------------|:-------|:---|:----|:------------------------------------|:------------------------------|
|
||||
| client_id | string | 是 | - | AppID | `YOUR_APP_ID` |
|
||||
| client_secret | string | 是 | - | AppSecret | `YOUR_APP_SECRET` |
|
||||
| code | string | 是 | - | 请求授权接口重定向返回的授权码 | `AUTHORIZATION_CODE_PLACEHOLDER` |
|
||||
| redirect_uri | string | 是 | - | 与请求授权时传入的 `redirect_uri` 一致,用于防止 MITM 和 CSRF 攻击 | `https://foo.com?state=123456` |
|
||||
| grant_type | string | 是 | - | 授权类型,固定为 `authorization_code` | `authorization_code` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl 'https://passportapi.115.com/open/authCodeToToken' \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'client_id=YOUR_APP_ID' \
|
||||
--data-urlencode 'client_secret=YOUR_APP_SECRET' \
|
||||
--data-urlencode 'code=AUTHORIZATION_CODE_PLACEHOLDER' \
|
||||
--data-urlencode 'redirect_uri=https://foo.com?state=123456' \
|
||||
--data-urlencode 'grant_type=authorization_code'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:-------------------|:-------|:--------------------------------|
|
||||
| state | int | 状态码:0-失败;1-成功 |
|
||||
| code | int | 错误码 |
|
||||
| message | string | 响应信息 |
|
||||
| data | object | 响应数据 |
|
||||
| data.access_token | string | 访问资源接口的凭证 |
|
||||
| data.refresh_token | string | 刷新 `access_token` 的凭证,有效期 1 年 |
|
||||
| data.expires_in | int | `access_token` 有效期,单位为秒 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": 1,
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"access_token": "ACCESS_TOKEN_PLACEHOLDER",
|
||||
"refresh_token": "REFRESH_TOKEN_PLACEHOLDER",
|
||||
"expires_in": 7200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 必须在服务端安全保存并使用 AppSecret,不得在客户端代码、公开仓库或日志中泄露。
|
||||
- `redirect_uri` 必须与请求授权时传入的值一致。
|
||||
- `access_token` 和 `refresh_token` 属于敏感凭证,应按照敏感数据规范存储。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,87 @@
|
||||
## 请求授权
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 接口名称 | 请求授权 |
|
||||
| 接口版本 | v1.0 |
|
||||
| 接口路径 | /authorize |
|
||||
| 请求方法 | GET |
|
||||
| 接口状态 | 生产环境 |
|
||||
|
||||
### 接口说明
|
||||
|
||||
该接口用于发起 OAuth 2.0 授权码模式授权,建议由开发者服务端参与授权流程。
|
||||
|
||||
用户未登录时,接口会重定向到登录页面;用户已登录时,接口会自动完成授权并重定向到 `redirect_uri` 指定的地址。
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
https://passportapi.115.com/open/authorize
|
||||
```
|
||||
|
||||
### 请求方式
|
||||
|
||||
```
|
||||
GET
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
```
|
||||
OAuth 2.0 授权码模式
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||
|:-----------|:-------|:---|:----|:-----------------------------------------------|:---------------------|
|
||||
| client_id | string | 是 | - | AppID | `YOUR_APP_ID` |
|
||||
| redirect_uri | string | 是 | - | 授权完成后的重定向地址;接口会附加授权码 `code`,并原样附加请求中的 `state` | `https://foo.com/bar` |
|
||||
| response_type | string | 是 | - | 授权模式,固定为 `code` | `code` |
|
||||
| state | string | 否 | "" | 防止 CSRF 攻击的随机值,重定向时原样返回 | `123456` |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```shell
|
||||
curl -G 'https://passportapi.115.com/open/authorize' \
|
||||
--data-urlencode 'client_id=YOUR_APP_ID' \
|
||||
--data-urlencode 'redirect_uri=https://foo.com/bar' \
|
||||
--data-urlencode 'response_type=code' \
|
||||
--data-urlencode 'state=123456'
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
接口调用成功时会重定向到 `redirect_uri`,并通过查询参数返回授权码 `code` 和请求中携带的 `state`。接口调用失败时返回以下字段:
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
|:--------|:-------|:------------------|
|
||||
| state | int | 状态码:0-失败;1-成功 |
|
||||
| code | int | 错误码 |
|
||||
| data | object | 响应数据 |
|
||||
| message | string | 响应信息 |
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"state": 0,
|
||||
"code": 40140127,
|
||||
"data": {},
|
||||
"message": "response_type 错误"
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- `redirect_uri` 需要先在[115生活开放平台](https://open.115.com/)的应用管理中配置域名,并在请求时进行 URL 编码。
|
||||
- 强烈建议传入随机 `state`,并在换取 `access_token` 前验证重定向返回的 `state` 与请求值一致,以防止 CSRF 攻击。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,57 @@
|
||||
# 接入流程
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 文档名称 | 接入流程 |
|
||||
| 文档版本 | v1.0 |
|
||||
|
||||
## 1. 注册“115生活”账号
|
||||
|
||||
在使用 115 开放平台服务之前,开发者需要先通过[“115生活”官网](https://115.com/)注册“115生活”账号并登录,完成实名认证。
|
||||
|
||||
## 2. 提交入驻申请
|
||||
|
||||
访问[115生活开放平台](https://open.115.com/),按照页面流程填写相关信息。
|
||||
|
||||
### 2.1 选择开发者身份类型
|
||||
|
||||
开发者可以申请成为“个人开发者”或“企业开发者”。点击页面上方的“切换申请类型”,可以切换身份类型。
|
||||
|
||||
### 2.2 签署协议
|
||||
|
||||
认真阅读并确认相关协议内容;如无异议,勾选“同意”并点击“下一步”。
|
||||
|
||||
### 2.3 填写入驻资料
|
||||
|
||||
按照页面指引填写个人信息、API 对接需求、应用场景等入驻资料,然后点击“下一步”。
|
||||
|
||||
### 2.4 填写认证信息
|
||||
|
||||
根据所选择的开发者身份类型,按照页面指引填写并上传身份认证资料:
|
||||
|
||||
- **个人开发者**:姓名、身份证信息(证件号码、有效期)、证件照片(身份证件正反面照片、本人手持身份证照片)。
|
||||
- **企业开发者**:企业负责人或法人的姓名、联系方式、身份证信息(证件号码、有效期)、证件照片(身份证件正反面照片、加盖公章的营业执照影印件)、统一社会信用代码。
|
||||
|
||||
### 2.5 提交入驻申请
|
||||
|
||||
确认上述信息填写无误后,点击“提交验证”提交入驻申请。平台将在 7 个工作日内完成审核。
|
||||
|
||||
## 3. 创建应用
|
||||
|
||||
入驻申请通过后,进入 115 生活开放平台管理页面,选择“应用管理”,点击“创建应用”,按照页面指引填写应用信息、应用域名、应用描述、接口信息等内容。确认信息填写无误后提交应用申请,平台将在 7 个工作日内完成审核。
|
||||
|
||||
应用审核通过后,开发者可以获取该应用的 AppID、AppKey 和 AppSecret 等接入凭证。请妥善保存这些凭证,不要在客户端代码、公开仓库或沟通内容中泄露。
|
||||
|
||||
## 4. 接口调试
|
||||
|
||||
按照开放平台接口文档完成接入和调用后,即可正式使用开放平台能力。
|
||||
|
||||
开放平台 API 基础域名:`https://proapi.115.com/`
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,23 @@
|
||||
# 更新记录
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 文档名称 | 更新记录 |
|
||||
| 文档版本 | v1.0 |
|
||||
|
||||
## 更新内容
|
||||
|
||||
| 更新模块 | 更新内容 | 更新时间 |
|
||||
|:-----------|:-------------------------|:--------------|
|
||||
| 增值服务产品 | 新增增值服务产品,获得推广收益 | 2025年4月17日周四 |
|
||||
| 视频播放、云下载 | 新增视频播放、云下载接口 | 2025年4月3日周四 |
|
||||
| 接入授权 | 支持 H5 账号密码/短信授权 | 2025年4月2日周三 |
|
||||
| 基本框架 | 新增开放平台文档接口 | 2025年1月22日周三 |
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,30 @@
|
||||
# 概述
|
||||
|
||||
### 基本信息
|
||||
|
||||
| 属性 | 内容 |
|
||||
|:-----------|:---------------------------------|
|
||||
| 文档名称 | 概述 |
|
||||
| 文档版本 | v1.0 |
|
||||
|
||||
## 115生活简介
|
||||
|
||||
“115生活”是一款面向个人用户的数字生活平台,提供海量数据的安全存储、多端同步与快速访问。用户不仅可以便捷地管理和使用各类数字资源,还能使用多维社交、生活服务等多元化功能。
|
||||
|
||||
## 115生活开放平台能力说明
|
||||
|
||||
115生活开放平台提供“115生活”数据存储、同步、管理等功能的 API 服务。开发者通过对接 API,可以将“115生活”的存储能力集成到自己的应用中。
|
||||
|
||||
目前已开放以下能力:
|
||||
|
||||
- **用户管理能力**:用户授权与信息查询等。
|
||||
- **文件管理能力**:获取文件列表,查看文件属性,以及文件上传、下载、搜索、移动、删除等。
|
||||
- **视频管理能力**:视频文件的在线转码与播放等。
|
||||
- **云下载服务**:获取云下载任务列表、配额信息,以及添加、删除下载任务等。
|
||||
- **商业价值转化**:开发者参与“推广产品得收益”计划,可基于用户实际购买的产品获取相应推广收益。
|
||||
|
||||
### 修改历史
|
||||
|
||||
| 修改时间 | 修改说明 |
|
||||
|:-----------------------------|:-----|
|
||||
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||
@@ -0,0 +1,67 @@
|
||||
# 115 开放平台文档(离线版)
|
||||
|
||||
> 来源:https://open.115.com/doc/ (115生活开放平台开发者文档)
|
||||
> 抓取时间:2026-09-05,共 42 篇 Markdown 文档,目录结构与官网一致。
|
||||
> 目录索引:[tree-index.json](tree-index.json)(官网原始目录树,程序化遍历可用)。
|
||||
|
||||
## 文档目录
|
||||
|
||||
- [概述](115开放平台/简介/概述.md)
|
||||
- [更新记录](115开放平台/简介/更新记录.md)
|
||||
- [接入流程](115开放平台/接入指南/接入流程.md)
|
||||
- [开发须知](115开放平台/接入指南/开发须知.md)
|
||||
- [授权错误码](115开放平台/接入指南/授权错误码.md)
|
||||
- **接入授权/**
|
||||
- **手机扫码授权PKCE模式/**
|
||||
- [获取设备码和二维码内容](115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/获取设备码和二维码内容.md)
|
||||
- [轮询二维码状态](115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/轮询二维码状态.md)
|
||||
- [获取access_token](115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/获取access_token.md)
|
||||
- **授权码模式/**
|
||||
- [请求授权](115开放平台/接入指南/接入授权/授权码模式/请求授权.md)
|
||||
- [用授权码换取access_token](115开放平台/接入指南/接入授权/授权码模式/用授权码换取access_token.md)
|
||||
- [刷新access_token](115开放平台/接入指南/接入授权/刷新access_token.md)
|
||||
- [开发者商业价值转化:推广产品得收益](115开放平台/API列表/开发者商业价值转化:推广产品得收益.md)
|
||||
- **用户管理/**
|
||||
- [用户信息](115开放平台/API列表/用户管理/用户信息.md)
|
||||
- **文件管理/**
|
||||
- **文件上传/**
|
||||
- [上传流程](115开放平台/API列表/文件管理/文件上传/上传流程.md)
|
||||
- [获取上传凭证](115开放平台/API列表/文件管理/文件上传/获取上传凭证.md)
|
||||
- [文件上传](115开放平台/API列表/文件管理/文件上传/文件上传.md)
|
||||
- [断点续传](115开放平台/API列表/文件管理/文件上传/断点续传.md)
|
||||
- [新建文件夹](115开放平台/API列表/文件管理/新建文件夹.md)
|
||||
- [获取文件列表](115开放平台/API列表/文件管理/获取文件列表.md)
|
||||
- **获取文件(夹)详情/**
|
||||
- [按ID获取](115开放平台/API列表/文件管理/获取文件(夹)详情/按ID获取.md)
|
||||
- [按路径获取](115开放平台/API列表/文件管理/获取文件(夹)详情/按路径获取.md)
|
||||
- [文件搜索](115开放平台/API列表/文件管理/文件搜索.md)
|
||||
- [文件复制](115开放平台/API列表/文件管理/文件复制.md)
|
||||
- [文件移动](115开放平台/API列表/文件管理/文件移动.md)
|
||||
- [获取文件下载地址](115开放平台/API列表/文件管理/获取文件下载地址.md)
|
||||
- [文件(夹)更新](115开放平台/API列表/文件管理/文件(夹)更新.md)
|
||||
- [删除文件](115开放平台/API列表/文件管理/删除文件.md)
|
||||
- [回收站列表](115开放平台/API列表/文件管理/回收站列表.md)
|
||||
- [回收站还原](115开放平台/API列表/文件管理/回收站还原.md)
|
||||
- [删除或清空回收站](115开放平台/API列表/文件管理/删除或清空回收站.md)
|
||||
- **视频播放/**
|
||||
- [记忆视频播放进度](115开放平台/API列表/视频播放/记忆视频播放进度.md)
|
||||
- [视频字幕列表](115开放平台/API列表/视频播放/视频字幕列表.md)
|
||||
- [获取视频播放进度](115开放平台/API列表/视频播放/获取视频播放进度.md)
|
||||
- [获取视频在线播放地址](115开放平台/API列表/视频播放/获取视频在线播放地址.md)
|
||||
- [提交视频转码](115开放平台/API列表/视频播放/提交视频转码.md)
|
||||
- **云下载/**
|
||||
- [解析BT种子](115开放平台/API列表/云下载/解析BT种子.md)
|
||||
- [获取用户云下载任务列表](115开放平台/API列表/云下载/获取用户云下载任务列表.md)
|
||||
- [获取云下载配额信息](115开放平台/API列表/云下载/获取云下载配额信息.md)
|
||||
- [清空云下载任务](115开放平台/API列表/云下载/清空云下载任务.md)
|
||||
- [添加云下载链接任务](115开放平台/API列表/云下载/添加云下载链接任务.md)
|
||||
- [删除用户云下载任务](115开放平台/API列表/云下载/删除用户云下载任务.md)
|
||||
- [添加云下载BT任务](115开放平台/API列表/云下载/添加云下载BT任务.md)
|
||||
|
||||
## API 接口速查
|
||||
|
||||
主要 API 域名与认证方式(详见各文档):
|
||||
|
||||
- 开放 API 基础地址:`https://proapi.115.com/open/...`
|
||||
- 认证方式:`Authorization: Bearer access_token`
|
||||
- OAuth 授权相关文档见 `115开放平台/接入指南/接入授权/`
|
||||
@@ -0,0 +1,308 @@
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/doc/",
|
||||
"children": [
|
||||
{
|
||||
"name": "115开放平台",
|
||||
"path": "/doc/115开放平台/",
|
||||
"children": [
|
||||
{
|
||||
"name": "简介",
|
||||
"path": "/doc/115开放平台/简介/",
|
||||
"children": [
|
||||
{
|
||||
"name": "概述.md",
|
||||
"path": "/doc/115开放平台/简介/概述.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "更新记录.md",
|
||||
"path": "/doc/115开放平台/简介/更新记录.md",
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"name": "接入指南",
|
||||
"path": "/doc/115开放平台/接入指南/",
|
||||
"children": [
|
||||
{
|
||||
"name": "接入流程.md",
|
||||
"path": "/doc/115开放平台/接入指南/接入流程.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "开发须知.md",
|
||||
"path": "/doc/115开放平台/接入指南/开发须知.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "授权错误码.md",
|
||||
"path": "/doc/115开放平台/接入指南/授权错误码.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "接入授权",
|
||||
"path": "/doc/115开放平台/接入指南/接入授权/",
|
||||
"children": [
|
||||
{
|
||||
"name": "手机扫码授权PKCE模式",
|
||||
"path": "/doc/115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/",
|
||||
"children": [
|
||||
{
|
||||
"name": "获取设备码和二维码内容.md",
|
||||
"path": "/doc/115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/获取设备码和二维码内容.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "轮询二维码状态.md",
|
||||
"path": "/doc/115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/轮询二维码状态.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "获取access_token.md",
|
||||
"path": "/doc/115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/获取access_token.md",
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"name": "授权码模式",
|
||||
"path": "/doc/115开放平台/接入指南/接入授权/授权码模式/",
|
||||
"children": [
|
||||
{
|
||||
"name": "请求授权.md",
|
||||
"path": "/doc/115开放平台/接入指南/接入授权/授权码模式/请求授权.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "用授权码换取access_token.md",
|
||||
"path": "/doc/115开放平台/接入指南/接入授权/授权码模式/用授权码换取access_token.md",
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"name": "刷新access_token.md",
|
||||
"path": "/doc/115开放平台/接入指南/接入授权/刷新access_token.md",
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"name": "API列表",
|
||||
"path": "/doc/115开放平台/API列表/",
|
||||
"children": [
|
||||
{
|
||||
"name": "开发者商业价值转化:推广产品得收益.md",
|
||||
"path": "/doc/115开放平台/API列表/开发者商业价值转化:推广产品得收益.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "用户管理",
|
||||
"path": "/doc/115开放平台/API列表/用户管理/",
|
||||
"children": [
|
||||
{
|
||||
"name": "用户信息.md",
|
||||
"path": "/doc/115开放平台/API列表/用户管理/用户信息.md",
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"name": "文件管理",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/",
|
||||
"children": [
|
||||
{
|
||||
"name": "文件上传",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/文件上传/",
|
||||
"children": [
|
||||
{
|
||||
"name": "上传流程.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/文件上传/上传流程.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "获取上传凭证.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/文件上传/获取上传凭证.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "文件上传.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/文件上传/文件上传.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "断点续传.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/文件上传/断点续传.md",
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"name": "新建文件夹.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/新建文件夹.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "获取文件列表.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/获取文件列表.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "获取文件(夹)详情",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/获取文件(夹)详情/",
|
||||
"children": [
|
||||
{
|
||||
"name": "按ID获取.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/获取文件(夹)详情/按ID获取.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "按路径获取.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/获取文件(夹)详情/按路径获取.md",
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"name": "文件搜索.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/文件搜索.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "文件复制.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/文件复制.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "文件移动.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/文件移动.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "获取文件下载地址.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/获取文件下载地址.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "文件(夹)更新.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/文件(夹)更新.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "删除文件.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/删除文件.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "回收站列表.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/回收站列表.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "回收站还原.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/回收站还原.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "删除或清空回收站.md",
|
||||
"path": "/doc/115开放平台/API列表/文件管理/删除或清空回收站.md",
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"name": "视频播放",
|
||||
"path": "/doc/115开放平台/API列表/视频播放/",
|
||||
"children": [
|
||||
{
|
||||
"name": "记忆视频播放进度.md",
|
||||
"path": "/doc/115开放平台/API列表/视频播放/记忆视频播放进度.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "视频字幕列表.md",
|
||||
"path": "/doc/115开放平台/API列表/视频播放/视频字幕列表.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "获取视频播放进度.md",
|
||||
"path": "/doc/115开放平台/API列表/视频播放/获取视频播放进度.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "获取视频在线播放地址.md",
|
||||
"path": "/doc/115开放平台/API列表/视频播放/获取视频在线播放地址.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "提交视频转码.md",
|
||||
"path": "/doc/115开放平台/API列表/视频播放/提交视频转码.md",
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
},
|
||||
{
|
||||
"name": "云下载",
|
||||
"path": "/doc/115开放平台/API列表/云下载/",
|
||||
"children": [
|
||||
{
|
||||
"name": "解析BT种子.md",
|
||||
"path": "/doc/115开放平台/API列表/云下载/解析BT种子.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "获取用户云下载任务列表.md",
|
||||
"path": "/doc/115开放平台/API列表/云下载/获取用户云下载任务列表.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "获取云下载配额信息.md",
|
||||
"path": "/doc/115开放平台/API列表/云下载/获取云下载配额信息.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "清空云下载任务.md",
|
||||
"path": "/doc/115开放平台/API列表/云下载/清空云下载任务.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "添加云下载链接任务.md",
|
||||
"path": "/doc/115开放平台/API列表/云下载/添加云下载链接任务.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "删除用户云下载任务.md",
|
||||
"path": "/doc/115开放平台/API列表/云下载/删除用户云下载任务.md",
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"name": "添加云下载BT任务.md",
|
||||
"path": "/doc/115开放平台/API列表/云下载/添加云下载BT任务.md",
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
}
|
||||
],
|
||||
"type": "directory"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# 贡献规范
|
||||
|
||||
感谢你愿意帮助 MMTL 变得更稳定。这个项目主要面向 NAS、Docker 部署、媒体库整理、订阅下载和多端播放场景;提交 Issue 或 Pull Request 时,请尽量提供可复现、可验证的信息。
|
||||
感谢你愿意帮助 MeBox 变得更稳定。这个项目主要面向 NAS、Docker 部署、媒体库整理、订阅下载和多端播放场景;提交 Issue 或 Pull Request 时,请尽量提供可复现、可验证的信息。
|
||||
|
||||
## Issue 提交规范
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
- 期望行为:你认为正确结果应该是什么。
|
||||
- 复现步骤:从哪个页面、点击什么、填写什么、触发什么任务。
|
||||
- 部署方式:Docker 第一档 / 第二档 / 第三档、裸机运行、反代方式等。
|
||||
- 环境信息:NAS 型号或系统、Docker / Compose 版本、浏览器、MMTL 镜像版本。
|
||||
- 环境信息:NAS 型号或系统、Docker / Compose 版本、浏览器、MeBox 镜像版本。
|
||||
- 相关配置:路径映射、下载器保存路径、媒体库路径、站点类型等。请隐藏 Cookie、API Key、密码和 Token。
|
||||
- 日志和任务信息:优先提供应用日志、任务队列详情、浏览器控制台错误、网络请求错误。
|
||||
|
||||
@@ -28,14 +28,14 @@ Docker 部署常用命令:
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker compose logs --tail=300 mmtl
|
||||
docker compose exec mmtl sh -lc 'ls -la /data/logs || true'
|
||||
docker compose logs --tail=300 mebox
|
||||
docker compose exec mebox sh -lc 'ls -la /data/logs || true'
|
||||
```
|
||||
|
||||
PostgreSQL 部署查询示例:
|
||||
|
||||
```bash
|
||||
docker compose exec postgres psql -U mmtl -d mmtl -c "select key,value,updated_at from settings order by updated_at desc limit 30;"
|
||||
docker compose exec postgres psql -U mebox -d mebox -c "select key,value,updated_at from settings order by updated_at desc limit 30;"
|
||||
```
|
||||
|
||||
请勿公开粘贴以下敏感信息:
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
# =============================================================================
|
||||
# Multi-architecture build for MMTL.
|
||||
# Multi-architecture build for MeBox.
|
||||
#
|
||||
# Stage 1 (frontend) : Node 20 -> static SPA bundle
|
||||
# Stage 1 (frontend) : Node 20.19+ -> static SPA bundle
|
||||
# Stage 2 (backend) : Go 1.25 -> single static binary (CGO_ENABLED=0)
|
||||
# Stage 3 (runtime) : Alpine 3.23 -> ffmpeg + tzdata + non-root user
|
||||
#
|
||||
# Build:
|
||||
# docker buildx build --platform linux/amd64,linux/arm64 \
|
||||
# --build-arg VERSION=MMTL-v0.1.16 -t mmtl:latest --push .
|
||||
# --build-arg VERSION=MeBox-v0.1.16 -t mebox:latest --push .
|
||||
#
|
||||
# Optional Intel VAAPI/QSV runtime packages:
|
||||
# docker buildx build --build-arg WITH_VAAPI=true ...
|
||||
# =============================================================================
|
||||
|
||||
# ---- Stage 1: frontend (always build on the host architecture) -------------
|
||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS frontend
|
||||
FROM --platform=$BUILDPLATFORM node:20.19-alpine AS frontend
|
||||
ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org/
|
||||
WORKDIR /app/web
|
||||
COPY web/package*.json ./
|
||||
@@ -39,7 +39,7 @@ COPY . .
|
||||
COPY --from=frontend /app/web/dist ./web/dist
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
|
||||
go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o mmtl ./cmd/server
|
||||
go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o mebox ./cmd/server
|
||||
|
||||
# ---- Stage 3: runtime ------------------------------------------------------
|
||||
FROM alpine:3.23
|
||||
@@ -64,22 +64,22 @@ RUN apk add --no-cache \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# Non-root user for the long-running process.
|
||||
RUN addgroup -S mmtl && adduser -S mmtl -G mmtl
|
||||
RUN addgroup -S mebox && adduser -S mebox -G mebox
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=backend /app/mmtl /usr/local/bin/mmtl
|
||||
COPY --from=backend /app/mebox /usr/local/bin/mebox
|
||||
COPY --from=frontend /app/web/dist /app/web/dist
|
||||
|
||||
RUN mkdir -p /data /cache /media \
|
||||
&& chown -R mmtl:mmtl /data /cache /media
|
||||
&& chown -R mebox:mebox /data /cache /media
|
||||
|
||||
# Default environment (overridable via docker-compose / `docker run -e`).
|
||||
ENV MMTL_APP_PORT=8080 \
|
||||
MMTL_APP_DATA_DIR=/data \
|
||||
MMTL_APP_WEB_DIR=/app/web/dist \
|
||||
MMTL_DATABASE_DB_PATH=/data/mmtl.db \
|
||||
MMTL_CACHE_CACHE_DIR=/cache \
|
||||
MMTL_LOGGING_LEVEL=info \
|
||||
ENV MEBOX_APP_PORT=8080 \
|
||||
MEBOX_APP_DATA_DIR=/data \
|
||||
MEBOX_APP_WEB_DIR=/app/web/dist \
|
||||
MEBOX_DATABASE_DB_PATH=/data/mebox.db \
|
||||
MEBOX_CACHE_CACHE_DIR=/cache \
|
||||
MEBOX_LOGGING_LEVEL=info \
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
# MMTL (My Movie and TV Library)
|
||||
# MeBox
|
||||
|
||||
<p align="center">
|
||||
<img src="web/public/brand/logo-192.png" width="96" height="96" alt="MMTL Logo" />
|
||||
<img src="web/public/brand/logo-192.png" width="96" height="96" alt="MeBox Logo" />
|
||||
</p>
|
||||
|
||||
<h3 align="center">适合 NAS、家庭共享和多端播放的私人媒体中心</h3>
|
||||
<h3 align="center">面向 NAS 与家庭影音场景的私人媒体中心</h3>
|
||||
|
||||
<p align="center">
|
||||
<strong>Docker 一键部署 · PostgreSQL 主库 · Redis 热缓存 · OpenSearch 搜索增强 · Emby 协议兼容 · Bot 通知</strong>
|
||||
<strong>媒体库 · 刮削整理 · 网盘 STRM · 兼容 Emby/Jellyfin 客户端 · 远程 Emby 挂载 · 多用户权限 · Docker 一键部署</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#项目简介">项目简介</a> ·
|
||||
<a href="#快速开始">快速开始</a> ·
|
||||
<a href="#三挡部署">三挡部署</a> ·
|
||||
<a href="#路径映射">路径映射</a> ·
|
||||
<a href="#旧-sqlite-迁移">旧 SQLite 迁移</a> ·
|
||||
<a href="#部署档位">部署档位</a> ·
|
||||
<a href="#鸣谢">鸣谢</a> ·
|
||||
<a href="#开发构建">开发构建</a> ·
|
||||
<a href="README_EN.md">English</a> ·
|
||||
<a href="CONTRIBUTING.md">贡献规范</a> ·
|
||||
<a href="https://mgo.3jzs.com">在线演示</a>
|
||||
<a href="https://t.me/MeBoxGroup">Telegram 群组</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -31,383 +32,254 @@
|
||||
|
||||
## 项目简介
|
||||
|
||||
MMTL 是一个自托管媒体管理系统,面向 NAS、小主机、家庭影音和多用户共享场景。它把媒体库、刮削、下载整理、订阅、网盘播放、Emby 协议兼容、用户权限和 Bot 通知放在一个后台里,目标是让用户只维护一套服务,就能给网页端、手机端、电视端和第三方播放器使用。
|
||||
**MeBox** 是一个自托管私人媒体管理系统,适合 NAS、小主机、家庭共享和多端播放场景。本项目由 [MediaStationGo](https://github.com/ShukeBta/MediaStationGo) fork 并持续二开维护,在保留「一套服务覆盖网页、手机、电视与第三方播放器」思路的同时,围绕网盘播放、任务队列、远程挂载和权限体系做了大量增强。
|
||||
|
||||
核心能力:
|
||||
你可以把 MeBox 理解为:
|
||||
|
||||
- 一个带现代 Web UI 的**媒体库后台**
|
||||
- 一个兼容 Emby/Jellyfin 客户端的**协议网关**
|
||||
- 一个连接本地硬盘、下载目录与网盘存储的**整理与播放入口**
|
||||
|
||||
### 核心能力
|
||||
|
||||
| 模块 | 说明 |
|
||||
| --- | --- |
|
||||
| **媒体库** | 电影、电视剧、动漫、综艺、音乐与自定义库;多根目录、扫库、海报墙、继续观看 |
|
||||
| **元数据刮削** | TMDb、Bangumi、Douban、TheTVDB、Fanart 等;支持 NFO、手动匹配、刮削队列 |
|
||||
| **播放** | 网页播放器、HLS 转码、弹幕、字幕、播放配置档、观看历史与收藏 |
|
||||
| **Emby/Jellyfin 客户端兼容** | 内置完整 Emby 服务端协议实现:Infuse、SenPlayer、Fileball、Emby/Jellyfin 官方客户端等可直接把本服务当作 Emby 服务器添加,使用 MeBox 账号登录,海报墙、进度同步、多用户无缝衔接 |
|
||||
| **远程 Emby 挂载** | 将远程 Emby 媒体库挂载到本地界面统一浏览(无需单独开 Emby 客户端) |
|
||||
| **网盘与 STRM** | OpenList、CloudDrive2、115、WebDAV 等;STRM 同步、上传/下载队列、直链/302 播放 |
|
||||
| **下载与整理** | 下载目录定时自动整理(智能分类、自动注册媒体库)、文件管理器(复制/移动/硬链/软链) |
|
||||
| **用户与权限** | 管理员/普通用户、有效期、成人内容开关、播放配置 PIN、细粒度操作权限 |
|
||||
| **运维能力** | 统一任务队列、存储统计、DLNA 投屏、系统设置与日志 |
|
||||
|
||||
### 技术栈
|
||||
|
||||
- **后端**:Go · Gin · GORM · SQLite / PostgreSQL · 可选 Redis · 可选 OpenSearch
|
||||
- **前端**:React 18 · Vite · TypeScript · Tailwind CSS · Zustand
|
||||
- **部署**:Docker Compose 多档模板,支持 amd64 / arm64 镜像与单文件可执行发布
|
||||
|
||||
---
|
||||
|
||||
- **媒体库管理**:电影、电视剧、动漫、综艺、音乐和自定义媒体库统一管理。
|
||||
- **Emby 协议兼容**:Infuse、VidHub、SenPlayer、Fileball 等客户端可按 Emby/Jellyfin 方式添加服务器。
|
||||
- **本地 + 网盘**:支持本地硬盘、下载目录、OpenList、CloudDrive2、WebDAV、STRMURL 和 302 反代播放。
|
||||
- **订阅下载入库**:连接 qBittorrent 后支持搜索、订阅、下载完成整理、刮削和入库通知。
|
||||
- **多用户与权限**:管理员/普通用户、有效期、成人内容开关、设备管理、注册码和 Telegram Bot 绑定。
|
||||
- **灵活部署**:单镜像 SQLite 一键起步,或按规模选择 PostgreSQL、Redis、OpenSearch,低配 NAS 到大库检索都能覆盖。
|
||||
## 快速开始
|
||||
|
||||
最推荐使用 Docker Compose。仓库提供四份独立完整模板,全部不依赖 `.env`。想最省心就下载单镜像档(SQLite,只有一个镜像);只需要按需修改访问端口、媒体目录、下载目录和可选硬件设备。需要多用户/高并发再选第一档起的 PostgreSQL 档位。
|
||||
推荐使用 Docker Compose。仓库提供四份**互相独立**的完整模板,无需 `.env` 即可起步。
|
||||
|
||||
```bash
|
||||
mkdir -p MMTL
|
||||
cd MMTL
|
||||
# 最省心:单镜像 + SQLite,只启动一个容器
|
||||
curl -fsSL https://raw.githubusercontent.com/truewhile/MMTL/main/docker-compose.simple.yml -o docker-compose.yml
|
||||
# 或第一档:PostgreSQL(多用户/高并发更稳)
|
||||
# curl -fsSL https://raw.githubusercontent.com/truewhile/MMTL/main/docker-compose.yml -o docker-compose.yml
|
||||
mkdir -p MeBox && cd MeBox
|
||||
|
||||
# 最省心:单镜像 + 内置 SQLite
|
||||
curl -fsSL https://raw.githubusercontent.com/truewhile/MeBox/main/docker-compose.simple.yml -o docker-compose.yml
|
||||
|
||||
# 或多用户场景:PostgreSQL 第一档
|
||||
# curl -fsSL https://raw.githubusercontent.com/truewhile/MeBox/main/docker-compose.yml -o docker-compose.yml
|
||||
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
启动后访问:
|
||||
浏览器访问:
|
||||
|
||||
```text
|
||||
http://服务器IP:18080
|
||||
```
|
||||
|
||||
默认账号:
|
||||
默认账号:`admin` / `admin123`(首次登录后请立即修改密码)
|
||||
|
||||
```text
|
||||
admin / admin123
|
||||
```
|
||||
|
||||
首次登录后请立刻修改管理员密码。
|
||||
> 💡 **Emby 用户无缝切换**:MeBox 完整兼容 Emby/Jellyfin 客户端协议。手机、电视、平板上的 Infuse、SenPlayer、Fileball、Emby/Jellyfin 官方客户端,直接按「添加 Emby 服务器」填入 `http://服务器IP:18080`,用 MeBox 账号登录即可,无需改变原有使用习惯。
|
||||
|
||||
镜像地址:
|
||||
|
||||
```text
|
||||
GHCR:ghcr.io/truewhile/mmtl:latest
|
||||
ghcr.io/truewhile/mebox:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部署档位
|
||||
|
||||
MMTL 推荐按机器资源和用户规模选择部署档位。每份 Compose 文件都是完整文件,不需要再叠加多个 `-f`。想一个镜像跑起来就选单镜像档(SQLite);需要多用户 / 高并发时再用 PostgreSQL 三档。Redis 和 OpenSearch 是增强组件,不替代 PostgreSQL。
|
||||
按机器资源选择档位。每份 Compose 文件均可单独使用,**不要**叠加多个 `-f`。
|
||||
|
||||
| 档位 | 完整配置文件 | 组件 | 适合场景 |
|
||||
| 档位 | 配置文件 | 组件 | 适合场景 |
|
||||
| --- | --- | --- | --- |
|
||||
| 单镜像档 | `docker-compose.simple.yml` | MMTL + 内置 SQLite | 新手、单人使用、只想一个镜像跑起来的低配机器 |
|
||||
| 第一档 | `docker-compose.yml` | MMTL + PostgreSQL | 大多数 NAS、个人/家庭使用、低内存机器 |
|
||||
| 第二档 | `docker-compose.standard.yml` | MMTL + PostgreSQL + Redis | 多用户、Emby 客户端频繁刷新、首页/媒体列表访问较多 |
|
||||
| 第三档 | `docker-compose.search.yml` | MMTL + PostgreSQL + Redis + OpenSearch | 超大媒体库、复杂全文搜索、后续需要独立搜索索引 |
|
||||
| 单镜像档 | `docker-compose.simple.yml` | MeBox + SQLite | 新手、单人、低配 NAS,只想一个容器跑起来 |
|
||||
| 第一档 | `docker-compose.yml` | MeBox + PostgreSQL | 大多数家庭 NAS,多用户更稳 |
|
||||
| 第二档 | `docker-compose.standard.yml` | + Redis | 多用户、Emby 客户端频繁刷新、首页/列表访问多 |
|
||||
| 第三档 | `docker-compose.search.yml` | + OpenSearch | 超大媒体库、复杂全文搜索(内存占用更高) |
|
||||
|
||||
### 单镜像档:SQLite(最省心)
|
||||
### 单镜像档要点
|
||||
|
||||
只启动 MMTL 一个镜像,主数据库用内置 SQLite,不需要 PostgreSQL / Redis / `.env`。变量最少、资源占用最低,适合新手和单人使用。日后需要多用户或更高并发时,保留 `./data` 后切换到第一档的 PostgreSQL 即可。
|
||||
|
||||
```bash
|
||||
mkdir -p MMTL
|
||||
cd MMTL
|
||||
curl -fsSL https://raw.githubusercontent.com/truewhile/MMTL/main/docker-compose.simple.yml -o docker-compose.yml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
第一次部署通常只需要改 `docker-compose.yml` 里的这几处:
|
||||
- 只启动 **一个** MeBox 容器,数据在 `./data/mebox.db`
|
||||
- 通常只需改端口与媒体目录挂载
|
||||
- **不要**设置 `MEBOX_DATABASE_DSN`,否则会切到 PostgreSQL
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "18080:8080" # 改左边 18080 即可
|
||||
- "18080:8080"
|
||||
volumes:
|
||||
- ./data:/data # 必须备份
|
||||
- ./media:/media # 改左边为你的媒体目录,例如 /vol1/1000/Media:/media
|
||||
# - /dev/dri:/dev/dri # Intel 核显硬解需要时取消注释
|
||||
- ./cache:/cache # 可重建
|
||||
- ./media:/media # 改成你的媒体目录
|
||||
```
|
||||
|
||||
网页后台添加媒体库时填写容器内路径:
|
||||
网页添加媒体库时填写容器内路径,例如 `/media`、`/media/电影`。
|
||||
|
||||
```text
|
||||
/media
|
||||
/media/电影
|
||||
/media/电视剧
|
||||
```
|
||||
### PostgreSQL 档位要点
|
||||
|
||||
关键数据目录:
|
||||
|
||||
```text
|
||||
./data JWT 密钥、运行配置、SQLite 主数据库(mmtl.db)——必须备份
|
||||
./cache 海报/临时缓存,可重建
|
||||
./media 媒体库
|
||||
```
|
||||
|
||||
> 单镜像模式请不要配置 `MMTL_DATABASE_DSN`;一旦填了 DSN 就会切回 PostgreSQL。
|
||||
|
||||
### 第一档:PostgreSQL
|
||||
|
||||
第一档是默认推荐部署。它只启动主服务和 PostgreSQL,资源占用最低,适合绝大多数 NAS。
|
||||
|
||||
```bash
|
||||
mkdir -p MMTL
|
||||
cd MMTL
|
||||
curl -fsSL https://raw.githubusercontent.com/truewhile/MMTL/main/docker-compose.yml -o docker-compose.yml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
关键数据目录:
|
||||
|
||||
```text
|
||||
./postgres PostgreSQL 主数据库,必须备份
|
||||
./data JWT 密钥、运行配置、旧 SQLite 迁移源
|
||||
./cache 海报、临时文件、转码缓存,可删除重建
|
||||
```
|
||||
|
||||
### 第二档:PostgreSQL + Redis
|
||||
|
||||
第二档是独立完整文件,包含第一档全部配置并额外启用 Redis。Redis 用作热缓存,能减轻多用户和 Emby 客户端频繁刷新时的数据库压力。
|
||||
|
||||
```bash
|
||||
mkdir -p MMTL
|
||||
cd MMTL
|
||||
curl -fsSL https://raw.githubusercontent.com/truewhile/MMTL/main/docker-compose.standard.yml -o docker-compose.yml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Redis 数据目录是 `./redis`。它主要保存缓存,通常可重建;真正需要备份的仍然是 `./postgres` 和 `./data`。
|
||||
|
||||
### 第三档:PostgreSQL + Redis + OpenSearch
|
||||
|
||||
第三档是独立完整文件,包含第二档全部配置并额外启用 OpenSearch,用于大库全文搜索和独立搜索索引。OpenSearch 常驻内存明显更高,低配 NAS 不建议开启。
|
||||
|
||||
```bash
|
||||
mkdir -p MMTL
|
||||
cd MMTL
|
||||
curl -fsSL https://raw.githubusercontent.com/truewhile/MMTL/main/docker-compose.search.yml -o docker-compose.yml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
OpenSearch 数据目录是 `./opensearch`。搜索索引可重建,但重建大库索引会花时间;机器资源足够时再开启第三档。
|
||||
|
||||
## 配置示例
|
||||
|
||||
仓库内提供四份推荐 Compose 文件:
|
||||
|
||||
```text
|
||||
docker-compose.simple.yml 单镜像档:MMTL + 内置 SQLite
|
||||
docker-compose.yml 第一档:MMTL + PostgreSQL
|
||||
docker-compose.standard.yml 第二档:MMTL + PostgreSQL + Redis
|
||||
docker-compose.search.yml 第三档:MMTL + PostgreSQL + Redis + OpenSearch
|
||||
```
|
||||
|
||||
仓库只保留面向用户部署和项目维护的必要文件。旧的本地部署脚本、发包脚本、开发机辅助脚本、`.env` 示例和旧高级 Compose 模板已经移除;Linux / Docker 用户按上面四个 Compose 文件部署即可。开发者本地生成的 `bin/`、`data/`、`cache/`、`logs/`、`.tmp/`、`tools/` 等目录已列入 `.gitignore`,不应提交到仓库。
|
||||
|
||||
如果直接下载为 `docker-compose.yml`,启动命令统一是:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
如果保留原始文件名,也可以这样启动:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.simple.yml up -d
|
||||
docker compose -f docker-compose.standard.yml up -d
|
||||
docker compose -f docker-compose.search.yml up -d
|
||||
```
|
||||
|
||||
常用配置片段如下,注释保留为中文,方便直接复制到 NAS 上调整:
|
||||
- 主库在 `./postgres`,配置与密钥在 `./data`
|
||||
- 若存在旧版 `./data/mebox.db`,首次启动会自动迁移到 PostgreSQL
|
||||
- 迁移完成后可将 `MEBOX_DATABASE_DB_PATH` 改为不存在路径,避免重复检查:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mmtl:
|
||||
image: ghcr.io/truewhile/MMTL:latest
|
||||
ports:
|
||||
# 左边是宿主机访问端口,右边是容器内端口。
|
||||
- "18080:8080"
|
||||
volumes:
|
||||
# 运行数据:JWT 密钥、配置、旧 SQLite 迁移源。
|
||||
- ./data:/data
|
||||
|
||||
# 缓存目录:海报、临时文件、转码缓存,可删除重建。
|
||||
- ./cache:/cache
|
||||
|
||||
# 媒体库目录:自动整理/重命名/入库需要写权限。
|
||||
- /vol1/1000/Media:/media
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
|
||||
# PostgreSQL 主数据库。
|
||||
MMTL_DATABASE_TYPE: postgres
|
||||
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||
|
||||
# 旧 SQLite 迁移源:只在从旧版 data/mmtl.db 导入时使用。
|
||||
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||
|
||||
# 路径换算:宿主机路径和容器路径必须一一对应。
|
||||
MMTL_MEDIA_DIR: /vol1/1000/Media
|
||||
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||
MMTL_DOWNLOAD_DIR: /vol1/1000/Downloads
|
||||
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||
MEBOX_DATABASE_DB_PATH: /data/no-sqlite-migration.db
|
||||
```
|
||||
|
||||
### 必须备份与可重建
|
||||
|
||||
| 路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `./data` | JWT 密钥、运行配置、SQLite 主库或迁移源 |
|
||||
| `./postgres` | PostgreSQL 主库(PG 档位) |
|
||||
| `./cache` | 海报/转码缓存,可重建 |
|
||||
| `./redis` | 热缓存,可重建 |
|
||||
| `./opensearch` | 搜索索引,可重建 |
|
||||
|
||||
### 更新镜像
|
||||
|
||||
```bash
|
||||
docker compose pull mebox
|
||||
docker compose up -d --no-deps mebox
|
||||
```
|
||||
|
||||
日常更新只拉 `mebox` 服务即可,不要随意 `docker compose pull` 升级 PostgreSQL/Redis/OpenSearch 基础镜像。
|
||||
|
||||
---
|
||||
|
||||
## 路径映射
|
||||
|
||||
路径映射是 Docker 部署里最容易填错的地方。原则是:`volumes` 左边是宿主机真实路径,右边是容器内路径;环境变量里也要保持对应关系。
|
||||
Docker 部署最常见的问题是路径填错。记住:
|
||||
|
||||
- `volumes` **左侧**是宿主机真实路径,**右侧**是容器内路径
|
||||
- 网页后台添加媒体库时,应填写**容器内**路径(如 `/media/电影`)
|
||||
- 若使用自动整理/下载入库,`MEBOX_MEDIA_DIR` 与 `MEBOX_DOWNLOAD_DIR` 需与挂载一致
|
||||
|
||||
NAS 示例:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- /vol1/1000/Docker/moviepilot-v2/media:/vol1/1000/Docker/moviepilot-v2/media
|
||||
- /vol1/1000/qBittorrent/downloads:/vol1/1000/qBittorrent/downloads
|
||||
- /vol1/1000/Media:/media
|
||||
- /vol1/1000/Downloads:/downloads
|
||||
environment:
|
||||
MMTL_MEDIA_DIR: /vol1/1000/Docker/moviepilot-v2/media
|
||||
MMTL_MEDIA_CONTAINER_DIR: /vol1/1000/Docker/moviepilot-v2/media
|
||||
MMTL_DOWNLOAD_DIR: /vol1/1000/qBittorrent/downloads
|
||||
MMTL_DOWNLOAD_CONTAINER_DIR: /vol1/1000/qBittorrent/downloads
|
||||
MEBOX_MEDIA_DIR: /vol1/1000/Media
|
||||
MEBOX_MEDIA_CONTAINER_DIR: /media
|
||||
MEBOX_DOWNLOAD_DIR: /vol1/1000/Downloads
|
||||
MEBOX_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||
```
|
||||
|
||||
Windows Docker Desktop 示例:
|
||||
---
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- D:/Media:/media
|
||||
environment:
|
||||
MMTL_MEDIA_DIR: D:/Media
|
||||
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||
```
|
||||
## 首次使用建议
|
||||
|
||||
如果后台添加媒体库时填的是 `/vol1/...`,Compose 里也建议把同一个 `/vol1/...` 挂进容器,避免自动整理和下载入库时路径不可访问。
|
||||
1. **创建媒体库** → 填写 `/media/...` → 执行扫库
|
||||
2. **配置元数据源** → 系统设置中添加 TMDb、Bangumi 等 API
|
||||
3. **(可选)配置下载目录自动整理** → 文件管理中将下载目录设为整理源,下载完成后自动分类入库
|
||||
4. **(可选)配置网盘账号** → STRM 管理中添加 OpenList / 115 / WebDAV 等
|
||||
5. **第三方播放器** → 以 Emby 服务器添加 `http://服务器IP:18080`,使用 MeBox 账号登录
|
||||
|
||||
## 旧 SQLite 迁移
|
||||
|
||||
新版推荐 PostgreSQL 作为主数据库。`MMTL_DATABASE_DB_PATH` 不是主库路径,而是旧 SQLite 数据的迁移源。
|
||||
|
||||
迁移步骤:
|
||||
|
||||
1. 把旧版 `mmtl.db` 放到 `./data/mmtl.db`。
|
||||
2. 保持 `MMTL_DATABASE_DB_PATH: /data/mmtl.db`。
|
||||
3. 启动一次,确认日志显示迁移完成,网页数据正常。
|
||||
4. 备份 `./postgres` 和 `./data`。
|
||||
5. 确认不再需要 SQLite 后,把迁移源改成不存在的路径,例如:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
# 已完成 SQLite 迁移后,建议改成不存在的路径,避免下次启动重复检查旧库。
|
||||
MMTL_DATABASE_DB_PATH: /data/no-sqlite-migration.db
|
||||
```
|
||||
|
||||
不要删除 `./postgres`。PostgreSQL 已经是主数据库,删除它会丢失账号、媒体库、订阅、配置和历史数据。
|
||||
|
||||
## 日志与 STRM 路径
|
||||
|
||||
Compose 模板默认把完整应用日志写入 `./data/logs/app.log`,同时拆分 `./data/logs/warn.log` 和 `./data/logs/error.log`。Docker 自身日志也会保留 10 个 50MB 文件:
|
||||
|
||||
```bash
|
||||
docker compose logs -f mmtl
|
||||
tail -f ./data/logs/app.log
|
||||
tail -f ./data/logs/error.log
|
||||
```
|
||||
|
||||
如果要排查订阅、站点搜索、自动整理或 STRM 生成问题,保持 `MMTL_LOGGING_LEVEL: info`;需要更细日志时临时改成 `debug`,确认后再改回 `info`。
|
||||
|
||||
STRM 输出目录请使用容器内可写路径,例如 `/data/strm`,或你已经挂载进容器的媒体目录。旧版本保存过 `/app/data/strm` 的部署会在生成时自动迁移到当前 `MMTL_APP_DATA_DIR`,默认就是 `/data`。
|
||||
|
||||
## 更新与备份
|
||||
|
||||
更新镜像:
|
||||
|
||||
```bash
|
||||
docker compose pull mmtl
|
||||
docker compose up -d --no-deps mmtl
|
||||
```
|
||||
|
||||
不要执行裸 `docker compose pull` 做日常更新。PostgreSQL / Redis / OpenSearch 是数据与缓存基础组件,compose 已设置为 `pull_policy: missing`,首次部署缺镜像时会拉取,日常更新只建议拉取 `mmtl`。需要升级这些基础组件时,请先备份 `./postgres`,再手动修改镜像版本并单独拉取。
|
||||
|
||||
如果第二档或第三档保留了原始文件名,更新时指定对应完整文件:
|
||||
|
||||
```bash
|
||||
# 第二档
|
||||
docker compose -f docker-compose.standard.yml pull mmtl
|
||||
docker compose -f docker-compose.standard.yml up -d --no-deps mmtl
|
||||
|
||||
# 第三档
|
||||
docker compose -f docker-compose.search.yml pull mmtl
|
||||
docker compose -f docker-compose.search.yml up -d --no-deps mmtl
|
||||
```
|
||||
|
||||
必须备份:
|
||||
|
||||
```text
|
||||
./postgres PostgreSQL 主数据库
|
||||
./data JWT 密钥、运行配置、旧 SQLite 迁移源
|
||||
```
|
||||
|
||||
可重建:
|
||||
|
||||
```text
|
||||
./cache 图片缓存、临时文件、转码缓存
|
||||
./redis Redis 热缓存
|
||||
./opensearch 搜索索引
|
||||
```
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
**启动后还是反复迁移 SQLite?**
|
||||
**扫库或入库很慢?**
|
||||
先确认路径映射与数据库档位。网盘扫描还受接口限速与目录规模影响;大库可考虑第二档 Redis 或第三档 OpenSearch。
|
||||
|
||||
确认旧数据已经迁移成功后,把 `MMTL_DATABASE_DB_PATH` 改成不存在的路径,例如 `/data/no-sqlite-migration.db`,然后重启容器。
|
||||
**下载目录文件没有被自动整理?**
|
||||
确认下载目录已通过 `volumes` 挂进容器,且 `MEBOX_DOWNLOAD_*` 环境变量对应正确。MeBox 负责目录整理入库,qBittorrent 等下载器按普通软件自行部署即可。
|
||||
|
||||
**扫库或入库速度很慢?**
|
||||
**硬链接失败(cross-device link)?**
|
||||
硬链接要求源与目标在同一文件系统/子卷;跨盘、跨 btrfs 子卷或网盘挂载时请改用复制或软链接。
|
||||
|
||||
先确认数据库档位和路径映射正确。第一档已经足够大多数场景;第二档 Redis 能缓解频繁刷新造成的数据库压力;第三档主要增强搜索,不会替代媒体扫描本身。网盘扫描还会受网盘接口响应、目录数量和网络质量影响。
|
||||
**日志保留时间太短?**
|
||||
|
||||
**qBittorrent 下载完成后无法整理?**
|
||||
默认应用日志为 `20MB x 5`,容器 stdout 日志为 `20m x 3`。排障时可在 compose 中调大
|
||||
`MEBOX_LOGGING_MAX_SIZE_MB`、`MEBOX_LOGGING_MAX_BACKUPS` 与服务的 `logging.options.max-size/max-file`。
|
||||
|
||||
确认 qBittorrent 保存路径已经通过 `volumes` 挂载进 MMTL 容器,并且 `MMTL_DOWNLOAD_DIR` 与 `MMTL_DOWNLOAD_CONTAINER_DIR` 对应正确。
|
||||
**第三方播放器连不上?**
|
||||
确认地址为 `http://IP:18080`,使用 MeBox 用户账号;反代部署需正确配置外部 URL 与 HTTPS 头。
|
||||
|
||||
**硬链接目录在 Docker / NAS 上看不到内容?**
|
||||
|
||||
硬链接不能直接链接“目录”本身,只能链接目录里的文件。文件管理器执行目录硬链接时会递归创建目标目录结构,并为每个文件创建硬链接。硬链接还要求源文件和目标文件在容器内属于同一个文件系统/子卷;如果下载目录和媒体目录是两个独立 bind mount、不同硬盘、不同 btrfs 子卷或网盘挂载,系统会返回 `invalid cross-device link`,此时请选择“复制”或“软链接”。
|
||||
|
||||
**第三方播放器无法连接?**
|
||||
|
||||
确认播放器填写的是 `http://服务器IP:18080`,账号密码使用 MMTL 用户账号。反代部署时需要正确设置外部访问地址和 HTTPS 头。
|
||||
---
|
||||
|
||||
## 开发构建
|
||||
|
||||
本地开发需要 Go、Node.js 和 npm。
|
||||
后端通过 `go:embed` 嵌入 `web/dist`,**编译前必须先构建前端**。
|
||||
前端构建要求 Node.js `20.19+` 或 `22.12+`。
|
||||
|
||||
```bash
|
||||
# 后端测试
|
||||
go test ./...
|
||||
|
||||
# 前端依赖与构建
|
||||
npm --prefix web install
|
||||
npm --prefix web ci
|
||||
npm --prefix web run build
|
||||
|
||||
# 本地运行后端
|
||||
go run ./cmd/server
|
||||
|
||||
# 本地运行前端开发服务器
|
||||
npm --prefix web run dev
|
||||
go test ./...
|
||||
go run ./cmd/server # http://127.0.0.1:8080
|
||||
npm --prefix web run dev # http://127.0.0.1:3000
|
||||
```
|
||||
|
||||
前端开发服务器默认访问:
|
||||
CI 会在 Release 中提供 Windows / Linux / macOS 的 amd64、arm64 单文件可执行程序。
|
||||
|
||||
```text
|
||||
http://127.0.0.1:3000
|
||||
Windows 本地打包:
|
||||
|
||||
```powershell
|
||||
.\scripts\build-windows.ps1 -Version dev
|
||||
```
|
||||
|
||||
后端健康检查:
|
||||
Windows 可执行程序使用项目 Logo,不显示控制台窗口;启动后会常驻系统托盘。托盘菜单可打开 MeBox、切换开机自启、查看日志、重启或退出。
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8080/api/health
|
||||
```
|
||||
---
|
||||
|
||||
## 鸣谢
|
||||
|
||||
MeBox 在 [MediaStationGo](https://github.com/ShukeBta/MediaStationGo) 的基础上 fork 并持续演进。感谢上游项目在媒体库架构、Emby 协议兼容和自托管体验上的奠基工作。
|
||||
|
||||
项目中许多网盘同步、STRM 与媒体整理相关的设计与实现,也参考了 [qmediasync](https://github.com/qicfan/qmediasync)。感谢该项目的思路与实践经验。
|
||||
|
||||
---
|
||||
|
||||
## 贡献与反馈
|
||||
|
||||
提交 Bug、功能建议或 Pull Request 前,请先阅读 [贡献规范](CONTRIBUTING.md)。
|
||||
提交 Issue 或 Pull Request 前,请阅读 [贡献规范](CONTRIBUTING.md) 与 [安全策略](SECURITY.md)。
|
||||
|
||||
- Bug 反馈请使用 Issue 模板,并提供部署方式、复现步骤、日志和关键配置。
|
||||
- 功能建议请说明使用场景、期望行为和可接受的替代方案。
|
||||
- 安全漏洞请不要公开发 Issue,按 [安全策略](SECURITY.md) 使用私密渠道报告。
|
||||
- Pull Request 请从独立分支或 fork 分支发起,不要直接向 `main` 推送。
|
||||
- 分支名建议使用 `fix/...`、`feat/...`、`docs/...` 或 `test/...`,例如 `docs/contribution-guidelines`。
|
||||
- 提交前按改动范围运行 `go test ./...`、`npm --prefix web run build` 或定向测试,并在 PR 中说明验证结果。
|
||||
- Bug 请附部署方式、复现步骤与相关日志
|
||||
- 功能建议请说明使用场景与期望行为
|
||||
- PR 请从独立分支发起,提交前运行 `go test ./...` 与 `npm --prefix web run build`
|
||||
|
||||
---
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/?repos=ShukeBta%2FMMTL&type=date&legend=top-left">
|
||||
<a href="https://www.star-history.com/?repos=truewhile%2FMeBox&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=truewhile/MeBox&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=truewhile/MeBox&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=truewhile/MeBox&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目使用 GPL-3.0 License。详见 [LICENSE](LICENSE)。
|
||||
本项目采用 [GPL-3.0](LICENSE) 许可证。
|
||||
|
||||
---
|
||||
|
||||
## 赞赏
|
||||
|
||||
如果 MeBox 帮你把家庭影音折腾明白了,欢迎请作者喝杯咖啡 ☕
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/donation-qr.png" width="320" alt="WhileTrue 的赞赏码" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>Telegram 交流群</strong>:<a href="https://t.me/MeBoxGroup">https://t.me/MeBoxGroup</a><br/>
|
||||
使用问题、功能建议、更新动态,欢迎来群里聊
|
||||
</p>
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
# MMTL (My Movie and TV Library)
|
||||
# MeBox
|
||||
|
||||
<p align="center">
|
||||
<img src="web/public/brand/logo-192.png" width="96" height="96" alt="MMTL Logo" />
|
||||
<img src="web/public/brand/logo-192.png" width="96" height="96" alt="MeBox Logo" />
|
||||
</p>
|
||||
|
||||
<h3 align="center">A lightweight, polished, NAS-friendly private media center</h3>
|
||||
<h3 align="center">A self-hosted media center for NAS and home theater</h3>
|
||||
|
||||
<p align="center">
|
||||
<strong>Docker-first setup · Multi-user management · Media library · Metadata · Downloads · Emby-protocol clients · Cloud playback</strong>
|
||||
<strong>Libraries · Metadata · Cloud STRM · Emby/Jellyfin client compatible · Remote Emby mounts · Multi-user · Docker-first</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">中文</a> ·
|
||||
<a href="#overview">Overview</a> ·
|
||||
<a href="#quick-start">Quick Start</a> ·
|
||||
<a href="#docker-compose-recommended">Docker Compose</a> ·
|
||||
<a href="#faq">FAQ</a> ·
|
||||
<a href="https://mgo.3jzs.com">Live Demo</a>
|
||||
<a href="#deployment-tiers">Deployment</a> ·
|
||||
<a href="#acknowledgements">Acknowledgements</a> ·
|
||||
<a href="#development">Development</a> ·
|
||||
<a href="https://t.me/MeBoxGroup">Telegram</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -27,78 +29,51 @@
|
||||
|
||||
---
|
||||
|
||||
## What is it?
|
||||
## Overview
|
||||
|
||||
MMTL is a self-hosted media center for personal libraries, home NAS, and home-theater users.
|
||||
**MeBox** is a self-hosted private media management system for NAS, mini PCs, family sharing, and multi-device playback. This repository is a maintained fork of [MediaStationGo](https://github.com/ShukeBta/MediaStationGo), extended with stronger cloud playback, task queues, remote mounts, and permission controls.
|
||||
|
||||
It helps you:
|
||||
In practice, MeBox gives you:
|
||||
|
||||
- Manage movies, TV shows, anime, variety shows, music, and adult libraries.
|
||||
- Create multiple user accounts for family members, friends, or different devices.
|
||||
- Scan files and enrich posters, summaries, years, seasons, and episodes.
|
||||
- Play in the web UI, or log in with a MMTL account from Emby-protocol apps such as Infuse, VidHub, SenPlayer, and Emby clients.
|
||||
- Connect qBittorrent for search, subscriptions, downloads, and post-download organization.
|
||||
- Connect OpenList, CloudDrive2, WebDAV, and other storage backends with STRMURL or 302 redirect playback.
|
||||
- Run on NAS, mini PCs, VPS, Linux, Windows Docker Desktop, or any Docker-friendly host.
|
||||
- A modern **web media library**
|
||||
- An **Emby/Jellyfin-compatible protocol gateway** for third-party players
|
||||
- A single panel for **local disks, download folders, and cloud storage**
|
||||
|
||||
> The project is moving fast. With the default PostgreSQL deployment, back up both `data/` and `postgres/`.
|
||||
### Key capabilities
|
||||
|
||||
---
|
||||
| Area | Highlights |
|
||||
| --- | --- |
|
||||
| **Libraries** | Movies, TV, anime, variety, music, custom libraries; multi-root scanning; poster wall; continue watching |
|
||||
| **Metadata** | TMDb, Bangumi, Douban, TheTVDB, Fanart, NFO import, manual matching, scrape queue |
|
||||
| **Playback** | Web player, HLS transcoding, danmaku, subtitles, play profiles, history and favourites |
|
||||
| **Emby/Jellyfin client compatible** | Full Emby server protocol implementation: Infuse, SenPlayer, Fileball, and official Emby/Jellyfin clients can add MeBox as an Emby server and sign in with MeBox accounts — poster walls, watch progress, and multi-user work out of the box |
|
||||
| **Remote Emby mounts** | Browse remote Emby libraries inside MeBox without a separate Emby client |
|
||||
| **Cloud & STRM** | OpenList, CloudDrive2, 115, WebDAV; STRM sync; upload/download queues; direct or 302 playback |
|
||||
| **Downloads & organize** | Scheduled download-folder organization (smart classification, auto library registration), file manager (copy/move/hardlink/symlink) |
|
||||
| **Users & permissions** | Admin/regular users, expiry, NSFW toggle, play-profile PIN, granular permissions |
|
||||
| **Operations** | Unified task queue, storage stats, DLNA casting, settings and logs |
|
||||
|
||||
## Key Highlights
|
||||
### Tech stack
|
||||
|
||||
- **One server, many clients**: deploy MMTL once; you do not need to run a separate Emby server.
|
||||
- **Emby-protocol compatibility**: add the server in third-party players as an Emby/Jellyfin-compatible server, then log in with your MMTL username and password.
|
||||
- **Multi-user management**: supports admins, regular users, account enable/disable, expiry dates, device management, Bot registration, and redeem codes.
|
||||
- **Local + cloud media in one place**: manage local disks, download folders, OpenList, CloudDrive2, WebDAV, and other storage backends from one panel.
|
||||
- **Download-to-library workflow**: connect qBittorrent for search, subscriptions, download completion organization, and metadata matching.
|
||||
- **NAS-friendly**: simple Docker Compose deployment. The primary database lives under `postgres/`, while runtime secrets and files live under `data/`.
|
||||
|
||||
---
|
||||
|
||||
## Who is it for?
|
||||
|
||||
- **Beginners** who want to edit one `docker-compose.yml` and start the service.
|
||||
- **NAS users** who want a low-resource media center for local disks and cloud storage.
|
||||
- **PT/download users** who want downloads, organization, metadata, and playback in one panel.
|
||||
- **External-player users** who want to log in to Emby-protocol third-party apps with one MMTL account.
|
||||
- **Family-sharing users** who want separate user accounts without deploying a separate media server for each person.
|
||||
- **Developers** who want to study or extend a Go + React self-hosted media app.
|
||||
|
||||
---
|
||||
|
||||
## Live Demo
|
||||
|
||||
- URL: [https://mgo.3jzs.com](https://mgo.3jzs.com)
|
||||
- Username: `admin`
|
||||
- Password: `admin123`
|
||||
|
||||
> The demo is for feature preview only. Do not save private API keys, tracker cookies, or personal data there.
|
||||
- **Backend**: Go, Gin, GORM, SQLite or PostgreSQL, optional Redis and OpenSearch
|
||||
- **Frontend**: React 18, Vite, TypeScript, Tailwind CSS, Zustand
|
||||
- **Deployment**: Standalone Docker Compose templates, amd64/arm64 images, single-binary releases
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
Docker Compose is the recommended path. Beginners do not need `.env`, bare-metal binaries, or source builds. Use the single-image SQLite template if you want the smallest possible setup.
|
||||
Docker Compose is the recommended path. The repo ships four **standalone** templates; no `.env` is required.
|
||||
|
||||
```bash
|
||||
mkdir -p MMTL
|
||||
cd MMTL
|
||||
# Simplest option: one MMTL container + SQLite
|
||||
curl -fsSL https://raw.githubusercontent.com/ShukeBta/MMTL/main/docker-compose.simple.yml -o docker-compose.yml
|
||||
# Or tier 1: MMTL + PostgreSQL
|
||||
# curl -fsSL https://raw.githubusercontent.com/ShukeBta/MMTL/main/docker-compose.yml -o docker-compose.yml
|
||||
```
|
||||
mkdir -p MeBox && cd MeBox
|
||||
|
||||
Edit `docker-compose.yml`:
|
||||
# Simplest: one container with built-in SQLite
|
||||
curl -fsSL https://raw.githubusercontent.com/truewhile/MeBox/main/docker-compose.simple.yml -o docker-compose.yml
|
||||
|
||||
```bash
|
||||
vi docker-compose.yml
|
||||
```
|
||||
# Or PostgreSQL tier for multi-user setups
|
||||
# curl -fsSL https://raw.githubusercontent.com/truewhile/MeBox/main/docker-compose.yml -o docker-compose.yml
|
||||
|
||||
Start:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
@@ -108,478 +83,173 @@ Open:
|
||||
http://SERVER_IP:18080
|
||||
```
|
||||
|
||||
Default login:
|
||||
Default login: `admin` / `admin123` — change the password immediately.
|
||||
|
||||
> 💡 **Seamless for Emby users**: MeBox fully implements the Emby/Jellyfin client protocol. Infuse, SenPlayer, Fileball, and official Emby/Jellyfin apps on phones, TVs, and tablets can add it as an Emby server at `http://SERVER_IP:18080` and sign in with MeBox accounts — no change to your existing workflow.
|
||||
|
||||
Image:
|
||||
|
||||
```text
|
||||
Username: admin
|
||||
Password: admin123
|
||||
ghcr.io/truewhile/mebox:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker Compose Recommended
|
||||
## Deployment tiers
|
||||
|
||||
The repository `docker-compose.yml` is the lightweight recommended template: no `.env` required, and by default it only starts `MMTL + PostgreSQL`. This is the best starting point for most NAS users.
|
||||
Pick one compose file. Do **not** stack multiple `-f` files.
|
||||
|
||||
If you already have an older `./data/mmtl.db`, the first start with the new compose file automatically imports it into PostgreSQL. Keep `./data`; it still stores the JWT secret, runtime data, and the old SQLite migration source.
|
||||
| Tier | File | Stack | Best for |
|
||||
| --- | --- | --- | --- |
|
||||
| Single image | `docker-compose.simple.yml` | MeBox + SQLite | Beginners, single-user, low-resource NAS |
|
||||
| Tier 1 | `docker-compose.yml` | MeBox + PostgreSQL | Most home NAS deployments |
|
||||
| Tier 2 | `docker-compose.standard.yml` | + Redis | Multi-user, frequent Emby client refreshes |
|
||||
| Tier 3 | `docker-compose.search.yml` | + OpenSearch | Very large libraries, advanced full-text search |
|
||||
|
||||
### Deployment modes
|
||||
### Single-image notes
|
||||
|
||||
| Mode | Command | Best for |
|
||||
| --- | --- | --- |
|
||||
| Single image: SQLite | `docker compose -f docker-compose.simple.yml up -d` | Beginners and single-user setups that want one image only, no PostgreSQL/Redis |
|
||||
| Lightweight: PG only | `docker compose up -d` | Most NAS devices, lowest resource use |
|
||||
| Standard: PG + Redis | `docker compose -f docker-compose.standard.yml up -d` | Multi-user use and frequent Emby client refreshes |
|
||||
| Search enhanced: PG + Redis + OpenSearch | `docker compose -f docker-compose.search.yml up -d` | Huge libraries and future standalone search indexing |
|
||||
- Only one MeBox container; database lives in `./data/mebox.db`
|
||||
- Do **not** set `MEBOX_DATABASE_DSN` or it switches to PostgreSQL
|
||||
- Back up `./data`; `./cache` can be rebuilt
|
||||
|
||||
Each compose file is standalone. Do not stack multiple `-f` files together.
|
||||
### PostgreSQL notes
|
||||
|
||||
The single-image `docker-compose.simple.yml` runs only MMTL with a built-in SQLite database — the simplest starting point. Do not set `MMTL_DATABASE_DSN` there, or it switches back to PostgreSQL. Move up to the PostgreSQL modes for multi-user or high-concurrency use (keep `./data` when you switch). Redis and OpenSearch are enhancement layers, not source databases. Do not enable OpenSearch by default on low-memory NAS devices.
|
||||
- Primary DB: `./postgres`; secrets and runtime files: `./data`
|
||||
- Existing `./data/mebox.db` migrates automatically on first start
|
||||
- After migration, point `MEBOX_DATABASE_DB_PATH` at a non-existent file to disable re-checks
|
||||
|
||||
### Database Choice And Disabling SQLite
|
||||
### Backup
|
||||
|
||||
The current Docker Compose setup uses PostgreSQL by default. SQLite is no longer the primary database in the recommended Docker deployment. The runtime database is controlled by:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
MMTL_DATABASE_TYPE: postgres
|
||||
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||
```
|
||||
|
||||
`MMTL_DATABASE_DB_PATH` is only used as a one-time migration source for old SQLite data:
|
||||
|
||||
- Fresh installs: `docker compose up -d` uses PostgreSQL and does not create a new SQLite primary database.
|
||||
- Upgrades: if `./data/mmtl.db` exists, the first start with the new compose file imports it into PostgreSQL.
|
||||
- Migration fills missing rows by primary key and skips rows that already exist. If it fails partway through, a later start continues the remaining tables.
|
||||
- After a successful import, PostgreSQL gets a completion marker in the `settings` table, so the old SQLite file is not imported again.
|
||||
- Redis is a hot cache and OpenSearch is a search index; neither is a source database.
|
||||
|
||||
Recommended SQLite to PostgreSQL upgrade flow:
|
||||
|
||||
```bash
|
||||
docker compose pull mmtl
|
||||
docker compose up -d --no-deps mmtl
|
||||
docker compose logs -f mmtl
|
||||
```
|
||||
|
||||
After you see `sqlite data migrated to postgres`, or after the web UI shows your users, libraries, and settings correctly, you can stop using the old SQLite file as a migration source.
|
||||
|
||||
To make the deployment PostgreSQL-only after migration, keep PostgreSQL selected and point the old SQLite migration path at a non-existent file:
|
||||
|
||||
> Only do this after the web UI confirms that users, libraries, settings, and media rows are already present in PostgreSQL.
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
MMTL_DATABASE_TYPE: postgres
|
||||
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||
MMTL_DATABASE_DB_PATH: /data/disabled-sqlite-migration.db
|
||||
```
|
||||
|
||||
Then rename or move the old host-side SQLite file as an offline backup:
|
||||
|
||||
```bash
|
||||
mv data/mmtl.db data/mmtl.sqlite.bak
|
||||
```
|
||||
|
||||
For bare-metal or custom `config.yaml` deployments, use the same idea:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
type: postgres
|
||||
dsn: postgres://mmtl:mmtl@127.0.0.1:5432/mmtl?sslmode=disable
|
||||
db_path: ""
|
||||
```
|
||||
|
||||
Do not delete `./postgres`. After migration, it is the real primary database. Keep `./data` too, because it stores the JWT secret and runtime files.
|
||||
|
||||
### Choose an image source
|
||||
|
||||
Both image sources are supported. Pick one and put it in `image:`:
|
||||
|
||||
| Source | Image | Best for |
|
||||
| --- | --- | --- |
|
||||
| GitHub Container Registry (GHCR) | `ghcr.io/shukebta/mmtl:latest` | Recommended default, follows repository releases |
|
||||
| Docker Hub | `shukbet/mmtl:latest` | Backup source when GHCR is slow or unavailable |
|
||||
|
||||
To pin a version, first confirm the tag exists on the repository Packages page. Use this format:
|
||||
|
||||
```yaml
|
||||
image: ghcr.io/shukebta/mmtl:<version-tag>
|
||||
# If GHCR does not have that tag, use Docker Hub as the backup:
|
||||
# image: shukbet/mmtl:MMTL-v0.0.72
|
||||
```
|
||||
|
||||
For the simplest setup, keep GHCR `latest`.
|
||||
|
||||
Manual pull examples:
|
||||
|
||||
```bash
|
||||
# GitHub Container Registry
|
||||
docker pull ghcr.io/shukebta/mmtl:latest
|
||||
|
||||
# Docker Hub backup
|
||||
docker pull shukbet/mmtl:latest
|
||||
```
|
||||
|
||||
Focus on this part:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./cache:/cache
|
||||
- ./media:/media
|
||||
- ./downloads:/downloads
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
| Host path | Container path | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `./data` | app `/data` | Settings, JWT secret, old SQLite migration source; the primary DB is under `./postgres` |
|
||||
| `./cache` | app `/cache` | Cache; safe to clean when needed |
|
||||
| `./media` | `/media` | Media libraries; use `/media/...` in the web UI |
|
||||
| `./downloads` | `/downloads` | Download directory and organization source |
|
||||
| `./postgres` | PostgreSQL `/var/lib/postgresql/data` | New default primary database; back this up |
|
||||
| `./redis` | Redis `/data` | Used only in standard mode; hot cache, rebuildable |
|
||||
| `./opensearch` | OpenSearch `/usr/share/opensearch/data` | Used only in search-enhanced mode; higher memory use |
|
||||
|
||||
If your NAS paths are:
|
||||
|
||||
```text
|
||||
/vol1/1000/Media
|
||||
/vol1/1000/Downloads
|
||||
```
|
||||
|
||||
change the compose file to:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./cache:/cache
|
||||
- /vol1/1000/Media:/media
|
||||
- /vol1/1000/Downloads:/downloads
|
||||
|
||||
environment:
|
||||
MMTL_MEDIA_DIR: /vol1/1000/Media
|
||||
MMTL_DOWNLOAD_DIR: /vol1/1000/Downloads
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- The left side of `volumes` is the real path on your host/NAS.
|
||||
- The right side is the container path. Keep `/media` and `/downloads` unless you know why you are changing them.
|
||||
- In the web UI, create libraries with container paths such as `/media/Movies` or `/media/TV`.
|
||||
- Do not write NAS absolute paths as `./vol1/...`; `./` means a folder under the current compose directory.
|
||||
- On Windows Docker Desktop, paths like `D:/Media:/media` and `D:/Downloads:/downloads` are fine.
|
||||
- If you only scan/play existing media and never organize into the library, you may add `:ro`; if you use organize/rename/ingest, the media mount must stay writable.
|
||||
|
||||
### Minimal compose example
|
||||
|
||||
The root `docker-compose.yml` follows this style:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mmtl:
|
||||
# Pick one image source:
|
||||
# GitHub Container Registry (GHCR):
|
||||
image: ghcr.io/shukebta/mmtl:latest
|
||||
# Docker Hub backup:
|
||||
# image: shukbet/mmtl:latest
|
||||
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
# Browser: http://SERVER_IP:18080
|
||||
ports:
|
||||
- "18080:8080"
|
||||
|
||||
# Let the container reach qBittorrent running on the host:
|
||||
# qB URL example: http://host.docker.internal:8085
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
volumes:
|
||||
# Application data. Back this up before upgrades.
|
||||
- ./data:/data
|
||||
- ./cache:/cache
|
||||
|
||||
# Beginners can create ./media and ./downloads.
|
||||
# NAS users should replace source with real absolute paths.
|
||||
# create_host_path=false prevents Docker from silently creating an empty
|
||||
# folder when the host path is wrong.
|
||||
- type: bind
|
||||
source: ./media
|
||||
target: /media
|
||||
bind:
|
||||
create_host_path: false
|
||||
- type: bind
|
||||
source: ./downloads
|
||||
target: /downloads
|
||||
bind:
|
||||
create_host_path: false
|
||||
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
PUID: "1000"
|
||||
PGID: "1000"
|
||||
|
||||
MMTL_APP_HOST: 0.0.0.0
|
||||
MMTL_APP_PORT: 8080
|
||||
MMTL_APP_WEB_DIR: /app/web/dist
|
||||
MMTL_APP_DATA_DIR: /data
|
||||
|
||||
# Lightweight mode uses PostgreSQL by default.
|
||||
# Old SQLite data migrates from this path on first start.
|
||||
MMTL_DATABASE_TYPE: postgres
|
||||
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||
# After migration, change this to /data/disabled-sqlite-migration.db to disable the SQLite migration source.
|
||||
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||
MMTL_CACHE_CACHE_DIR: /cache
|
||||
|
||||
# Use /media and /downloads in the web UI and downloader by default.
|
||||
# Only set MMTL_*_DIR to real host paths when migrating old
|
||||
# libraries/tasks that already stored host paths.
|
||||
MMTL_MEDIA_DIR: /media
|
||||
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||
MMTL_DOWNLOAD_DIR: /downloads
|
||||
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: mmtl
|
||||
POSTGRES_USER: mmtl
|
||||
POSTGRES_PASSWORD: mmtl
|
||||
volumes:
|
||||
- ./postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mmtl -d mmtl"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
```
|
||||
|
||||
> Note: PostgreSQL is the primary database. Lightweight mode still has short in-process caching. Redis is a cross-process hot cache, and OpenSearch is a search enhancement layer; neither is a source database.
|
||||
|
||||
---
|
||||
|
||||
## First-time Setup
|
||||
|
||||
1. **Create a library**
|
||||
- Go to the library page.
|
||||
- Use a container path such as `/media/Movies`.
|
||||
- Start a scan.
|
||||
|
||||
2. **Connect qBittorrent**
|
||||
- Go to download client settings.
|
||||
- If qBittorrent runs on the host, try `http://host.docker.internal:8085`.
|
||||
|
||||
3. **Configure metadata providers**
|
||||
- Go to system settings / external APIs.
|
||||
- Add TMDb, Bangumi, TheTVDB, Fanart, Douban, or other providers when needed.
|
||||
|
||||
4. **Use external players**
|
||||
- Add the server as an Emby/Jellyfin-compatible server.
|
||||
- Server URL: `http://SERVER_IP:18080`.
|
||||
- Use the username and password created in MMTL. No separate Emby server is required.
|
||||
- Admins can create regular users in the web UI or Bot so each person can log in with their own account.
|
||||
|
||||
5. **Use cloud playback**
|
||||
- Configure OpenList, CloudDrive2, WebDAV, or another provider in storage settings.
|
||||
- Choose STRMURL or 302 redirect playback in the admin settings.
|
||||
- The enabled option takes priority. If both are disabled, playback falls back to the normal server playback path.
|
||||
|
||||
---
|
||||
|
||||
## Update, Backup, Logs
|
||||
| Path | Notes |
|
||||
| --- | --- |
|
||||
| `./data` | JWT secret, config, SQLite DB or migration source |
|
||||
| `./postgres` | PostgreSQL primary DB |
|
||||
| `./cache`, `./redis`, `./opensearch` | Rebuildable |
|
||||
|
||||
### Update
|
||||
|
||||
```bash
|
||||
docker compose pull mmtl
|
||||
docker compose up -d --no-deps mmtl
|
||||
docker compose pull mebox
|
||||
docker compose up -d --no-deps mebox
|
||||
```
|
||||
|
||||
### Logs
|
||||
---
|
||||
|
||||
```bash
|
||||
docker compose logs -f mmtl
|
||||
tail -f ./data/logs/app.log
|
||||
tail -f ./data/logs/error.log
|
||||
## Path mapping
|
||||
|
||||
The most common Docker mistake is mixing host paths with container paths.
|
||||
|
||||
- Left side of `volumes` = real host/NAS path
|
||||
- Right side = container path; use `/media/...` in the web UI
|
||||
- Keep `MEBOX_MEDIA_DIR` / `MEBOX_DOWNLOAD_DIR` aligned with mounts when organizing or ingesting downloads
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- /vol1/1000/Media:/media
|
||||
- /vol1/1000/Downloads:/downloads
|
||||
environment:
|
||||
MEBOX_MEDIA_DIR: /vol1/1000/Media
|
||||
MEBOX_MEDIA_CONTAINER_DIR: /media
|
||||
MEBOX_DOWNLOAD_DIR: /vol1/1000/Downloads
|
||||
MEBOX_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||
```
|
||||
|
||||
The compose templates keep full application logs in `./data/logs/app.log` and split warnings/errors into `warn.log` and `error.log`. Keep `MMTL_LOGGING_LEVEL=info` while diagnosing subscription, site search, organizer, or STRM generation issues; temporarily switch to `debug` only when deeper tracing is needed.
|
||||
---
|
||||
|
||||
Use a writable container path for STRM output, such as `/data/strm` or a mounted media path. Deployments that previously saved `/app/data/strm` are migrated automatically to the configured `MMTL_APP_DATA_DIR`, which defaults to `/data`.
|
||||
## First-time setup
|
||||
|
||||
### Backup
|
||||
|
||||
For the default PostgreSQL deployment, back up:
|
||||
|
||||
```text
|
||||
data/
|
||||
postgres/
|
||||
```
|
||||
|
||||
`postgres/` is the primary database and contains users, libraries, settings, and media metadata. `data/` stores the JWT secret, runtime files, and optional old SQLite migration source.
|
||||
|
||||
If you enabled the extended modes, these are optional:
|
||||
|
||||
```text
|
||||
redis/ # hot cache, safe to rebuild
|
||||
opensearch/ # search index, rebuildable; backing it up can save reindex time on huge libraries
|
||||
```
|
||||
|
||||
`cache/` is usually not important. If you explicitly still use `database.type=sqlite`, the primary database remains `data/mmtl.db`.
|
||||
|
||||
### Stop
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
1. Create a library with a container path such as `/media/Movies`, then scan
|
||||
2. Add metadata providers (TMDb, Bangumi, etc.) in system settings
|
||||
3. Optionally set up download-folder auto-organization under file management so finished downloads land in the right library
|
||||
4. Optionally configure cloud accounts under STRM management
|
||||
5. Add the server in Emby-compatible players at `http://SERVER_IP:18080` using MeBox credentials
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### 1. The web page does not open
|
||||
**Library scan is slow**
|
||||
Check path mapping and DB tier. Cloud scans also depend on API limits and folder size.
|
||||
|
||||
Check the container:
|
||||
**Downloaded files are not organized**
|
||||
Ensure the download directory is mounted into the container and env vars match. MeBox handles folder organization; run qBittorrent or any downloader yourself as a regular app.
|
||||
|
||||
```bash
|
||||
docker ps
|
||||
docker compose logs --tail=100 mmtl
|
||||
```
|
||||
**Hardlink fails with cross-device link**
|
||||
Hardlinks require the same filesystem/subvolume; use copy or symlink across disks or cloud mounts.
|
||||
|
||||
Then open:
|
||||
|
||||
```text
|
||||
http://SERVER_IP:18080
|
||||
```
|
||||
|
||||
### 2. The library cannot find files
|
||||
|
||||
Most cases are path mistakes.
|
||||
|
||||
- Docker maps media to `/media`.
|
||||
- In the web UI, use `/media/Movies`, not the original NAS path.
|
||||
- Docker maps downloads to `/downloads`; use `/downloads` as the organization source when possible.
|
||||
|
||||
### 3. qBittorrent cannot connect
|
||||
|
||||
If qBittorrent is on the host, try:
|
||||
|
||||
```text
|
||||
http://host.docker.internal:8085
|
||||
```
|
||||
|
||||
If qBittorrent is on another machine, use that machine's LAN IP.
|
||||
|
||||
### 4. NAS CPU usage is high
|
||||
|
||||
Suggested settings:
|
||||
|
||||
- Set `ffprobe.max_concurrent` to `1`.
|
||||
- Enable automatic organization, scrape-after-scan, and boot cloud scan only when you really need them.
|
||||
- Avoid frequent full-library scans on large libraries. Prefer manual scan or scheduled night sync.
|
||||
|
||||
### 5. Should I use `.env`?
|
||||
|
||||
Beginners should not. Editing `docker-compose.yml` directly is easier to understand.
|
||||
|
||||
`.env` is not required by the provided deployment templates. For the single-image template, edit `docker-compose.simple.yml` directly and only adjust the port, volume paths, and optional hardware device mapping.
|
||||
**External player cannot connect**
|
||||
Use `http://IP:18080` and a MeBox user account; reverse proxies need correct external URL and HTTPS headers.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
| Area | Features |
|
||||
| --- | --- |
|
||||
| Libraries | Movies, TV shows, anime, variety, music, adult content |
|
||||
| Metadata | NFO, local artwork, TMDb, TheTVDB, Bangumi, Douban, Fanart, JavBus/JavDB |
|
||||
| Playback | Web playback, HTTP Range, HLS transcoding, direct links, STRMURL, 302 redirect |
|
||||
| External clients | Emby-protocol compatible APIs; MMTL accounts can log in to third-party players |
|
||||
| User management | Multi-user accounts, admin/regular users, expiry dates, device management, Bot registration and redeem codes |
|
||||
| Downloads | qBittorrent, site search, subscriptions, post-download organization |
|
||||
| File manager | Browse, organize, copy, move, hardlink, symlink |
|
||||
| Operations | Task queue, recycle bin, duplicate files, notifications, logs |
|
||||
| AI | OpenAI-compatible API, AI search, recommendations, assistant |
|
||||
|
||||
Directory hardlinks are handled by recreating the directory tree and hardlinking
|
||||
each contained file. Linux cannot hardlink a directory itself. Hardlinks still
|
||||
require the source and target files to be on the same filesystem/subvolume from
|
||||
inside the container; if media and downloads are separate bind mounts, disks,
|
||||
btrfs subvolumes, or cloud mounts, use copy or symlink instead.
|
||||
|
||||
## Development
|
||||
|
||||
Regular users should use Docker. Developers can run:
|
||||
The backend embeds `web/dist` via `go:embed`. Build the frontend first.
|
||||
|
||||
```bash
|
||||
go run ./cmd/server
|
||||
```
|
||||
npm --prefix web ci
|
||||
npm --prefix web run build
|
||||
|
||||
Frontend:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Tests:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
cd web && npm run build
|
||||
go run ./cmd/server
|
||||
npm --prefix web run dev
|
||||
```
|
||||
|
||||
---
|
||||
Release builds ship single-file binaries for Windows, Linux, and macOS on amd64 and arm64.
|
||||
|
||||
## Community and Friends
|
||||
Build the Windows executable locally:
|
||||
|
||||
- Telegram group: <https://t.me/MMTL>
|
||||
- NodeSeek: [https://www.nodeseek.com/](https://www.nodeseek.com/)
|
||||
- LINUX DO: [https://linux.do/](https://linux.do/)
|
||||
```powershell
|
||||
.\scripts\build-windows.ps1 -Version dev
|
||||
```
|
||||
|
||||
The Windows executable uses the project logo and runs without a console window. It stays in the notification area, with menu actions for opening MeBox, toggling auto-start, viewing logs, restarting, and exiting.
|
||||
|
||||
---
|
||||
|
||||
## Donation
|
||||
## Acknowledgements
|
||||
|
||||
If MMTL saves you time, feel free to buy the author a bowl of noodles.
|
||||
MeBox is forked from and continues to evolve [MediaStationGo](https://github.com/ShukeBta/MediaStationGo). Thank you to the upstream project for the media-library architecture, Emby-protocol compatibility, and self-hosted foundation.
|
||||
|
||||
<img width="200" height="200" alt="WeChat Donation QR" src="https://github.com/user-attachments/assets/d6077de5-8305-400d-8b82-470ef05d926e" />
|
||||
Many cloud sync, STRM, and media-organization ideas in this project were also informed by [qmediasync](https://github.com/qicfan/qmediasync). Thank you for the reference implementation and design patterns.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md) before opening issues or pull requests.
|
||||
|
||||
---
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/?repos=ShukeBta%2FMMTL&type=date&legend=top-left">
|
||||
<a href="https://www.star-history.com/?repos=truewhile%2FMeBox&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=ShukeBta/MMTL&type=date&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=truewhile/MeBox&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=truewhile/MeBox&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=truewhile/MeBox&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## License and Non-Commercial Statement
|
||||
## License
|
||||
|
||||
This project uses `GPL-3.0` as its base license. See [LICENSE](LICENSE).
|
||||
|
||||
The maintainers also state and request:
|
||||
|
||||
- The project is intended for personal learning, home NAS, self-hosted media, non-commercial research, and community collaboration.
|
||||
- Without explicit written permission from the author, do not use this project or derivative versions for commercial resale, paid hosting, paid SaaS, pre-installed commercial devices, closed-source redistribution, or other profit-oriented commercial use.
|
||||
- For commercial cooperation, enterprise deployment, custom development, integrated redistribution, or commercial authorization, contact the author first.
|
||||
- If there is any interpretive difference between this README and the formal `GPL-3.0` license text, the code license is governed by [LICENSE](LICENSE); commercial usage should additionally obtain author permission.
|
||||
This project is licensed under [GPL-3.0](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
<p align="center">Made with ❤️ by ShukeBta</p>
|
||||
## Support & Donate
|
||||
|
||||
If MeBox makes your home theater life easier, feel free to buy the maintainer a coffee ☕
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/donation-qr.png" width="320" alt="WhileTrue donation QR" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>Telegram group</strong>: <a href="https://t.me/MeBoxGroup">https://t.me/MeBoxGroup</a><br/>
|
||||
Questions, feature requests, and release news — come chat with us
|
||||
</p>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 安全策略
|
||||
|
||||
MMTL 是自托管媒体系统,常部署在 NAS、家庭网络、Docker、反向代理和第三方下载器环境中。安全问题通常会同时涉及应用代码、容器配置、路径映射、站点 Cookie / API Key、下载器凭据和外部访问入口。请按本策略报告和处理安全问题。
|
||||
MeBox 是自托管媒体系统,常部署在 NAS、家庭网络、Docker、反向代理和第三方下载器环境中。安全问题通常会同时涉及应用代码、容器配置、路径映射、站点 Cookie / API Key、下载器凭据和外部访问入口。请按本策略报告和处理安全问题。
|
||||
|
||||
## 支持范围
|
||||
|
||||
我们优先支持以下版本和部署方式的安全修复:
|
||||
|
||||
- 当前 `main` 分支。
|
||||
- 最新发布镜像:`ghcr.io/shukebta/mmtl:latest`。
|
||||
- 最新发布镜像:`ghcr.io/truewhile/mebox:latest`。
|
||||
- README 中推荐的 Docker Compose 第一档、第二档、第三档部署方式。
|
||||
|
||||
历史版本、私有魔改镜像、未公开补丁分支和非标准部署仍可报告,但维护者可能要求先在最新 `main` 或最新镜像中复现。
|
||||
@@ -16,7 +16,7 @@ MMTL 是自托管媒体系统,常部署在 NAS、家庭网络、Docker、反
|
||||
|
||||
请不要在公开 Issue、PR、讨论区或群聊中披露可利用细节。优先使用 GitHub Security Advisory 私密报告:
|
||||
|
||||
<https://github.com/ShukeBta/MMTL/security/advisories/new>
|
||||
<https://github.com/truewhile/MeBox/security/advisories/new>
|
||||
|
||||
如果无法使用 GitHub 私密报告,可以先通过项目 README 中的社区入口联系维护者,说明“需要私下报告安全问题”,不要直接贴出利用细节、密钥或完整日志。
|
||||
|
||||
@@ -68,14 +68,14 @@ MMTL 是自托管媒体系统,常部署在 NAS、家庭网络、Docker、反
|
||||
|
||||
## 自托管安全基线
|
||||
|
||||
部署 MMTL 时建议:
|
||||
部署 MeBox 时建议:
|
||||
|
||||
- 首次登录后立即修改默认 `admin / admin123`。
|
||||
- 不要把 PostgreSQL、Redis、OpenSearch、qBittorrent WebUI 暴露到公网。
|
||||
- 反向代理公网访问时启用 HTTPS,并限制管理后台访问来源。
|
||||
- 使用强随机的 JWT / 加密密钥,妥善备份 `./data` 和数据库。
|
||||
- 不要在 Issue、PR、截图或日志中公开站点 Cookie、API Key、Passkey、下载器密码。
|
||||
- Docker `volumes` 只挂载 MMTL 需要访问的目录,媒体库目录需要写入时再授予写权限。
|
||||
- Docker `volumes` 只挂载 MeBox 需要访问的目录,媒体库目录需要写入时再授予写权限。
|
||||
- 定期更新镜像,并在升级前备份 `./postgres` 和 `./data`。
|
||||
|
||||
## 安全修复 PR
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/truewhile/MeBox/internal/config"
|
||||
"github.com/truewhile/MeBox/internal/database"
|
||||
"github.com/truewhile/MeBox/internal/helper"
|
||||
"github.com/truewhile/MeBox/internal/repository"
|
||||
"github.com/truewhile/MeBox/internal/service"
|
||||
)
|
||||
|
||||
// application owns the long-running MeBox server and all resources created
|
||||
// during startup. Platform entry points decide how the application is
|
||||
// controlled: a console signal loop or a Windows notification-area icon.
|
||||
type application struct {
|
||||
cfg *config.Config
|
||||
logger *zap.Logger
|
||||
embyCompatLogger *zap.Logger
|
||||
serverManager *serverManager
|
||||
services *service.Container
|
||||
|
||||
closeMu sync.Mutex
|
||||
closeFuncs []func()
|
||||
shutdown sync.Once
|
||||
shutdownErr error
|
||||
}
|
||||
|
||||
func newApplication() (*application, error) {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("config load failed: %w", err)
|
||||
}
|
||||
|
||||
logger, closeLogger, err := newLoggerWithCloser(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("logger init failed: %w", err)
|
||||
}
|
||||
|
||||
app := &application{
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
closeFuncs: []func(){closeLogger},
|
||||
}
|
||||
if err := app.start(); err != nil {
|
||||
app.closeLoggers()
|
||||
return nil, err
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (a *application) start() error {
|
||||
embyCompatLogger, closeEmbyCompatLogger, err := newEmbyCompatLogger(a.cfg)
|
||||
if err != nil {
|
||||
a.logger.Error("Emby compatibility logger init failed", zap.Error(err))
|
||||
return fmt.Errorf("Emby compatibility logger init failed: %w", err)
|
||||
}
|
||||
a.embyCompatLogger = embyCompatLogger
|
||||
a.addCloser(closeEmbyCompatLogger)
|
||||
|
||||
appVersion := effectiveVersion(version)
|
||||
a.logger.Info("starting MeBox",
|
||||
zap.String("version", appVersion),
|
||||
zap.Int("port", a.cfg.App.Port),
|
||||
zap.String("data_dir", a.cfg.App.DataDir),
|
||||
zap.String("emby_compat_log", embyCompatLogPath(a.cfg)),
|
||||
)
|
||||
|
||||
for _, dir := range []string{a.cfg.App.DataDir, a.cfg.Cache.CacheDir} {
|
||||
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||
a.logger.Error("create dir failed", zap.String("dir", dir), zap.Error(err))
|
||||
return fmt.Errorf("create dir %s failed: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
db, err := database.Open(a.cfg, a.logger)
|
||||
if err != nil {
|
||||
a.logger.Error("database open failed", zap.Error(err))
|
||||
return fmt.Errorf("database open failed: %w", err)
|
||||
}
|
||||
if err := waitForDatabase(db, a.logger); err != nil {
|
||||
a.logger.Error("database not ready", zap.Error(err))
|
||||
return fmt.Errorf("database not ready: %w", err)
|
||||
}
|
||||
if err := database.AutoMigrate(db); err != nil {
|
||||
a.logger.Error("auto-migrate failed", zap.Error(err))
|
||||
return fmt.Errorf("auto-migrate failed: %w", err)
|
||||
}
|
||||
if err := database.MigrateSQLiteToCurrentIfNeeded(a.cfg, db, a.logger); err != nil {
|
||||
a.logger.Error("sqlite to postgres migration failed", zap.Error(err))
|
||||
return fmt.Errorf("sqlite to postgres migration failed: %w", err)
|
||||
}
|
||||
|
||||
repos := repository.New(db)
|
||||
service.ApplyRuntimeSettings(context.Background(), a.cfg, repos, a.logger)
|
||||
applyCPUThreadLimit(a.cfg, a.logger)
|
||||
a.services = service.NewWithVersion(a.cfg, a.logger, repos, appVersion)
|
||||
|
||||
// 一次性清洗历史脏数据: 老版本把单集 episode id / 单集名写进整剧字段, 导致
|
||||
// 同一部剧被拆成多张单集卡。清空被污染的字段并重置为 pending(借后续重刮修正)。
|
||||
if cleaned, err := a.services.NormalizePollutedEpisodeMetadata(context.Background()); err != nil {
|
||||
a.logger.Warn("polluted episode metadata cleanup failed", zap.Error(err))
|
||||
} else if cleaned > 0 {
|
||||
a.logger.Info("polluted episode metadata cleanup completed", zap.Int("media_count", cleaned))
|
||||
}
|
||||
|
||||
if err := a.services.Auth.SeedAdmin(context.Background()); err != nil {
|
||||
a.logger.Warn("seed admin failed", zap.Error(err))
|
||||
}
|
||||
|
||||
router := buildRouter(a.cfg, a.logger, a.embyCompatLogger, a.services)
|
||||
a.serverManager = newServerManager(a.cfg, a.logger, router)
|
||||
a.services.ReloadHTTPServer = a.serverManager.Reload
|
||||
if err := a.serverManager.Start(); err != nil {
|
||||
a.logger.Error("listen failed", zap.Error(err))
|
||||
return fmt.Errorf("listen failed: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
scheme := "http"
|
||||
if a.cfg.App.HTTPSEnabled {
|
||||
scheme = "https"
|
||||
}
|
||||
if publicIP := getPublicIP(3 * time.Second); publicIP != "" {
|
||||
a.logger.Info("server public endpoint",
|
||||
zap.String("public", fmt.Sprintf("%s://%s:%d", scheme, publicIP, a.cfg.App.Port)),
|
||||
)
|
||||
}
|
||||
}()
|
||||
helper.Go(a.logger, "services.boot", a.services.Boot)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown is idempotent and safe to call from both the tray handler and the
|
||||
// systray exit callback.
|
||||
func (a *application) Shutdown() error {
|
||||
a.shutdown.Do(func() {
|
||||
a.logger.Info("shutdown requested")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
if a.serverManager != nil {
|
||||
if err := a.serverManager.Shutdown(ctx); err != nil {
|
||||
a.shutdownErr = err
|
||||
a.logger.Error("graceful shutdown failed", zap.Error(err))
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
|
||||
if a.services != nil {
|
||||
a.services.Close()
|
||||
}
|
||||
a.logger.Info("MeBox stopped")
|
||||
_ = a.logger.Sync()
|
||||
if a.embyCompatLogger != nil {
|
||||
_ = a.embyCompatLogger.Sync()
|
||||
}
|
||||
a.closeLoggers()
|
||||
})
|
||||
return a.shutdownErr
|
||||
}
|
||||
|
||||
func (a *application) addCloser(fn func()) {
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
a.closeMu.Lock()
|
||||
a.closeFuncs = append(a.closeFuncs, fn)
|
||||
a.closeMu.Unlock()
|
||||
}
|
||||
|
||||
func (a *application) closeLoggers() {
|
||||
a.closeMu.Lock()
|
||||
closers := append([]func(){}, a.closeFuncs...)
|
||||
a.closeFuncs = nil
|
||||
a.closeMu.Unlock()
|
||||
|
||||
for i := len(closers) - 1; i >= 0; i-- {
|
||||
closers[i]()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *application) localURL() string {
|
||||
config.RuntimeMu.RLock()
|
||||
scheme := "http"
|
||||
if a.cfg.App.HTTPSEnabled {
|
||||
scheme = "https"
|
||||
}
|
||||
port := a.cfg.App.Port
|
||||
config.RuntimeMu.RUnlock()
|
||||
return fmt.Sprintf("%s://127.0.0.1:%d", scheme, port)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ShukeBta/MMTL/internal/config"
|
||||
"github.com/truewhile/MeBox/internal/config"
|
||||
)
|
||||
|
||||
const defaultLogMaxSizeMB = 20
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/ShukeBta/MMTL/internal/config"
|
||||
"github.com/truewhile/MeBox/internal/config"
|
||||
)
|
||||
|
||||
// newLogger 根据 cfg.Logging 构建 Zap。
|
||||
@@ -98,3 +98,41 @@ func logFilePaths(cfg *config.Config) (string, string, string) {
|
||||
}
|
||||
return filepath.Join(out, "app.log"), filepath.Join(out, "warn.log"), filepath.Join(out, "error.log")
|
||||
}
|
||||
|
||||
// newEmbyCompatLogger 构建只写入 Emby 兼容日志文件的独立 Zap 实例。
|
||||
// 它不参与 app.log 的日志级别过滤,始终记录 INFO 及以上,确保成功请求也能
|
||||
// 用于还原客户端的接口调用顺序;轮转参数沿用 logging 配置。
|
||||
func newEmbyCompatLogger(cfg *config.Config) (*zap.Logger, func(), error) {
|
||||
encoderCfg := zap.NewProductionEncoderConfig()
|
||||
encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
var encoder zapcore.Encoder
|
||||
if strings.EqualFold(strings.TrimSpace(cfg.Logging.Format), "console") {
|
||||
encoder = zapcore.NewConsoleEncoder(encoderCfg)
|
||||
} else {
|
||||
encoder = zapcore.NewJSONEncoder(encoderCfg)
|
||||
}
|
||||
|
||||
writer, err := newRotatingFileWriter(embyCompatLogPath(cfg), cfg.Logging)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
log := zap.New(
|
||||
zapcore.NewCore(encoder, writer, zap.InfoLevel),
|
||||
zap.AddCaller(),
|
||||
zap.AddStacktrace(zapcore.ErrorLevel),
|
||||
zap.ErrorOutput(zapcore.Lock(os.Stderr)),
|
||||
)
|
||||
return log, func() { _ = writer.Close() }, nil
|
||||
}
|
||||
|
||||
func embyCompatLogPath(cfg *config.Config) string {
|
||||
out := strings.TrimSpace(cfg.Logging.OutputPath)
|
||||
if out == "" || strings.EqualFold(out, "stdout") || strings.EqualFold(out, "stderr") {
|
||||
return filepath.Join(cfg.App.DataDir, "logs", "emby-compat.log")
|
||||
}
|
||||
if ext := filepath.Ext(out); ext != "" {
|
||||
base := strings.TrimSuffix(out, ext)
|
||||
return base + ".emby-compat" + ext
|
||||
}
|
||||
return filepath.Join(out, "emby-compat.log")
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MMTL/internal/config"
|
||||
"github.com/truewhile/MeBox/internal/config"
|
||||
)
|
||||
|
||||
func TestProductionLoggerWritesConfiguredInfoToAppLogAndSplitsWarnError(t *testing.T) {
|
||||
@@ -116,3 +116,34 @@ func TestRotatingFileWriterCapsFileSize(t *testing.T) {
|
||||
}
|
||||
_ = writer.Sync()
|
||||
}
|
||||
|
||||
func TestEmbyCompatLoggerWritesDedicatedFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := &config.Config{}
|
||||
cfg.App.DataDir = dir
|
||||
cfg.Logging.Format = "json"
|
||||
cfg.Logging.OutputPath = filepath.Join(dir, "logs")
|
||||
cfg.Logging.EnableRotation = true
|
||||
cfg.Logging.MaxSizeMB = 1
|
||||
cfg.Logging.MaxBackups = 2
|
||||
|
||||
log, closeFn, err := newEmbyCompatLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
log.Info("emby request", zap.String("client", "Infuse"))
|
||||
_ = log.Sync()
|
||||
closeFn()
|
||||
|
||||
path := filepath.Join(dir, "logs", "emby-compat.log")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(data), "emby request") || !strings.Contains(string(data), "Infuse") {
|
||||
t.Fatalf("dedicated Emby log missing request data: %s", data)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "logs", "app.log")); !os.IsNotExist(err) {
|
||||
t.Fatalf("Emby logger must not write app.log, stat err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package main is the MMTL HTTP server entry point.
|
||||
// Package main is the MeBox HTTP server entry point.
|
||||
//
|
||||
// MMTL is a Go rewrite of the legacy Python implementation,
|
||||
// MeBox is a Go rewrite of the legacy Python implementation,
|
||||
// adopting the same tech stack as cropflre/nowen-video:
|
||||
//
|
||||
// Backend: Go 1.25 + Gin + GORM + PostgreSQL/SQLite + Viper + Zap + JWT
|
||||
@@ -11,20 +11,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MMTL/internal/config"
|
||||
"github.com/ShukeBta/MMTL/internal/database"
|
||||
"github.com/ShukeBta/MMTL/internal/repository"
|
||||
"github.com/ShukeBta/MMTL/internal/service"
|
||||
)
|
||||
|
||||
// version is overwritten at build time via -ldflags="-X main.version=...".
|
||||
@@ -35,7 +23,7 @@ func effectiveVersion(buildVersion string) string {
|
||||
if buildVersion != "" && buildVersion != "dev" {
|
||||
return buildVersion
|
||||
}
|
||||
if envVersion := strings.TrimSpace(os.Getenv("MMTL_VERSION")); envVersion != "" {
|
||||
if envVersion := strings.TrimSpace(os.Getenv("MEBOX_VERSION")); envVersion != "" {
|
||||
return envVersion
|
||||
}
|
||||
if buildVersion == "" {
|
||||
@@ -45,95 +33,5 @@ func effectiveVersion(buildVersion string) string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "config load failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger, err := newLogger(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "logger init failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
appVersion := effectiveVersion(version)
|
||||
logger.Info("starting MMTL",
|
||||
zap.String("version", appVersion),
|
||||
zap.Int("port", cfg.App.Port),
|
||||
zap.String("data_dir", cfg.App.DataDir),
|
||||
)
|
||||
|
||||
// Ensure data / cache / web dirs exist.
|
||||
for _, d := range []string{cfg.App.DataDir, cfg.Cache.CacheDir} {
|
||||
if err := os.MkdirAll(d, 0o750); err != nil {
|
||||
logger.Fatal("create dir failed", zap.String("dir", d), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
db, err := database.Open(cfg, logger)
|
||||
if err != nil {
|
||||
logger.Fatal("database open failed", zap.Error(err))
|
||||
}
|
||||
if err := waitForDatabase(db, logger); err != nil {
|
||||
logger.Fatal("database not ready", zap.Error(err))
|
||||
}
|
||||
if err := database.AutoMigrate(db); err != nil {
|
||||
logger.Fatal("auto-migrate failed", zap.Error(err))
|
||||
}
|
||||
if err := database.MigrateSQLiteToCurrentIfNeeded(cfg, db, logger); err != nil {
|
||||
logger.Fatal("sqlite to postgres migration failed", zap.Error(err))
|
||||
}
|
||||
|
||||
repos := repository.New(db)
|
||||
service.ApplyRuntimeSettings(context.Background(), cfg, repos, logger)
|
||||
applyCPUThreadLimit(cfg, logger)
|
||||
services := service.NewWithVersion(cfg, logger, repos, appVersion)
|
||||
|
||||
// 一次性清洗历史脏数据: 老版本把单集 episode id / 单集名写进整剧字段, 导致
|
||||
// 同一部剧被拆成多张单集卡。清空被污染的字段并重置为 pending(借后续重刮修正)。
|
||||
if cleaned, err := services.NormalizePollutedEpisodeMetadata(context.Background()); err != nil {
|
||||
logger.Warn("polluted episode metadata cleanup failed", zap.Error(err))
|
||||
} else if cleaned > 0 {
|
||||
logger.Info("polluted episode metadata cleanup completed", zap.Int("media_count", cleaned))
|
||||
}
|
||||
|
||||
if err := services.Auth.SeedAdmin(context.Background()); err != nil {
|
||||
logger.Warn("seed admin failed", zap.Error(err))
|
||||
}
|
||||
|
||||
router := buildRouter(cfg, logger, services)
|
||||
|
||||
serverMgr := newServerManager(cfg, logger, router)
|
||||
services.ReloadHTTPServer = serverMgr.Reload
|
||||
if err := serverMgr.Start(); err != nil {
|
||||
logger.Fatal("listen failed", zap.Error(err))
|
||||
}
|
||||
go func() {
|
||||
scheme := "http"
|
||||
if cfg.App.HTTPSEnabled {
|
||||
scheme = "https"
|
||||
}
|
||||
if publicIP := getPublicIP(3 * time.Second); publicIP != "" {
|
||||
logger.Info("server public endpoint",
|
||||
zap.String("public", fmt.Sprintf("%s://%s:%d", scheme, publicIP, cfg.App.Port)),
|
||||
)
|
||||
}
|
||||
}()
|
||||
go services.Boot()
|
||||
|
||||
// Graceful shutdown.
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-stop
|
||||
logger.Info("shutdown requested")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
if err := serverMgr.Shutdown(ctx); err != nil {
|
||||
logger.Error("graceful shutdown failed", zap.Error(err))
|
||||
}
|
||||
services.Close()
|
||||
logger.Info("MMTL stopped")
|
||||
runProgram()
|
||||
}
|
||||
|
||||
@@ -12,23 +12,23 @@ import (
|
||||
)
|
||||
|
||||
func TestEffectiveVersionPrefersBuildVersion(t *testing.T) {
|
||||
t.Setenv("MMTL_VERSION", "MMTL-v0.1.15")
|
||||
t.Setenv("MEBOX_VERSION", "MeBox-v0.1.15")
|
||||
|
||||
if got := effectiveVersion("MMTL-v0.1.16"); got != "MMTL-v0.1.16" {
|
||||
t.Fatalf("effectiveVersion = %q, want MMTL-v0.1.16", got)
|
||||
if got := effectiveVersion("MeBox-v0.1.16"); got != "MeBox-v0.1.16" {
|
||||
t.Fatalf("effectiveVersion = %q, want MeBox-v0.1.16", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveVersionUsesEnvWhenBuildVersionIsDev(t *testing.T) {
|
||||
t.Setenv("MMTL_VERSION", " MMTL-v0.1.16 ")
|
||||
t.Setenv("MEBOX_VERSION", " MeBox-v0.1.16 ")
|
||||
|
||||
if got := effectiveVersion("dev"); got != "MMTL-v0.1.16" {
|
||||
t.Fatalf("effectiveVersion = %q, want MMTL-v0.1.16", got)
|
||||
if got := effectiveVersion("dev"); got != "MeBox-v0.1.16" {
|
||||
t.Fatalf("effectiveVersion = %q, want MeBox-v0.1.16", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveVersionDefaultsToDev(t *testing.T) {
|
||||
t.Setenv("MMTL_VERSION", "")
|
||||
t.Setenv("MEBOX_VERSION", "")
|
||||
|
||||
if got := effectiveVersion(""); got != "dev" {
|
||||
t.Fatalf("effectiveVersion = %q, want dev", got)
|
||||
@@ -52,7 +52,7 @@ func TestServeSPANoCachesIndexAndServesRoutes(t *testing.T) {
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
serveSPA(router, webDir)
|
||||
serveSPA(router, os.DirFS(webDir))
|
||||
|
||||
for _, path := range []string{"/", "/login", "/library/e1c3507e-2878-40ae-a0e1-6b6e44b7fa7a", "/media/abc"} {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
@@ -76,6 +76,9 @@ func TestServeSPAServesAssetsImmutableAndBypassesAPIRoutes(t *testing.T) {
|
||||
if err := os.MkdirAll(filepath.Join(webDir, "assets"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(webDir, "fonts"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(webDir, "brand"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -85,7 +88,10 @@ func TestServeSPAServesAssetsImmutableAndBypassesAPIRoutes(t *testing.T) {
|
||||
if err := os.WriteFile(filepath.Join(webDir, "assets", "app.js"), []byte("console.log('ok')"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(webDir, "brand", "mmtl-logo.svg"), []byte("<svg></svg>"), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(webDir, "fonts", "geist-400.woff2"), []byte("wOF2-test-font"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(webDir, "brand", "mebox-logo.svg"), []byte("<svg></svg>"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(webDir, "artwork-cache-sw.js"), []byte("self.addEventListener('fetch', () => {})"), 0o644); err != nil {
|
||||
@@ -93,7 +99,7 @@ func TestServeSPAServesAssetsImmutableAndBypassesAPIRoutes(t *testing.T) {
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
serveSPA(router, webDir)
|
||||
serveSPA(router, os.DirFS(webDir))
|
||||
|
||||
assetReq := httptest.NewRequest(http.MethodGet, "/assets/app.js", nil)
|
||||
assetResp := httptest.NewRecorder()
|
||||
@@ -105,7 +111,30 @@ func TestServeSPAServesAssetsImmutableAndBypassesAPIRoutes(t *testing.T) {
|
||||
t.Fatalf("asset Cache-Control = %q, want immutable", got)
|
||||
}
|
||||
|
||||
brandReq := httptest.NewRequest(http.MethodGet, "/brand/mmtl-logo.svg", nil)
|
||||
fontReq := httptest.NewRequest(http.MethodGet, "/fonts/geist-400.woff2", nil)
|
||||
fontResp := httptest.NewRecorder()
|
||||
router.ServeHTTP(fontResp, fontReq)
|
||||
if fontResp.Code != http.StatusOK {
|
||||
t.Fatalf("font status = %d, want 200", fontResp.Code)
|
||||
}
|
||||
if got := fontResp.Header().Get("Cache-Control"); !strings.Contains(got, "max-age=86400") {
|
||||
t.Fatalf("font Cache-Control = %q, want max-age=86400", got)
|
||||
}
|
||||
if got := fontResp.Body.String(); got != "wOF2-test-font" {
|
||||
t.Fatalf("font body = %q, want wOF2-test-font", got)
|
||||
}
|
||||
|
||||
missingFontReq := httptest.NewRequest(http.MethodGet, "/fonts/missing.woff2", nil)
|
||||
missingFontResp := httptest.NewRecorder()
|
||||
router.ServeHTTP(missingFontResp, missingFontReq)
|
||||
if missingFontResp.Code != http.StatusNotFound {
|
||||
t.Fatalf("missing font status = %d, want 404", missingFontResp.Code)
|
||||
}
|
||||
if strings.Contains(missingFontResp.Body.String(), "index") {
|
||||
t.Fatalf("missing font should not serve SPA index: %q", missingFontResp.Body.String())
|
||||
}
|
||||
|
||||
brandReq := httptest.NewRequest(http.MethodGet, "/brand/mebox-logo.svg", nil)
|
||||
brandResp := httptest.NewRecorder()
|
||||
router.ServeHTTP(brandResp, brandReq)
|
||||
if brandResp.Code != http.StatusOK {
|
||||
@@ -155,7 +184,7 @@ func TestServeSPAServesAssetsImmutableAndBypassesAPIRoutes(t *testing.T) {
|
||||
func TestServeSPAMissingIndexReportsExplicit404(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
serveSPA(router, t.TempDir())
|
||||
serveSPA(router, os.DirFS(t.TempDir()))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func runProgram() {
|
||||
app, err := newApplication()
|
||||
if err != nil {
|
||||
reportError("MeBox 启动失败", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-stop
|
||||
|
||||
if err := app.Shutdown(); err != nil {
|
||||
reportError("MeBox 退出失败", err)
|
||||
}
|
||||
}
|
||||
|
||||
func reportError(title string, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "%s: %v\n", title, err)
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"fyne.io/systray"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
|
||||
"github.com/truewhile/MeBox/internal/brand"
|
||||
)
|
||||
|
||||
const (
|
||||
runRegistryKey = `Software\Microsoft\Windows\CurrentVersion\Run`
|
||||
runRegistryValue = "MeBox"
|
||||
singleInstanceName = `Local\MeBox-Server`
|
||||
)
|
||||
|
||||
var errAlreadyRunning = errors.New("MeBox is already running")
|
||||
|
||||
func runProgram() {
|
||||
if err := prepareWorkingDirectory(); err != nil {
|
||||
reportError("MeBox 启动失败", fmt.Errorf("切换工作目录失败: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
instance, err := acquireSingleInstance()
|
||||
if errors.Is(err, errAlreadyRunning) {
|
||||
reportError("MeBox", errors.New("MeBox 已在运行,请查看右下角托盘图标"))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
reportError("MeBox 启动失败", fmt.Errorf("创建单实例锁失败: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
app, err := newApplication()
|
||||
if err != nil {
|
||||
_ = instance.Close()
|
||||
reportError("MeBox 启动失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
controller := &trayController{app: app}
|
||||
systray.Run(controller.onReady, controller.onExit)
|
||||
|
||||
// Release the mutex before starting the replacement process. The new
|
||||
// process must be able to acquire it immediately after the old one exits.
|
||||
_ = instance.Close()
|
||||
|
||||
if !controller.readyClosed.Load() {
|
||||
_ = app.Shutdown()
|
||||
reportError("MeBox 启动失败", errors.New("系统托盘初始化失败"))
|
||||
return
|
||||
}
|
||||
if controller.restartRequested.Load() {
|
||||
if err := launchSelf(); err != nil {
|
||||
reportError("MeBox 重启失败", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type trayController struct {
|
||||
app *application
|
||||
|
||||
readyClosed atomic.Bool
|
||||
restartRequested atomic.Bool
|
||||
shutdownStarted atomic.Bool
|
||||
}
|
||||
|
||||
func (c *trayController) onReady() {
|
||||
defer func() {
|
||||
c.readyClosed.Store(true)
|
||||
}()
|
||||
|
||||
systray.SetIcon(brand.Icon)
|
||||
systray.SetTooltip("MeBox")
|
||||
|
||||
mOpen := systray.AddMenuItem("打开 MeBox", "在浏览器中打开 MeBox")
|
||||
mAutoStart := systray.AddMenuItemCheckbox("开机自启", "登录 Windows 后自动启动 MeBox", autoStartEnabled())
|
||||
mLogs := systray.AddMenuItem("查看日志", "打开 MeBox 应用日志")
|
||||
systray.AddSeparator()
|
||||
mRestart := systray.AddMenuItem("重启 MeBox", "重启 MeBox 服务")
|
||||
mQuit := systray.AddMenuItem("退出 MeBox", "停止服务并退出")
|
||||
|
||||
initialAutoStart := mAutoStart.Checked()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-mOpen.ClickedCh:
|
||||
if err := openURL(c.app.localURL()); err != nil {
|
||||
reportError("MeBox", fmt.Errorf("打开 MeBox 失败: %w", err))
|
||||
}
|
||||
case <-mAutoStart.ClickedCh:
|
||||
enable := !mAutoStart.Checked()
|
||||
if err := setAutoStart(enable); err != nil {
|
||||
if initialAutoStart {
|
||||
mAutoStart.Check()
|
||||
} else {
|
||||
mAutoStart.Uncheck()
|
||||
}
|
||||
reportError("MeBox", fmt.Errorf("更新开机自启设置失败: %w", err))
|
||||
continue
|
||||
}
|
||||
if enable {
|
||||
mAutoStart.Check()
|
||||
} else {
|
||||
mAutoStart.Uncheck()
|
||||
}
|
||||
initialAutoStart = enable
|
||||
case <-mLogs.ClickedCh:
|
||||
if err := c.app.openLog(); err != nil {
|
||||
reportError("MeBox", fmt.Errorf("打开日志失败: %w", err))
|
||||
}
|
||||
case <-mRestart.ClickedCh:
|
||||
c.restart()
|
||||
case <-mQuit.ClickedCh:
|
||||
c.quit()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *trayController) onExit() {
|
||||
_ = c.app.Shutdown()
|
||||
}
|
||||
|
||||
func (c *trayController) quit() {
|
||||
if !c.shutdownStarted.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
_ = c.app.Shutdown()
|
||||
systray.Quit()
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *trayController) restart() {
|
||||
if !c.shutdownStarted.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
c.restartRequested.Store(true)
|
||||
go func() {
|
||||
_ = c.app.Shutdown()
|
||||
systray.Quit()
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *application) openLog() error {
|
||||
appLog, _, _ := logFilePaths(a.cfg)
|
||||
if appLog != "" {
|
||||
if _, err := os.Stat(appLog); err == nil {
|
||||
return openPath(appLog)
|
||||
}
|
||||
_ = os.MkdirAll(filepath.Dir(appLog), 0o750)
|
||||
return openPath(filepath.Dir(appLog))
|
||||
}
|
||||
|
||||
logDir := filepath.Join(a.cfg.App.DataDir, "logs")
|
||||
if err := os.MkdirAll(logDir, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
return openPath(logDir)
|
||||
}
|
||||
|
||||
func prepareWorkingDirectory() error {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exeDir := filepath.Dir(exe)
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if samePath(exeDir, cwd) || looksLikeProjectDirectory(cwd) {
|
||||
return nil
|
||||
}
|
||||
return os.Chdir(exeDir)
|
||||
}
|
||||
|
||||
func looksLikeProjectDirectory(dir string) bool {
|
||||
for _, name := range []string{"go.mod", "config.yaml", "data", filepath.Join("web", "dist")} {
|
||||
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func samePath(left, right string) bool {
|
||||
return strings.EqualFold(filepath.Clean(left), filepath.Clean(right))
|
||||
}
|
||||
|
||||
type singleInstance struct {
|
||||
handle windows.Handle
|
||||
}
|
||||
|
||||
func acquireSingleInstance() (*singleInstance, error) {
|
||||
name, err := windows.UTF16PtrFromString(singleInstanceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handle, err := windows.CreateMutex(nil, false, name)
|
||||
if errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
|
||||
if handle != 0 {
|
||||
_ = windows.CloseHandle(handle)
|
||||
}
|
||||
return nil, errAlreadyRunning
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &singleInstance{handle: handle}, nil
|
||||
}
|
||||
|
||||
func (s *singleInstance) Close() error {
|
||||
if s == nil || s.handle == 0 {
|
||||
return nil
|
||||
}
|
||||
err := windows.CloseHandle(s.handle)
|
||||
s.handle = 0
|
||||
return err
|
||||
}
|
||||
|
||||
func launchSelf() error {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command(exe, os.Args[1:]...)
|
||||
cmd.Dir = cwd
|
||||
cmd.Env = os.Environ()
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
CreationFlags: windows.CREATE_NEW_PROCESS_GROUP | windows.DETACHED_PROCESS,
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Process.Release()
|
||||
}
|
||||
|
||||
func autoStartEnabled() bool {
|
||||
key, err := registry.OpenKey(registry.CURRENT_USER, runRegistryKey, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
value, _, err := key.GetStringValue(runRegistryValue)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(strings.Trim(value, `"`)), filepath.Clean(exe))
|
||||
}
|
||||
|
||||
func setAutoStart(enabled bool) error {
|
||||
key, _, err := registry.CreateKey(registry.CURRENT_USER, runRegistryKey, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
if !enabled {
|
||||
if err := key.DeleteValue(runRegistryValue); err != nil && !errors.Is(err, registry.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return key.SetStringValue(runRegistryValue, syscall.EscapeArg(filepath.Clean(exe)))
|
||||
}
|
||||
|
||||
func openURL(url string) error {
|
||||
return shellOpen(url)
|
||||
}
|
||||
|
||||
func openPath(path string) error {
|
||||
return shellOpen(path)
|
||||
}
|
||||
|
||||
func shellOpen(target string) error {
|
||||
targetPtr, err := windows.UTF16PtrFromString(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verbPtr, err := windows.UTF16PtrFromString("open")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.ShellExecute(0, verbPtr, targetPtr, nil, nil, 1)
|
||||
}
|
||||
|
||||
func reportError(title string, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
text, textErr := windows.UTF16PtrFromString(title + "\r\n\r\n" + err.Error())
|
||||
if textErr != nil {
|
||||
return
|
||||
}
|
||||
caption, captionErr := windows.UTF16PtrFromString("MeBox")
|
||||
if captionErr != nil {
|
||||
return
|
||||
}
|
||||
_, _ = windows.MessageBox(0, text, caption, windows.MB_OK|windows.MB_ICONERROR|windows.MB_SETFOREGROUND)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
// Regenerate the linked Windows resources after changing the project logo or
|
||||
// manifest:
|
||||
//
|
||||
// go generate ./cmd/server
|
||||
//
|
||||
//go:generate go run github.com/akavel/rsrc@v0.10.2 -arch amd64 -ico ../../internal/brand/logo.ico -manifest winres/mebox.manifest -o rsrc_windows_amd64.syso
|
||||
//go:generate go run github.com/akavel/rsrc@v0.10.2 -arch arm64 -ico ../../internal/brand/logo.ico -manifest winres/mebox.manifest -o rsrc_windows_arm64.syso
|
||||
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -9,19 +11,24 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MMTL/internal/config"
|
||||
"github.com/ShukeBta/MMTL/internal/handler"
|
||||
"github.com/ShukeBta/MMTL/internal/middleware"
|
||||
"github.com/ShukeBta/MMTL/internal/service"
|
||||
"github.com/truewhile/MeBox/internal/config"
|
||||
"github.com/truewhile/MeBox/internal/handler"
|
||||
"github.com/truewhile/MeBox/internal/middleware"
|
||||
"github.com/truewhile/MeBox/internal/service"
|
||||
|
||||
"github.com/truewhile/MeBox/web"
|
||||
)
|
||||
|
||||
func buildRouter(cfg *config.Config, logger *zap.Logger, svc *service.Container) *gin.Engine {
|
||||
func buildRouter(cfg *config.Config, logger *zap.Logger, embyCompatLogger *zap.Logger, svc *service.Container) *gin.Engine {
|
||||
if !cfg.App.Debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(middleware.RequestLogger(logger))
|
||||
r.Use(middleware.EmbyCompatLogger(embyCompatLogger, func(path string) bool {
|
||||
return !isFrontendLibraryRoute(path) && handler.IsEmbyPath(path)
|
||||
}))
|
||||
if !cfg.App.Debug && len(cfg.App.CORSOrigins) == 0 {
|
||||
logger.Warn("CORS: no origins configured in production — CORS headers will be omitted (same-origin enforced). Set app.cors_origins for cross-origin access.")
|
||||
}
|
||||
@@ -29,60 +36,103 @@ func buildRouter(cfg *config.Config, logger *zap.Logger, svc *service.Container)
|
||||
|
||||
handler.Register(r, cfg, logger, svc)
|
||||
|
||||
if cfg.App.WebDir != "" {
|
||||
serveSPA(r, cfg.App.WebDir)
|
||||
// Prefer a directory on disk when configured explicitly (e.g. the Docker image
|
||||
// mounts web/dist from the build stage, or an operator overrides app.web_dir
|
||||
// with a custom skin). Otherwise fall back to the SPA embedded into the binary,
|
||||
// which is what makes the cross-platform single-file artifacts work.
|
||||
uiFS := webui.DistFS()
|
||||
if dir := cfg.App.WebDir; dir != "" {
|
||||
disk := os.DirFS(dir)
|
||||
if index, err := fs.Stat(disk, "index.html"); err == nil && !index.IsDir() {
|
||||
uiFS = disk
|
||||
}
|
||||
}
|
||||
serveSPA(r, uiFS)
|
||||
return r
|
||||
}
|
||||
|
||||
// serveSPA serves the React build artifacts and falls back to index.html for
|
||||
// non-API, non-asset paths so client-side routing keeps working.
|
||||
func serveSPA(r *gin.Engine, webDir string) {
|
||||
// non-API, non-asset paths so client-side routing keeps working. The UI tree
|
||||
// comes from root, which is either the compiled-in SPA or an on-disk web dir.
|
||||
func serveSPA(r *gin.Engine, root fs.FS) {
|
||||
assets := r.Group("/assets")
|
||||
assets.Use(middleware.GzipStatic())
|
||||
assets.Use(func(c *gin.Context) {
|
||||
c.Header("Cache-Control", "public, max-age=31536000, immutable")
|
||||
c.Next()
|
||||
})
|
||||
assets.Static("/", filepath.Join(webDir, "assets"))
|
||||
assets.GET("/*filepath", serveFSDir(root, "assets"))
|
||||
fonts := r.Group("/fonts")
|
||||
fonts.Use(middleware.GzipStatic())
|
||||
fonts.Use(func(c *gin.Context) {
|
||||
c.Header("Cache-Control", "public, max-age=86400")
|
||||
c.Next()
|
||||
})
|
||||
fonts.GET("/*filepath", serveFSDir(root, "fonts"))
|
||||
brand := r.Group("/brand")
|
||||
brand.Use(func(c *gin.Context) {
|
||||
setNoCacheHeaders(c)
|
||||
c.Next()
|
||||
})
|
||||
brand.Static("/", filepath.Join(webDir, "brand"))
|
||||
brand.GET("/*filepath", serveFSDir(root, "brand"))
|
||||
for _, rootFile := range []string{"/favicon.ico", "/favicon.svg", "/artwork-cache-sw.js"} {
|
||||
filePath := filepath.Join(webDir, strings.TrimPrefix(rootFile, "/"))
|
||||
r.GET(rootFile, serveNoCacheFile(filePath))
|
||||
r.HEAD(rootFile, serveNoCacheFile(filePath))
|
||||
name := strings.TrimPrefix(rootFile, "/")
|
||||
r.GET(rootFile, serveFSFile(root, name))
|
||||
r.HEAD(rootFile, serveFSFile(root, name))
|
||||
}
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
r.NoRoute(middleware.GzipStatic(), func(c *gin.Context) {
|
||||
if handler.TryHandleEmbyNormalizedRoute(c, r) {
|
||||
return
|
||||
}
|
||||
path := c.Request.URL.Path
|
||||
if shouldBypassSPAFallback(path) {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
serveSPAIndex(c, filepath.Join(webDir, "index.html"))
|
||||
setNoCacheHeaders(c)
|
||||
data, err := fs.ReadFile(root, "index.html")
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "MeBox web UI not found")
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
|
||||
})
|
||||
}
|
||||
|
||||
func serveNoCacheFile(filePath string) gin.HandlerFunc {
|
||||
// serveFSDir serves a static subdirectory of root. A missing asset returns 404.
|
||||
func serveFSDir(root fs.FS, dir string) gin.HandlerFunc {
|
||||
sub, err := fs.Sub(root, dir)
|
||||
if err != nil {
|
||||
return func(c *gin.Context) { c.Status(http.StatusNotFound) }
|
||||
}
|
||||
handler := http.StripPrefix("/"+dir, http.FileServerFS(sub))
|
||||
return func(c *gin.Context) {
|
||||
setNoCacheHeaders(c)
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.File(filePath)
|
||||
handler.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func serveSPAIndex(c *gin.Context, indexPath string) {
|
||||
setNoCacheHeaders(c)
|
||||
if _, err := os.Stat(indexPath); err != nil {
|
||||
c.String(http.StatusNotFound, "MMTL web UI not found: %s", indexPath)
|
||||
return
|
||||
// serveFSFile serves a single root-level file (favicon / service worker) with
|
||||
// no-cache headers. It reads from root, which may be the embedded SPA or disk.
|
||||
func serveFSFile(root fs.FS, name string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
setNoCacheHeaders(c)
|
||||
data, err := fs.ReadFile(root, name)
|
||||
if err != nil {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, mimeTypeByName(name), data)
|
||||
}
|
||||
}
|
||||
|
||||
// mimeTypeByName returns an HTTP content type guessed from a file extension.
|
||||
func mimeTypeByName(name string) string {
|
||||
switch mime.TypeByExtension(filepath.Ext(name)) {
|
||||
case "":
|
||||
return "application/octet-stream"
|
||||
default:
|
||||
return mime.TypeByExtension(filepath.Ext(name))
|
||||
}
|
||||
c.File(indexPath)
|
||||
}
|
||||
|
||||
func setNoCacheHeaders(c *gin.Context) {
|
||||
@@ -137,6 +187,11 @@ func isFrontendLibraryRoute(path string) bool {
|
||||
if strings.Contains(id, "/") {
|
||||
return false
|
||||
}
|
||||
// 远程 Emby 挂载库的伪装 ID(embyremote~account~remote)也是前端库路由,
|
||||
// 需要交给 SPA 而非当作 Emby API 路径 404。
|
||||
if strings.HasPrefix(id, "embyremote~") {
|
||||
return true
|
||||
}
|
||||
if len(id) != 36 {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MMTL/internal/config"
|
||||
"github.com/ShukeBta/MMTL/internal/service"
|
||||
"github.com/truewhile/MeBox/internal/config"
|
||||
"github.com/truewhile/MeBox/internal/service"
|
||||
)
|
||||
|
||||
// tlsPair 记录当前正在服务的证书,用于判断是否需要重新绑定监听。
|
||||
@@ -27,7 +27,7 @@ type tlsPair struct {
|
||||
version string
|
||||
}
|
||||
|
||||
// serverManager 负责 MMTL 的 HTTP/HTTPS 监听。HTTPS 设置保存后调用 Reload,
|
||||
// serverManager 负责 MeBox 的 HTTP/HTTPS 监听。HTTPS 设置保存后调用 Reload,
|
||||
// 在同一个端口上把明文 HTTP 与 TLS 监听热切换,无需重启进程:
|
||||
//
|
||||
// - 关闭旧监听释放端口(同一进程内 Windows 不允许重复绑定同一端口);
|
||||
@@ -130,26 +130,35 @@ func (m *serverManager) Shutdown(ctx context.Context) error {
|
||||
// desiredPair 根据当前配置计算目标监听形态:nil 表示明文 HTTP,非 nil 表示 TLS。
|
||||
// 证书/私钥按"路径优先、内容兜底"解析,并校验是否匹配。
|
||||
func (m *serverManager) desiredPair() (*tlsPair, error) {
|
||||
if m.cfg == nil || !m.cfg.App.HTTPSEnabled {
|
||||
// 与 ApplyRuntimeSetting 的写锁配对:HTTPS 相关字段可能被运行时设置
|
||||
// 热更新,无锁读存在数据竞争(string 撕裂)。
|
||||
config.RuntimeMu.RLock()
|
||||
httpsEnabled := m.cfg != nil && m.cfg.App.HTTPSEnabled
|
||||
cert := m.cfg.App.SSLCert
|
||||
certPath := m.cfg.App.SSLCertPath
|
||||
key := m.cfg.App.SSLKey
|
||||
keyPath := m.cfg.App.SSLKeyPath
|
||||
config.RuntimeMu.RUnlock()
|
||||
if !httpsEnabled {
|
||||
return nil, nil
|
||||
}
|
||||
certPEM, err := service.ResolveSSLMaterial(m.cfg.App.SSLCert, m.cfg.App.SSLCertPath, "证书")
|
||||
certPEM, err := service.ResolveSSLMaterial(cert, certPath, "证书")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyPEM, err := service.ResolveSSLMaterial(m.cfg.App.SSLKey, m.cfg.App.SSLKeyPath, "私钥")
|
||||
keyPEM, err := service.ResolveSSLMaterial(key, keyPath, "私钥")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := service.ValidateSSLKeyPair(certPEM, keyPEM); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
|
||||
pairCert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SSL 证书/私钥无效:%v", err)
|
||||
}
|
||||
return &tlsPair{
|
||||
cert: cert,
|
||||
cert: pairCert,
|
||||
certPEM: certPEM,
|
||||
keyPEM: keyPEM,
|
||||
version: certPEM + "\x00" + keyPEM,
|
||||
@@ -171,6 +180,8 @@ func (m *serverManager) maybeStartAutoReloadLocked() {
|
||||
|
||||
// pathBased 是否至少有一侧证书/私钥通过文件路径配置。
|
||||
func (m *serverManager) pathBased() bool {
|
||||
config.RuntimeMu.RLock()
|
||||
defer config.RuntimeMu.RUnlock()
|
||||
return strings.TrimSpace(m.cfg.App.SSLCertPath) != "" || strings.TrimSpace(m.cfg.App.SSLKeyPath) != ""
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MMTL/internal/config"
|
||||
"github.com/truewhile/MeBox/internal/config"
|
||||
)
|
||||
|
||||
func makeTestPairPEM(t *testing.T) (certPEM, keyPEM string) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MMTL/internal/config"
|
||||
"github.com/truewhile/MeBox/internal/config"
|
||||
)
|
||||
|
||||
func applyCPUThreadLimit(cfg *config.Config, logger *zap.Logger) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="*"
|
||||
name="MeBox"
|
||||
type="win32"
|
||||
/>
|
||||
<description>MeBox media server</description>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -1,7 +1,7 @@
|
||||
# MMTL 第三档完整 Docker Compose 部署文件
|
||||
# MeBox 第三档完整 Docker Compose 部署文件
|
||||
#
|
||||
# 组件:
|
||||
# MMTL + PostgreSQL + Redis + OpenSearch
|
||||
# MeBox + PostgreSQL + Redis + OpenSearch
|
||||
#
|
||||
# 使用方式二选一:
|
||||
# 1. 保存为 docker-compose.yml 后执行:
|
||||
@@ -19,12 +19,8 @@
|
||||
# admin / admin123
|
||||
|
||||
services:
|
||||
mmtl:
|
||||
# 镜像二选一:
|
||||
# 方式一:GitHub 仓库镜像 GHCR(默认,推荐)
|
||||
image: ghcr.io/shukebta/mmtl:latest
|
||||
# 方式二:Docker Hub 备用(GHCR 拉取慢或不可用时使用)
|
||||
# image: shukbet/mmtl:latest
|
||||
mebox:
|
||||
image: ghcr.io/truewhile/mebox:latest
|
||||
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
@@ -59,7 +55,7 @@ services:
|
||||
bind:
|
||||
create_host_path: false
|
||||
|
||||
# 下载目录。需要和 qBittorrent 保存路径保持一致。
|
||||
# 下载目录。需要和下载器保存路径保持一致。
|
||||
# NAS 示例:source: /vol1/1000/Downloads
|
||||
# Windows Docker Desktop 示例:source: D:/Downloads
|
||||
- type: bind
|
||||
@@ -78,45 +74,45 @@ services:
|
||||
PUID: "1000"
|
||||
PGID: "1000"
|
||||
|
||||
MMTL_APP_HOST: 0.0.0.0
|
||||
MMTL_APP_PORT: 8080
|
||||
MMTL_APP_WEB_DIR: /app/web/dist
|
||||
MMTL_APP_DATA_DIR: /data
|
||||
MMTL_LOGGING_LEVEL: info
|
||||
MMTL_LOGGING_FORMAT: console
|
||||
MMTL_LOGGING_OUTPUT_PATH: /data/logs
|
||||
MMTL_LOGGING_MAX_SIZE_MB: "50"
|
||||
MMTL_LOGGING_MAX_BACKUPS: "20"
|
||||
MMTL_LOGGING_MAX_AGE_DAYS: "30"
|
||||
MEBOX_APP_HOST: 0.0.0.0
|
||||
MEBOX_APP_PORT: 8080
|
||||
MEBOX_APP_WEB_DIR: /app/web/dist
|
||||
MEBOX_APP_DATA_DIR: /data
|
||||
MEBOX_LOGGING_LEVEL: info
|
||||
MEBOX_LOGGING_FORMAT: console
|
||||
MEBOX_LOGGING_OUTPUT_PATH: /data/logs
|
||||
MEBOX_LOGGING_MAX_SIZE_MB: "20"
|
||||
MEBOX_LOGGING_MAX_BACKUPS: "5"
|
||||
MEBOX_LOGGING_MAX_AGE_DAYS: "30"
|
||||
|
||||
MMTL_DATABASE_TYPE: postgres
|
||||
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||
MEBOX_DATABASE_TYPE: postgres
|
||||
MEBOX_DATABASE_DSN: postgres://mebox:mebox@postgres:5432/mebox?sslmode=disable
|
||||
MEBOX_DATABASE_DB_PATH: /data/mebox.db
|
||||
|
||||
MMTL_CACHE_REDIS_URL: redis://redis:6379/0
|
||||
MMTL_CACHE_CACHE_DIR: /cache
|
||||
MEBOX_CACHE_REDIS_URL: redis://redis:6379/0
|
||||
MEBOX_CACHE_CACHE_DIR: /cache
|
||||
|
||||
# OpenSearch 只做搜索索引,主数据仍以 PostgreSQL 为准。
|
||||
MMTL_SEARCH_BACKEND: opensearch
|
||||
MMTL_SEARCH_OPENSEARCH_URL: http://opensearch:9200
|
||||
MMTL_SEARCH_INDEX: mmtl_media
|
||||
MEBOX_SEARCH_BACKEND: opensearch
|
||||
MEBOX_SEARCH_OPENSEARCH_URL: http://opensearch:9200
|
||||
MEBOX_SEARCH_INDEX: mebox_media
|
||||
|
||||
MMTL_UPDATE_IMAGE: ghcr.io/shukebta/mmtl:latest
|
||||
MEBOX_UPDATE_IMAGE: ghcr.io/truewhile/mebox:latest
|
||||
|
||||
# 默认推荐在网页里使用容器路径 /media。
|
||||
# 如果旧媒体库已经保存了宿主机路径 /vol1/1000/Media,
|
||||
# 再把这里改成同一个宿主机真实路径用于旧路径换算。
|
||||
MMTL_MEDIA_DIR: /media
|
||||
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||
MMTL_DOWNLOAD_DIR: /downloads
|
||||
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||
MEBOX_MEDIA_DIR: /media
|
||||
MEBOX_MEDIA_CONTAINER_DIR: /media
|
||||
MEBOX_DOWNLOAD_DIR: /downloads
|
||||
MEBOX_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||
|
||||
MMTL_TRANSCODER_ENABLED: "true"
|
||||
MMTL_TRANSCODER_HARDWARE_ACCEL: "false"
|
||||
MMTL_TRANSCODER_REALTIME: "true"
|
||||
MMTL_TRANSCODER_THREADS: "2"
|
||||
MMTL_TRANSCODER_MAX_CONCURRENT: "1"
|
||||
MMTL_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
||||
MEBOX_TRANSCODER_ENABLED: "true"
|
||||
MEBOX_TRANSCODER_HARDWARE_ACCEL: "false"
|
||||
MEBOX_TRANSCODER_REALTIME: "true"
|
||||
MEBOX_TRANSCODER_THREADS: "2"
|
||||
MEBOX_TRANSCODER_MAX_CONCURRENT: "1"
|
||||
MEBOX_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "busybox wget -qO- http://127.0.0.1:8080/api/health || exit 1"]
|
||||
@@ -128,31 +124,31 @@ services:
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "10"
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
# 首次部署允许拉取;日常更新请只 pull mmtl。
|
||||
# 首次部署允许拉取;日常更新请只 pull mebox。
|
||||
pull_policy: missing
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: mmtl
|
||||
POSTGRES_USER: mmtl
|
||||
POSTGRES_PASSWORD: mmtl
|
||||
POSTGRES_DB: mebox
|
||||
POSTGRES_USER: mebox
|
||||
POSTGRES_PASSWORD: mebox
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- ./postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mmtl -d mmtl"]
|
||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mebox -d mebox"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "10"
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
@@ -176,8 +172,8 @@ services:
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "10"
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
|
||||
opensearch:
|
||||
image: opensearchproject/opensearch:2
|
||||
@@ -200,5 +196,5 @@ services:
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "10"
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# MMTL 单镜像部署模板(SQLite)
|
||||
# MeBox 单镜像部署模板(SQLite)
|
||||
#
|
||||
# 适合:新手、单人使用、低配 NAS / 小主机。
|
||||
# 特点:只有一个镜像,不需要 PostgreSQL / Redis / .env。
|
||||
@@ -12,12 +12,10 @@
|
||||
# 首次登录后请立刻修改密码。
|
||||
|
||||
services:
|
||||
mmtl:
|
||||
image: ghcr.io/shukebta/mmtl:latest
|
||||
# Docker Hub 备用:
|
||||
# image: shukbet/mmtl:latest
|
||||
mebox:
|
||||
image: ghcr.io/truewhile/mebox:latest
|
||||
|
||||
container_name: mmtl
|
||||
container_name: mebox
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
|
||||
@@ -33,23 +31,7 @@ services:
|
||||
- ./cache:/cache
|
||||
|
||||
# 媒体库。网页里添加媒体库时填写 /media 或 /media/子目录。
|
||||
# NAS 示例:source: /vol1/1000/Media
|
||||
# Windows Docker Desktop 示例:source: D:/Media
|
||||
# create_host_path=false 可以避免路径写错时 Docker 自动创建空文件夹。
|
||||
- type: bind
|
||||
source: ./media
|
||||
target: /media
|
||||
bind:
|
||||
create_host_path: false
|
||||
|
||||
# 下载目录。qBittorrent / Transmission 的保存目录建议也对齐到 /downloads。
|
||||
# NAS 示例:source: /vol1/1000/Downloads
|
||||
# Windows Docker Desktop 示例:source: D:/Downloads
|
||||
- type: bind
|
||||
source: ./downloads
|
||||
target: /downloads
|
||||
bind:
|
||||
create_host_path: false
|
||||
- ./media:/media
|
||||
|
||||
# 可选:Intel 核显硬解/转码。需要时取消注释,并在后台开启硬件加速。
|
||||
# - /dev/dri:/dev/dri
|
||||
@@ -61,35 +43,33 @@ services:
|
||||
TZ: Asia/Shanghai
|
||||
|
||||
# Linux/NAS 文件权限。写入文件权限异常时,改成宿主机实际 uid/gid。
|
||||
PUID: "1000"
|
||||
PGID: "1000"
|
||||
PUID: "0"
|
||||
PGID: "0"
|
||||
|
||||
MMTL_APP_HOST: 0.0.0.0
|
||||
MMTL_APP_PORT: 8080
|
||||
MMTL_APP_WEB_DIR: /app/web/dist
|
||||
MMTL_APP_DATA_DIR: /data
|
||||
MEBOX_APP_HOST: 0.0.0.0
|
||||
MEBOX_APP_PORT: 8080
|
||||
MEBOX_APP_WEB_DIR: /app/web/dist
|
||||
MEBOX_APP_DATA_DIR: /data
|
||||
|
||||
# 单镜像档固定使用 SQLite。主数据库文件:./data/mmtl.db。
|
||||
MMTL_DATABASE_TYPE: sqlite
|
||||
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||
MMTL_CACHE_CACHE_DIR: /cache
|
||||
# 单镜像档固定使用 SQLite。主数据库文件:./data/mebox.db。
|
||||
MEBOX_DATABASE_TYPE: sqlite
|
||||
MEBOX_DATABASE_DB_PATH: /data/mebox.db
|
||||
MEBOX_CACHE_CACHE_DIR: /cache
|
||||
|
||||
# 路径映射保持容器内统一,网页和下载器里优先使用 /media、/downloads。
|
||||
MMTL_MEDIA_DIR: /media
|
||||
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||
MMTL_DOWNLOAD_DIR: /downloads
|
||||
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||
MEBOX_MEDIA_DIR: /media
|
||||
MEBOX_MEDIA_CONTAINER_DIR: /media
|
||||
|
||||
# 完整应用日志默认写入 ./data/logs/app.log;排查复杂问题时可临时改成 debug。
|
||||
MMTL_LOGGING_LEVEL: info
|
||||
MMTL_LOGGING_FORMAT: console
|
||||
MMTL_LOGGING_OUTPUT_PATH: /data/logs
|
||||
MMTL_LOGGING_MAX_SIZE_MB: "50"
|
||||
MMTL_LOGGING_MAX_BACKUPS: "20"
|
||||
MMTL_LOGGING_MAX_AGE_DAYS: "30"
|
||||
MEBOX_LOGGING_LEVEL: info
|
||||
MEBOX_LOGGING_FORMAT: console
|
||||
MEBOX_LOGGING_OUTPUT_PATH: /data/logs
|
||||
MEBOX_LOGGING_MAX_SIZE_MB: "20"
|
||||
MEBOX_LOGGING_MAX_BACKUPS: "5"
|
||||
MEBOX_LOGGING_MAX_AGE_DAYS: "30"
|
||||
|
||||
extra_hosts:
|
||||
# 容器访问宿主机服务用,例如 qBittorrent: http://host.docker.internal:8085
|
||||
# 容器访问宿主机服务(如下载器等)用: http://host.docker.internal:8085
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
healthcheck:
|
||||
@@ -102,5 +82,5 @@ services:
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "10"
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# MMTL 第二档完整 Docker Compose 部署文件
|
||||
# MeBox 第二档完整 Docker Compose 部署文件
|
||||
#
|
||||
# 组件:
|
||||
# MMTL + PostgreSQL + Redis
|
||||
# MeBox + PostgreSQL + Redis
|
||||
#
|
||||
# 使用方式二选一:
|
||||
# 1. 保存为 docker-compose.yml 后执行:
|
||||
@@ -16,12 +16,8 @@
|
||||
# admin / admin123
|
||||
|
||||
services:
|
||||
mmtl:
|
||||
# 镜像二选一:
|
||||
# 方式一:GitHub 仓库镜像 GHCR(默认,推荐)
|
||||
image: ghcr.io/shukebta/mmtl:latest
|
||||
# 方式二:Docker Hub 备用(GHCR 拉取慢或不可用时使用)
|
||||
# image: shukbet/mmtl:latest
|
||||
mebox:
|
||||
image: ghcr.io/truewhile/mebox:latest
|
||||
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
@@ -54,7 +50,7 @@ services:
|
||||
bind:
|
||||
create_host_path: false
|
||||
|
||||
# 下载目录。需要和 qBittorrent 保存路径保持一致。
|
||||
# 下载目录。需要和下载器保存路径保持一致。
|
||||
# NAS 示例:source: /vol1/1000/Downloads
|
||||
# Windows Docker Desktop 示例:source: D:/Downloads
|
||||
- type: bind
|
||||
@@ -73,39 +69,39 @@ services:
|
||||
PUID: "1000"
|
||||
PGID: "1000"
|
||||
|
||||
MMTL_APP_HOST: 0.0.0.0
|
||||
MMTL_APP_PORT: 8080
|
||||
MMTL_APP_WEB_DIR: /app/web/dist
|
||||
MMTL_APP_DATA_DIR: /data
|
||||
MMTL_LOGGING_LEVEL: info
|
||||
MMTL_LOGGING_FORMAT: console
|
||||
MMTL_LOGGING_OUTPUT_PATH: /data/logs
|
||||
MMTL_LOGGING_MAX_SIZE_MB: "50"
|
||||
MMTL_LOGGING_MAX_BACKUPS: "20"
|
||||
MMTL_LOGGING_MAX_AGE_DAYS: "30"
|
||||
MEBOX_APP_HOST: 0.0.0.0
|
||||
MEBOX_APP_PORT: 8080
|
||||
MEBOX_APP_WEB_DIR: /app/web/dist
|
||||
MEBOX_APP_DATA_DIR: /data
|
||||
MEBOX_LOGGING_LEVEL: info
|
||||
MEBOX_LOGGING_FORMAT: console
|
||||
MEBOX_LOGGING_OUTPUT_PATH: /data/logs
|
||||
MEBOX_LOGGING_MAX_SIZE_MB: "20"
|
||||
MEBOX_LOGGING_MAX_BACKUPS: "5"
|
||||
MEBOX_LOGGING_MAX_AGE_DAYS: "30"
|
||||
|
||||
MMTL_DATABASE_TYPE: postgres
|
||||
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||
MEBOX_DATABASE_TYPE: postgres
|
||||
MEBOX_DATABASE_DSN: postgres://mebox:mebox@postgres:5432/mebox?sslmode=disable
|
||||
MEBOX_DATABASE_DB_PATH: /data/mebox.db
|
||||
|
||||
# Redis 只做热缓存,源数据仍在 PostgreSQL;Redis 丢失可自动重建。
|
||||
MMTL_CACHE_REDIS_URL: redis://redis:6379/0
|
||||
MMTL_CACHE_CACHE_DIR: /cache
|
||||
MEBOX_CACHE_REDIS_URL: redis://redis:6379/0
|
||||
MEBOX_CACHE_CACHE_DIR: /cache
|
||||
|
||||
MMTL_UPDATE_IMAGE: ghcr.io/shukebta/mmtl:latest
|
||||
MEBOX_UPDATE_IMAGE: ghcr.io/truewhile/mebox:latest
|
||||
|
||||
# 路径换算配置。左边宿主机真实路径要和 volumes 左边保持一致。
|
||||
MMTL_MEDIA_DIR: /media
|
||||
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||
MMTL_DOWNLOAD_DIR: /downloads
|
||||
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||
MEBOX_MEDIA_DIR: /media
|
||||
MEBOX_MEDIA_CONTAINER_DIR: /media
|
||||
MEBOX_DOWNLOAD_DIR: /downloads
|
||||
MEBOX_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||
|
||||
MMTL_TRANSCODER_ENABLED: "true"
|
||||
MMTL_TRANSCODER_HARDWARE_ACCEL: "false"
|
||||
MMTL_TRANSCODER_REALTIME: "true"
|
||||
MMTL_TRANSCODER_THREADS: "2"
|
||||
MMTL_TRANSCODER_MAX_CONCURRENT: "1"
|
||||
MMTL_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
||||
MEBOX_TRANSCODER_ENABLED: "true"
|
||||
MEBOX_TRANSCODER_HARDWARE_ACCEL: "false"
|
||||
MEBOX_TRANSCODER_REALTIME: "true"
|
||||
MEBOX_TRANSCODER_THREADS: "2"
|
||||
MEBOX_TRANSCODER_MAX_CONCURRENT: "1"
|
||||
MEBOX_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "busybox wget -qO- http://127.0.0.1:8080/api/health || exit 1"]
|
||||
@@ -117,31 +113,31 @@ services:
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "10"
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
# 首次部署允许拉取;日常更新请只 pull mmtl。
|
||||
# 首次部署允许拉取;日常更新请只 pull mebox。
|
||||
pull_policy: missing
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: mmtl
|
||||
POSTGRES_USER: mmtl
|
||||
POSTGRES_PASSWORD: mmtl
|
||||
POSTGRES_DB: mebox
|
||||
POSTGRES_USER: mebox
|
||||
POSTGRES_PASSWORD: mebox
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- ./postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mmtl -d mmtl"]
|
||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mebox -d mebox"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "10"
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
@@ -165,5 +161,5 @@ services:
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "10"
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# MMTL 最简单 Docker Compose 部署文件
|
||||
# MeBox 最简单 Docker Compose 部署文件
|
||||
#
|
||||
# 新手建议:
|
||||
# 1. 不用 .env。
|
||||
@@ -15,12 +15,8 @@
|
||||
# admin / admin123
|
||||
|
||||
services:
|
||||
mmtl:
|
||||
# 镜像二选一:
|
||||
# 方式一:GitHub 仓库镜像 GHCR(默认,推荐)
|
||||
image: ghcr.io/shukebta/mmtl:latest
|
||||
# 方式二:Docker Hub 备用(GHCR 拉取慢或不可用时使用)
|
||||
# image: shukbet/mmtl:latest
|
||||
mebox:
|
||||
image: ghcr.io/truewhile/mebox:latest
|
||||
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
@@ -33,7 +29,7 @@ services:
|
||||
ports:
|
||||
- "18080:8080"
|
||||
|
||||
# 让容器可以访问宿主机上的 qBittorrent。
|
||||
# 让容器可以访问宿主机上的服务(如下载器)。
|
||||
# qB 地址通常可填:http://host.docker.internal:8085
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
@@ -87,52 +83,52 @@ services:
|
||||
PGID: "1000"
|
||||
|
||||
# 程序基础配置,通常不用改。
|
||||
MMTL_APP_HOST: 0.0.0.0
|
||||
MMTL_APP_PORT: 8080
|
||||
MMTL_APP_WEB_DIR: /app/web/dist
|
||||
MMTL_APP_DATA_DIR: /data
|
||||
MEBOX_APP_HOST: 0.0.0.0
|
||||
MEBOX_APP_PORT: 8080
|
||||
MEBOX_APP_WEB_DIR: /app/web/dist
|
||||
MEBOX_APP_DATA_DIR: /data
|
||||
# 详细应用日志会保存在 ./data/logs/app.log,warn/error 也会拆分保存。
|
||||
# 排查复杂问题时可临时改成 debug。
|
||||
MMTL_LOGGING_LEVEL: info
|
||||
MMTL_LOGGING_FORMAT: console
|
||||
MMTL_LOGGING_OUTPUT_PATH: /data/logs
|
||||
MMTL_LOGGING_MAX_SIZE_MB: "50"
|
||||
MMTL_LOGGING_MAX_BACKUPS: "20"
|
||||
MMTL_LOGGING_MAX_AGE_DAYS: "30"
|
||||
MEBOX_LOGGING_LEVEL: info
|
||||
MEBOX_LOGGING_FORMAT: console
|
||||
MEBOX_LOGGING_OUTPUT_PATH: /data/logs
|
||||
MEBOX_LOGGING_MAX_SIZE_MB: "20"
|
||||
MEBOX_LOGGING_MAX_BACKUPS: "5"
|
||||
MEBOX_LOGGING_MAX_AGE_DAYS: "30"
|
||||
|
||||
# 轻量模式默认只使用 PostgreSQL,适合大多数 NAS。
|
||||
# 旧版 ./data/mmtl.db 存在时,首次启动会自动迁移到 PostgreSQL。
|
||||
MMTL_DATABASE_TYPE: postgres
|
||||
MMTL_DATABASE_DSN: postgres://mmtl:mmtl@postgres:5432/mmtl?sslmode=disable
|
||||
# 旧版 ./data/mmtl.db 或 ./data/mebox.db 存在时,首次启动会自动迁移到 PostgreSQL。
|
||||
MEBOX_DATABASE_TYPE: postgres
|
||||
MEBOX_DATABASE_DSN: postgres://mebox:mebox@postgres:5432/mebox?sslmode=disable
|
||||
|
||||
# SQLite 旧库迁移源:
|
||||
# - 首次从旧版 ./data/mmtl.db 导入时保持此路径。
|
||||
# - 首次从旧版 SQLite(mmtl.db / mebox.db)导入时保持此路径。
|
||||
# - 确认迁移完成后,建议改成 /data/no-sqlite-migration.db 这类不存在的路径。
|
||||
MMTL_DATABASE_DB_PATH: /data/mmtl.db
|
||||
MMTL_CACHE_CACHE_DIR: /cache
|
||||
MEBOX_DATABASE_DB_PATH: /data/mebox.db
|
||||
MEBOX_CACHE_CACHE_DIR: /cache
|
||||
|
||||
# 管理面板热更新默认拉取此镜像,并用 Watchtower 一次性重建当前容器。
|
||||
MMTL_UPDATE_IMAGE: ghcr.io/shukebta/mmtl:latest
|
||||
MEBOX_UPDATE_IMAGE: ghcr.io/truewhile/mebox:latest
|
||||
|
||||
# 路径换算配置。
|
||||
# 默认推荐在网页里使用容器路径 /media。
|
||||
# 如果旧媒体库已经保存了宿主机路径 /vol1/1000/Media,
|
||||
# 再把这里改成同一个宿主机真实路径用于旧路径换算。
|
||||
MMTL_MEDIA_DIR: /media
|
||||
MMTL_MEDIA_CONTAINER_DIR: /media
|
||||
MEBOX_MEDIA_DIR: /media
|
||||
MEBOX_MEDIA_CONTAINER_DIR: /media
|
||||
|
||||
# 默认推荐下载器保存路径使用 /downloads。
|
||||
# 如果下载器只能返回宿主机路径,再改成同一个宿主机真实路径。
|
||||
MMTL_DOWNLOAD_DIR: /downloads
|
||||
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||
MEBOX_DOWNLOAD_DIR: /downloads
|
||||
MEBOX_DOWNLOAD_CONTAINER_DIR: /downloads
|
||||
|
||||
# NAS 友好的低负载默认值。
|
||||
MMTL_TRANSCODER_ENABLED: "true"
|
||||
MMTL_TRANSCODER_HARDWARE_ACCEL: "false"
|
||||
MMTL_TRANSCODER_REALTIME: "true"
|
||||
MMTL_TRANSCODER_THREADS: "2"
|
||||
MMTL_TRANSCODER_MAX_CONCURRENT: "1"
|
||||
MMTL_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
||||
MEBOX_TRANSCODER_ENABLED: "true"
|
||||
MEBOX_TRANSCODER_HARDWARE_ACCEL: "false"
|
||||
MEBOX_TRANSCODER_REALTIME: "true"
|
||||
MEBOX_TRANSCODER_THREADS: "2"
|
||||
MEBOX_TRANSCODER_MAX_CONCURRENT: "1"
|
||||
MEBOX_TRANSCODER_IDLE_TIMEOUT_SECONDS: "120"
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "busybox wget -qO- http://127.0.0.1:8080/api/health || exit 1"]
|
||||
@@ -145,30 +141,30 @@ services:
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "10"
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
# 首次部署允许拉取;日常更新请只 pull mmtl。
|
||||
# 首次部署允许拉取;日常更新请只 pull mebox。
|
||||
# 如需升级 PostgreSQL,请先备份 ./postgres 后再手动调整镜像版本并拉取。
|
||||
pull_policy: missing
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: mmtl
|
||||
POSTGRES_USER: mmtl
|
||||
POSTGRES_PASSWORD: mmtl
|
||||
POSTGRES_DB: mebox
|
||||
POSTGRES_USER: mebox
|
||||
POSTGRES_PASSWORD: mebox
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
# PostgreSQL 主数据目录。升级/重建容器时必须保留。
|
||||
- ./postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mmtl -d mmtl"]
|
||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U mebox -d mebox"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "10"
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
run_uid="${PUID:-$(id -u mmtl 2>/dev/null || echo 1000)}"
|
||||
run_gid="${PGID:-$(id -g mmtl 2>/dev/null || echo 1000)}"
|
||||
run_uid="${PUID:-$(id -u mebox 2>/dev/null || echo 1000)}"
|
||||
run_gid="${PGID:-$(id -g mebox 2>/dev/null || echo 1000)}"
|
||||
|
||||
case "$run_uid" in
|
||||
''|*[!0-9]*)
|
||||
@@ -19,10 +19,10 @@ case "$run_gid" in
|
||||
esac
|
||||
|
||||
if [ "$run_uid" = "0" ]; then
|
||||
exec mmtl
|
||||
exec mebox
|
||||
fi
|
||||
|
||||
chown -R "$run_uid:$run_gid" /data /cache 2>/dev/null || true
|
||||
chown "$run_uid:$run_gid" /media 2>/dev/null || true
|
||||
|
||||
exec su-exec "$run_uid:$run_gid" mmtl
|
||||
exec su-exec "$run_uid:$run_gid" mebox
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
# 【开源推荐】MeBox:把 NAS / 网盘 / 远程 Emby 统一家里的观影入口,Docker 一键部署
|
||||
|
||||
> 配图已托管在 GitHub 仓库(`raw.githubusercontent.com` 直链),发帖时可直接引用,或下载 `docs/tutorial-screenshots/` 后作为附件上传。
|
||||
|
||||
---
|
||||
|
||||
## 写在前面
|
||||
|
||||
给论坛的朋友们推荐一个我维护的开源项目 —— **MeBox**,一个面向 NAS 与家庭影音场景的**自托管私人媒体中心**(GPL-3.0,Go + React)。
|
||||
|
||||
GitHub:https://github.com/truewhile/MeBox
|
||||
|
||||
一句话介绍:**部署一个服务,同时获得媒体库后台、网盘 STRM 整理、Emby 客户端协议网关三件套。** 内置完整 Emby/Jellyfin 服务端协议实现——手机、电视、平板上的 Infuse、SenPlayer、Fileball、Emby/Jellyfin 官方客户端直接「添加 Emby 服务器」就能连,一套账号体系全搞定,Emby 老用户零学习成本。
|
||||
|
||||
项目 fork 自 MediaStationGo 并持续二开,围绕网盘播放、任务队列、远程挂载和权限体系做了大量增强。
|
||||
|
||||
---
|
||||
|
||||
## 它能解决什么问题?
|
||||
|
||||
家里看电影电视的痛点,MeBox 基本一把梭:
|
||||
|
||||
| 痛点 | MeBox 的解法 |
|
||||
| --- | --- |
|
||||
| 硬盘散落各处,海报墙乱七八糟 | 多根目录媒体库 + TMDb/Bangumi/Douban 自动刮削,海报墙、继续观看、多季剧集一应俱全 |
|
||||
| 网盘资源看一部下一部太麻烦 | OpenList / CloudDrive2 / 115 / WebDAV 接入,STRM 同步 + 直链/302 播放,不占本地空间 |
|
||||
| 已经有一台 Emby,出门还得开 App | **远程 Emby 挂载**:把远程 Emby 的媒体库直接挂进 MeBox 界面统一浏览 |
|
||||
| 家人乱动设置、小孩看不该看的 | 多用户 + 有效期 + 成人内容开关 + 播放配置 PIN,细粒度权限 |
|
||||
| 每个设备装一套专属 App 太折腾 | **完整兼容 Emby/Jellyfin 客户端**:Infuse、SenPlayer、Fileball、官方客户端按「添加 Emby 服务器」填地址 + MeBox 账号即可,海报墙、观看进度、多用户直接同步 |
|
||||
|
||||
---
|
||||
|
||||
## 特点一览
|
||||
|
||||
**1. 现代化 Web UI,海报墙开箱即用**
|
||||
|
||||

|
||||
|
||||
深色系登录页,默认账号 `admin / admin123`(首次登录请立即改密)。
|
||||
|
||||

|
||||
|
||||
首页自带焦点推荐轮播 + 媒体库入口卡片,继续观看、最近添加直接呈现。
|
||||
|
||||
**2. 媒体库与刮削**
|
||||
|
||||

|
||||
|
||||
20 个媒体库、1600+ 条目一眼尽收:每库自带封面拼贴、条目数统计,支持「全库修复+重刮」「刮削队列」批量处理。
|
||||
|
||||

|
||||
|
||||
库内海报墙带评分、集数角标,支持按最后集添加日期排序,点开即看。
|
||||
|
||||
**3. 详情页与多季管理**
|
||||
|
||||

|
||||
|
||||
剧情简介、类型标签、多季分集(特别篇/第 1-N 季)、每集缩略图与时长;一键立即播放、调用外部播放器、加入收藏。
|
||||
|
||||
**4. Emby/Jellyfin 客户端无缝兼容**
|
||||
|
||||
这是我最想强调的一点:**MeBox 内置了完整的 Emby 服务端协议实现**。手机、电视、平板上的 Infuse、SenPlayer、Fileball,甚至 Emby/Jellyfin 官方客户端,都不需要任何插件或改造——按「添加 Emby 服务器」填入 `http://服务器IP:18080`,用 MeBox 账号登录,海报墙、观看进度、收藏、多用户权限全部无缝衔接。已经习惯 Emby 生态的朋友可以零成本迁移,家人只用电视端 App 也完全无感。
|
||||
|
||||
**5. 网页播放器 + 弹幕自动匹配**
|
||||
|
||||

|
||||
|
||||
内置网页播放器支持 HLS 转码、字幕、播放配置档;**弹幕按剧名自动匹配全季分集**(截图中自动匹配到《一拳超人》39 集),屏幕占比/透明度/字号随意调,追新番体验直接拉满。
|
||||
|
||||
**6. 网盘 STRM:网盘当本地盘用**
|
||||
|
||||

|
||||
|
||||
添加网盘账号(**115 支持二维码扫码登录**)→ 添加同步目录 → 系统把网盘/本地目录里的视频生成 `.strm` 文件,元数据经下载/上传队列双向同步,播放走直链/302 不落盘。
|
||||
|
||||
**7. 远程 Emby 挂载(特色功能)**
|
||||
|
||||

|
||||
|
||||
已有远程 Emby 服务器?填一次账号,按需勾选要挂载的媒体库(支持同服务器多线路自动切换、直连开关、排序),远程库直接出现在 MeBox 首页,不必再开 Emby 客户端。
|
||||
|
||||
**8. 任务队列统一管理**
|
||||
|
||||

|
||||
|
||||
刮削 / 下载 / 上传三类任务统一看板,排队中、进行中、已匹配、失败分类计数,支持搜索与批量清理。
|
||||
|
||||
**9. 下载与自动整理**
|
||||
|
||||

|
||||
|
||||
配合任意下载器(qBittorrent、Transmission 等下载到本地目录即可),MeBox 定时自动整理入媒体库:智能分类子库、自动注册目的地媒体库、复制/移动/硬链/软链多种整理方式,命名规则可配。
|
||||
|
||||
**10. 多用户与权限**
|
||||
|
||||

|
||||
|
||||
管理员/普通用户分级、单实例用户数上限、账号有效期、成人内容开关、播放配置 PIN——给家人开号放心给。
|
||||
|
||||
**11. 运维省心**
|
||||
|
||||

|
||||
|
||||
FFmpeg/FFprobe 一键下载安装、转码与硬件加速开关、TMDb 语言、识别词、弹幕、Adult/NSFW 开关全在设置页分组管理;另有 DLNA 投屏、存储统计、海报墙聚合视图:
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 使用教程:从零到海报墙只要 5 步
|
||||
|
||||
### 第 1 步:Docker 一键部署
|
||||
|
||||
推荐 Docker Compose(仓库提供 4 份互相独立的完整模板,无需 `.env`):
|
||||
|
||||
```bash
|
||||
mkdir -p MeBox && cd MeBox
|
||||
|
||||
# 最省心:单镜像 + 内置 SQLite
|
||||
curl -fsSL https://raw.githubusercontent.com/truewhile/MeBox/main/docker-compose.simple.yml -o docker-compose.yml
|
||||
|
||||
# 多用户/大数据量可选 PostgreSQL 档、Redis 档、OpenSearch 档,见仓库 README「部署档位」
|
||||
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
浏览器访问 `http://服务器IP:18080`,镜像:`ghcr.io/truewhile/mebox:latest`(amd64 / arm64 都有,也提供 Windows/Linux/macOS 单文件可执行程序,不想装 Docker 直接下载跑)。
|
||||
|
||||
### 第 2 步:登录并修改密码
|
||||
|
||||
默认账号 `admin / admin123`,登录后右上角头像 → 个人资料修改密码。
|
||||
|
||||
### 第 3 步:创建媒体库 + 扫库
|
||||
|
||||
后台 → 媒体库 → 管理媒体库,添加本地路径(Docker 部署记得填**容器内**路径,如 `/media/电影`,`volumes` 左侧挂宿主机真实目录)→ 执行扫库。
|
||||
|
||||
### 第 4 步:配置元数据刮削
|
||||
|
||||
系统设置 → 外部 API,填入 TMDb / Bangumi / Douban 等 API Key;媒体库页可对单个库「全库修复+重刮」,刮削进度在任务队列实时可见。
|
||||
|
||||
### 第 5 步(可选但强烈推荐):
|
||||
|
||||
- **网盘用户**:STRM 管理 → 添加网盘账号(115 可扫码)→ 添加同步目录 → 生成 STRM 后直链播放;
|
||||
- **已有 Emby**:Emby 挂载 → 添加 Emby 账号 → 勾选要挂载的媒体库;
|
||||
- **第三方播放器(Emby 客户端全兼容)**:Infuse / SenPlayer / Fileball / Emby、Jellyfin 官方客户端,按「添加 Emby 服务器」填 `http://服务器IP:18080`,用 MeBox 账号登录即可,原有使用习惯完全不变;
|
||||
- **下载党**:qBittorrent 等任意下载器把视频下到下载目录,在文件管理里把它设为整理源,下完自动分类入库。
|
||||
|
||||
### 路径映射小抄(Docker 最常见坑)
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- /vol1/1000/Media:/media # 左:宿主机真实路径;右:容器内路径(网页里填这个)
|
||||
environment:
|
||||
MEBOX_MEDIA_DIR: /vol1/1000/Media
|
||||
MEBOX_MEDIA_CONTAINER_DIR: /media
|
||||
```
|
||||
|
||||
硬链接要求同一文件系统/子卷,跨盘请改复制或软链。
|
||||
|
||||
---
|
||||
|
||||
## 部署档位怎么选?
|
||||
|
||||
| 档位 | 文件 | 组件 | 适合 |
|
||||
| --- | --- | --- | --- |
|
||||
| 极简 | `docker-compose.simple.yml` | 单镜像 + SQLite | 个人使用、低配设备 |
|
||||
| 标准 | `docker-compose.yml` | + PostgreSQL | 多用户家庭共享 |
|
||||
| 增强 | `docker-compose.standard.yml` | + Redis | 大媒体库高频访问 |
|
||||
| 搜索 | `docker-compose.search.yml` | + OpenSearch | 超大库全文搜索 |
|
||||
|
||||
---
|
||||
|
||||
## 技术栈与致谢
|
||||
|
||||
- 后端:Go · Gin · GORM · SQLite/PostgreSQL · 可选 Redis / OpenSearch
|
||||
- 前端:React 18 · Vite · TypeScript · Tailwind CSS · Zustand
|
||||
- 部署:Docker Compose 多档模板,amd64/arm64 镜像 + 单文件可执行
|
||||
|
||||
感谢上游 [MediaStationGo](https://github.com/ShukeBta/MediaStationGo) 的奠基,网盘同步/STRM/整理部分参考了 [qmediasync](https://github.com/qicfan/qmediasync) 的思路。
|
||||
|
||||
---
|
||||
|
||||
## 链接
|
||||
|
||||
- GitHub:https://github.com/truewhile/MeBox
|
||||
- Issue / PR:欢迎提 bug(附部署方式+复现步骤+日志)与功能建议
|
||||
- License:GPL-3.0
|
||||
|
||||
觉得有用的话求个 Star ⭐,也欢迎论坛里的朋友反馈使用体验,我长期维护。
|
||||
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 620 KiB |
|
After Width: | Height: | Size: 743 KiB |
|
After Width: | Height: | Size: 793 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 612 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 376 KiB |
|
After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -1,58 +1,77 @@
|
||||
module github.com/ShukeBta/MMTL
|
||||
module github.com/truewhile/MeBox
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.12.2
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.5.2
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/gin-contrib/gzip v1.2.6
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/metatube-community/metatube-sdk-go v1.4.0
|
||||
github.com/redis/go-redis/v9 v9.7.3
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/sys v0.20.0
|
||||
gorm.io/driver/postgres v1.5.7
|
||||
gorm.io/gorm v1.30.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/image v0.45.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/time v0.15.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/disintegration/imaging v1.6.3-0.20201218193011-d40f48ce0f09 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/esimov/pigo v1.4.7-0.20240801095032-7465ed14de47 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
@@ -65,19 +84,18 @@ require (
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
modernc.org/libc v1.70.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.47.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,35 +1,47 @@
|
||||
fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA=
|
||||
fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.5.2 h1:40yUSXwdkWN851BHCq6uiDhleh7A4+0yIBS+IUAqZVY=
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.5.2/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/disintegration/imaging v1.6.3-0.20201218193011-d40f48ce0f09 h1:MJFqtdxTq94XqUgg7DcGCaOIXrDTJE/tPHK66Jshguc=
|
||||
github.com/disintegration/imaging v1.6.3-0.20201218193011-d40f48ce0f09/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/esimov/pigo v1.4.7-0.20240801095032-7465ed14de47 h1:48iGRx9HamDuG4pCbPG5IXt4bKHhgn33KGynzHUgeIA=
|
||||
github.com/esimov/pigo v1.4.7-0.20240801095032-7465ed14de47/go.mod h1:bIH3W2QoHvchgAPl6DUwIUguIHJwvMS6sePQIvhxPTc=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/gzip v1.2.6 h1:OtN8DplD5DNZCSLAnQ5HxRkD2qZ5VU+JhOrcfJrcRvg=
|
||||
github.com/gin-contrib/gzip v1.2.6/go.mod h1:BQy8/+JApnRjAVUplSGZiVtD2k8GmIE2e9rYu/hLzzU=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
@@ -40,53 +52,61 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
||||
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/metatube-community/metatube-sdk-go v1.4.0 h1:NBbjxU5SuzgMABM58/f/tmbuTg+hn+Nv1PnlF+gihGI=
|
||||
github.com/metatube-community/metatube-sdk-go v1.4.0/go.mod h1:FRgPioGhlkXgD6RmOKQDWITwxlsl0K9cXONFz06jymw=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -94,20 +114,25 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
|
||||
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
@@ -131,15 +156,15 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
@@ -148,41 +173,54 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
|
||||
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
|
||||
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -191,16 +229,35 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
|
||||
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
||||
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
|
||||
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||