Compare commits

..

29 Commits

Author SHA1 Message Date
truewhile ef8b017c63 Merge pull request #19 from truewhile/main
拉取最新代码
2026-09-02 17:13:21 +08:00
github-actions[bot] e923c9d7a1 chore: bump version to 0.0.81 [skip ci] 2026-09-02 09:10:18 +00:00
truewhile bbb512760a feat: make user limit configurable from admin user management (#18)
Store the per-instance user cap in settings (default 20) and expose
GET/PUT /admin/users/limit endpoints. The user management page now lets
admins view and update the limit without touching system settings.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 17:10:07 +08:00
github-actions[bot] da99182222 chore: bump version to 0.0.80 [skip ci] 2026-09-02 08:55:31 +00:00
truewhile c43d1ca637 优化部署配置 2026-09-02 16:55:16 +08:00
github-actions[bot] b51eee3d94 chore: bump version to 0.0.79 [skip ci] 2026-09-02 08:26:41 +00:00
truewhile b0fe40142a Rebrand MMTL to MeBox (name, logo, Docker image) (#17)
* Rebrand MMTL to MeBox across codebase and assets

Rename the project display name, Go module path, environment variable
prefix (MEBOX_*), Docker image references, and UI branding from MMTL/mmtl
to MeBox/mebox. Replace logo assets with the new MeBox icon and keep
legacy SQLite migration support for existing mmtl.db deployments.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix logo icons: use cube-only crop without truncated text

Previous icon generation cropped too much of the source image, including
partial MeBox wordmark text that was cut off in square icon containers.
Regenerate logo-64/192/512, favicon, and SVG from cube-only region.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 16:26:28 +08:00
github-actions[bot] a50ceaf1a7 chore: bump version to 0.0.78 [skip ci] 2026-09-02 07:51:44 +00:00
truewhile bf49323789 feat(strm): auto-append remote directory tail to local output path (#16)
When adding or editing a cloud sync path, append the last segment of the
remote directory to the local output directory (e.g. /home/ubuntu/strm + 电影).
Remote changes replace the previous auto-appended tail; local browse and blur
commit the base path with the current remote tail.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 15:51:31 +08:00
github-actions[bot] 47118c8a53 chore: bump version to 0.0.77 [skip ci] 2026-09-02 07:16:02 +00:00
truewhile 67b85840dc perf(strm): 优化 OpenList 元数据下载速度 (#15)
* Add favourite buttons to library list and series detail pages

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix favourites list for remote Emby mounted media

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Route favourites to library series detail when appropriate

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix favourites link to library series for remote anime paths

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix remote Emby playback history and continue watching

- Preserve duration_ms when web player reports duration=0
- Add GET /playback/:id/resume for reliable per-item resume
- Switch home continue watching to /watch-history/continue API
- Set library_id on remote media for play-profile visibility
- Keep continue-watching rows when remote metadata hydration fails

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix mobile sort dropdown overflowing off-screen

Align dropdown to button left edge on small screens and cap width
to viewport so sort options stay fully visible on mobile.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Remove stale Windows binary backup files

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix STRM account edit form not echoing saved config

Return config_preview from account API for server/url/username and
secret flags. Merge partial config updates so empty password/token
fields do not wipe stored credentials.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* perf(strm): speed up OpenList metadata downloads with provider-aware concurrency

- Split download semaphore: 115 stays capped at 3 for WAF safety; OpenList/CloudDrive2
  WebDAV metadata uses strm.download_threads (default raised to 6, max 16)
- Scope 115 WAF cooldown to 115 tasks only so OpenList downloads are not paused
- Requeue claimed tasks back to pending when slot acquisition fails or 115 is cooling
- Add tests for separate semaphores and requeue behavior

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 15:15:49 +08:00
github-actions[bot] fbc33862fb chore: bump version to 0.0.76 [skip ci] 2026-09-02 06:14:43 +00:00
truewhile 04fc50a4b0 Add library favourite buttons and fix remote favourites list (#14)
* Add favourite buttons to library list and series detail pages

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix favourites list for remote Emby mounted media

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Route favourites to library series detail when appropriate

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix favourites link to library series for remote anime paths

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 14:14:32 +08:00
github-actions[bot] dab9a1073a chore: bump version to 0.0.75 [skip ci] 2026-09-02 05:48:38 +00:00
truewhile bd3d4ad357 Add favourite buttons to library list and series detail pages (#13)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 13:48:25 +08:00
github-actions[bot] bd5bfdcc10 chore: bump version to 0.0.74 [skip ci] 2026-09-02 03:43:54 +00:00
truewhile 73dc37fef8 Remove obsolete file manager organize options (#12)
Remove UI options for features that no longer exist:
- qB download auto-organize
- downloader smart classification
- seeding (keep seeding)

Also remove related warning banners and simplify move transfer labels.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 11:43:39 +08:00
github-actions[bot] f1b7d99a33 chore: bump version to 0.0.73 [skip ci] 2026-09-02 03:35:57 +00:00
truewhile 638dae0c91 fix(layout): hide duplicate media nav in mobile drawer (#11)
On narrow screens the media drawer duplicated bottom navigation and
user menu links (home, libraries, favourites, playlists, history).
Show only management links in the drawer when browsing media.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 11:35:44 +08:00
truewhile 791722aec6 feat(libraries): add pin-to-top for media library list (#10)
Allow users to pin frequently used libraries to the top of the
Libraries page. Pinned order is stored in localStorage and applied
to both entry cards and preview shelves.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 11:35:20 +08:00
github-actions[bot] 1c1da494b7 chore: bump version to 0.0.72 [skip ci] 2026-09-02 03:20:07 +00:00
truewhile 97b3a7bd7f feat(emby): support multiple connection lines for remote mount (#9)
Add account-level multi-line configuration for emby_remote mounts.
Lines are stored in strm_accounts.config as a urls JSON array with
backward compatibility for the legacy single url field. The backend
automatically fails over to the next line on connection errors and
persists the working active_line index.

Frontend: multi-line editor on Emby mount page and STRM account dialog
with add/remove/reorder, line names, and URL validation.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 11:19:56 +08:00
github-actions[bot] 543615a2a8 chore: bump version to 0.0.71 [skip ci] 2026-09-02 03:02:51 +00:00
truewhile 2b7828e1fe fix: close user menu on navigation and outside click (#8)
Portal the profile dropdown to document.body with a full-screen backdrop
so clicks outside reliably dismiss it above transformed page layers. Also
close the menu whenever the route changes.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 11:02:41 +08:00
github-actions[bot] 0b1fe52e24 chore: bump version to 0.0.70 [skip ci] 2026-09-02 02:49:11 +00:00
truewhile 2fc2c17b05 fix: prevent mobile sidebar nav items from stacking on short screens (#7)
Narrow drawers rendered media links in a flex-1 overflow region above a
non-shrinking admin block, so the browse items were clipped into a
compressed active pill. Scroll media and admin together, and distinguish
/libraries from /libraries?from=admin when highlighting.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 10:48:58 +08:00
github-actions[bot] be96acb035 chore: bump version to 0.0.69 [skip ci] 2026-09-02 02:44:26 +00:00
truewhile 6a72e7ab82 Merge pull request #6 from truewhile/cursor/frontend-nav-mobile-af8f
Improve mobile navigation and fix playlists page routing
2026-09-02 10:44:10 +08:00
Cursor Agent 4ebd477ca2 Fix playlists route matching /play and improve playlists page UI
- Add isPlayerRoute() so /playlists no longer hides header and bottom nav
- Add mobile back navigation for playlists, favourites, and history
- Redesign PlaylistsPage with PageHeader, empty state, and card list

Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 02:30:14 +00:00
478 changed files with 3602 additions and 1603 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ verify-downloads/
.tmp_*
.tmp-deploy-*
.tmp-deploy-data/
.mmtl.pid
.mebox.pid
*.db
*.db-journal
*.db-shm
+2 -2
View File
@@ -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: 适合快速交流部署经验、使用问题和排查线索。
+1 -1
View File
@@ -12,7 +12,7 @@ assignees: ""
## 期望方案
请描述你希望 MMTL 如何工作。
请描述你希望 MeBox 如何工作。
## 使用场景
+1 -1
View File
@@ -1,4 +1,4 @@
name: MMTL CodeQL
name: MeBox CodeQL
paths-ignore:
- internal/service/fileid_other.go
+12 -12
View File
@@ -83,8 +83,8 @@ jobs:
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
echo "tag=MeBox-v${NEW_VERSION}" >> $GITHUB_OUTPUT
echo "tag=mebox-v${NEW_VERSION}" >> $GITHUB_OUTPUT
# 3. 更新 VERSION 文件
- name: Update version file
@@ -129,7 +129,7 @@ jobs:
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 }}
@@ -192,7 +192,7 @@ jobs:
set -eux
# tag 已由 version-and-publish 推送;若 release 已存在则忽略(--verify-tag 幂等)
gh release create "$RELEASE_TAG" \
--title "MMTL ${{ needs.version-and-publish.outputs.new_version }}" \
--title "MeBox ${{ needs.version-and-publish.outputs.new_version }}" \
--notes "自动化发布 ${{ needs.version-and-publish.outputs.new_version }}" \
--verify-tag --latest || true
@@ -238,16 +238,16 @@ jobs:
run: |
CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \
go build -trimpath -ldflags="-s -w -X main.version=${{ needs.version-and-publish.outputs.tag }}" \
-o "dist/mmtl-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}" ./cmd/server
-o "dist/mebox-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}" ./cmd/server
- name: Package
run: |
mkdir -p package/mmtl
cp "dist/mmtl-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}" package/mmtl/mmtl${{ matrix.ext }}
cp README.md package/mmtl/ 2>/dev/null || true
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 "../mmtl_${{ matrix.goos }}_${{ matrix.goarch }}.zip" mmtl)
(cd package && zip -r "../mebox_${{ matrix.goos }}_${{ matrix.goarch }}.zip" mebox)
else
tar -czf "mmtl_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz" -C package mmtl
tar -czf "mebox_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz" -C package mebox
fi
- name: Upload to GitHub Release
env:
@@ -255,8 +255,8 @@ jobs:
RELEASE_TAG: ${{ needs.version-and-publish.outputs.tag }}
run: |
set -eux
PKG="mmtl_${{ matrix.goos }}_${{ matrix.goarch }}.zip"
TAR="mmtl_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz"
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
+7 -7
View File
@@ -4,7 +4,7 @@
# 产出:
# 1. 前端 + 后端编译验证(go vet / go test / go build)
# 2. 多平台可执行二进制 artifact(linux/amd64、linux/arm64、windows/amd64)
# 3. ghcr.io/{owner}/mmtl:beta 多架构 Docker 镜像(linux/amd64 + linux/arm64)
# 3. ghcr.io/{owner}/mebox:beta 多架构 Docker 镜像(linux/amd64 + linux/arm64)
#
# 与 main 分支的发布流(Auto-docker-publish.yml)隔离:beta 不做版本递增、
# 不打 release tag,只构建带 -beta 标识的产物供测试。
@@ -73,21 +73,21 @@ jobs:
# 多平台可执行文件(嵌入刚构建的 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/mmtl-beta-linux-amd64 ./cmd/server
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/mmtl-beta-linux-arm64 ./cmd/server
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 -X main.version=${{ steps.version.outputs.full_version }}" -o dist/mmtl-beta-windows-amd64.exe ./cmd/server
run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w -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: mmtl-beta-binaries
name: mebox-beta-binaries
path: dist/*
if-no-files-found: error
# ─────────────────────────────────────────────────────────────────────────────
# 2) Beta Docker 镜像(ghcr.io/{owner}/mmtl:beta)
# 2) Beta Docker 镜像(ghcr.io/{owner}/mebox:beta)
# ─────────────────────────────────────────────────────────────────────────────
docker-beta:
name: Build & push beta Docker image
@@ -123,7 +123,7 @@ jobs:
push: true
provenance: false
sbom: false
tags: ghcr.io/${{ github.repository_owner }}/mmtl:beta
tags: ghcr.io/${{ github.repository_owner }}/mebox:beta
labels: |
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.source=${{ github.repository }}
+2 -2
View File
@@ -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 }}
+1 -1
View File
@@ -32,7 +32,7 @@ logs/
.tmp-deploy-cache/
.tmp-deploy-server.*
.tmp-live-server.*
.mmtl.pid
.mebox.pid
*.log
*.db
*.db-journal
+5 -5
View File
@@ -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;"
```
请勿公开粘贴以下敏感信息:
+12 -12
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.6
# =============================================================================
# Multi-architecture build for MMTL.
# Multi-architecture build for MeBox.
#
# Stage 1 (frontend) : Node 20 -> static SPA bundle
# Stage 2 (backend) : Go 1.25 -> single static binary (CGO_ENABLED=0)
@@ -8,7 +8,7 @@
#
# 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 ...
@@ -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
+32 -32
View File
@@ -1,7 +1,7 @@
# 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>
@@ -31,9 +31,9 @@
## 项目简介
**MMTL** 是一个自托管私人媒体管理系统,适合 NAS、小主机、家庭共享和多端播放场景。本项目由 [MediaStationGo](https://github.com/ShukeBta/MediaStationGo) fork 并持续二开维护,在保留「一套服务覆盖网页、手机、电视与第三方播放器」思路的同时,围绕网盘播放、任务队列、远程挂载和权限体系做了大量增强。
**MeBox** 是一个自托管私人媒体管理系统,适合 NAS、小主机、家庭共享和多端播放场景。本项目由 [MediaStationGo](https://github.com/ShukeBta/MediaStationGo) fork 并持续二开维护,在保留「一套服务覆盖网页、手机、电视与第三方播放器」思路的同时,围绕网盘播放、任务队列、远程挂载和权限体系做了大量增强。
你可以把 MMTL 理解为:
你可以把 MeBox 理解为:
- 一个带现代 Web UI 的**媒体库后台**
- 一个兼容 Emby/Jellyfin 客户端的**协议网关**
@@ -46,7 +46,7 @@
| **媒体库** | 电影、电视剧、动漫、综艺、音乐与自定义库;多根目录、扫库、海报墙、继续观看 |
| **元数据刮削** | TMDb、Bangumi、Douban、TheTVDB、Fanart 等;支持 NFO、手动匹配、刮削队列 |
| **播放** | 网页播放器、HLS 转码、弹幕、字幕、播放配置档、观看历史与收藏 |
| **Emby 协议** | Infuse、SenPlayer、Fileball 等客户端可直接添加本服务,使用 MMTL 账号登录 |
| **Emby 协议** | Infuse、SenPlayer、Fileball 等客户端可直接添加本服务,使用 MeBox 账号登录 |
| **远程 Emby 挂载** | 将远程 Emby 媒体库挂载到本地界面统一浏览(无需单独开 Emby 客户端) |
| **网盘与 STRM** | OpenList、CloudDrive2、115、WebDAV 等;STRM 同步、上传/下载队列、直链/302 播放 |
| **下载与整理** | qBittorrent 接入、站点搜索与订阅、下载后自动整理、文件管理器(复制/移动/硬链/软链) |
@@ -66,13 +66,13 @@
推荐使用 Docker Compose。仓库提供四份**互相独立**的完整模板,无需 `.env` 即可起步。
```bash
mkdir -p MMTL && cd MMTL
mkdir -p MeBox && cd MeBox
# 最省心:单镜像 + 内置 SQLite
curl -fsSL https://raw.githubusercontent.com/truewhile/MMTL/main/docker-compose.simple.yml -o docker-compose.yml
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/MMTL/main/docker-compose.yml -o docker-compose.yml
# curl -fsSL https://raw.githubusercontent.com/truewhile/MeBox/main/docker-compose.yml -o docker-compose.yml
docker compose up -d
```
@@ -88,7 +88,7 @@ http://服务器IP:18080
镜像地址:
```text
ghcr.io/truewhile/mmtl:latest
ghcr.io/truewhile/mebox:latest
```
---
@@ -99,16 +99,16 @@ ghcr.io/truewhile/mmtl:latest
| 档位 | 配置文件 | 组件 | 适合场景 |
| --- | --- | --- | --- |
| 单镜像档 | `docker-compose.simple.yml` | MMTL + SQLite | 新手、单人、低配 NAS,只想一个容器跑起来 |
| 第一档 | `docker-compose.yml` | MMTL + PostgreSQL | 大多数家庭 NAS,多用户更稳 |
| 单镜像档 | `docker-compose.simple.yml` | MeBox + SQLite | 新手、单人、低配 NAS,只想一个容器跑起来 |
| 第一档 | `docker-compose.yml` | MeBox + PostgreSQL | 大多数家庭 NAS,多用户更稳 |
| 第二档 | `docker-compose.standard.yml` | + Redis | 多用户、Emby 客户端频繁刷新、首页/列表访问多 |
| 第三档 | `docker-compose.search.yml` | + OpenSearch | 超大媒体库、复杂全文搜索(内存占用更高) |
### 单镜像档要点
- 只启动 **一个** MMTL 容器,数据在 `./data/mmtl.db`
- 只启动 **一个** MeBox 容器,数据在 `./data/mebox.db`
- 通常只需改端口与媒体目录挂载
- **不要**设置 `MMTL_DATABASE_DSN`,否则会切到 PostgreSQL
- **不要**设置 `MEBOX_DATABASE_DSN`,否则会切到 PostgreSQL
```yaml
ports:
@@ -124,11 +124,11 @@ volumes:
### PostgreSQL 档位要点
- 主库在 `./postgres`,配置与密钥在 `./data`
- 若存在旧版 `./data/mmtl.db`,首次启动会自动迁移到 PostgreSQL
- 迁移完成后可将 `MMTL_DATABASE_DB_PATH` 改为不存在路径,避免重复检查:
- 若存在旧版 `./data/mebox.db`,首次启动会自动迁移到 PostgreSQL
- 迁移完成后可将 `MEBOX_DATABASE_DB_PATH` 改为不存在路径,避免重复检查:
```yaml
MMTL_DATABASE_DB_PATH: /data/no-sqlite-migration.db
MEBOX_DATABASE_DB_PATH: /data/no-sqlite-migration.db
```
### 必须备份与可重建
@@ -144,11 +144,11 @@ MMTL_DATABASE_DB_PATH: /data/no-sqlite-migration.db
### 更新镜像
```bash
docker compose pull mmtl
docker compose up -d --no-deps mmtl
docker compose pull mebox
docker compose up -d --no-deps mebox
```
日常更新只拉 `mmtl` 服务即可,不要随意 `docker compose pull` 升级 PostgreSQL/Redis/OpenSearch 基础镜像。
日常更新只拉 `mebox` 服务即可,不要随意 `docker compose pull` 升级 PostgreSQL/Redis/OpenSearch 基础镜像。
---
@@ -158,7 +158,7 @@ Docker 部署最常见的问题是路径填错。记住:
- `volumes` **左侧**是宿主机真实路径,**右侧**是容器内路径
- 网页后台添加媒体库时,应填写**容器内**路径(如 `/media/电影`)
- 若使用自动整理/下载入库,`MMTL_MEDIA_DIR` 与 `MMTL_DOWNLOAD_DIR` 需与挂载一致
- 若使用自动整理/下载入库,`MEBOX_MEDIA_DIR` 与 `MEBOX_DOWNLOAD_DIR` 需与挂载一致
NAS 示例:
@@ -167,10 +167,10 @@ volumes:
- /vol1/1000/Media:/media
- /vol1/1000/Downloads:/downloads
environment:
MMTL_MEDIA_DIR: /vol1/1000/Media
MMTL_MEDIA_CONTAINER_DIR: /media
MMTL_DOWNLOAD_DIR: /vol1/1000/Downloads
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
MEBOX_MEDIA_DIR: /vol1/1000/Media
MEBOX_MEDIA_CONTAINER_DIR: /media
MEBOX_DOWNLOAD_DIR: /vol1/1000/Downloads
MEBOX_DOWNLOAD_CONTAINER_DIR: /downloads
```
---
@@ -181,7 +181,7 @@ environment:
2. **配置元数据源** → 系统设置中添加 TMDb、Bangumi 等 API
3. **(可选)连接 qBittorrent** → 下载客户端设置,宿主机可用 `http://host.docker.internal:8085`
4. **(可选)配置网盘账号** → STRM 管理中添加 OpenList / 115 / WebDAV 等
5. **第三方播放器** → 以 Emby 服务器添加 `http://服务器IP:18080`,使用 MMTL 账号登录
5. **第三方播放器** → 以 Emby 服务器添加 `http://服务器IP:18080`,使用 MeBox 账号登录
---
@@ -191,13 +191,13 @@ environment:
先确认路径映射与数据库档位。网盘扫描还受接口限速与目录规模影响;大库可考虑第二档 Redis 或第三档 OpenSearch。
**qBittorrent 下载后无法整理?**
确认下载目录已通过 `volumes` 挂进容器,且 `MMTL_DOWNLOAD_*` 环境变量对应正确。
确认下载目录已通过 `volumes` 挂进容器,且 `MEBOX_DOWNLOAD_*` 环境变量对应正确。
**硬链接失败(cross-device link)?**
硬链接要求源与目标在同一文件系统/子卷;跨盘、跨 btrfs 子卷或网盘挂载时请改用复制或软链接。
**第三方播放器连不上?**
确认地址为 `http://IP:18080`,使用 MMTL 用户账号;反代部署需正确配置外部 URL 与 HTTPS 头。
确认地址为 `http://IP:18080`,使用 MeBox 用户账号;反代部署需正确配置外部 URL 与 HTTPS 头。
---
@@ -220,7 +220,7 @@ CI 会在 Release 中提供 Windows / Linux / macOS 的 amd64、arm64 单文件
## 鸣谢
MMTL 在 [MediaStationGo](https://github.com/ShukeBta/MediaStationGo) 的基础上 fork 并持续演进。感谢上游项目在媒体库架构、Emby 协议兼容和自托管体验上的奠基工作。
MeBox 在 [MediaStationGo](https://github.com/ShukeBta/MediaStationGo) 的基础上 fork 并持续演进。感谢上游项目在媒体库架构、Emby 协议兼容和自托管体验上的奠基工作。
项目中许多网盘同步、STRM 与媒体整理相关的设计与实现,也参考了 [qmediasync](https://github.com/qicfan/qmediasync)。感谢该项目的思路与实践经验。
@@ -238,11 +238,11 @@ MMTL 在 [MediaStationGo](https://github.com/ShukeBta/MediaStationGo) 的基础
## Star History
<a href="https://www.star-history.com/?repos=truewhile%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=truewhile/MMTL&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=truewhile/MMTL&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=truewhile/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>
+30 -30
View File
@@ -1,7 +1,7 @@
# 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 self-hosted media center for NAS and home theater</h3>
@@ -30,9 +30,9 @@
## Overview
**MMTL** 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.
**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.
In practice, MMTL gives you:
In practice, MeBox gives you:
- A modern **web media library**
- An **Emby/Jellyfin-compatible protocol gateway** for third-party players
@@ -45,8 +45,8 @@ In practice, MMTL gives you:
| **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 protocol** | Add MMTL in Infuse, SenPlayer, Fileball, etc. and sign in with MMTL accounts |
| **Remote Emby mounts** | Browse remote Emby libraries inside MMTL without a separate Emby client |
| **Emby protocol** | Add MeBox in Infuse, SenPlayer, Fileball, etc. and sign in with MeBox accounts |
| **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** | qBittorrent, site search/subscriptions, post-download organization, file manager |
| **Users & permissions** | Admin/regular users, expiry, NSFW toggle, play-profile PIN, granular permissions |
@@ -65,13 +65,13 @@ In practice, MMTL gives you:
Docker Compose is the recommended path. The repo ships four **standalone** templates; no `.env` is required.
```bash
mkdir -p MMTL && cd MMTL
mkdir -p MeBox && cd MeBox
# Simplest: one container with built-in SQLite
curl -fsSL https://raw.githubusercontent.com/truewhile/MMTL/main/docker-compose.simple.yml -o docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/truewhile/MeBox/main/docker-compose.simple.yml -o docker-compose.yml
# Or PostgreSQL tier for multi-user setups
# curl -fsSL https://raw.githubusercontent.com/truewhile/MMTL/main/docker-compose.yml -o docker-compose.yml
# curl -fsSL https://raw.githubusercontent.com/truewhile/MeBox/main/docker-compose.yml -o docker-compose.yml
docker compose up -d
```
@@ -87,7 +87,7 @@ Default login: `admin` / `admin123` — change the password immediately.
Image:
```text
ghcr.io/truewhile/mmtl:latest
ghcr.io/truewhile/mebox:latest
```
---
@@ -98,22 +98,22 @@ Pick one compose file. Do **not** stack multiple `-f` files.
| Tier | File | Stack | Best for |
| --- | --- | --- | --- |
| Single image | `docker-compose.simple.yml` | MMTL + SQLite | Beginners, single-user, low-resource NAS |
| Tier 1 | `docker-compose.yml` | MMTL + PostgreSQL | Most home NAS deployments |
| 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 |
### Single-image notes
- Only one MMTL container; database lives in `./data/mmtl.db`
- Do **not** set `MMTL_DATABASE_DSN` or it switches to PostgreSQL
- 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
### PostgreSQL notes
- Primary DB: `./postgres`; secrets and runtime files: `./data`
- Existing `./data/mmtl.db` migrates automatically on first start
- After migration, point `MMTL_DATABASE_DB_PATH` at a non-existent file to disable re-checks
- 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
### Backup
@@ -126,8 +126,8 @@ Pick one compose file. Do **not** stack multiple `-f` files.
### Update
```bash
docker compose pull mmtl
docker compose up -d --no-deps mmtl
docker compose pull mebox
docker compose up -d --no-deps mebox
```
---
@@ -138,7 +138,7 @@ 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 `MMTL_MEDIA_DIR` / `MMTL_DOWNLOAD_DIR` aligned with mounts when organizing or ingesting downloads
- Keep `MEBOX_MEDIA_DIR` / `MEBOX_DOWNLOAD_DIR` aligned with mounts when organizing or ingesting downloads
Example:
@@ -147,10 +147,10 @@ volumes:
- /vol1/1000/Media:/media
- /vol1/1000/Downloads:/downloads
environment:
MMTL_MEDIA_DIR: /vol1/1000/Media
MMTL_MEDIA_CONTAINER_DIR: /media
MMTL_DOWNLOAD_DIR: /vol1/1000/Downloads
MMTL_DOWNLOAD_CONTAINER_DIR: /downloads
MEBOX_MEDIA_DIR: /vol1/1000/Media
MEBOX_MEDIA_CONTAINER_DIR: /media
MEBOX_DOWNLOAD_DIR: /vol1/1000/Downloads
MEBOX_DOWNLOAD_CONTAINER_DIR: /downloads
```
---
@@ -161,7 +161,7 @@ environment:
2. Add metadata providers (TMDb, Bangumi, etc.) in system settings
3. Optionally connect qBittorrent (`http://host.docker.internal:8085` when qB runs on the host)
4. Optionally configure cloud accounts under STRM management
5. Add the server in Emby-compatible players at `http://SERVER_IP:18080` using MMTL credentials
5. Add the server in Emby-compatible players at `http://SERVER_IP:18080` using MeBox credentials
---
@@ -177,7 +177,7 @@ Ensure the download directory is mounted into the container and env vars match.
Hardlinks require the same filesystem/subvolume; use copy or symlink across disks or cloud mounts.
**External player cannot connect**
Use `http://IP:18080` and a MMTL user account; reverse proxies need correct external URL and HTTPS headers.
Use `http://IP:18080` and a MeBox user account; reverse proxies need correct external URL and HTTPS headers.
---
@@ -200,7 +200,7 @@ Release builds ship single-file binaries for Windows, Linux, and macOS on amd64
## Acknowledgements
MMTL 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.
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.
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.
@@ -214,11 +214,11 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md) before ope
## Star History
<a href="https://www.star-history.com/?repos=truewhile%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=truewhile/MMTL&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=truewhile/MMTL&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=truewhile/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>
+5 -5
View File
@@ -1,13 +1,13 @@
# 安全策略
MMTL 是自托管媒体系统,常部署在 NAS、家庭网络、Docker、反向代理和第三方下载器环境中。安全问题通常会同时涉及应用代码、容器配置、路径映射、站点 Cookie / API Key、下载器凭据和外部访问入口。请按本策略报告和处理安全问题。
MeBox 是自托管媒体系统,常部署在 NAS、家庭网络、Docker、反向代理和第三方下载器环境中。安全问题通常会同时涉及应用代码、容器配置、路径映射、站点 Cookie / API Key、下载器凭据和外部访问入口。请按本策略报告和处理安全问题。
## 支持范围
我们优先支持以下版本和部署方式的安全修复:
- 当前 `main` 分支。
- 最新发布镜像:`ghcr.io/truewhile/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
+1 -1
View File
@@ -1 +1 @@
0.0.68
0.0.81
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"sync"
"time"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
const defaultLogMaxSizeMB = 20
+1 -1
View File
@@ -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。
+1 -1
View File
@@ -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) {
+9 -9
View File
@@ -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
@@ -21,10 +21,10 @@ import (
"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"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/database"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
// version is overwritten at build time via -ldflags="-X main.version=...".
@@ -35,7 +35,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 == "" {
@@ -59,7 +59,7 @@ func main() {
defer func() { _ = logger.Sync() }()
appVersion := effectiveVersion(version)
logger.Info("starting MMTL",
logger.Info("starting MeBox",
zap.String("version", appVersion),
zap.Int("port", cfg.App.Port),
zap.String("data_dir", cfg.App.DataDir),
@@ -135,5 +135,5 @@ func main() {
logger.Error("graceful shutdown failed", zap.Error(err))
}
services.Close()
logger.Info("MMTL stopped")
logger.Info("MeBox stopped")
}
+9 -9
View File
@@ -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)
@@ -85,7 +85,7 @@ 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, "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 {
@@ -105,7 +105,7 @@ func TestServeSPAServesAssetsImmutableAndBypassesAPIRoutes(t *testing.T) {
t.Fatalf("asset Cache-Control = %q, want immutable", got)
}
brandReq := httptest.NewRequest(http.MethodGet, "/brand/mmtl-logo.svg", nil)
brandReq := httptest.NewRequest(http.MethodGet, "/brand/mebox-logo.svg", nil)
brandResp := httptest.NewRecorder()
router.ServeHTTP(brandResp, brandReq)
if brandResp.Code != http.StatusOK {
+6 -6
View File
@@ -11,12 +11,12 @@ 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/ShukeBta/MMTL/web"
"github.com/truewhile/MeBox/web"
)
func buildRouter(cfg *config.Config, logger *zap.Logger, svc *service.Container) *gin.Engine {
@@ -78,7 +78,7 @@ func serveSPA(r *gin.Engine, root fs.FS) {
setNoCacheHeaders(c)
data, err := fs.ReadFile(root, "index.html")
if err != nil {
c.String(http.StatusNotFound, "MMTL web UI not found")
c.String(http.StatusNotFound, "MeBox web UI not found")
return
}
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
+3 -3
View File
@@ -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 不允许重复绑定同一端口);
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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) {
+38 -38
View File
@@ -1,7 +1,7 @@
# MMTL 第三档完整 Docker Compose 部署文件
# MeBox 第三档完整 Docker Compose 部署文件
#
# 组件:
# MMTL + PostgreSQL + Redis + OpenSearch
# MeBox + PostgreSQL + Redis + OpenSearch
#
# 使用方式二选一:
# 1. 保存为 docker-compose.yml 后执行:
@@ -19,8 +19,8 @@
# admin / admin123
services:
mmtl:
image: ghcr.io/truewhile/mmtl:latest
mebox:
image: ghcr.io/truewhile/mebox:latest
restart: unless-stopped
init: true
@@ -74,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: "50"
MEBOX_LOGGING_MAX_BACKUPS: "20"
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/truewhile/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"]
@@ -129,18 +129,18 @@ services:
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
+23 -41
View File
@@ -1,4 +1,4 @@
# MMTL 单镜像部署模板(SQLite)
# MeBox 单镜像部署模板(SQLite)
#
# 适合:新手、单人使用、低配 NAS / 小主机。
# 特点:只有一个镜像,不需要 PostgreSQL / Redis / .env。
@@ -12,10 +12,10 @@
# 首次登录后请立刻修改密码。
services:
mmtl:
image: ghcr.io/truewhile/mmtl:latest
mebox:
image: ghcr.io/truewhile/mebox:latest
container_name: mmtl
container_name: mebox
restart: unless-stopped
init: true
@@ -31,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
@@ -59,32 +43,30 @@ 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: "50"
MEBOX_LOGGING_MAX_BACKUPS: "20"
MEBOX_LOGGING_MAX_AGE_DAYS: "30"
extra_hosts:
# 容器访问宿主机服务用,例如 qBittorrent: http://host.docker.internal:8085
+35 -35
View File
@@ -1,7 +1,7 @@
# MMTL 第二档完整 Docker Compose 部署文件
# MeBox 第二档完整 Docker Compose 部署文件
#
# 组件:
# MMTL + PostgreSQL + Redis
# MeBox + PostgreSQL + Redis
#
# 使用方式二选一:
# 1. 保存为 docker-compose.yml 后执行:
@@ -16,8 +16,8 @@
# admin / admin123
services:
mmtl:
image: ghcr.io/truewhile/mmtl:latest
mebox:
image: ghcr.io/truewhile/mebox:latest
restart: unless-stopped
init: true
@@ -69,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: "50"
MEBOX_LOGGING_MAX_BACKUPS: "20"
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/truewhile/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"]
@@ -118,18 +118,18 @@ services:
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
+35 -35
View File
@@ -1,4 +1,4 @@
# MMTL 最简单 Docker Compose 部署文件
# MeBox 最简单 Docker Compose 部署文件
#
# 新手建议:
# 1. 不用 .env。
@@ -15,8 +15,8 @@
# admin / admin123
services:
mmtl:
image: ghcr.io/truewhile/mmtl:latest
mebox:
image: ghcr.io/truewhile/mebox:latest
restart: unless-stopped
init: true
@@ -83,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: "50"
MEBOX_LOGGING_MAX_BACKUPS: "20"
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/truewhile/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"]
@@ -146,20 +146,20 @@ services:
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
+4 -4
View File
@@ -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
+3 -3
View File
@@ -1,4 +1,4 @@
module github.com/ShukeBta/MMTL
module github.com/truewhile/MeBox
go 1.25.0
@@ -14,10 +14,12 @@ require (
github.com/shirou/gopsutil/v3 v3.24.5
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.9.0
github.com/ulikunitz/xz v0.5.12
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.21.0
golang.org/x/sys v0.20.0
golang.org/x/time v0.15.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.5.7
gorm.io/gorm v1.30.0
)
@@ -68,7 +70,6 @@ require (
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/ulikunitz/xz v0.5.12 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.3.0 // indirect
@@ -77,7 +78,6 @@ require (
golang.org/x/text v0.20.0 // indirect
google.golang.org/protobuf v1.31.0 // 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
+2 -2
View File
@@ -4,7 +4,7 @@
// 1. 内置默认值
// 2. 工作目录中的 config.yaml(嵌套格式)
// 3. config/*.yaml 分片文件(按模块)
// 4. 以 MMTL_ 为前缀的环境变量
// 4. 以 MEBOX_ 为前缀的环境变量
package config
import (
@@ -17,7 +17,7 @@ import (
)
// EnvPrefix 是所有环境变量驱动的覆盖使用的前缀。
const EnvPrefix = "MMTL"
const EnvPrefix = "MeBox"
// Load 从默认值 / 文件 / 环境读取配置。
//
+13 -13
View File
@@ -40,13 +40,13 @@ func TestLoadDefaults(t *testing.T) {
if cfg.Database.MaxOpenConns != defaultDatabaseMaxOpenConns {
t.Fatalf("expected default MaxOpenConns %d, got %d", defaultDatabaseMaxOpenConns, cfg.Database.MaxOpenConns)
}
if cfg.Cache.RedisPrefix != "mmtl" {
if cfg.Cache.RedisPrefix != "mebox" {
t.Fatalf("expected default redis prefix, got %q", cfg.Cache.RedisPrefix)
}
if cfg.Cache.MediaTTLSeconds != 15 {
t.Fatalf("expected default media cache ttl 15, got %d", cfg.Cache.MediaTTLSeconds)
}
if cfg.Search.Index != "mmtl_media" {
if cfg.Search.Index != "mebox_media" {
t.Fatalf("expected default search index, got %q", cfg.Search.Index)
}
if cfg.Database.MaxIdleConns != defaultDatabaseMaxIdleConns {
@@ -74,7 +74,7 @@ func TestLoadDefaults(t *testing.T) {
}
}
// TestEnvOverride checks that MMTL_* env vars override the defaults.
// TestEnvOverride checks that MEBOX_* env vars override the defaults.
func TestEnvOverride(t *testing.T) {
dir := t.TempDir()
wd, _ := os.Getwd()
@@ -83,16 +83,16 @@ func TestEnvOverride(t *testing.T) {
t.Fatalf("chdir: %v", err)
}
t.Setenv("MMTL_APP_PORT", "9090")
t.Setenv("MMTL_DATABASE_TYPE", "postgres")
t.Setenv("MMTL_DATABASE_DSN", "postgres://mmtl:secret@postgres:5432/mmtl?sslmode=disable")
t.Setenv("MMTL_CACHE_REDIS_URL", "redis://redis:6379/0")
t.Setenv("MMTL_CACHE_MEDIA_TTL_SECONDS", "30")
t.Setenv("MMTL_SEARCH_BACKEND", "opensearch")
t.Setenv("MMTL_SEARCH_OPENSEARCH_URL", "http://opensearch:9200")
t.Setenv("MMTL_LICENSE_SERVER_URL", "https://license.example.com")
t.Setenv("MMTL_LICENSE_HMAC_SECRET", "override-secret")
t.Setenv("MMTL_LICENSE_PUBLIC_KEY", "override-public-key")
t.Setenv("MEBOX_APP_PORT", "9090")
t.Setenv("MEBOX_DATABASE_TYPE", "postgres")
t.Setenv("MEBOX_DATABASE_DSN", "postgres://mebox:secret@postgres:5432/mebox?sslmode=disable")
t.Setenv("MEBOX_CACHE_REDIS_URL", "redis://redis:6379/0")
t.Setenv("MEBOX_CACHE_MEDIA_TTL_SECONDS", "30")
t.Setenv("MEBOX_SEARCH_BACKEND", "opensearch")
t.Setenv("MEBOX_SEARCH_OPENSEARCH_URL", "http://opensearch:9200")
t.Setenv("MEBOX_LICENSE_SERVER_URL", "https://license.example.com")
t.Setenv("MEBOX_LICENSE_HMAC_SECRET", "override-secret")
t.Setenv("MEBOX_LICENSE_PUBLIC_KEY", "override-public-key")
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
+4 -4
View File
@@ -25,7 +25,7 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("app.server_url", "")
v.SetDefault("database.type", "auto")
v.SetDefault("database.db_path", "./data/mmtl.db")
v.SetDefault("database.db_path", "./data/mebox.db")
v.SetDefault("database.dsn", "")
v.SetDefault("database.wal_mode", true)
v.SetDefault("database.busy_timeout", 5000)
@@ -46,12 +46,12 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("cache.images_max_size_mb", 500)
v.SetDefault("cache.cleanup_interval_min", 60)
v.SetDefault("cache.redis_url", "")
v.SetDefault("cache.redis_prefix", "mmtl")
v.SetDefault("cache.redis_prefix", "mebox")
v.SetDefault("cache.media_ttl_seconds", 15)
v.SetDefault("search.backend", "")
v.SetDefault("search.opensearch_url", "")
v.SetDefault("search.index", "mmtl_media")
v.SetDefault("search.index", "mebox_media")
v.SetDefault("search.username", "")
v.SetDefault("search.password", "")
@@ -64,7 +64,7 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("flaresolverr.enabled", false)
v.SetDefault("flaresolverr.url", "http://localhost:8191")
v.SetDefault("flaresolverr.session", "mmtl")
v.SetDefault("flaresolverr.session", "mebox")
v.SetDefault("flaresolverr.timeout", 60)
v.SetDefault("downloads.smart_classify", true)
+3 -3
View File
@@ -15,7 +15,7 @@ func (c *Config) normalize() error {
c.App.DataDir = "./data"
}
if c.Database.DBPath == "" {
c.Database.DBPath = filepath.Join(c.App.DataDir, "mmtl.db")
c.Database.DBPath = filepath.Join(c.App.DataDir, "mebox.db")
}
if c.Database.Type == "" {
c.Database.Type = "auto"
@@ -48,14 +48,14 @@ func (c *Config) normalize() error {
c.Cache.ImagesMaxSizeMB = 0
}
if c.Cache.RedisPrefix == "" {
c.Cache.RedisPrefix = "mmtl"
c.Cache.RedisPrefix = "mebox"
}
if c.Cache.MediaTTLSeconds < 1 {
c.Cache.MediaTTLSeconds = 15
}
c.Search.Backend = strings.ToLower(strings.TrimSpace(c.Search.Backend))
if c.Search.Index == "" {
c.Search.Index = "mmtl_media"
c.Search.Index = "mebox_media"
}
if c.Secrets.JWTSecret == "" {
// 持久化自动生成的密钥以在操作员忘记配置时保持会话稳定。
+1 -1
View File
@@ -14,7 +14,7 @@ func TestSaveDatabaseConfig(t *testing.T) {
t.Fatalf("chdir: %v", err)
}
dsn := "postgres://admin:pass@127.0.0.1:5432/mmtl?sslmode=disable"
dsn := "postgres://admin:pass@127.0.0.1:5432/mebox?sslmode=disable"
if err := SaveDatabaseConfig("postgres", dsn); err != nil {
t.Fatalf("SaveDatabaseConfig error: %v", err)
}
+1 -1
View File
@@ -152,7 +152,7 @@ type AIConfig struct {
MaxConcurrent int `mapstructure:"max_concurrent"`
}
// LicenseConfig configures the optional MMTL license server bridge.
// LicenseConfig configures the optional MeBox license server bridge.
type LicenseConfig struct {
ServerURL string `mapstructure:"server_url"`
HMACSecret string `mapstructure:"hmac_secret"`
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
// Open initialises the configured GORM database. database.type=auto chooses
+2 -2
View File
@@ -12,8 +12,8 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
)
// DatabaseStatus describes the currently active database engine and runtime metrics.
+10 -10
View File
@@ -6,8 +6,8 @@ import (
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
)
func TestMaskDSN(t *testing.T) {
@@ -16,16 +16,16 @@ func TestMaskDSN(t *testing.T) {
want string
}{
{
in: "postgres://admin:secret123@localhost:5432/mmtl?sslmode=disable",
want: "postgres://admin:******@localhost:5432/mmtl?sslmode=disable",
in: "postgres://admin:secret123@localhost:5432/mebox?sslmode=disable",
want: "postgres://admin:******@localhost:5432/mebox?sslmode=disable",
},
{
in: "host=localhost port=5432 user=admin password=secret dbname=mmtl sslmode=disable",
want: "host=localhost port=5432 user=admin password=****** dbname=mmtl sslmode=disable",
in: "host=localhost port=5432 user=admin password=secret dbname=mebox sslmode=disable",
want: "host=localhost port=5432 user=admin password=****** dbname=mebox sslmode=disable",
},
{
in: "sqlite://data/mmtl.db",
want: "sqlite://data/mmtl.db",
in: "sqlite://data/mebox.db",
want: "sqlite://data/mebox.db",
},
{
in: "",
@@ -53,7 +53,7 @@ func TestInspectDatabaseStatus(t *testing.T) {
cfg := &config.Config{}
cfg.Database.Type = "sqlite"
cfg.Database.DBPath = "./data/mmtl.db"
cfg.Database.DBPath = "./data/mebox.db"
st := InspectDatabaseStatus(db, cfg)
if st == nil {
@@ -62,7 +62,7 @@ func TestInspectDatabaseStatus(t *testing.T) {
if st.Type != "sqlite" {
t.Fatalf("expected sqlite, got %s", st.Type)
}
if st.DBPath != "./data/mmtl.db" {
if st.DBPath != "./data/mebox.db" {
t.Fatalf("expected db_path, got %s", st.DBPath)
}
if st.TableCounts["users"] != 1 {
+5 -5
View File
@@ -8,8 +8,8 @@ import (
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
)
func TestOpenRequiresConfig(t *testing.T) {
@@ -28,7 +28,7 @@ func TestOpenRequiresConfig(t *testing.T) {
func TestOpenSQLiteWithNilLoggerConfiguresPool(t *testing.T) {
cfg := &config.Config{}
cfg.Database.Type = "sqlite"
cfg.Database.DBPath = filepath.Join(t.TempDir(), "mmtl.db")
cfg.Database.DBPath = filepath.Join(t.TempDir(), "mebox.db")
cfg.Database.WALMode = true
cfg.Database.BusyTimeout = 5000
cfg.Database.CacheSize = -2000
@@ -278,7 +278,7 @@ func TestSQLiteMigrationCompleteMarker(t *testing.T) {
func TestSQLiteMigrationFallsBackToDataDirDefaultPath(t *testing.T) {
dir := t.TempDir()
sqlitePath := filepath.Join(dir, "mmtl.db")
sqlitePath := filepath.Join(dir, "mebox.db")
src, err := gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{})
if err != nil {
t.Fatal(err)
@@ -362,7 +362,7 @@ func TestSQLiteMigrationFallsBackToDataDirDefaultPath(t *testing.T) {
func TestOpenSQLiteMigrationSourceUsesFallbackSourcePath(t *testing.T) {
dir := t.TempDir()
sqlitePath := filepath.Join(dir, "mmtl.db")
sqlitePath := filepath.Join(dir, "mebox.db")
src, err := gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{})
if err != nil {
t.Fatal(err)
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func ensureLibraryRootsCompatibility(db *gorm.DB) error {
+1 -1
View File
@@ -3,7 +3,7 @@ package database
import (
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
// AutoMigrate creates tables for every model registered in the model package.
@@ -7,7 +7,7 @@ import (
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func TestEnsureEmbyMountsCompatibility(t *testing.T) {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
// MigrateSQLiteToCurrentIfNeeded copies an existing SQLite database into
@@ -9,7 +9,7 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func resetBootstrapTargetBeforeSQLiteMigrationIfSafe(src, target *gorm.DB, log *zap.Logger) error {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/truewhile/MeBox/internal/model"
)
func copyModelTables(src, target *gorm.DB, batchSize int) (map[string]int64, int64, error) {
+23 -16
View File
@@ -12,7 +12,7 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
func openSQLiteMigrationSource(cfg *config.Config, sqlitePath string) (*gorm.DB, error) {
@@ -36,23 +36,30 @@ func sqliteMigrationSourcePath(cfg *config.Config, log *zap.Logger) (string, err
}
}
fallback := filepath.Join(strings.TrimSpace(cfg.App.DataDir), "mmtl.db")
if fallback == "" || sameCleanPath(configured, fallback) {
return "", nil
// Prefer the new default filename, then fall back to legacy MMTL SQLite files.
candidates := []string{
filepath.Join(strings.TrimSpace(cfg.App.DataDir), "mebox.db"),
filepath.Join(strings.TrimSpace(cfg.App.DataDir), "mmtl.db"),
}
exists, err := regularFileExists(fallback)
if err != nil {
return "", fmt.Errorf("stat default sqlite migration source: %w", err)
for _, fallback := range candidates {
if fallback == "" || sameCleanPath(configured, fallback) {
continue
}
exists, err := regularFileExists(fallback)
if err != nil {
return "", fmt.Errorf("stat default sqlite migration source: %w", err)
}
if !exists {
continue
}
if log != nil && configured != "" {
log.Warn("configured sqlite migration source not found; using data-dir default",
zap.String("configured", configured),
zap.String("fallback", fallback))
}
return fallback, nil
}
if !exists {
return "", nil
}
if log != nil && configured != "" {
log.Warn("configured sqlite migration source not found; using data-dir default",
zap.String("configured", configured),
zap.String("fallback", fallback))
}
return fallback, nil
return "", nil
}
func regularFileExists(path string) (bool, error) {
+10 -10
View File
@@ -8,14 +8,14 @@ import (
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/truewhile/MeBox/internal/config"
)
func installSQLiteWriteGate(db *gorm.DB) {
if db == nil {
return
}
const lockedKey = "mmtl:sqlite_write_locked"
const lockedKey = "mebox:sqlite_write_locked"
gate := newSQLiteWriteGate()
lock := func(tx *gorm.DB) {
ctx := context.Background()
@@ -39,14 +39,14 @@ func installSQLiteWriteGate(db *gorm.DB) {
}
lock(tx)
}
_ = db.Callback().Create().Before("gorm:create").Register("mmtl:sqlite_write_lock", lock)
_ = db.Callback().Create().After("gorm:create").Register("mmtl:sqlite_write_unlock", unlock)
_ = db.Callback().Update().Before("gorm:update").Register("mmtl:sqlite_write_lock", lock)
_ = db.Callback().Update().After("gorm:update").Register("mmtl:sqlite_write_unlock", unlock)
_ = db.Callback().Delete().Before("gorm:delete").Register("mmtl:sqlite_write_lock", lock)
_ = db.Callback().Delete().After("gorm:delete").Register("mmtl:sqlite_write_unlock", unlock)
_ = db.Callback().Raw().Before("gorm:raw").Register("mmtl:sqlite_write_lock", rawLock)
_ = db.Callback().Raw().After("gorm:raw").Register("mmtl:sqlite_write_unlock", unlock)
_ = db.Callback().Create().Before("gorm:create").Register("mebox:sqlite_write_lock", lock)
_ = db.Callback().Create().After("gorm:create").Register("mebox:sqlite_write_unlock", unlock)
_ = db.Callback().Update().Before("gorm:update").Register("mebox:sqlite_write_lock", lock)
_ = db.Callback().Update().After("gorm:update").Register("mebox:sqlite_write_unlock", unlock)
_ = db.Callback().Delete().Before("gorm:delete").Register("mebox:sqlite_write_lock", lock)
_ = db.Callback().Delete().After("gorm:delete").Register("mebox:sqlite_write_unlock", unlock)
_ = db.Callback().Raw().Before("gorm:raw").Register("mebox:sqlite_write_lock", rawLock)
_ = db.Callback().Raw().After("gorm:raw").Register("mebox:sqlite_write_unlock", unlock)
}
func isReadOnlySQL(sql string) bool {
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/service"
)
const embyCtxUserName = "emby_user_name"
+68 -4
View File
@@ -10,8 +10,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/service"
)
func listUsersHandler(svc *service.Container) gin.HandlerFunc {
@@ -31,7 +31,66 @@ func listUsersHandler(svc *service.Container) gin.HandlerFunc {
for i := range users {
users[i].PopulateComputedFields()
}
c.JSON(http.StatusOK, users)
maxUsers, err := service.LoadMaxUsers(c.Request.Context(), svc.Repo)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"users": users,
"max_users": maxUsers,
"current_users": len(users),
})
}
}
type updateUserLimitReq struct {
MaxUsers int `json:"max_users" binding:"required"`
}
func getUserLimitHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
maxUsers, err := service.LoadMaxUsers(c.Request.Context(), svc.Repo)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
currentUsers, err := svc.Repo.User.Count(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"max_users": maxUsers,
"current_users": currentUsers,
})
}
}
func updateUserLimitHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
var req updateUserLimitReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := service.SaveMaxUsers(c.Request.Context(), svc.Repo, req.MaxUsers); err != nil {
if errors.Is(err, service.ErrInvalidMaxUsers) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
currentUsers, err := svc.Repo.User.Count(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"max_users": req.MaxUsers,
"current_users": currentUsers,
})
}
}
@@ -288,7 +347,12 @@ func writeUserMutationError(c *gin.Context, svc *service.Container, err error) {
case errors.Is(err, service.ErrUsernameTaken):
c.JSON(http.StatusConflict, gin.H{"error": "username already taken"})
case errors.Is(err, service.ErrUserLimitReached):
c.JSON(http.StatusBadRequest, gin.H{"error": "user limit reached", "max_users": service.UserLimit})
maxUsers, loadErr := service.LoadMaxUsers(c.Request.Context(), svc.Repo)
if loadErr != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": loadErr.Error()})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "user limit reached", "max_users": maxUsers})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
+2 -2
View File
@@ -8,7 +8,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
type DatabaseConnectionPayload struct {
@@ -38,7 +38,7 @@ func (p *DatabaseConnectionPayload) BuildDSN() string {
user := strings.TrimSpace(p.User)
dbname := strings.TrimSpace(p.DBName)
if dbname == "" {
dbname = "mmtl"
dbname = "mebox"
}
sslmode := strings.TrimSpace(p.SSLMode)
if sslmode == "" {
+6 -6
View File
@@ -9,8 +9,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/service"
)
func TestBuildDSN(t *testing.T) {
@@ -30,10 +30,10 @@ func TestBuildDSN(t *testing.T) {
Port: 5432,
User: "postgres",
Password: "secretpassword",
DBName: "mmtl_prod",
DBName: "mebox_prod",
SSLMode: "disable",
},
want: "postgres://postgres:secretpassword@127.0.0.1:5432/mmtl_prod?sslmode=disable",
want: "postgres://postgres:secretpassword@127.0.0.1:5432/mebox_prod?sslmode=disable",
},
}
@@ -49,7 +49,7 @@ func TestGetDatabaseStatusHandler(t *testing.T) {
gin.SetMode(gin.TestMode)
cfg := &config.Config{}
cfg.Database.Type = "sqlite"
cfg.Database.DBPath = "./data/mmtl.db"
cfg.Database.DBPath = "./data/mebox.db"
svc := &service.Container{
Database: service.NewDatabaseAdminService(cfg, nil, nil, nil),
@@ -89,7 +89,7 @@ func TestSaveDatabaseConfigHandler(t *testing.T) {
r := gin.New()
r.POST("/api/admin/database/save-config", saveDatabaseConfigHandler(svc))
body := bytes.NewBufferString(`{"type":"postgres","host":"localhost","port":5432,"user":"admin","password":"pwd","dbname":"mmtl"}`)
body := bytes.NewBufferString(`{"type":"postgres","host":"localhost","port":5432,"user":"admin","password":"pwd","dbname":"mebox"}`)
req := httptest.NewRequest(http.MethodPost, "/api/admin/database/save-config", body)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/service"
)
type settingReq struct {
+3 -3
View File
@@ -11,9 +11,9 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestDeleteUserRefusesRecentRealtimeSession(t *testing.T) {
+55
View File
@@ -0,0 +1,55 @@
package handler
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestUserLimitHandlers(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(model.AllModels()...); err != nil {
t.Fatal(err)
}
repos := repository.New(db)
cfg := &service.Container{Repo: repos, Auth: service.NewAuthService(nil, zap.NewNop(), repos, service.NewTokenService(nil, zap.NewNop(), repos), service.NewPermissionService(zap.NewNop(), repos))}
router := gin.New()
router.GET("/admin/users/limit", getUserLimitHandler(cfg))
router.PUT("/admin/users/limit", updateUserLimitHandler(cfg))
req := httptest.NewRequest(http.MethodGet, "/admin/users/limit", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("GET limit status = %d body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), `"max_users":20`) {
t.Fatalf("expected default max_users=20, got %s", w.Body.String())
}
req = httptest.NewRequest(http.MethodPut, "/admin/users/limit", strings.NewReader(`{"max_users":42}`))
req.Header.Set("Content-Type", "application/json")
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("PUT limit status = %d body=%s", w.Code, w.Body.String())
}
got, err := service.LoadMaxUsers(t.Context(), repos)
if err != nil || got != 42 {
t.Fatalf("stored max users = %d err=%v, want 42", got, err)
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func listAPIConfigsHandler(svc *service.Container) gin.HandlerFunc {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
// ApiConfigHandler API 配置 HTTP 处理。
+11 -2
View File
@@ -7,8 +7,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/service"
)
type loginReq struct {
@@ -68,6 +68,15 @@ func registerHandler(svc *service.Container) gin.HandlerFunc {
c.JSON(http.StatusConflict, gin.H{"error": "username taken"})
return
}
if errors.Is(err, service.ErrUserLimitReached) {
maxUsers, loadErr := service.LoadMaxUsers(c.Request.Context(), svc.Repo)
if loadErr != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": loadErr.Error()})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "user limit reached", "max_users": maxUsers})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
+2 -2
View File
@@ -7,8 +7,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/service"
)
func setAccessTokenCookie(c *gin.Context, token string, maxAgeSeconds int) {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/truewhile/MeBox/internal/middleware"
)
func TestSetAccessTokenCookie(t *testing.T) {
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/service"
)
// refreshHandler returns a fresh token signed for the current user.
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func createBackupHandler(svc *service.Container) gin.HandlerFunc {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
// getDanmakuHandler returns danmaku for a media. ?kw= optionally overrides the
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func dlnaListHandler(svc *service.Container) gin.HandlerFunc {
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
// dlnaControlPath maps the action name to the AVTransport SOAP body.
+2 -2
View File
@@ -7,8 +7,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/service"
)
// embyError 返回 Emby 风格的错误(顶层 Code/Message)。
+4 -4
View File
@@ -14,10 +14,10 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestParseEmbyAuthByNameReqAcceptsLowercaseJSON(t *testing.T) {
+4 -4
View File
@@ -13,10 +13,10 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
// TestEmbyLoginThenAuthedRequest simulates the exact flow an Emby-compatible
@@ -12,10 +12,10 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestEmbyWithRequestAddressUsesHost(t *testing.T) {
@@ -24,7 +24,7 @@ func TestEmbyWithRequestAddressUsesHost(t *testing.T) {
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "http://192.168.1.4:18080/System/Info/Public", nil)
payload := embyWithRequestAddress(c, map[string]any{"Id": "mmtl-001"})
payload := embyWithRequestAddress(c, map[string]any{"Id": "mebox-001"})
if payload["LocalAddress"] != "http://192.168.1.4:18080" {
t.Fatalf("unexpected LocalAddress: %#v", payload["LocalAddress"])
@@ -42,7 +42,7 @@ func TestEmbyWithRequestAddressHonorsForwardedHeaders(t *testing.T) {
c.Request.Header.Set("X-Forwarded-Proto", "https")
c.Request.Header.Set("X-Forwarded-Host", "media.example.test")
payload := embyWithRequestAddress(c, map[string]any{"Id": "mmtl-001"})
payload := embyWithRequestAddress(c, map[string]any{"Id": "mebox-001"})
if payload["LocalAddress"] != "https://media.example.test" {
t.Fatalf("unexpected LocalAddress: %#v", payload["LocalAddress"])
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
var embyPlaceholderPNG = []byte{
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func parseEmbyItemsParams(c *gin.Context) service.ItemsParams {
+4 -4
View File
@@ -14,10 +14,10 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestEmbyItemImageServesWithoutAPIAuth(t *testing.T) {
+3 -3
View File
@@ -13,9 +13,9 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestEmbyVirtualFoldersRouteReturnsJSON(t *testing.T) {
+5 -5
View File
@@ -13,11 +13,11 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestMountedEmbyPlayingProgressAndResumePipeline(t *testing.T) {
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/service"
)
// embyMountView 挂载的对外 JSON(附带账号信息)。
+4 -4
View File
@@ -12,10 +12,10 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/database"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/database"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestReorderEmbyMountsHandler(t *testing.T) {
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func embyPlaybackInfoHandler(svc *service.Container) gin.HandlerFunc {
@@ -13,10 +13,10 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestEmbyLowercasePlaybackInfoRouteReturnsJSON(t *testing.T) {
@@ -12,10 +12,10 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestEmbyLowercaseVideoStreamRouteServesMedia(t *testing.T) {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
type embyPlayingReq struct {
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/service"
)
// registerEmbyRoutes 在 r 上挂双前缀("" + "/emby")的 Emby 兼容路由。
+1 -1
View File
@@ -3,7 +3,7 @@ package handler
import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func registerLowercaseEmbyAuthRoutes(auth *gin.RouterGroup, svc *service.Container) {
+9 -6
View File
@@ -14,10 +14,10 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestEmbyMarkPlayedRefreshesPlaybackDevice(t *testing.T) {
@@ -213,10 +213,13 @@ func TestEmbyAuthenticatedRequestRefreshesRealtimeUserActivity(t *testing.T) {
if w.Code != http.StatusOK {
t.Fatalf("admin users status: %d body=%s", w.Code, w.Body.String())
}
var users []model.User
if err := json.Unmarshal(w.Body.Bytes(), &users); err != nil {
var payload struct {
Users []model.User `json:"users"`
}
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode users: %v", err)
}
users := payload.Users
if len(users) != 1 {
t.Fatalf("users = %#v", users)
}
+3 -3
View File
@@ -7,8 +7,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/service"
)
func embySessionsHandler(svc *service.Container) gin.HandlerFunc {
@@ -29,7 +29,7 @@ func embySessionsHandler(svc *service.Container) gin.HandlerFunc {
}
row := gin.H{
"Id": sess.ID,
"ServerId": "mmtl-001",
"ServerId": "mebox-001",
"Client": sess.Client,
"DeviceId": sess.DeviceID,
"DeviceName": sess.DeviceName,
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func TestEmbySessionsReturnsRealtimeSession(t *testing.T) {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func embyNoContentHandler(_ *service.Container) gin.HandlerFunc {
@@ -14,10 +14,10 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
// TestEmbySubtitleOfficialRouteServesRawASS verifies that the official Emby
+4 -4
View File
@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func embySystemInfoHandler(svc *service.Container) gin.HandlerFunc {
@@ -84,9 +84,9 @@ func embyPublicSystemInfoPayload(c *gin.Context, svc *service.Container) map[str
return embyWithRequestAddress(c, svc.Emby.SystemInfoPublic())
}
return embyWithRequestAddress(c, map[string]any{
"Id": "mmtl-001",
"ServerId": "mmtl-001",
"ServerName": "MMTL",
"Id": "mebox-001",
"ServerId": "mebox-001",
"ServerName": "MeBox",
"Version": "4.8.10.0",
"ServerVersion": "4.8.10.0",
"ProductName": "Emby Server",
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/truewhile/MeBox/internal/middleware"
)
func signedTestToken(t *testing.T, secret string) string {
@@ -23,7 +23,7 @@ func signedTestTokenWithPurpose(t *testing.T, secret, purpose string) string {
Purpose: purpose,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
Issuer: "mmtl-test",
Issuer: "mebox-test",
Subject: "user-1",
},
}
+5 -5
View File
@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
// ─── Users / Auth ────────────────────────────────────────────────────────────
@@ -67,7 +67,7 @@ func embyAuthByNameHandler(svc *service.Container) gin.HandlerFunc {
embyRememberCompatSession(c, accessToken)
c.JSON(http.StatusOK, gin.H{
"AccessToken": accessToken,
"ServerId": "mmtl-001",
"ServerId": "mebox-001",
"User": userPayload,
"SessionInfo": gin.H{
"Id": resp.User.ID,
@@ -150,12 +150,12 @@ func embyGetUserByIDHandler(svc *service.Container) gin.HandlerFunc {
func embyFallbackUser(id string) gin.H {
if strings.TrimSpace(id) == "" {
id = "mmtl-user"
id = "mebox-user"
}
return gin.H{
"Id": id,
"Name": "MMTL",
"ServerId": "mmtl-001",
"Name": "MeBox",
"ServerId": "mebox-001",
"HasPassword": true,
"HasConfiguredPassword": true,
"HasConfiguredEasyPassword": false,
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func embyViewsHandler(svc *service.Container) gin.HandlerFunc {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
type fileFolderReq struct {
+3 -3
View File
@@ -8,8 +8,8 @@ import (
"github.com/gin-gonic/gin"
"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"
)
// Register attaches every API route to the engine.
@@ -46,7 +46,7 @@ func healthCheck(c *gin.Context) {
}
func versionInfo(c *gin.Context) {
c.JSON(200, gin.H{"name": "MMTL", "version": "0.1.0"})
c.JSON(200, gin.H{"name": "MeBox", "version": "0.1.0"})
}
// ─── 权限 Handler 包装 ────────────────────────────────────────────────────────
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
type manualScrapeApplyReq struct {
+4 -4
View File
@@ -11,9 +11,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/service"
)
type createLibraryReq struct {
@@ -509,7 +509,7 @@ func streamHandler(svc *service.Container) gin.HandlerFunc {
return func(c *gin.Context) {
ctx := c.Request.Context()
id := c.Param("id")
// 远程 Emby 条目:按挂载代理配置分流——代理走 MMTL 反代,否则 302 直连。
// 远程 Emby 条目:按挂载代理配置分流——代理走 MeBox 反代,否则 302 直连。
if svc.EmbyRemote != nil && service.IsEmbyRemoteID(id) {
if !enforceScopedPlaybackToken(c, id) {
return
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/service"
)
func recentMediaHandler(svc *service.Container) gin.HandlerFunc {
+3 -3
View File
@@ -12,9 +12,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/service"
)
// addMediaFavoriteHandler ensures the (user, media) row exists. If it
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func listLibraryRootsHandler(svc *service.Container) gin.HandlerFunc {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
func scanLibraryHandler(svc *service.Container) gin.HandlerFunc {
+5 -5
View File
@@ -18,11 +18,11 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/config"
"github.com/ShukeBta/MMTL/internal/middleware"
"github.com/ShukeBta/MMTL/internal/model"
"github.com/ShukeBta/MMTL/internal/repository"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/middleware"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/internal/repository"
"github.com/truewhile/MeBox/internal/service"
)
func TestQueueLibraryRootScanImportsNewLibraryMedia(t *testing.T) {
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"sync"
"time"
"github.com/ShukeBta/MMTL/internal/service"
"github.com/truewhile/MeBox/internal/service"
)
var metadataNoiseRE = regexp.MustCompile(`(?i)(自动订阅|订阅|全集|合集|complete|batch|season\s*\d+|s\d{1,2}|s\d{1,2}e\d{1,3}|第\s*\d+\s*季|第\s*\d+\s*[集话話期]|2160p|1080p|720p|4k|uhd|bluray|blu-ray|web-?dl|hdtv|remux|x26[45]|h\.?26[45]|hevc|avc|hdr10?\+?|dovi|dv|atmos|aac|ddp?5\.1|truehd|flac)`)

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