diff --git a/.dockerignore b/.dockerignore index 92fdeea..d070e55 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,8 +8,14 @@ docs/ data/ cache/ logs/ +.tmp_* +.tmp-deploy-* +.tmp-deploy-data/ +.mediastation.pid *.db *.db-journal +*.db-shm +*.db-wal *.log .env .env.* diff --git a/.gitignore b/.gitignore index d55d2f3..1677895 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ web/tsconfig.tsbuildinfo data/ cache/ logs/ +.tmp-deploy-data/ +.tmp-deploy-server.* +.mediastation.pid *.log *.db *.db-journal diff --git a/.tmp-deploy-data/.jwt_secret b/.tmp-deploy-data/.jwt_secret deleted file mode 100644 index b20cfbd..0000000 --- a/.tmp-deploy-data/.jwt_secret +++ /dev/null @@ -1 +0,0 @@ -39f30642a462624f2aa03896465943891e2d28db9f9a03aca94fd9af58d0c108 \ No newline at end of file diff --git a/.tmp-deploy-server.pid b/.tmp-deploy-server.pid deleted file mode 100644 index 554f591..0000000 --- a/.tmp-deploy-server.pid +++ /dev/null @@ -1 +0,0 @@ -29292 diff --git a/Makefile b/Makefile index ae2d400..0d46115 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build build-server build-web dev dev-web run docker docker-stop clean install-web tidy test vet smoke +.PHONY: all build build-server build-web dev dev-web run deploy docker docker-stop docker-push clean install-web tidy test vet smoke # ---- 默认目标 ---- all: build @@ -26,6 +26,9 @@ dev-web: run: build ./bin/mediastation-go +deploy: + ./scripts/deploy.sh + # ---- Docker ---- docker: docker compose up --build -d @@ -33,6 +36,9 @@ docker: docker-stop: docker compose down +docker-push: + ./scripts/docker-build-push.sh + # ---- 工具命令 ---- install-web: cd web && npm install diff --git a/README.md b/README.md index 90957fa..ccf413f 100644 --- a/README.md +++ b/README.md @@ -1,346 +1,204 @@ -
-
-
-
-
-
-
-
-
-
- 📖 为什么选择 - · - 🚀 快速开始 - · - ✨ 功能特性 - · - 🏗️ 项目结构 - · - ⚙️ 配置说明 - · - 🗺️ 路线图 -
+## 功能模块 ---- +| 模块 | 能力 | +| --- | --- | +| 媒体库 | 电影、电视剧、动漫、综艺、音乐、成人内容;自动封面、合集分季分集 | +| 刮削 | 本地 NFO/图片优先,TMDb/TheTVDB/Bangumi/豆瓣/Fanart/JavBus/JavDB 补全 | +| 播放 | 直链、HLS、字幕、续播、外部播放器、历史与收藏 | +| 发现 | TMDb / 豆瓣 / Bangumi 推荐入口,多源发现与订阅 | +| 下载 | qBittorrent、PT 站点、RSS/搜索订阅、自动整理 | +| 兼容 | Emby API、DLNA、外部客户端与三端 UI | +| 运维 | 任务、统计、存储、重复文件、回收站、NFO 导出 | +| AI | OpenAI 兼容 Base URL/API Key,智能搜索与推荐 | -## 🤔 为什么选择 MediaStationGo? - -> MediaStationGo 是 [MediaStation](https://github.com/ShukeBta/MediaStation) 从零开始的 Go 语言重写版,在保持完整功能体验的同时,将部署复杂度压缩到极致。 - -| - -### 原版 MediaStation -- 🐍 Python / FastAPI + Vue -- 📦 依赖 Python 运行时和虚拟环境 -- 🐳 必须使用 Docker 或复杂的 Python 环境 -- 📊 部署包 > 500 MB -- 🔧 需要 pip / npm 双构建链 - - | -- -### MediaStationGo ✨ -- 🚀 Go 1.25 + React 18 -- 📦 **单一静态二进制**(约 30 MB) -- 🐳 可选 Docker,也支持裸机直接运行 -- 🔥 零外部依赖(CGO 禁用) -- ⚡ `go build` 一条命令编译 - - | -
- Made with ❤️ by MediaStationGo Team -
+- `data/`、`cache/`、`logs/` +- `.tmp-deploy-data/`、`.tmp-deploy-server.*` +- `config.yaml`、`.env*` +- `*.db`、`*.db-wal`、`*.log` +- `web/dist/`、`node_modules/`、`bin/` + +推送代码前建议执行: + +```bash +git status --short +git ls-files | grep -E 'data/|cache/|\\.db|\\.log|jwt_secret|config.yaml|\\.env' || true +``` + +## 许可证 + +本项目遵循 `GPL-3.0` 许可证。详见 [LICENSE](LICENSE)。 diff --git a/README_EN.md b/README_EN.md index 753d97f..8833f5e 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,345 +1,204 @@ -
-
-
-
-
-
-
-
-
-
- 📖 Why - · - 🚀 Quick Start - · - ✨ Features - · - 🏗️ Layout - · - ⚙️ Config - · - 🗺️ Roadmap -
+## Feature Modules ---- +| Module | Capabilities | +| --- | --- | +| Libraries | Movies, TV, anime, variety, music, adult content; automatic covers and season/episode grouping | +| Scraping | Local NFO/images first, then TMDb/TheTVDB/Bangumi/Douban/Fanart/JavBus/JavDB enrichment | +| Playback | Direct play, HLS, subtitles, resume, external players, history, favorites | +| Discovery | TMDb / Douban / Bangumi recommendation sources and subscription entry points | +| Downloads | qBittorrent, PT sites, RSS/search subscriptions, automatic organization | +| Compatibility | Emby API, DLNA-ready structure, external clients, responsive three-end UI | +| Operations | Tasks, statistics, storage, duplicate files, recycle bin, NFO export | +| AI | OpenAI-compatible Base URL/API Key, smart search, recommendations | -## 🤔 Why MediaStationGo? - -> MediaStationGo is a from-scratch Go rewrite of [MediaStation](https://github.com/ShukeBta/MediaStation) — same full-featured media center experience, radically simpler deployment. - -| - -### Original MediaStation -- 🐍 Python / FastAPI + Vue -- 📦 Requires Python runtime & virtualenv -- 🐳 Docker mandatory or complex Python setup -- 📊 Deployment footprint > 500 MB -- 🔧 pip + npm dual build chains - - | -- -### MediaStationGo ✨ -- 🚀 Go 1.25 + React 18 -- 📦 **Single static binary** (~30 MB) -- 🐳 Docker optional — runs natively bare-metal -- 🔥 Zero external dependencies (CGO off) -- ⚡ One-command build: `go build` - - | -
- Made with ❤️ by MediaStationGo Team -
+- `data/`, `cache/`, `logs/` +- `.tmp-deploy-data/`, `.tmp-deploy-server.*`, `.mediastation.pid` +- `config.yaml`, `.env*` +- `*.db`, `*.db-wal`, `*.log` +- `web/dist/`, `node_modules/`, `bin/` + +Before pushing code, run: + +```bash +git status --short +git ls-files | grep -E 'data/|cache/|\\.db|\\.log|jwt_secret|config.yaml|\\.env' || true +``` + +## License + +This project is licensed under `GPL-3.0`. See [LICENSE](LICENSE) for details. diff --git a/internal/handler/ai.go b/internal/handler/ai.go index 02b1ae0..6db7659 100644 --- a/internal/handler/ai.go +++ b/internal/handler/ai.go @@ -72,10 +72,6 @@ func aiRecommendHandler(svc *service.Container) gin.HandlerFunc { func aiStatusHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "enabled": svc.AI.Enabled(), - "provider": svc.Cfg.AI.Provider, - "model": svc.Cfg.AI.Model, - }) + c.JSON(http.StatusOK, svc.AI.Status(c.Request.Context())) } } diff --git a/internal/helper/http.go b/internal/helper/http.go index da86710..17b060f 100644 --- a/internal/helper/http.go +++ b/internal/helper/http.go @@ -34,7 +34,7 @@ func NewSiteHTTPClient(timeoutSeconds int, useProxy bool) *http.Client { } if useProxy { tr.Proxy = func(r *http.Request) (*url.URL, error) { - return http.ProxyFromEnvironment(r) + return ProxyFromEnvironmentOrSystem(r) } } return &http.Client{ @@ -47,17 +47,17 @@ func NewSiteHTTPClient(timeoutSeconds int, useProxy bool) *http.Client { // These mimic a real Chrome browser to avoid WAF/bot detection. func HTTPHeaderPresets() map[string]string { return map[string]string{ - "User-Agent": model.DefaultUserAgent, - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", - "Accept-Encoding": "gzip, deflate, br", - "Connection": "keep-alive", + "User-Agent": model.DefaultUserAgent, + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", "Upgrade-Insecure-Requests": "1", - "Sec-Fetch-Dest": "document", - "Sec-Fetch-Mode": "navigate", - "Sec-Fetch-Site": "none", - "Sec-Fetch-User": "?1", - "Cache-Control": "max-age=0", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Cache-Control": "max-age=0", } } @@ -65,12 +65,12 @@ func HTTPHeaderPresets() map[string]string { // FlareSolverrRequest represents a request to FlareSolverr. type FlareSolverrRequest struct { - Cmd string `json:"cmd"` - URL string `json:"url"` - Session string `json:"session,omitempty"` - MaxTimeout int `json:"maxTimeout,omitempty"` - Proxy *FlareSolverrProxy `json:"proxy,omitempty"` - Cookies []FlareSolverrCookie `json:"cookies,omitempty"` + Cmd string `json:"cmd"` + URL string `json:"url"` + Session string `json:"session,omitempty"` + MaxTimeout int `json:"maxTimeout,omitempty"` + Proxy *FlareSolverrProxy `json:"proxy,omitempty"` + Cookies []FlareSolverrCookie `json:"cookies,omitempty"` } // FlareSolverrProxy represents proxy config for FlareSolverr. @@ -90,19 +90,19 @@ type FlareSolverrCookie struct { // FlareSolverrResponse represents FlareSolverr's response. type FlareSolverrResponse struct { - Status string `json:"status"` - Message string `json:"message"` + Status string `json:"status"` + Message string `json:"message"` Solution *FlareSolverrSolution `json:"solution,omitempty"` } // FlareSolverrSolution contains the solved challenge result. type FlareSolverrSolution struct { - URL string `json:"url"` - Status int `json:"status"` - Headers map[string]string `json:"headers"` + URL string `json:"url"` + Status int `json:"status"` + Headers map[string]string `json:"headers"` Cookies []FlareSolverrCookie `json:"cookies"` - UserAgent string `json:"userAgent"` - Response string `json:"response"` + UserAgent string `json:"userAgent"` + Response string `json:"response"` } // FetchURLWithFlareSolverr uses FlareSolverr to fetch a URL, @@ -123,10 +123,10 @@ func FetchURLWithFlareSolverr(flareSolverrURL string, targetURL string, cookieSt // Build request reqBody := FlareSolverrRequest{ - Cmd: "request.get", - URL: targetURL, + Cmd: "request.get", + URL: targetURL, MaxTimeout: timeout * 1000, - Cookies: cookies, + Cookies: cookies, } if proxyURL != "" { reqBody.Proxy = &FlareSolverrProxy{URL: proxyURL} diff --git a/internal/helper/proxy.go b/internal/helper/proxy.go new file mode 100644 index 0000000..74268cb --- /dev/null +++ b/internal/helper/proxy.go @@ -0,0 +1,75 @@ +package helper + +import ( + "net/http" + "net/url" + "strings" +) + +// ProxyFromEnvironmentOrSystem mirrors http.ProxyFromEnvironment, then falls +// back to the current user's OS proxy settings where supported. +func ProxyFromEnvironmentOrSystem(req *http.Request) (*url.URL, error) { + if proxy, err := http.ProxyFromEnvironment(req); proxy != nil || err != nil { + return proxy, err + } + return systemProxyForRequest(req) +} + +func proxyURLFromProxyServer(proxyServer, requestScheme string) (*url.URL, error) { + proxyServer = strings.TrimSpace(proxyServer) + if proxyServer == "" { + return nil, nil + } + + if !strings.Contains(proxyServer, "=") { + return normalizeProxyURL(proxyServer, "http") + } + + entries := strings.Split(proxyServer, ";") + values := map[string]string{} + first := "" + for _, entry := range entries { + key, value, ok := strings.Cut(strings.TrimSpace(entry), "=") + if !ok { + continue + } + key = strings.ToLower(strings.TrimSpace(key)) + value = strings.TrimSpace(value) + if key == "" || value == "" { + continue + } + if first == "" { + first = value + } + values[key] = value + } + + if value := values[strings.ToLower(requestScheme)]; value != "" { + return normalizeProxyURL(value, strings.ToLower(requestScheme)) + } + if value := values["http"]; value != "" { + return normalizeProxyURL(value, "http") + } + if value := values["https"]; value != "" { + return normalizeProxyURL(value, "http") + } + if value := values["socks"]; value != "" { + return normalizeProxyURL(value, "socks") + } + return normalizeProxyURL(first, "http") +} + +func normalizeProxyURL(raw, proxyKind string) (*url.URL, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, nil + } + if !strings.Contains(raw, "://") { + scheme := "http" + if strings.EqualFold(proxyKind, "socks") { + scheme = "socks5" + } + raw = scheme + "://" + raw + } + return url.Parse(raw) +} diff --git a/internal/helper/proxy_other.go b/internal/helper/proxy_other.go new file mode 100644 index 0000000..4ec5944 --- /dev/null +++ b/internal/helper/proxy_other.go @@ -0,0 +1,12 @@ +//go:build !windows + +package helper + +import ( + "net/http" + "net/url" +) + +func systemProxyForRequest(_ *http.Request) (*url.URL, error) { + return nil, nil +} diff --git a/internal/helper/proxy_windows.go b/internal/helper/proxy_windows.go new file mode 100644 index 0000000..54beef0 --- /dev/null +++ b/internal/helper/proxy_windows.go @@ -0,0 +1,73 @@ +//go:build windows + +package helper + +import ( + "net" + "net/http" + "net/url" + "path/filepath" + "strings" + + "golang.org/x/sys/windows/registry" +) + +const windowsInternetSettingsKey = `Software\Microsoft\Windows\CurrentVersion\Internet Settings` + +func systemProxyForRequest(req *http.Request) (*url.URL, error) { + if req == nil || req.URL == nil { + return nil, nil + } + + key, err := registry.OpenKey(registry.CURRENT_USER, windowsInternetSettingsKey, registry.QUERY_VALUE) + if err != nil { + return nil, nil + } + defer key.Close() + + enabled, _, err := key.GetIntegerValue("ProxyEnable") + if err != nil || enabled == 0 { + return nil, nil + } + proxyServer, _, err := key.GetStringValue("ProxyServer") + if err != nil || strings.TrimSpace(proxyServer) == "" { + return nil, nil + } + + if proxyOverride, _, err := key.GetStringValue("ProxyOverride"); err == nil { + if windowsProxyBypass(req.URL.Hostname(), proxyOverride) { + return nil, nil + } + } + return proxyURLFromProxyServer(proxyServer, req.URL.Scheme) +} + +func windowsProxyBypass(host, override string) bool { + host = strings.ToLower(strings.TrimSpace(host)) + if host == "" { + return false + } + if parsed := net.ParseIP(host); parsed != nil && parsed.IsLoopback() { + return true + } + + for _, rule := range strings.Split(override, ";") { + rule = strings.ToLower(strings.TrimSpace(rule)) + if rule == "" { + continue + } + if rule == "您的家庭影视站暂无内容
- 立即前往管理后台配置您的第一个媒体库,添加影视目录并触发后台刮削扫描任务。 + 前往管理后台添加媒体目录,扫描后首页将展示本周力荐、继续观看和最近入库。
前往管理后台 @@ -107,10 +87,8 @@ export function HomePage() { return (- {featuredItem.overview} -
- ) : ( -- 家庭私人媒体中心收藏。极高视听品质,支持多端原生无损解码及HLS转码播放。 -
- )} ++ {featuredItem.overview || '家庭私人媒体中心收藏。支持多端播放、外部播放器、智能刮削与订阅下载。'} +
- {/* Metadata Badges */}MEDIASTATION PICK
-{featuredItem.title}
-按整部电影、剧集、番剧和综艺合集展示新增内容。
媒体库加载中…
} return ( -所有电影、剧集、番剧、综艺分类统一在这里查看。
+按影视分类浏览所有目录;每个文件夹会自动使用库内海报生成封面。
+暂无媒体库,请到管理后台添加目录。
{lib.path}
- -{preview.total.toLocaleString()} 个媒体条目
+{library.path}
+ + 进入 + +https://api2.m-team.cchttps://api.m-team.cchttps://test2.m-team.cc(开发测试环境){form.type === 'mteam' - ? '馒头:控制台 → 实验室 → 存取令牌' + ? '馒头:控制台 → 实验室 → 存取令牌;第三方工具通过 x-api-key 请求头访问' : '站点的访问 API Key'}