🔧 Fix Cloudflare build issues and restore Docker support
✅ Cloudflare Pages Fixes: - Fix IPTV page Suspense boundary issue for static generation - Resolve ESLint warnings (unused imports, console statements) - Remove dependency on useSearchParams for static builds - Improve error handling without console.error 🐳 Docker Support Restored: - Add complete Docker configuration (Dockerfile, docker-compose.yml) - Support both basic (localstorage) and Redis deployment modes - Multi-platform builds (amd64/arm64) via GitHub Actions - Production-ready with health checks and proper security 📚 Documentation Updates: - Comprehensive deployment guide for all platforms - Docker quick start and production examples - Environment variable documentation - Updated README with multi-platform support 🎯 Key Improvements: - Multiple deployment options: Cloudflare + Docker + Vercel - Better error boundaries and loading states - Optimized build processes for each platform - Enhanced developer experience This fixes the Cloudflare Pages deployment while providing Docker as an alternative self-hosting option.pull/2/head
parent
d811fb5aa6
commit
b8e1eaab17
|
@ -0,0 +1,61 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build
|
||||||
|
|
||||||
|
# Production
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose*.yml
|
||||||
|
|
||||||
|
# GitHub
|
||||||
|
.github/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.tgz
|
||||||
|
*.tar.gz
|
|
@ -0,0 +1,164 @@
|
||||||
|
name: Build Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- '.github/**'
|
||||||
|
- '!.github/workflows/docker-build.yml'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha,scope=${{ github.ref_name }}-${{ matrix.platform }}
|
||||||
|
cache-to: type=gha,mode=max,scope=${{ github.ref_name }}-${{ matrix.platform }}
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-${{ strategy.job-index }}
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
needs: build
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/digests
|
||||||
|
pattern: digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: /tmp/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Inspect image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Docker Build Summary
|
||||||
|
run: |
|
||||||
|
echo "🐳 Docker build completed!"
|
||||||
|
echo "📦 Multi-platform support: linux/amd64, linux/arm64"
|
||||||
|
echo "🔄 Cache optimization enabled"
|
||||||
|
if [ "${{ github.event_name }}" != "pull_request" ]; then
|
||||||
|
echo "🚀 Images pushed to GitHub Container Registry"
|
||||||
|
echo "📋 Available tags:"
|
||||||
|
echo " - latest (from main branch)"
|
||||||
|
echo " - version tags (from git tags)"
|
||||||
|
echo " - branch names (from pushes)"
|
||||||
|
else
|
||||||
|
echo "🧪 Build test completed (no push for PR)"
|
||||||
|
fi
|
|
@ -0,0 +1,64 @@
|
||||||
|
# ---- 第 1 阶段:安装依赖 ----
|
||||||
|
FROM --platform=$BUILDPLATFORM node:20-alpine AS deps
|
||||||
|
|
||||||
|
# 启用 corepack 并激活 pnpm(Node20 默认提供 corepack)
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 仅复制依赖清单,提高构建缓存利用率
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
# 安装所有依赖(含 devDependencies,后续会裁剪)
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# ---- 第 2 阶段:构建项目 ----
|
||||||
|
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制依赖
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
# 复制全部源代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 设置Docker环境标识
|
||||||
|
ENV DOCKER_ENV=true
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# For Docker builds, force dynamic rendering to read runtime environment variables.
|
||||||
|
RUN sed -i "/const inter = Inter({ subsets: \['latin'] });/a export const dynamic = 'force-dynamic';" src/app/layout.tsx
|
||||||
|
|
||||||
|
# 生成生产构建
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# ---- 第 3 阶段:生成运行时镜像 ----
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
|
||||||
|
# 创建非 root 用户
|
||||||
|
RUN addgroup -g 1001 -S nodejs && adduser -u 1001 -S nextjs -G nodejs
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV DOCKER_ENV=true
|
||||||
|
|
||||||
|
# 从构建器中复制 standalone 输出
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
# 从构建器中复制 scripts 目录
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts
|
||||||
|
# 从构建器中复制 start.js
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/start.js ./start.js
|
||||||
|
# 从构建器中复制 public 和 .next/static 目录
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/config.json ./config.json
|
||||||
|
|
||||||
|
# 切换到非特权用户
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# 使用自定义启动脚本,先预加载配置再启动服务器
|
||||||
|
CMD ["node", "start.js"]
|
107
README.md
107
README.md
|
@ -43,6 +43,8 @@
|
||||||
- [技术栈](#技术栈)
|
- [技术栈](#技术栈)
|
||||||
- [快速部署](#快速部署)
|
- [快速部署](#快速部署)
|
||||||
- [Cloudflare Pages 部署](#cloudflare-pages-部署)
|
- [Cloudflare Pages 部署](#cloudflare-pages-部署)
|
||||||
|
- [Docker 部署](#docker-部署)
|
||||||
|
- [Vercel 部署](#vercel-部署)
|
||||||
- [环境变量](#环境变量)
|
- [环境变量](#环境变量)
|
||||||
- [配置说明](#配置说明)
|
- [配置说明](#配置说明)
|
||||||
- [IPTV功能](#iptv功能)
|
- [IPTV功能](#iptv功能)
|
||||||
|
@ -62,16 +64,24 @@
|
||||||
| 语言 | TypeScript 4 |
|
| 语言 | TypeScript 4 |
|
||||||
| 播放器 | [ArtPlayer](https://github.com/zhw2590582/ArtPlayer) · [HLS.js](https://github.com/video-dev/hls.js/) |
|
| 播放器 | [ArtPlayer](https://github.com/zhw2590582/ArtPlayer) · [HLS.js](https://github.com/video-dev/hls.js/) |
|
||||||
| 代码质量 | ESLint · Prettier · Jest |
|
| 代码质量 | ESLint · Prettier · Jest |
|
||||||
| 部署 | Cloudflare Pages · Vercel |
|
| 部署 | Cloudflare Pages · Docker · Vercel |
|
||||||
|
|
||||||
## 快速部署
|
## 快速部署
|
||||||
|
|
||||||
本项目专为 **Cloudflare Pages** 优化,推荐使用Cloudflare部署以获得最佳性能。
|
KatelyaTV 支持多种部署方式,你可以根据需求选择最适合的方案:
|
||||||
|
|
||||||
### 一键部署到Cloudflare Pages
|
### 🚀 推荐部署方案
|
||||||
|
|
||||||
|
1. **Cloudflare Pages** - 免费、全球CDN、无服务器
|
||||||
|
2. **Docker** - 自托管、完全控制、支持Redis
|
||||||
|
3. **Vercel** - 快速部署、适合开发测试
|
||||||
|
|
||||||
|
### 一键部署
|
||||||
|
|
||||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/katelya77/KatelyaTV)
|
[](https://deploy.workers.cloudflare.com/?url=https://github.com/katelya77/KatelyaTV)
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https://github.com/katelya77/KatelyaTV)
|
||||||
|
|
||||||
## Cloudflare Pages 部署
|
## Cloudflare Pages 部署
|
||||||
|
|
||||||
**Cloudflare Pages 的环境变量建议设置为密钥而非文本**
|
**Cloudflare Pages 的环境变量建议设置为密钥而非文本**
|
||||||
|
@ -167,6 +177,97 @@ CREATE INDEX IF NOT EXISTS idx_play_records_updated ON play_records(updatedAt);
|
||||||
- `PASSWORD`: 管理员密码
|
- `PASSWORD`: 管理员密码
|
||||||
4. **重新部署**
|
4. **重新部署**
|
||||||
|
|
||||||
|
## Docker 部署
|
||||||
|
|
||||||
|
Docker部署适合需要完全控制和自托管的用户,支持本地存储和Redis两种模式。
|
||||||
|
|
||||||
|
### 快速开始 (本地存储)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 下载项目
|
||||||
|
git clone https://github.com/katelya77/KatelyaTV.git
|
||||||
|
cd KatelyaTV
|
||||||
|
|
||||||
|
# 使用基础配置启动
|
||||||
|
docker-compose --profile basic up -d
|
||||||
|
|
||||||
|
# 或者直接运行
|
||||||
|
docker run -d \
|
||||||
|
--name katelyatv \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-e PASSWORD=your_password \
|
||||||
|
ghcr.io/katelya77/katelyatv:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 `http://localhost:3000` 即可使用。
|
||||||
|
|
||||||
|
### 生产环境 (Redis)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用Redis配置启动
|
||||||
|
docker-compose --profile redis up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
这将启动:
|
||||||
|
- KatelyaTV 主服务 (端口3000)
|
||||||
|
- Redis 数据库 (数据持久化)
|
||||||
|
- 自动网络配置
|
||||||
|
|
||||||
|
### 自定义配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.override.yml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
katelyatv:
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_SITE_NAME=我的影视站
|
||||||
|
- USERNAME=admin
|
||||||
|
- PASSWORD=secure_password
|
||||||
|
volumes:
|
||||||
|
- ./custom-config.json:/app/config.json:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker环境变量
|
||||||
|
|
||||||
|
| 变量名 | 说明 | 默认值 |
|
||||||
|
|--------|------|--------|
|
||||||
|
| USERNAME | 管理员用户名 | admin |
|
||||||
|
| PASSWORD | 管理员密码 | 必填 |
|
||||||
|
| NEXT_PUBLIC_STORAGE_TYPE | 存储类型 | localstorage |
|
||||||
|
| REDIS_URL | Redis连接URL | 空 |
|
||||||
|
| NEXT_PUBLIC_SITE_NAME | 站点名称 | KatelyaTV |
|
||||||
|
|
||||||
|
### 更新镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 拉取最新镜像
|
||||||
|
docker-compose pull
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
docker-compose --profile redis up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vercel 部署
|
||||||
|
|
||||||
|
### 基础部署
|
||||||
|
|
||||||
|
1. Fork 本仓库到你的 GitHub 账户
|
||||||
|
2. 在 [Vercel](https://vercel.com) 导入项目
|
||||||
|
3. 设置环境变量 `PASSWORD`
|
||||||
|
4. 部署完成
|
||||||
|
|
||||||
|
### Upstash Redis 支持
|
||||||
|
|
||||||
|
1. 在 [Upstash](https://upstash.com) 创建Redis实例
|
||||||
|
2. 在Vercel设置环境变量:
|
||||||
|
- `NEXT_PUBLIC_STORAGE_TYPE`: `upstash`
|
||||||
|
- `UPSTASH_URL`: Redis端点
|
||||||
|
- `UPSTASH_TOKEN`: Redis令牌
|
||||||
|
- `USERNAME`: 管理员用户名
|
||||||
|
- `PASSWORD`: 管理员密码
|
||||||
|
3. 重新部署
|
||||||
|
|
||||||
## 环境变量
|
## 环境变量
|
||||||
|
|
||||||
| 变量 | 说明 | 可选值 | 默认值 |
|
| 变量 | 说明 | 可选值 | 默认值 |
|
||||||
|
|
25
RELEASE.md
25
RELEASE.md
|
@ -13,7 +13,7 @@
|
||||||
- **移动端优化**:完美适配手机和平板设备
|
- **移动端优化**:完美适配手机和平板设备
|
||||||
|
|
||||||
### 🛠️ 技术架构升级
|
### 🛠️ 技术架构升级
|
||||||
- **专注Cloudflare部署**:移除Docker配置,专为Cloudflare Pages优化
|
- **多平台部署支持**:Cloudflare Pages、Docker、Vercel三种部署方式
|
||||||
- **iOS Safari完美兼容**:修复iOS设备登录界面显示问题
|
- **iOS Safari完美兼容**:修复iOS设备登录界面显示问题
|
||||||
- **性能大幅提升**:优化资源加载和渲染性能
|
- **性能大幅提升**:优化资源加载和渲染性能
|
||||||
- **代码质量改进**:TypeScript严格模式,更好的错误处理
|
- **代码质量改进**:TypeScript严格模式,更好的错误处理
|
||||||
|
@ -33,10 +33,10 @@
|
||||||
|
|
||||||
## 🚀 部署和配置优化
|
## 🚀 部署和配置优化
|
||||||
|
|
||||||
### Cloudflare Pages专属优化
|
### 多平台部署优化
|
||||||
- **一键部署**:简化的Cloudflare Pages部署流程
|
- **Cloudflare Pages**:一键部署,全球CDN,D1数据库集成
|
||||||
- **环境变量优化**:更清晰的配置说明和最佳实践
|
- **Docker支持**:完整的Docker配置,支持Redis数据库
|
||||||
- **D1数据库集成**:完整的SQL初始化脚本
|
- **Vercel部署**:快速部署,Upstash Redis集成
|
||||||
- **性能监控**:内置性能优化和错误追踪
|
- **性能监控**:内置性能优化和错误追踪
|
||||||
|
|
||||||
### 配置管理改进
|
### 配置管理改进
|
||||||
|
@ -66,8 +66,8 @@
|
||||||
- **性能优化**:减少bundle大小,提升加载速度
|
- **性能优化**:减少bundle大小,提升加载速度
|
||||||
|
|
||||||
### 构建优化
|
### 构建优化
|
||||||
- **移除Docker依赖**:简化部署流程
|
- **多平台CI/CD**:支持Cloudflare和Docker构建
|
||||||
- **Cloudflare工作流**:专属的CI/CD流程
|
- **GitHub Actions优化**:并行构建,多架构支持
|
||||||
- **缓存策略优化**:更好的静态资源管理
|
- **缓存策略优化**:更好的静态资源管理
|
||||||
- **错误处理增强**:更友好的错误提示
|
- **错误处理增强**:更友好的错误提示
|
||||||
|
|
||||||
|
@ -101,10 +101,13 @@
|
||||||
# 新增环境变量
|
# 新增环境变量
|
||||||
NEXT_PUBLIC_SITE_NAME=KatelyaTV
|
NEXT_PUBLIC_SITE_NAME=KatelyaTV
|
||||||
|
|
||||||
# 移除的环境变量(不再需要)
|
# Docker环境变量(新增)
|
||||||
DOCKER_ENV
|
DOCKER_ENV=true
|
||||||
HOSTNAME
|
HOSTNAME=0.0.0.0
|
||||||
PORT
|
PORT=3000
|
||||||
|
|
||||||
|
# 支持的存储类型
|
||||||
|
NEXT_PUBLIC_STORAGE_TYPE=localstorage|d1|upstash|redis
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🙏 致谢
|
## 🙏 致谢
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# 基础版本 - 使用本地存储
|
||||||
|
katelyatv-basic:
|
||||||
|
build: .
|
||||||
|
container_name: katelyatv-basic
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
environment:
|
||||||
|
- PASSWORD=your_password
|
||||||
|
- NEXT_PUBLIC_SITE_NAME=KatelyaTV
|
||||||
|
# 如需自定义配置,可挂载文件
|
||||||
|
# volumes:
|
||||||
|
# - ./config.json:/app/config.json:ro
|
||||||
|
profiles:
|
||||||
|
- basic
|
||||||
|
|
||||||
|
# Redis版本 - 推荐用于生产环境
|
||||||
|
katelyatv:
|
||||||
|
build: .
|
||||||
|
container_name: katelyatv
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
environment:
|
||||||
|
- USERNAME=admin
|
||||||
|
- PASSWORD=admin_password
|
||||||
|
- NEXT_PUBLIC_STORAGE_TYPE=redis
|
||||||
|
- REDIS_URL=redis://katelyatv-redis:6379
|
||||||
|
- NEXT_PUBLIC_ENABLE_REGISTER=true
|
||||||
|
- NEXT_PUBLIC_SITE_NAME=KatelyaTV
|
||||||
|
networks:
|
||||||
|
- katelyatv-network
|
||||||
|
depends_on:
|
||||||
|
- katelyatv-redis
|
||||||
|
# 如需自定义配置,可挂载文件
|
||||||
|
# volumes:
|
||||||
|
# - ./config.json:/app/config.json:ro
|
||||||
|
profiles:
|
||||||
|
- redis
|
||||||
|
|
||||||
|
katelyatv-redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: katelyatv-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
networks:
|
||||||
|
- katelyatv-network
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
profiles:
|
||||||
|
- redis
|
||||||
|
|
||||||
|
networks:
|
||||||
|
katelyatv-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis-data:
|
|
@ -1,11 +1,11 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
import { ArrowLeft, Upload, Download, RefreshCw, Settings, Tv } from 'lucide-react';
|
import { ArrowLeft, Download, RefreshCw, Tv, Upload } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import IPTVPlayer from '@/components/IPTVPlayer';
|
|
||||||
import IPTVChannelList from '@/components/IPTVChannelList';
|
import IPTVChannelList from '@/components/IPTVChannelList';
|
||||||
|
import IPTVPlayer from '@/components/IPTVPlayer';
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
|
|
||||||
interface IPTVChannel {
|
interface IPTVChannel {
|
||||||
|
@ -19,15 +19,18 @@ interface IPTVChannel {
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function IPTVPage() {
|
function IPTVPageContent() {
|
||||||
const [channels, setChannels] = useState<IPTVChannel[]>([]);
|
const [channels, setChannels] = useState<IPTVChannel[]>([]);
|
||||||
const [currentChannel, setCurrentChannel] = useState<IPTVChannel | undefined>();
|
const [currentChannel, setCurrentChannel] = useState<IPTVChannel | undefined>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [showChannelList, setShowChannelList] = useState(true);
|
const [showChannelList, setShowChannelList] = useState(true);
|
||||||
const [m3uUrl, setM3uUrl] = useState('');
|
const [m3uUrl, setM3uUrl] = useState('');
|
||||||
|
|
||||||
// 示例频道数据 (可以从M3U文件加载)
|
|
||||||
const sampleChannels: IPTVChannel[] = [
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 示例频道数据
|
||||||
|
const defaultChannels: IPTVChannel[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'CGTN',
|
name: 'CGTN',
|
||||||
|
@ -66,19 +69,18 @@ export default function IPTVPage() {
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// 从本地存储加载频道列表
|
// 从本地存储加载频道列表
|
||||||
const savedChannels = localStorage.getItem('iptv-channels');
|
const savedChannels = localStorage.getItem('iptv-channels');
|
||||||
if (savedChannels) {
|
if (savedChannels) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(savedChannels);
|
const parsed = JSON.parse(savedChannels);
|
||||||
setChannels(parsed);
|
setChannels(parsed);
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error('加载频道列表失败:', error);
|
// 加载失败时使用示例频道
|
||||||
setChannels(sampleChannels);
|
setChannels(defaultChannels);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setChannels(sampleChannels);
|
setChannels(defaultChannels);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载上次选择的频道
|
// 加载上次选择的频道
|
||||||
|
@ -87,8 +89,8 @@ export default function IPTVPage() {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(savedCurrentChannel);
|
const parsed = JSON.parse(savedCurrentChannel);
|
||||||
setCurrentChannel(parsed);
|
setCurrentChannel(parsed);
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error('加载当前频道失败:', error);
|
// 加载当前频道失败时忽略
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -168,8 +170,8 @@ export default function IPTVPage() {
|
||||||
} else {
|
} else {
|
||||||
alert('M3U文件解析失败,请检查文件格式');
|
alert('M3U文件解析失败,请检查文件格式');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error('加载M3U失败:', error);
|
// 加载M3U失败
|
||||||
alert('加载M3U文件失败,请检查URL是否正确');
|
alert('加载M3U文件失败,请检查URL是否正确');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -374,3 +376,22 @@ export default function IPTVPage() {
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function IPTVPage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={
|
||||||
|
<PageLayout>
|
||||||
|
<div className="container mx-auto px-4 py-6">
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-500 mx-auto mb-4"></div>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">加载IPTV播放器...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
}>
|
||||||
|
<IPTVPageContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { Search, Play, Star, StarOff, Tv, Globe, Heart } from 'lucide-react';
|
import { Globe, Heart, Play, Search, Star, StarOff, Tv } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
interface IPTVChannel {
|
interface IPTVChannel {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Play, Pause, Volume2, VolumeX, Maximize, Settings } from 'lucide-react';
|
import { Maximize, Pause, Play, Volume2, VolumeX } from 'lucide-react';
|
||||||
|
|
||||||
interface IPTVChannel {
|
interface IPTVChannel {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -17,7 +17,7 @@ interface IPTVPlayerProps {
|
||||||
onChannelChange?: (channel: IPTVChannel) => void;
|
onChannelChange?: (channel: IPTVChannel) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IPTVPlayer({ channels, currentChannel, onChannelChange }: IPTVPlayerProps) {
|
export function IPTVPlayer({ channels, currentChannel, onChannelChange: _onChannelChange }: IPTVPlayerProps) {
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
|
@ -38,7 +38,9 @@ export function IPTVPlayer({ channels, currentChannel, onChannelChange }: IPTVPl
|
||||||
const handleCanPlay = () => {
|
const handleCanPlay = () => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
video.play().catch(console.error);
|
video.play().catch(() => {
|
||||||
|
// 忽略播放错误
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleError = () => {
|
const handleError = () => {
|
||||||
|
@ -68,7 +70,9 @@ export function IPTVPlayer({ channels, currentChannel, onChannelChange }: IPTVPl
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
video.pause();
|
video.pause();
|
||||||
} else {
|
} else {
|
||||||
video.play().catch(console.error);
|
video.play().catch(() => {
|
||||||
|
// 忽略播放错误
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setIsPlaying(!isPlaying);
|
setIsPlaying(!isPlaying);
|
||||||
};
|
};
|
||||||
|
@ -97,7 +101,9 @@ export function IPTVPlayer({ channels, currentChannel, onChannelChange }: IPTVPl
|
||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
} else {
|
} else {
|
||||||
video.requestFullscreen().catch(console.error);
|
video.requestFullscreen().catch(() => {
|
||||||
|
// 忽略全屏错误
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,12 +117,13 @@ export function IPTVPlayer({ channels, currentChannel, onChannelChange }: IPTVPl
|
||||||
}, 3000);
|
}, 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const groupedChannels = channels.reduce((acc, channel) => {
|
// 按组分类频道(暂未使用,为未来功能预留)
|
||||||
const group = channel.group || '其他';
|
// const groupedChannels = channels.reduce((acc, channel) => {
|
||||||
if (!acc[group]) acc[group] = [];
|
// const group = channel.group || '其他';
|
||||||
acc[group].push(channel);
|
// if (!acc[group]) acc[group] = [];
|
||||||
return acc;
|
// acc[group].push(channel);
|
||||||
}, {} as Record<string, IPTVChannel[]>);
|
// return acc;
|
||||||
|
// }, {} as Record<string, IPTVChannel[]>);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full bg-black rounded-lg overflow-hidden">
|
<div className="relative w-full h-full bg-black rounded-lg overflow-hidden">
|
||||||
|
|
Loading…
Reference in New Issue