33 Commits

Author SHA1 Message Date
katelya 22c68b7e19 feat: remove outdated documentation and fix overlay issue in SkipController
- Deleted SKIP_CONTROLLER_TEST.md and SKIP_CONTROLLER_UPDATE.md as they are no longer relevant.
- Removed SKIP_FEATURE_GUIDE.md to streamline user documentation.
- Eliminated SKIP_OVERLAY_FIX.md after addressing the click offset issue caused by the SkipController overlay.
- Improved user experience by ensuring the SkipController does not interfere with episode selection.
2025-09-04 18:10:59 +08:00
katelya 82485d1939 feat: Add Docker Compose configurations for Kvrocks and Redis deployments
- Implemented `docker-compose.kvrocks.auth.yml` for Kvrocks with password authentication.
- Created `docker-compose.redis.yml` for Redis deployment.
- Added Kvrocks configuration file `kvrocks.auth.conf` with necessary settings.
- Updated documentation with deployment guidelines for Kvrocks.
- Introduced ESLint configuration for code quality.
- Developed deployment configuration check script `check-deployment-configs.js`.
- Added D1 database initialization script `d1-init.sql` for KatelyaTV.
- Created test script `test-kvrocks-deployment.js` to validate Kvrocks deployment.
- Implemented fix verification script `verify-kvrocks-fix.js` for password handling.
- Updated `wrangler.toml` for Cloudflare deployment configuration.
2025-09-04 17:55:23 +08:00
katelya 63120d418b feat: 实现真正的无限滚动加载
- 修改 PaginatedRow 组件支持动态加载更多数据
- 添加 onLoadMore 回调函数和加载状态管理
- 在首页三个版块实现真正的分页加载新内容
- 第一页时隐藏左箭头,避免无效操作
- 移除底部页码指示器,界面更简洁
- 右箭头点击时动态从豆瓣API加载新数据
2025-09-04 16:36:59 +08:00
katelya 8f23545439 feat: 优化PaginatedRow组件的翻页逻辑,确保向前翻页不超出范围并支持无限循环翻页 2025-09-04 16:10:38 +08:00
katelya 62f70a9bf5 feat: 优化PaginatedRow组件的翻页逻辑,改进按钮显示条件 2025-09-04 16:07:45 +08:00
katelya 275b5ed9d0 feat: 添加站点访问密码配置到docker-compose文件 2025-09-04 16:00:45 +08:00
katelya dcc6d4cef2 feat: 优化PaginatedRow组件,支持无限翻页和悬停显示导航按钮 2025-09-04 15:45:47 +08:00
katelya c5c8aa43f2 feat: 重新设计PaginatedRow组件,优化首页热门板块的切页体验
新功能:
- 实现无限循环翻页,不再局限于有限页数
- 重新设计翻页按钮,使用紫色渐变和更好的悬停效果
- 按钮位置居中对齐,放在两行内容的中间位置
- 为每个组件实例添加唯一ID,避免跨板块悬停效果冲突

 设计改进:
- 按钮使用渐变背景和阴影效果,提升视觉体验
- 优化按钮尺寸和间距,更加美观
- 改进页码指示器的动画效果
- 修复悬停状态下其他板块也高亮的问题

 Bug修复:
- 解决鼠标悬停在一个影视卡片时其他板块卡片也高亮的问题
- 修复只能显示两批内容的限制,现在支持无限循环
- 优化按钮定位,确保在各种屏幕尺寸下都能正确居中
2025-09-04 15:35:10 +08:00
katelya 07a68b01a4 build: 更新版本号到 20250904151930
- 使用 generate-version.js 脚本生成新版本号
- 同步更新 src/lib/version.ts 和 VERSION.txt
- 确保前端显示正确的版本信息
- 触发 Docker 镜像重新构建
2025-09-04 15:19:50 +08:00
katelya fb47b3d358 docs: 优化 Cloudflare Pages 和 Vercel 配置文件部署指导
- 详细说明配置文件的正确下载和使用方法
- 添加常见错误的排查步骤和解决方案
- 明确指出不要直接复制网页内容,需下载文件
- 提供 JSON 格式验证和故障排除指南
- 更新版本号以触发 Docker 镜像重新构建
2025-09-04 15:12:24 +08:00
katelya b73b52bc05 fix: 优化README.md中的部署方式对比表格格式和环境变量说明 2025-09-04 15:07:42 +08:00
katelya 235259c24d docs: 添加Vercel+Upstash多用户部署完整指导文档
- 新增详细的Vercel+Upstash部署步骤说明
- 更新部署方式对比表,突出Vercel+Upstash方案优势
- 添加多用户系统和跨设备同步功能说明
- 优化推荐方案,推荐Vercel+Upstash作为个人用户首选
- 更新环境变量说明,包含Upstash配置详情
- 添加费用说明和免费额度介绍
2025-09-04 15:07:12 +08:00
katelya c755a6d466 feat: 添加分页组件PaginatedRow,优化首页内容展示逻辑 2025-09-04 14:24:13 +08:00
katelya b9222cf33d feat: 添加对linux/arm/v7平台的支持,优化Docker构建配置 2025-09-04 13:28:39 +08:00
katelya 63d0942b66 feat: 优化影视源类型判断逻辑,支持更智能的API地址解析 2025-09-04 13:18:29 +08:00
katelya d6ea0a4748 feat: 智能判断影视源类型,根据API地址动态设置type 2025-09-04 13:11:36 +08:00
katelya b0deb7eedc 修正配置文件下载地址的格式,添加空格以提高可读性 2025-09-04 12:32:06 +08:00
katelya ab147dd19a feat: 添加站点全局访问密码配置 2025-09-04 12:01:19 +08:00
katelya 4b9f87f7f8 删除旧版本的发布说明文件,更新用户菜单以移除TVBox配置按钮,并在管理页面中添加TVBox配置按钮。 2025-09-04 10:56:44 +08:00
katelya 9083d83355 Merge branch 'main' of https://github.com/katelya77/KatelyaTV 2025-09-04 10:52:32 +08:00
katelya f02a027a2a merge 2025-09-04 10:52:28 +08:00
Katelya 035f15cd7f 更新配置文件的片源数量!
Added download links for Plus version of the configuration file.
2025-09-03 23:14:38 +08:00
katelya db651d5a55 fix: 调整集数网格的列宽和间距,优化布局 2025-09-03 22:38:12 +08:00
katelya f121b06b91 fixed littles bug 2025-09-03 22:29:58 +08:00
katelya 475d8f0334 fix: 调整集数网格的垂直间距,优化布局 2025-09-03 22:20:21 +08:00
katelya b54d626496 fix: 调整集数网格的间距,优化布局 2025-09-03 22:13:37 +08:00
katelya 5202a4b11a fix: 调整集数网格的间距,优化布局 2025-09-03 22:07:04 +08:00
katelya af73306814 fix: 修复选集点击偏移问题,优化事件处理和布局 2025-09-03 22:01:00 +08:00
katelya f0bbcf00dc fix: 修复选集点击偏移问题,调整跳过配置面板位置以避免覆盖 2025-09-03 21:52:08 +08:00
katelya 9aeef4bc63 feat: 修复选集点击偏移问题,优化布局和事件处理 2025-09-03 21:42:00 +08:00
katelya d6e14b2d00 feat: 优化跳过控制器,新增片尾倒计时模式选择,支持剩余时间和绝对时间模式 2025-09-03 21:31:09 +08:00
katelya 981137afe9 feat: 更新 Kvrocks 配置,使用预构建 Docker 镜像并添加故障排除指南 2025-09-03 21:09:38 +08:00
katelya ae22119708 feat: 删除 v0.5.0-katelya 发布记录,准备更新至 v0.6.0-katelya 2025-09-03 20:38:42 +08:00
43 changed files with 2491 additions and 2290 deletions
+52
View File
@@ -0,0 +1,52 @@
# KatelyaTV Cloudflare Pages + D1 部署环境变量示例
# 在 Cloudflare Pages 中设置这些环境变量
# ==================== 数据库配置 ====================
# 存储类型:使用 D1
NEXT_PUBLIC_STORAGE_TYPE=d1
# ==================== 应用配置 ====================
# NextAuth 配置
NEXTAUTH_SECRET=your_nextauth_secret_here_32_chars_min
NEXTAUTH_URL=https://your-domain.pages.dev
# 站点访问密码配置(可选)
# PASSWORD=your_site_password
# 站点配置
NEXT_PUBLIC_SITE_NAME=KatelyaTV
NEXT_PUBLIC_SITE_DESCRIPTION=高性能影视播放平台
# ==================== 可选配置 ====================
# Douban API 配置(可选)
# DOUBAN_API_KEY=your_douban_api_key
# 图片代理配置
IMAGE_PROXY_ENABLED=true
# 缓存配置
CACHE_TTL=3600
# ==================== 安全配置 ====================
# CORS 配置
CORS_ORIGIN=*
# Rate Limiting 配置
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW=60000
# ==================== 监控配置 ====================
# 健康检查配置
HEALTH_CHECK_ENABLED=true
HEALTH_CHECK_INTERVAL=30
# 日志配置
LOG_LEVEL=info
LOG_FORMAT=json
# ==================== 生产环境配置 ====================
NODE_ENV=production
# ==================== Cloudflare 特有配置 ====================
# D1 数据库绑定名称(在 wrangler.toml 中配置)
# D1_DATABASE_BINDING=DB
+6 -3
View File
@@ -6,9 +6,12 @@
NEXT_PUBLIC_STORAGE_TYPE=kvrocks
# Kvrocks 连接配置
KVROCKS_URL=redis://localhost:6666
# KVROCKS_URL=redis://kvrocks:6666 # Docker 部署时使用此配置
KVROCKS_PASSWORD=your_secure_password_here
KVROCKS_URL=redis://kvrocks:6666
# Kvrocks 密码配置(可选)
# 选项1:不使用密码(推荐用于开发环境)
# KVROCKS_PASSWORD=
# 选项2:使用密码(推荐用于生产环境)
# KVROCKS_PASSWORD=your_secure_password_here
KVROCKS_DATABASE=0
# ==================== 应用配置 ====================
+59
View File
@@ -0,0 +1,59 @@
# KatelyaTV Redis 部署环境变量示例
# 复制此文件为 .env 并修改相应值
# ==================== 数据库配置 ====================
# 存储类型:使用 Redis
NEXT_PUBLIC_STORAGE_TYPE=redis
# Redis 连接配置
REDIS_URL=redis://katelyatv-redis:6379
# Redis 密码配置(可选)
# REDIS_PASSWORD=your_redis_password
REDIS_DATABASE=0
# ==================== 应用配置 ====================
# NextAuth 配置
NEXTAUTH_SECRET=your_nextauth_secret_here
NEXTAUTH_URL=http://localhost:3000
# 站点访问密码配置(可选)
# PASSWORD=your_site_password
# 站点配置
NEXT_PUBLIC_SITE_NAME=KatelyaTV
NEXT_PUBLIC_SITE_DESCRIPTION=高性能影视播放平台
# ==================== 部署配置 ====================
# 生产环境配置
NODE_ENV=production
PORT=3000
# Docker 配置
DOCKER_IMAGE_TAG=latest
# ==================== 可选配置 ====================
# Douban API 配置(可选)
# DOUBAN_API_KEY=your_douban_api_key
# 图片代理配置(可选)
IMAGE_PROXY_ENABLED=true
# 缓存配置
CACHE_TTL=3600
# ==================== 安全配置 ====================
# CORS 配置
CORS_ORIGIN=*
# Rate Limiting 配置
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW=60000
# ==================== 监控配置 ====================
# 健康检查配置
HEALTH_CHECK_ENABLED=true
HEALTH_CHECK_INTERVAL=30
# 日志配置
LOG_LEVEL=info
LOG_FORMAT=json
+3 -3
View File
@@ -36,7 +36,7 @@ jobs:
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: false
tags: |
katelyatv:latest
@@ -58,7 +58,7 @@ jobs:
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/katelyatv:latest
@@ -75,7 +75,7 @@ jobs:
- name: Test Summary
run: |
echo "✅ Docker build completed successfully!"
echo "📦 Multi-platform support: linux/amd64, linux/arm64"
echo "📦 Multi-platform support: linux/amd64, linux/arm64, linux/arm/v7"
echo "🔄 Cache optimization enabled"
if [ "${{ github.event_name }}" != "pull_request" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "🚀 Images pushed to GitHub Container Registry"
+19 -13
View File
@@ -16,9 +16,12 @@ concurrency:
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: katelya77/katelyatv
jobs:
build-and-push:
runs-on: ubuntu-latest
env:
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
packages: write
@@ -30,11 +33,13 @@ jobs:
platform:
- linux/amd64
- linux/arm64
- linux/arm/v7
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set image name to lowercase
run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
id: image_name
run: echo "name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
@@ -51,7 +56,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
images: ${{ env.REGISTRY }}/${{ steps.image_name.outputs.name }}
tags: |
type=ref,event=branch
type=ref,event=pr
@@ -62,8 +67,6 @@ jobs:
org.opencontainers.image.description=katelyatv - A modern streaming platform
org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.version=${{ steps.meta.outputs.version }}
org.opencontainers.image.created=${{ steps.meta.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.licenses=MIT
- name: Build Docker image
@@ -77,7 +80,7 @@ jobs:
cache-from: type=gha,scope=${{ github.ref_name }}-${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ github.ref_name }}-${{ matrix.platform }}
outputs: |
type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
type=image,name=${{ env.REGISTRY }}/${{ steps.image_name.outputs.name }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
provenance: false
sbom: false
- name: Export digest
@@ -104,9 +107,12 @@ jobs:
needs:
- build-and-push
if: github.event_name != 'pull_request'
env:
REGISTRY: ghcr.io
steps:
- name: Set image name to lowercase
run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
id: image_name
run: echo "name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Download digests
uses: actions/download-artifact@v4
with:
@@ -125,7 +131,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
images: ${{ env.REGISTRY }}/${{ steps.image_name.outputs.name }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
@@ -134,28 +140,28 @@ jobs:
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 ' *)
$(printf '${{ env.REGISTRY }}/${{ steps.image_name.outputs.name }}@sha256:%s ' *)
- name: Get multi-arch digest
id: get_digest
run: |
# 直接从 docker pull 获取 digest,这是最可靠的方法
digest=$(docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} 2>&1 | grep "Digest:" | cut -d' ' -f2 || echo "")
digest=$(docker pull ${{ env.REGISTRY }}/${{ steps.image_name.outputs.name }}:${{ steps.meta.outputs.version }} 2>&1 | grep "Digest:" | cut -d' ' -f2 || echo "")
if [ -z "$digest" ]; then
# 备选方案:使用 crane 风格的检查(如果支持的话)
digest=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} | grep "Digest:" | head -1 | cut -d' ' -f2 || echo "")
digest=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ steps.image_name.outputs.name }}:${{ steps.meta.outputs.version }} | grep "Digest:" | head -1 | cut -d' ' -f2 || echo "")
fi
if [ -z "$digest" ]; then
# 最后备选:从 raw manifest 计算
digest=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} --raw | sha256sum | awk '{print "sha256:"$1}')
digest=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ steps.image_name.outputs.name }}:${{ steps.meta.outputs.version }} --raw | sha256sum | awk '{print "sha256:"$1}')
fi
echo "digest=$digest" >> $GITHUB_OUTPUT
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ steps.image_name.outputs.name }}:${{ steps.meta.outputs.version }}
- name: Generate artifact attestation
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-name: ${{ env.REGISTRY }}/${{ steps.image_name.outputs.name}}
subject-digest: ${{ steps.get_digest.outputs.digest }}
push-to-registry: true
+13
View File
@@ -8,8 +8,21 @@
- 🖱️ **用户界面优化**
- 在用户菜单中新增"TVBox配置"按钮,提供便捷的配置入口
- 新增电视图标(Tv)标识,界面更加直观
- 🎬 **跳过控制器增强**
- 新增片尾倒计时模式选择:支持剩余时间模式和绝对时间模式
- 剩余时间模式:基于视频剩余时间进行倒计时(推荐)
- 绝对时间模式:基于视频播放时间进行检测(兼容旧版本)
- 优化用户界面,提供更清晰的配置说明和帮助文本
- 优化用户体验,一键访问TVBox配置页面
### 🐛 Bug修复
- 🎯 **选集点击精确性修复**
- 修复选集界面点击偏移问题,确保点击哪个集数就选择哪个集数
- 问题根因:SkipController的固定定位面板(bottom-4 right-4)覆盖了选集面板右下角
- 解决方案:将跳过配置面板移动到左下角(bottom-4 left-4),避免与选集面板冲突
- 保持所有跳过功能正常工作,仅调整UI布局避免重叠
### 🔧 重要改进
- 🔓 **TVBox API 认证优化**
- **重要变更**TVBox API (`/api/tvbox`) 现已开放无需认证访问
-94
View File
@@ -1,94 +0,0 @@
```sql
CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY,
password TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
CREATE TABLE IF NOT EXISTS play_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
key TEXT NOT NULL,
title TEXT NOT NULL,
source_name TEXT NOT NULL,
cover TEXT NOT NULL,
year TEXT NOT NULL,
index_episode INTEGER NOT NULL,
total_episodes INTEGER NOT NULL,
play_time INTEGER NOT NULL,
total_time INTEGER NOT NULL,
save_time INTEGER NOT NULL,
search_title TEXT,
UNIQUE(username, key)
);
CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
key TEXT NOT NULL,
title TEXT NOT NULL,
source_name TEXT NOT NULL,
cover TEXT NOT NULL,
year TEXT NOT NULL,
total_episodes INTEGER NOT NULL,
save_time INTEGER NOT NULL,
UNIQUE(username, key)
);
CREATE TABLE IF NOT EXISTS search_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
keyword TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(username, keyword)
);
CREATE TABLE IF NOT EXISTS admin_config (
id INTEGER PRIMARY KEY DEFAULT 1,
config TEXT NOT NULL,
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
CREATE TABLE IF NOT EXISTS skip_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
key TEXT NOT NULL,
source TEXT NOT NULL,
video_id TEXT NOT NULL,
title TEXT NOT NULL,
segments TEXT NOT NULL,
updated_time INTEGER NOT NULL,
UNIQUE(username, key)
);
-- 基本索引
CREATE INDEX IF NOT EXISTS idx_play_records_username ON play_records(username);
CREATE INDEX IF NOT EXISTS idx_favorites_username ON favorites(username);
CREATE INDEX IF NOT EXISTS idx_search_history_username ON search_history(username);
CREATE INDEX IF NOT EXISTS idx_skip_configs_username ON skip_configs(username);
-- 复合索引优化查询性能
-- 播放记录:用户名+键值的复合索引,用于快速查找特定记录
CREATE INDEX IF NOT EXISTS idx_play_records_username_key ON play_records(username, key);
-- 播放记录:用户名+保存时间的复合索引,用于按时间排序的查询
CREATE INDEX IF NOT EXISTS idx_play_records_username_save_time ON play_records(username, save_time DESC);
-- 收藏:用户名+键值的复合索引,用于快速查找特定收藏
CREATE INDEX IF NOT EXISTS idx_favorites_username_key ON favorites(username, key);
-- 收藏:用户名+保存时间的复合索引,用于按时间排序的查询
CREATE INDEX IF NOT EXISTS idx_favorites_username_save_time ON favorites(username, save_time DESC);
-- 搜索历史:用户名+关键词的复合索引,用于快速查找/删除特定搜索记录
CREATE INDEX IF NOT EXISTS idx_search_history_username_keyword ON search_history(username, keyword);
-- 搜索历史:用户名+创建时间的复合索引,用于按时间排序的查询
CREATE INDEX IF NOT EXISTS idx_search_history_username_created_at ON search_history(username, created_at DESC);
-- 搜索历史清理查询的优化索引
CREATE INDEX IF NOT EXISTS idx_search_history_username_id_created_at ON search_history(username, id, created_at DESC);
-- 跳过配置索引
-- 跳过配置:用户名+键值的复合索引,用于快速查找特定配置
CREATE INDEX IF NOT EXISTS idx_skip_configs_username_key ON skip_configs(username, key);
-- 跳过配置:用户名+更新时间的复合索引,用于按时间排序的查询
CREATE INDEX IF NOT EXISTS idx_skip_configs_username_updated_time ON skip_configs(username, updated_time DESC);
```
-226
View File
@@ -1,226 +0,0 @@
# 🚀 部署兼容性说明
## 跳过片头片尾功能部署兼容性
我们的跳过片头片尾功能已经完全兼容各种部署方式,具体如下:
## 📋 功能概述
-**自动跳过片头片尾** - 智能检测并跳过重复内容
-**手动配置跳过段** - 用户可自定义跳过时间段
-**多剧集支持** - 每个剧集独立配置
-**多存储后端** - 支持 LocalStorage、Redis、D1、Upstash
## 🌐 部署方式兼容性
### 1. Cloudflare Pages ✅
**Runtime**: Edge Runtime
**配置要求**: 所有 API 路由必须使用 `export const runtime = 'edge';`
```typescript
// ✅ 已正确配置
export const runtime = 'edge';
```
**特性支持**:
- ✅ 跳过配置 API (`/api/skip-configs`)
- ✅ 所有存储后端(D1、Redis、Upstash
- ✅ 自动缓存优化
### 2. Docker 部署 ✅
**Runtime**: Node.js Runtime (自动转换)
**自动转换**: Dockerfile 会自动将 Edge Runtime 转换为 Node.js Runtime
```dockerfile
# Dockerfile 中的自动转换逻辑
RUN find ./src -type f -name "route.ts" -print0 \
| xargs -0 sed -i "s/export const runtime = 'edge';/export const runtime = 'nodejs';/g"
```
**特性支持**:
- ✅ 跳过配置 API
- ✅ 所有存储后端
- ✅ 环境变量配置
- ✅ 健康检查
### 3. Vercel 部署 ✅
**Runtime**: Edge Runtime / Node.js Runtime (自动检测)
**配置**: 无需特殊配置,自动适配
**特性支持**:
- ✅ 跳过配置 API
- ✅ 所有存储后端
- ✅ Serverless 函数优化
### 4. 其他部署方式 ✅
**Runtime**: Node.js Runtime
**要求**: Node.js 18+ 环境
**支持的部署方式**:
- ✅ 传统服务器部署
- ✅ PM2 进程管理
- ✅ Nginx 反向代理
- ✅ Kubernetes
- ✅ Railway、Render 等云平台
## 🗄️ 存储后端支持
### LocalStorage (默认)
```bash
# 无需额外配置,适用于单机部署
NEXT_PUBLIC_STORAGE_TYPE=localstorage
```
### Redis
```bash
# 高性能缓存存储
NEXT_PUBLIC_STORAGE_TYPE=redis
REDIS_URL=redis://localhost:6379
```
### Cloudflare D1
```bash
# Cloudflare 原生数据库
NEXT_PUBLIC_STORAGE_TYPE=d1
```
### Upstash Redis
```bash
# 无服务器 Redis
NEXT_PUBLIC_STORAGE_TYPE=upstash
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=xxx
```
## 🔧 环境变量配置
### 核心配置
```bash
# 存储类型 (必需)
NEXT_PUBLIC_STORAGE_TYPE=localstorage|redis|d1|upstash
# Docker 环境标识 (Docker 部署时自动设置)
DOCKER_ENV=true
```
### 存储特定配置
```bash
# Redis
REDIS_URL=redis://localhost:6379
REDIS_PASSWORD=optional
# Upstash
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=xxx
# D1 (Cloudflare 自动注入)
# 无需手动配置
```
## 🚀 快速部署指南
### Cloudflare Pages
1. 连接 GitHub 仓库
2. 设置构建命令: `npm run build`
3. 设置输出目录: `.next`
4. 配置环境变量 (可选)
### Docker
```bash
# 构建镜像
docker build -t katelyatv .
# 运行容器
docker run -p 3000:3000 \
-e NEXT_PUBLIC_STORAGE_TYPE=localstorage \
katelyatv
```
### Vercel
```bash
# 一键部署
npx vercel
# 或使用 Vercel CLI
vercel --prod
```
## 🧪 兼容性测试
运行兼容性测试脚本:
```bash
# 测试所有部署方式的兼容性
node scripts/test-docker-compatibility.js
```
## ⚠️ 注意事项
1. **Edge Runtime 限制**: 在 Cloudflare Pages 上,所有 API 路由必须使用 Edge Runtime
2. **存储选择**: 根据部署环境选择合适的存储后端
3. **环境变量**: 确保在生产环境中正确配置存储相关环境变量
4. **缓存策略**: LocalStorage 仅适用于单机部署,集群部署请使用 Redis
## 📊 性能建议
### 小型部署 (< 1000 用户)
- **推荐**: LocalStorage
- **优点**: 零配置,性能良好
- **缺点**: 仅支持单机
### 中型部署 (1000-10000 用户)
- **推荐**: Redis
- **优点**: 高性能,支持集群
- **缺点**: 需要 Redis 服务器
### 大型部署 (> 10000 用户)
- **推荐**: Cloudflare D1 + Redis 缓存
- **优点**: 高可用,全球分布
- **缺点**: 依赖 Cloudflare
## 🆘 故障排除
### 常见问题
1. **API 路由 404**
- 检查 Edge Runtime 配置
- 确认部署环境支持
2. **跳过配置保存失败**
- 检查存储后端配置
- 验证环境变量设置
3. **Docker 构建失败**
- 确认 Node.js 版本 ≥ 18
- 检查 pnpm 安装
4. **Cloudflare Pages 部署失败**
- 确认所有 API 路由有 Edge Runtime 配置
- 检查构建命令和输出目录
---
🎉 **恭喜!** 您的跳过片头片尾功能已完全兼容所有主流部署方式!
+6
View File
@@ -9,6 +9,9 @@ WORKDIR /app
# 仅复制依赖清单,提高构建缓存利用率
COPY package.json pnpm-lock.yaml ./
# 针对ARM架构优化:设置更大的内存限制和超时时间
ENV NODE_OPTIONS="--max-old-space-size=4096"
# 安装所有依赖(含 devDependencies,后续会裁剪)
RUN pnpm install --frozen-lockfile
@@ -17,6 +20,9 @@ FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
# 针对ARM架构优化:设置更大的内存限制
ENV NODE_OPTIONS="--max-old-space-size=4096"
# 复制依赖
COPY --from=deps /app/node_modules ./node_modules
# 复制全部源代码
-212
View File
@@ -1,212 +0,0 @@
# 📊 KatelyaTV 项目状态报告
## 🎯 项目概述
**KatelyaTV** 是一个功能完整的影视聚合播放器,基于现代 Web 技术栈构建,支持多平台部署和多种存储后端。该项目为在原始项目「MoonTV」基础上的二创与继承版本,延续其优秀架构并在此之上进行持续优化与维护。
**当前版本**: v0.1.0-katelya
**最后更新**: 2025-01-XX
**项目状态**: 🟢 生产就绪
## ✨ 功能完成度
### 🎬 核心功能
| 功能模块 | 状态 | 完成度 | 说明 |
|---------|------|--------|------|
| 多源聚合搜索 | ✅ 完成 | 100% | 集成20+个资源站点,支持智能去重 |
| 视频播放器 | ✅ 完成 | 100% | ArtPlayer + HLS.js,支持多种格式 |
| 观看历史记录 | ✅ 完成 | 100% | 智能进度记录,断点续播,多设备同步 |
| 收藏系统 | ✅ 完成 | 100% | 个性化片单,多端同步 |
| 用户管理 | ✅ 完成 | 100% | 注册、登录、权限管理 |
| PWA 支持 | ✅ 完成 | 100% | 离线缓存,桌面安装 |
| 响应式设计 | ✅ 完成 | 100% | 完美适配桌面和移动端 |
### 🎨 用户体验
| 特性 | 状态 | 完成度 | 说明 |
|------|------|--------|------|
| 深色模式 | ✅ 完成 | 100% | 自动跟随系统主题 |
| 移动端优化 | ✅ 完成 | 100% | 触摸友好,底部导航 |
| 动画效果 | ✅ 完成 | 100% | Framer Motion 流畅动画 |
| 加载状态 | ✅ 完成 | 100% | 骨架屏,进度条 |
| 错误处理 | ✅ 完成 | 100% | 友好提示,重试机制 |
### 🚀 技术特性
| 技术栈 | 状态 | 完成度 | 说明 |
|--------|------|--------|------|
| Next.js 14 | ✅ 完成 | 100% | App Router,最新特性 |
| TypeScript | ✅ 完成 | 100% | 类型安全,开发体验 |
| Tailwind CSS | ✅ 完成 | 100% | 原子化 CSS,主题系统 |
| 状态管理 | ✅ 完成 | 100% | React HooksContext API |
| 数据库支持 | ✅ 完成 | 100% | localStorage, Redis, D1, Upstash |
| 测试框架 | ✅ 完成 | 100% | Jest, Testing Library |
## 🏗️ 架构状态
### 前端架构
-**组件化设计**: 模块化组件,可复用性强
-**状态管理**: 合理的状态分层和更新机制
-**路由系统**: Next.js App Router,支持动态路由
-**样式系统**: Tailwind CSS + CSS 变量,主题切换
-**类型安全**: TypeScript 全覆盖,接口定义完整
### 后端架构
-**API 设计**: RESTful API,统一响应格式
-**数据存储**: 多存储后端支持,数据隔离
-**认证系统**: Cookie 认证,会话管理
-**缓存策略**: 智能缓存,减少重复请求
-**错误处理**: 统一错误处理,友好提示
### 部署架构
-**容器化**: Docker 支持,多架构镜像
-**云平台**: Vercel, Cloudflare Pages 支持
-**CI/CD**: GitHub Actions 自动化流程
-**监控**: 性能监控,错误追踪
-**安全**: 密码保护,访问控制
## 📱 平台兼容性
### 浏览器支持
-**Chrome**: 90+ (完全支持)
-**Firefox**: 88+ (完全支持)
-**Safari**: 14+ (完全支持)
-**Edge**: 90+ (完全支持)
### 设备支持
-**桌面端**: Windows, macOS, Linux (完全支持)
-**移动端**: iOS 14+, Android 8+ (完全支持)
-**平板**: iPad, Android 平板 (完全支持)
-**智能电视**: Android TV (部分支持)
### 存储后端
-**localStorage**: 单用户,浏览器存储
-**Redis**: 多用户,数据持久化
-**Cloudflare D1**: 多用户,边缘数据库
-**Upstash**: 多用户,托管 Redis
## 🔧 开发工具链
### 代码质量
-**ESLint**: 代码规范检查
-**Prettier**: 代码格式化
-**TypeScript**: 类型检查
-**Husky**: Git hooks
-**Lint-staged**: 提交前检查
### 测试覆盖
-**Jest**: 单元测试框架
-**Testing Library**: 组件测试
-**Mock**: API 模拟
-**Coverage**: 测试覆盖率
### 构建工具
-**Next.js**: 构建和优化
-**Tailwind**: CSS 构建
-**TypeScript**: 类型编译
-**SWC**: 快速编译
## 📊 性能指标
### 加载性能
-**首屏加载**: < 2s (优化后)
-**交互响应**: < 100ms
-**图片加载**: 懒加载 + 占位符
-**代码分割**: 按需加载
### 运行时性能
-**内存使用**: 优化内存泄漏
-**CPU 使用**: 减少不必要的计算
-**网络请求**: 智能缓存,减少重复
-**渲染性能**: 虚拟滚动,组件优化
## 🚀 部署状态
### 生产环境
-**Docker Hub**: 镜像可用
-**GitHub Packages**: 镜像可用
-**Vercel**: 部署就绪
-**Cloudflare**: 部署就绪
### 自动化流程
-**版本管理**: 自动化版本更新
-**构建部署**: CI/CD 流水线
-**测试验证**: 自动化测试
-**发布管理**: 自动化发布
## 📈 项目健康度
### 代码质量
- **代码覆盖率**: 85%+
- **类型覆盖率**: 100%
- **Lint 通过率**: 100%
- **测试通过率**: 100%
### 维护状态
- **依赖更新**: 定期更新
- **安全扫描**: 自动扫描
- **性能监控**: 持续监控
- **用户反馈**: 及时响应
### 社区活跃度
- **Issue 响应**: 24小时内
- **PR 审查**: 48小时内
- **文档更新**: 持续更新
- **版本发布**: 定期发布
## 🎯 下一步计划
### 短期目标 (1-2个月)
- [ ] 弹幕系统支持
- [ ] 字幕文件支持
- [ ] 下载功能
- [ ] 社交分享功能
### 中期目标 (3-6个月)
- [ ] 用户评分系统
- [ ] 推荐算法优化
- [ ] 多语言支持
- [ ] 高级搜索过滤
### 长期目标 (6-12个月)
- [ ] AI 内容推荐
- [ ] 社区功能
- [ ] 移动端原生应用
- [ ] 企业级功能
## 🏆 项目亮点
1. **技术先进性**: 使用最新的 Web 技术栈
2. **功能完整性**: 覆盖影视播放的完整流程
3. **部署灵活性**: 支持多种部署方式
4. **用户体验**: 现代化 UI 设计,流畅交互
5. **扩展性**: 模块化架构,易于扩展
6. **社区友好**: 完善的文档和贡献指南
## 📞 支持状态
- **问题反馈**: 24小时内响应
- **功能建议**: 48小时内评估
- **代码贡献**: 72小时内审查
- **紧急修复**: 12小时内处理
## 🎉 总结
KatelyaTV 项目目前处于**生产就绪**状态,核心功能完整,技术架构成熟,用户体验优秀。项目具备以下特点:
-**功能完整**: 所有核心功能均已实现
-**技术先进**: 使用最新的 Web 技术
-**部署灵活**: 支持多种部署方式
-**维护活跃**: 持续更新和维护
-**社区友好**: 完善的文档和指南
项目可以安全地用于生产环境,适合个人用户和中小型团队使用。
> 注:KatelyaTV 基于 MoonTV 二创与继承开发,保留并致谢原作者与社区贡献;如有授权或版权问题,请联系以尽快处理。
---
**最后更新**: 2025-01-XX
**维护状态**: 🟢 活跃维护
**推荐使用**: ✅ 生产就绪
+217 -1243
View File
@@ -4,31 +4,34 @@
<h1>KatelyaTV</h1>
<p><strong>跨平台 · 聚合搜索 · 即开即用 · 自托管影视聚合播放器</strong></p>
<p>基于 <code>Next.js 14</code> · <code>TypeScript</code> · <code>Tailwind CSS</code> · 多源聚合 / 播放记录 / 收藏同步 / 跳过片头片尾 / PWA</p>
<p>MoonTV 二创延续版 · 持续维护与增强</p>
<p>
<a href="#部署">🚀 部署</a> ·
<a href="#功能特性">✨ 功能</a> ·
<a href="#docker">🐳 Docker</a> ·
<a href="#环境变量">⚙️ 配置</a>
<a href="#-快速开始">🚀 快速开始</a> ·
<a href="#-功能特性">✨ 功能特性</a> ·
<a href="#-部署方案">📋 部署方案</a> ·
<a href="#-配置说明">⚙️ 配置说明</a>
</p>
</div>
## 📰 项目来源与声明
---
本项目自「MoonTV」演进而来,为其二创/继承版本,持续维护与改进功能与体验。保留并致谢原作者与社区贡献者;如有授权或版权问题请联系以处理。目标:在原作基础上提供更易部署、更友好、更稳定的体验。
## 📰 项目声明
> **🔔 重要变更通知**:应用户社区的宝贵建议,为确保项目的长期稳定运行和合规性,我们已将内置的视频源移除。现在用户需要自行配置资源站以使用本应用的完整功能。我们提供了经过测试的推荐配置文件,让您能够快速上手使用(具体配置文件见 README.md 内容底部)
本项目自「MoonTV」演进而来,为其二创/继承版本,持续维护与改进功能与体验。保留并致谢原作者与社区贡献者
> **🔔 重要变更**:应用户社区建议,为确保项目长期稳定运行和合规性,内置视频源已移除。现需要用户自行配置资源站以使用完整功能。我们提供了经过测试的推荐配置文件,让您快速上手使用。
---
## ✨ 功能特性
### 🎬 核心播放功能
### 🎬 核心功能
- **🔍 聚合搜索**:整合多个影视资源站,一键搜索全网内容
- **📺 高清播放**:基于 ArtPlayer 的强大播放器,支持多种格式和画质
- **📺 高清播放**:基于 ArtPlayer 的强大播放器,支持多种格式
- **⏭️ 智能跳过**:自动检测并跳过片头片尾,手动设置跳过时间段
- **🎯 断点续播**:自动记录播放进度,跨设备同步观看位置
- **📱 响应式设计**:完美适配手机、平板、电脑各种屏幕尺寸
- **📱 响应式设计**:完美适配手机、平板、电脑各种屏幕
### 💾 数据管理
@@ -37,1143 +40,196 @@
- **👥 多用户支持**:独立的用户系统,每个用户独享个人数据
- **🔄 数据同步**:支持多种存储后端(LocalStorage、Redis、D1、Upstash
### 🚀 部署与扩展
### 🚀 部署特性
- **🐳 Docker 一键部署**:提供完整的 Docker 镜像,开箱即用
- **☁️ 多平台支持**Vercel、Cloudflare Pages、传统服务器全兼容
- **☁️ 多平台支持**Vercel、Docker、Cloudflare Pages 全兼容
- **🔧 灵活配置**:支持自定义资源站、代理设置、主题配置
- **📱 PWA 支持**:可安装为桌面/手机应用,离线缓存
- **📺 TVBox 兼容**:支持 TVBox 配置接口,可导入到各种电视盒子应用
### 🎨 用户体验
- **🌓 深色模式**:支持明暗主题切换,护眼舒适
- **⚡ 性能优化**:智能缓存、懒加载、播放源优选算法
- **🔐 隐私保护**:本地部署,数据完全掌控
- **🌍 国际化**:多语言支持(规划中)
## 📋 技术栈
| 分类 | 主要依赖 |
| --------- | ----------------------------------------------------------------------------------------------------- |
| 前端框架 | [Next.js 14](https://nextjs.org/) · App Router |
| UI & 样式 | [Tailwind&nbsp;CSS 3](https://tailwindcss.com/) · [Framer Motion](https://www.framer.com/motion/) |
| 语言 | TypeScript 5 |
| 播放器 | [ArtPlayer](https://github.com/zhw2590582/ArtPlayer) · [HLS.js](https://github.com/video-dev/hls.js/) |
| 状态管理 | React Hooks · Context API |
| 代码质量 | ESLint · Prettier · Jest · Husky |
| 部署 | Docker · Vercel · CloudFlare pages |
## 📺 TVBox 兼容功能
KatelyaTV 新增了 TVBox 配置接口,可以将您的视频源导入到各种电视盒子应用中使用:
### ✨ 功能特点
- **🔄 自动同步**:自动同步 KatelyaTV 中配置的所有视频源
- **📋 标准格式**:支持 TVBox 标准 JSON 配置格式
- **🎬 内置解析**:集成多个视频解析接口,支持主流视频平台
- **🌐 跨域支持**:自动处理 CORS 跨域问题
- **📱 多格式**:支持 JSON 和 Base64 两种配置格式
### 🚀 快速使用
1. **访问配置页面**:在 KatelyaTV 中点击侧边栏的"TVBox 配置"或访问 `/config` 页面
2. **选择格式类型**:在页面中选择 JSON 或 Base64 格式
3. **复制配置链接**:点击复制按钮获取配置链接
4. **导入到 TVBox**:在 TVBox 应用中导入配置链接
### 🔗 API 端点
- **JSON 配置**`https://your-domain.com/api/tvbox?format=json`
- **Base64 配置**`https://your-domain.com/api/tvbox?format=base64`
- **视频解析**`https://your-domain.com/api/parse?url={视频地址}`
> 📖 详细使用说明请查看:[TVBox 配置指南](docs/TVBOX.md)
## 🚀 部署教程
> **💡 推荐方案**
>
> - 🆕 **个人用户**:优先选择 **Docker 单容器**(最简单)
> - 🏠 **家庭/团队**:选择 **Docker + Redis**(功能完整)
> - 🏢 **生产环境**:强烈推荐 **Docker + Kvrocks**(极高可靠性,零数据丢失风险)
### 📋 部署方式对比
| 方式 | 难度 | 成本 | 多用户 | 数据可靠性 | 推荐场景 |
| ----------------------- | ------ | -------- | ------ | ---------- | ------------------- |
| 🐳 **Docker 单容器** | ⭐ | 需服务器 | ❌ | ⭐⭐ | 个人使用,最简单 |
| 🐳 **Docker + Redis** | ⭐⭐ | 需服务器 | ✅ | ⭐⭐⭐ | 家庭/团队,功能完整 |
| 🏪 **Docker + Kvrocks** | ⭐⭐ | 需服务器 | ✅ | ⭐⭐⭐⭐⭐ | 生产环境,高可靠性 |
| ☁️ **Vercel** | ⭐ | 免费 | ❌ | ⭐ | 临时体验,无服务器 |
| 🌐 **Cloudflare** | ⭐⭐⭐ | 免费 | ✅ | ⭐⭐⭐ | 技术爱好者 |
- **📱 PWA 支持**:可安装为桌面/手机应用
- **📺 TVBox 兼容**:支持 TVBox 配置接口
---
## 🎯 方案一:Docker 单容器(推荐新手)
## 🚀 快速开始
> **适合场景**:个人使用,有服务器/NAS/电脑,想要最简单的部署方式
### 推荐方案选择
### 🔧 前置要求
| 用户类型 | 推荐方案 | 特点 |
| ----------- | ---------------- | -------------------- |
| 🆕 新手用户 | Docker 单容器 | 最简单,5 分钟部署 |
| 👥 多人使用 | Vercel + Upstash | 免费,支持多用户 |
| 🏠 自托管 | Docker + Redis | 功能完整,数据可控 |
| 🏢 生产环境 | Docker + Kvrocks | 高可靠性,零丢失风险 |
- 一台能联网的设备(服务器/NAS/Windows/Mac/Linux 都行)
- 已安装 Docker[Docker 官网下载](https://www.docker.com/get-started/)
---
### 📝 详细步骤
## 📋 部署方案
#### 第一步:拉取镜像
### 方案一:Docker 单容器(推荐新手)
**适合场景**:个人使用,最简单的部署方式
```bash
# 下载最新版本镜像(支持 ARM 和 x86 架构)
docker pull ghcr.io/katelya77/katelyatv:latest
```
#### 第二步:启动容器
```bash
# 一键启动(请把 your_password 改成你的密码)
# 一键启动
docker run -d \
--name katelyatv \
-p 3000:3000 \
--env PASSWORD=your_password \
--restart unless-stopped \
ghcr.io/katelya77/katelyatv:latest
# 访问 http://localhost:3000
```
> **Windows 用户注意**:在 PowerShell 中运行上述命令
#### 第三步:访问应用
1. 打开浏览器,访问:`http://你的服务器IP:3000`
2. 如果是本机安装,访问:`http://localhost:3000`
3. 输入你在第二步设置的密码即可进入
#### 第四步:自定义资源站配置
> **📢 重要说明**:为确保项目的长期稳定运行和避免潜在的法律风险,应用户社区的建议,我们已将内置的视频源移除。现在需要用户自行配置视频源以正常使用本应用。
##### 🔗 获取推荐的资源站配置
为了方便用户快速上手,我们提供了一个经过测试的资源站配置文件:
**配置文件下载地址**: [https://www.mediafire.com/file/xl3yo7la2ci378w/config.json/file](https://www.mediafire.com/file/xl3yo7la2ci378w/config.json/file)
##### 📋 配置步骤
1. **下载配置文件**:点击上方链接下载 `config.json` 文件
2. **保存到本地**:将文件保存到服务器的合适位置(如 `/opt/katelyatv/config.json`
3. **挂载配置文件**:按以下命令重新启动容器
**自定义配置文件(可选)**
```bash
# 先停止并删除旧容器
docker stop katelyatv && docker rm katelyatv
# 重新运行并挂载配置文件
# 挂载配置文件
docker run -d \
--name katelyatv \
-p 3000:3000 \
--env PASSWORD=your_password \
-v /path/to/your/config.json:/app/config.json:ro \
-v ./config.json:/app/config.json:ro \
--restart unless-stopped \
ghcr.io/katelya77/katelyatv:latest
```
> **路径说明**:把 `/path/to/your/config.json` 替换成你的配置文件完整路径
> **Windows 示例**`-v C:/Users/你的用户名/Desktop/config.json:/app/config.json:ro`
### 方案二:Docker + Redis(多用户推荐)
##### 🛡️ 免责声明
- 提供的配置文件仅为方便用户测试和学习使用
- 所有视频源均来源于公开的网络资源,请用户自行判断使用的合法性
- 我们不对任何第三方视频源的内容、质量或合法性负责
- 建议用户仅使用合法、正版的视频源
### 🛠️ 常用管理命令
**适合场景**:家庭/团队使用,支持多用户和数据同步
```bash
# 查看运行状态
docker ps
# 下载配置文件
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/docker-compose.redis.yml
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/.env.redis.example
cp .env.redis.example .env
# 查看日志
docker logs katelyatv
# 编辑环境变量
nano .env
# 重启应用
docker restart katelyatv
# 停止应用
docker stop katelyatv
# 删除容器
docker rm katelyatv
# 启动服务
docker compose -f docker-compose.redis.yml up -d
```
---
## 🎯 方案二:Docker + Redis(推荐进阶)
> **适合场景**:多人使用,需要账号系统、观看记录同步、收藏功能
### 🔧 前置要求
- 已完成方案一,确认单容器版本能正常运行
- 了解基本的 Docker Compose 概念
### 📝 详细步骤
#### 第一步:创建配置文件
在你的服务器上创建一个文件夹,比如 `/opt/katelyatv`
**重要环境变量配置**
```bash
# 创建目录
mkdir -p /opt/katelyatv
cd /opt/katelyatv
# 存储类型
NEXT_PUBLIC_STORAGE_TYPE=redis
# 创建 docker-compose.yml 文件
cat > docker-compose.yml << 'EOF'
version: '3.8'
# 管理员账号
USERNAME=admin
PASSWORD=your_admin_password
services:
# KatelyaTV 主应用
katelyatv:
image: ghcr.io/katelya77/katelyatv:latest
container_name: katelyatv
ports:
- "3000:3000"
environment:
# 管理员账号(请修改)
- USERNAME=admin
- PASSWORD=your_strong_password
# 启用 Redis 存储
- NEXT_PUBLIC_STORAGE_TYPE=redis
- REDIS_URL=redis://katelyatv-redis:6379
# 允许用户注册(可选)
- NEXT_PUBLIC_ENABLE_REGISTER=true
depends_on:
katelyatv-redis:
condition: service_healthy
restart: unless-stopped
# 可选:挂载自定义配置
# volumes:
# - ./config.json:/app/config.json:ro
# Redis配置
REDIS_URL=redis://katelyatv-redis:6379
# Redis 数据库
katelyatv-redis:
image: redis:7-alpine
container_name: katelyatv-redis
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- katelyatv-redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
volumes:
katelyatv-redis-data:
EOF
# 开启用户注册
NEXT_PUBLIC_ENABLE_REGISTER=true
```
#### 第二步:修改配置
### 方案三:Docker + Kvrocks(高可靠性)
编辑 `docker-compose.yml` 文件,**必须修改**以下内容:
- `PASSWORD=your_strong_password` 改成你的强密码
- `USERNAME=admin` 可以改成你喜欢的管理员用户名
#### 第三步:启动服务
**适合场景**:生产环境,需要极高的数据可靠性
```bash
# 启动所有服务
docker compose up -d
# 查看启动状态
docker compose ps
```
#### 第四步:验证部署
1. 访问 `http://你的服务器IP:3000`
2. 使用你设置的用户名和密码登录
3. 登录后访问 `http://你的服务器IP:3000/admin` 进入管理后台
4. 在管理后台可以配置资源站、管理用户等
#### 第五步:配置资源站
> **📢 重要提醒**:由于项目长期稳定运行的考虑,应用户建议已移除内置视频源,需要手动配置资源站。
##### 方法一:使用推荐配置文件(推荐)
1. **下载配置文件**[点击下载 config.json](https://www.mediafire.com/file/xl3yo7la2ci378w/config.json/file)
2. **修改 docker-compose.yml**:取消注释 volumes 部分
```yaml
# 将这两行的注释去掉
volumes:
- ./config.json:/app/config.json:ro
```
3. **重启服务**
```bash
docker compose down
docker compose up -d
```
##### 方法二:管理后台配置
1. 登录管理后台:`http://你的服务器IP:3000/admin`
2. 进入"站点配置"页面
3. 手动添加视频源 API 接口
### 🛠️ 管理命令
```bash
# 查看所有服务状态
docker compose ps
# 查看日志
docker compose logs -f
# 重启所有服务
docker compose restart
# 停止所有服务
docker compose down
# 更新到最新版本
docker compose pull
docker compose up -d
```
### 💾 备份数据
```bash
# 备份 Redis 数据
docker run --rm -v katelyatv-redis-data:/data -v $(pwd):/backup alpine tar czf /backup/redis-backup-$(date +%Y%m%d).tar.gz /data
# 恢复数据(如果需要)
docker run --rm -v katelyatv-redis-data:/data -v $(pwd):/backup alpine tar xzf /backup/redis-backup-20241201.tar.gz -C /
```
---
## 方案三:Docker + Kvrocks(高可靠性推荐)
> **适合场景**:生产环境,需要极高的数据可靠性,担心 Redis 数据丢失风险
### 🌟 Kvrocks 优势
- **🛡️ 极高可靠性**:基于 RocksDB,数据持久化到磁盘,几乎零丢失风险
- **⚡ 性能优异**:完全兼容 Redis 协议,性能接近甚至超越 Redis
- **💾 节省内存**:数据存储在磁盘,内存使用量大幅降低
- **🔄 无需 AOF/RDB**RocksDB 天然支持数据持久化,无需额外配置
- **📈 更好扩展性**:支持更大的数据集,不受内存限制
### 🔧 前置要求
- 服务器/NAS/电脑(支持 Docker
- 已安装 Docker 和 Docker Compose
### 📝 详细步骤
#### 第一步:下载配置文件
```bash
# 创建项目目录
mkdir katelyatv-kvrocks && cd katelyatv-kvrocks
# 下载 Kvrocks 部署配置
# 下载配置文件
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/docker-compose.kvrocks.yml
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/.env.kvrocks.example
# 复制环境变量模板
cp .env.kvrocks.example .env
```
#### 第二步:配置环境变量
```bash
# 编辑环境变量文件
# 编辑环境变量
nano .env
```
**重要配置项**
```bash
# 存储类型:使用 Kvrocks
NEXT_PUBLIC_STORAGE_TYPE=kvrocks
# Kvrocks 连接配置
KVROCKS_URL=redis://kvrocks:6666
KVROCKS_PASSWORD=your_secure_password_here # 改成你的密码
KVROCKS_DATABASE=0
# NextAuth 配置
NEXTAUTH_SECRET=your_nextauth_secret_here # 改成随机字符串
NEXTAUTH_URL=http://localhost:3000 # 改成你的域名
```
#### 第三步:启动服务
```bash
# 一键启动 KatelyaTV + Kvrocks
# 启动服务(无密码版本)
docker compose -f docker-compose.kvrocks.yml up -d
# 查看启动状态
docker compose -f docker-compose.kvrocks.yml ps
# 如需密码认证版本,使用:
# docker compose -f docker-compose.kvrocks.auth.yml up -d
```
#### 第四步:验证部署
**Kvrocks 优势**
- 🛡️ **极高可靠性**:基于 RocksDB,数据持久化到磁盘
-**性能优异**:完全兼容 Redis 协议,性能更佳
- 💾 **节省内存**:数据存储在磁盘,内存使用量大幅降低
> 详细部署指南请查看:[Kvrocks 部署文档](docs/KVROCKS_DEPLOYMENT.md)
### 方案四:Vercel + Upstash(免费推荐)
**适合场景**:无服务器,免费部署,支持多用户
1. **Fork 仓库**Fork [KatelyaTV](https://github.com/katelya77/KatelyaTV) 到你的 GitHub
2. **部署到 Vercel**
- 登录 [Vercel](https://vercel.com/),导入你的仓库
- 添加环境变量:`PASSWORD=your_password`
- 点击 Deploy
3. **配置多用户(可选)**
```bash
# 创建 Upstash Redis 数据库
# 在 Vercel 中添加环境变量:
NEXT_PUBLIC_STORAGE_TYPE=upstash
UPSTASH_URL=https://xxx.upstash.io
UPSTASH_TOKEN=your_token
NEXT_PUBLIC_ENABLE_REGISTER=true
USERNAME=admin
PASSWORD=admin_password
```
### 方案五:Cloudflare + D1(技术爱好者)
**适合场景**:全球 CDN 加速,免费但配置稍复杂
```bash
# 检查 Kvrocks 连接
docker compose -f docker-compose.kvrocks.yml exec kvrocks redis-cli -h localhost -p 6666 ping
# 下载配置文件
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/wrangler.toml
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/.env.cloudflare.example
# 查看日志
docker compose -f docker-compose.kvrocks.yml logs -f
# 创建 D1 数据库
wrangler d1 create katelyatv-db
wrangler d1 execute katelyatv-db --file=./scripts/d1-init.sql
# 部署
wrangler pages deploy
```
#### 第五步:访问应用
1. 浏览器访问:`http://你的服务器IP:3000`
2. 注册账号开始使用
### 🛠️ 管理命令
```bash
# 停止服务
docker compose -f docker-compose.kvrocks.yml stop
# 重启服务
docker compose -f docker-compose.kvrocks.yml restart
# 查看 Kvrocks 状态
docker compose -f docker-compose.kvrocks.yml exec kvrocks redis-cli -h localhost -p 6666 info
# 备份数据
docker compose -f docker-compose.kvrocks.yml exec kvrocks redis-cli -h localhost -p 6666 BGSAVE
# 数据量统计
docker compose -f docker-compose.kvrocks.yml exec kvrocks redis-cli -h localhost -p 6666 dbsize
```
### 🔒 数据备份与恢复
#### 备份数据
```bash
# 自动备份(推荐)
docker run --rm \
-v katelyatv-kvrocks_kvrocks-data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/kvrocks-backup-$(date +%Y%m%d).tar.gz /data
# 手动触发 RocksDB 备份
docker compose -f docker-compose.kvrocks.yml exec kvrocks redis-cli -h localhost -p 6666 BGSAVE
```
#### 恢复数据
```bash
# 先停止服务
docker compose -f docker-compose.kvrocks.yml down
# 恢复数据
docker run --rm \
-v katelyatv-kvrocks_kvrocks-data:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/kvrocks-backup-20241201.tar.gz -C /
# 重新启动
docker compose -f docker-compose.kvrocks.yml up -d
```
### 🚀 性能优化建议
1. **SSD 存储**:建议使用 SSD 存储以获得最佳性能
2. **内存配置**:为 Kvrocks 分配 512MB-1GB 内存即可
3. **磁盘空间**:预留足够磁盘空间,推荐至少 10GB
4. **监控配置**:定期检查磁盘使用率和性能指标
### ⚠️ 注意事项
- Kvrocks 端口 6666 仅限内部网络访问,确保安全
- 定期备份数据,虽然 Kvrocks 可靠性很高,但备份是好习惯
- 监控磁盘空间使用,避免磁盘满导致的问题
---
## 🎯 方案四:Vercel 部署(免服务器)
## ⚙️ 配置说明
> **适合场景**:没有服务器,想要快速体验,个人使用
### 🔧 环境变量
### 🔧 前置要求
| 变量名 | 说明 | 默认值 |
| ----------------------------- | ---------------- | ------------ |
| `PASSWORD` | 访问密码(必填) | 无 |
| `USERNAME` | 管理员用户名 | 无 |
| `SITE_NAME` | 站点名称 | KatelyaTV |
| `NEXT_PUBLIC_STORAGE_TYPE` | 存储类型 | localstorage |
| `REDIS_URL` | Redis 连接地址 | 无 |
| `NEXT_PUBLIC_ENABLE_REGISTER` | 开启用户注册 | false |
- GitHub 账号
- Vercel 账号(可用 GitHub 登录)
### 📁 视频源配置
### 📝 详细步骤
#### 第一步:Fork 仓库
1. 打开 [KatelyaTV GitHub 页面](https://github.com/katelya77/KatelyaTV)
2. 点击右上角 **Fork** 按钮
3. 等待 Fork 完成
#### 第二步:部署到 Vercel
1. 访问 [Vercel](https://vercel.com/),用 GitHub 账号登录
2. 点击 **Add New... → Project**
3. 找到你刚才 Fork 的 `KatelyaTV` 仓库,点击 **Import**
4. 在 **Environment Variables** 部分添加:
- Key: `PASSWORD`
- Value: `你的访问密码`(这是进入网站的密码)
5. 点击 **Deploy** 开始部署
#### 第三步:等待部署完成
- 通常需要 2-3 分钟
- 部署成功后会显示域名,比如 `https://your-project.vercel.app`
#### 第四步:访问和使用
1. 点击 Vercel 提供的域名链接
2. 输入你在第二步设置的密码
3. 开始使用!
### 🔧 自定义资源站
> **📢 重要说明**:由于项目长期稳定性考虑,应社区用户建议已移除内置视频源,需要配置资源站后才能正常使用。
> **重要**:为保障项目合规性,需要配置视频源才能正常使用。
#### 方法一:使用推荐配置(推荐)
1. **下载配置文件**[点击下载 config.json](https://www.mediafire.com/file/xl3yo7la2ci378w/config.json/file)
2. **替换仓库配置**
- 在你 Fork 的仓库中找到 `config.json` 文件
- 点击编辑按钮(铅笔图标)
- 将下载的配置内容复制替换原有内容
- 点击 **Commit changes**
3. **等待重新部署**:Vercel 会自动重新部署(约 1-2 分钟)
1. 下载配置文件
- [基础版 config.json](https://www.mediafire.com/file/xl3yo7la2ci378w/config.json/file)
- [Plus 版(94 个片源)](https://www.mediafire.com/file/fbpk1mlupxp3u3v/configplus.json/file)
2. 配置方式:
- **Docker**:挂载配置文件 `-v ./config.json:/app/config.json:ro`
- **Vercel**:替换仓库中的 `config.json` 文件内容
- **管理员界面**:登录后台 `/admin` 导入配置
#### 方法二:手动配置
如果你想添加自己的资源站
1. 在你 Fork 的仓库中找到 `config.json` 文件
2. 点击编辑按钮(铅笔图标)
3. 修改配置内容
4. 点击 **Commit changes**
5. Vercel 会自动重新部署
### ⚠️ 注意事项
- Vercel 版本不支持用户注册和账号系统
- 观看记录保存在浏览器本地,换设备会丢失
- 如果需要多用户功能,请考虑 Docker + Redis 方案
---
## 🎯 方案五:Cloudflare Pages(进阶用户)
> **适合场景**:技术爱好者,想要全球 CDN 加速,免费但配置复杂
### 🔧 前置要求
- GitHub 账号
- Cloudflare 账号
- 对前端构建有基本了解
### 📝 详细步骤
#### 第一步:Fork 仓库并连接
1. Fork [KatelyaTV 仓库](https://github.com/katelya77/KatelyaTV)
2. 登录 [Cloudflare](https://cloudflare.com)
3. 进入 **Workers 和 Pages** → 点击 **创建应用程序**
4. 选择 **Pages** → **连接到 Git**
5. 选择你 Fork 的仓库
#### 第二步:配置构建设置
在构建设置页面填写:
- **构建命令**: `pnpm install && pnpm pages:build`
- **构建输出目录**: `.vercel/output/static`
- **Root directory**: `./`(默认)
- **Node.js 版本**: `18`(推荐)
#### 第三步:设置兼容性
1. 点击 **保存并部署**
2. 等待首次构建完成(可能会失败,没关系)
3. 进入项目 **设置** → **兼容性标志**
4. 添加标志: `nodejs_compat`
#### 第四步:添加环境变量
在 **设置** → **环境变量** 中添加:
- `PASSWORD`: 你的访问密码
#### 第五步:重新部署
1. 进入 **部署** 页面
2. 点击最新部署旁的 **...** → **重试部署**
3. 等待部署成功
#### 第六步:配置资源站
> **📢 重要提醒**:为保障项目长期稳定运行,应用户建议已移除内置视频源,需要配置资源站。
##### 推荐配置方法:
1. **下载配置文件**[点击下载 config.json](https://www.mediafire.com/file/xl3yo7la2ci378w/config.json/file)
2. **更新仓库配置**
- 回到你的 GitHub 仓库
- 找到 `config.json` 文件,点击编辑
- 用下载的内容替换原有配置
- 提交更改
3. **等待自动部署**Cloudflare Pages 会自动重新构建部署
🎉 **完成!现在可以正常使用影视聚合功能了**
### 🗄️ 启用 D1 数据库(可选,支持多用户)
如果你想要用户系统和数据同步:
> ⚠️ **升级提醒**:如果你已有 D1 数据库,需要手动添加新功能表。请查看 [D1_MIGRATION.md](./D1_MIGRATION.md) 文件。
#### 第一步:创建 D1 数据库
1. 在 Cloudflare Dashboard 进入 **存储和数据库** → **D1 SQL 数据库**
2. 点击 **创建数据库**,名称随意(比如 `katelyatv-db`
#### 第二步:初始化数据库
1. 进入刚创建的数据库
2. 点击 **Explore Data**
3. 打开项目中的 [D1 初始化.md](https://github.com/katelya77/KatelyaTV/blob/main/D1%E5%88%9D%E5%A7%8B%E5%8C%96.md) 文件,复制所有 SQL 语句
4. 粘贴到查询窗口,点击 **Run All**
#### 第三步:绑定数据库
1. 回到 Pages 项目设置
2. 进入 **绑定** → **添加绑定**
3. 选择 **D1 数据库**
4. 变量名: `DB`
5. 选择你刚创建的数据库
#### 第四步:添加环境变量
在环境变量中追加:
- `NEXT_PUBLIC_STORAGE_TYPE`: `d1`
- `USERNAME`: 管理员用户名
- `PASSWORD`: 管理员密码
#### 第五步:重新部署
重新部署后,你就可以:
- 使用管理员账号登录
- 访问 `/admin` 管理后台
- 支持用户注册和数据同步
---
## 🆙 升级和维护
### Docker 升级
```bash
# 单容器版本
docker stop katelyatv
docker rm katelyatv
docker pull ghcr.io/katelya77/katelyatv:latest
# 然后重新运行启动命令
# Compose 版本
docker compose pull
docker compose up -d
```
### Vercel 升级
- 自动升级:当原仓库更新时,你的 Fork 仓库会收到更新提示
- 手动升级:在你的 Fork 仓库点击 **Sync fork** 按钮
### Cloudflare 升级
- 同 Vercel,通过 Git 同步自动触发重新构建
### 🚨 常见问题排查
| 问题 | 现象 | 解决方法 |
| --------------- | -------------------------- | ---------------------------------- |
| 无法访问 | 浏览器显示无法连接 | 检查端口 3000 是否开放,防火墙设置 |
| 403 Forbidden | 显示访问被拒绝 | 检查 PASSWORD 环境变量是否设置正确 |
| Docker 启动失败 | 容器无法启动 | 查看日志 `docker logs katelyatv` |
| Redis 连接失败 | 无法登录或保存数据 | 检查 Redis 容器是否正常运行 |
| 构建失败 | Vercel/Cloudflare 部署失败 | 查看构建日志,检查环境变量设置 |
需要帮助?可以在 [GitHub Issues](https://github.com/katelya77/KatelyaTV/issues) 提问。
## 🐳 Docker
推荐方式。镜像多架构 (`linux/amd64`,`linux/arm64`),基于 Alpine,体积小启动快。
### 🚀 快速开始
#### 1. 基础部署(LocalStorage,最快验证)
```bash
# 拉取最新镜像(支持 amd64/arm64 多架构)
docker pull ghcr.io/katelya77/katelyatv:latest
# 快速启动(LocalStorage 存储)
docker run -d \
--name katelyatv \
-p 3000:3000 \
--env PASSWORD=your_secure_password \
--restart unless-stopped \
ghcr.io/katelya77/katelyatv:latest
```
访问 `http://服务器IP:3000` 即可使用。(需要在服务器控制台开放 3000 端口)
> Windows 本地构建如遇 Node Standalone `EPERM symlink`:优先使用 **Docker 镜像** 或在 **WSL2** 环境构建;无需修改源码。
#### 2. 自定义配置(挂载 config.json
```bash
# 创建配置文件目录
mkdir -p ./katelyatv-config
# 将你的 config.json 放入该目录,然后运行:
docker run -d \
--name katelyatv \
-p 3000:3000 \
--env PASSWORD=your_secure_password \
-v ./katelyatv-config/config.json:/app/config.json:ro \
--restart unless-stopped \
ghcr.io/katelya77/katelyatv:latest
```
#### 3. 常用运维命令
```bash
# 查看容器状态
docker ps
# 查看日志
docker logs katelyatv
# 查看实时日志
docker logs -f katelyatv
```
#### 4. 升级镜像
```bash
# 停止并删除旧容器
docker stop katelyatv && docker rm katelyatv
# 拉取最新镜像
docker pull ghcr.io/katelya77/katelyatv:latest
# 重新创建容器(使用相同的配置)
docker run -d \
--name katelyatv \
-p 3000:3000 \
--env PASSWORD=your_secure_password \
--restart unless-stopped \
ghcr.io/katelya77/katelyatv:latest
```
### 📦 镜像特性
- **🏗️ 多架构支持**:同时支持 `linux/amd64` 和 `linux/arm64` 架构
- **⚡ 优化构建**:基于 Alpine Linux,镜像体积小,启动速度快
- **🔒 安全可靠**:定期更新底层依赖,修复安全漏洞
- **🚀 开箱即用**:内置所有必要依赖,无需额外配置
### 🔧 常用操作
```bash
# 进入容器终端(调试用)
docker exec -it katelyatv /bin/sh
# 重启容器
docker restart katelyatv
# 停止容器
docker stop katelyatv
# 查看容器资源使用情况
docker stats katelyatv
# 备份容器(如果有挂载卷)
docker run --rm -v katelyatv_data:/data -v $(pwd):/backup alpine tar czf /backup/katelyatv-backup.tar.gz /data
```
## 🐙 Docker Compose 最佳实践
Docker Compose 是管理多容器应用的最佳方式,特别适合需要数据库支持的部署场景。
## 📚 功能使用教程
### ⏭️ 跳过片头片尾功能
KatelyaTV 提供了智能的跳过片头片尾功能,帮助您快速进入正片内容。
#### 🎯 如何使用
1. **自动检测**:系统会自动检测已设置的跳过片段,在观看时显示跳过按钮
2. **手动设置**:在播放页面标题右侧点击"跳过设置"按钮
3. **添加片段**:选择片头或片尾类型,设置开始和结束时间
4. **保存配置**:配置会自动保存并应用到当前剧集
#### ⚙️ 功能特点
- **智能检测**:自动识别播放时间是否在跳过区间内
- **手动配置**:支持精确设置跳过时间段(精确到秒)
- **多类型支持**:支持片头、片尾等不同类型的跳过片段
- **跨设备同步**:配置数据支持多设备同步(需使用 Redis/D1/Upstash 存储)
- **个性化**:每个用户可独立设置不同的跳过偏好
#### 💾 存储支持
| 存储类型 | 支持状态 | 同步能力 | 推荐场景 |
| ------------- | -------- | --------- | --------------- |
| LocalStorage | ✅ | ❌ 单设备 | 个人本地使用 |
| Redis | ✅ | ✅ 多设备 | 家庭/团队使用 |
| Cloudflare D1 | ✅ | ✅ 多设备 | Cloudflare 部署 |
| Upstash | ✅ | ✅ 多设备 | 无服务器部署 |
> ⚠️ **D1 用户注意**:如果你之前已经部署了项目并使用 D1 数据库,需要手动更新数据库表结构才能使用跳过功能。请参考 [D1_MIGRATION.md](./D1_MIGRATION.md) 进行升级。
#### 🛠️ 使用技巧
- **最佳时机**:建议在剧集开始播放后设置,可以实时看到当前播放时间
- **时间精度**:支持小数点精度,如 `90.5` 秒
- **批量设置**:一次设置后,所有相同剧集都会应用相同规则
- **删除管理**:可以随时删除不需要的跳过片段
## 📁 配置说明
### 📝 LocalStorage(基础单机)
适合个人使用,数据存储在浏览器本地:
```yaml
# docker-compose.yml
version: '3.8'
services:
katelyatv:
image: ghcr.io/katelya77/katelyatv:latest
container_name: katelyatv
restart: unless-stopped
ports:
- '3000:3000'
environment:
- PASSWORD=your_secure_password
- SITE_NAME=我的影视站
- ANNOUNCEMENT=欢迎使用 KatelyaTV!请遵守相关法律法规。
# 可选:挂载自定义配置
# volumes:
# - ./config.json:/app/config.json:ro
healthcheck:
test:
[
'CMD',
'wget',
'--quiet',
'--tries=1',
'--spider',
'http://localhost:3000',
]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
```
**启动命令:**
```bash
# 创建并启动服务
docker compose up -d
# 查看服务状态
docker compose ps
# 查看日志
docker compose logs -f katelyatv
```
### 🔐 Redis 版本(推荐:多用户 + 同步)
支持多用户、跨设备数据同步、完整的用户权限管理:
```yaml
# docker-compose.yml
version: '3.8'
services:
katelyatv:
image: ghcr.io/katelya77/katelyatv:latest
container_name: katelyatv
restart: unless-stopped
ports:
- '3000:3000'
environment:
# 基础配置
- SITE_NAME=KatelyaTV 影视站
- ANNOUNCEMENT=支持多用户注册,请合理使用!
# 管理员账号(重要!)
- USERNAME=admin
- PASSWORD=admin_super_secure_password
# Redis 存储配置
- NEXT_PUBLIC_STORAGE_TYPE=redis
- REDIS_URL=redis://katelyatv-redis:6379
# 用户功能
- NEXT_PUBLIC_ENABLE_REGISTER=true
# 可选:搜索配置
- NEXT_PUBLIC_SEARCH_MAX_PAGE=8
networks:
- katelyatv-network
depends_on:
katelyatv-redis:
condition: service_healthy
# 可选:挂载自定义配置和持久化数据
# volumes:
# - ./config.json:/app/config.json:ro
# - ./logs:/app/logs
healthcheck:
test:
[
'CMD',
'wget',
'--quiet',
'--tries=1',
'--spider',
'http://localhost:3000',
]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
katelyatv-redis:
image: redis:7-alpine
container_name: katelyatv-redis
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
networks:
- katelyatv-network
volumes:
# Redis 数据持久化
- katelyatv-redis-data:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
# 可选:端口映射(用于外部访问 Redis)
# ports:
# - '6379:6379'
networks:
katelyatv-network:
driver: bridge
name: katelyatv-network
volumes:
katelyatv-redis-data:
driver: local
name: katelyatv-redis-data
```
**完整部署流程:**
```bash
# 1. 创建项目目录
mkdir katelyatv && cd katelyatv
# 2. 创建 docker-compose.yml 文件(复制上面的内容)
nano docker-compose.yml
# 3. 检查配置文件语法
docker compose config
# 4. 启动所有服务
docker compose up -d
# 5. 查看服务状态
docker compose ps
# 6. 查看启动日志
docker compose logs -f
# 7. 等待服务完全启动(通常需要 30-60 秒)
# 检查健康状态
docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
# 8. 首次访问 http://your-server:3000
# 使用管理员账号 admin / admin_super_secure_password 登录
# 然后访问 /admin 进行管理员配置
```
**🔍 部署验证步骤:**
```bash
# 验证 Redis 连接
docker compose exec katelyatv-redis redis-cli ping
# 应该返回 "PONG"
# 验证 KatelyaTV 服务
curl -I http://localhost:3000
# 应该返回 HTTP 200 状态码
# 查看服务启动顺序
docker compose logs --timestamps | grep "Ready in"
```
### 🔄 管理与维护
```bash
# 更新到最新版本
docker compose pull && docker compose up -d
# 备份 Redis 数据
docker compose exec katelyatv-redis redis-cli BGSAVE
docker run --rm -v katelyatv-redis-data:/data -v $(pwd):/backup alpine tar czf /backup/redis-backup-$(date +%Y%m%d).tar.gz /data
# 查看资源使用情况
docker compose stats
# 重启特定服务
docker compose restart katelyatv
# 查看特定服务日志
docker compose logs -f katelyatv-redis
# 进入容器调试
docker compose exec katelyatv /bin/sh
# 完全清理(注意:会删除所有数据!)
docker compose down -v --remove-orphans
```
### 🚨 重要注意事项
1. **修改默认密码**:部署后请立即修改 `admin` 账号的默认密码
2. **数据备份**:定期备份 Redis 数据卷,避免数据丢失
3. **端口安全**:确保服务器防火墙正确配置,只开放必要端口
4. **资源监控**:定期检查容器资源使用情况,必要时调整配置
5. **日志管理**:配置日志轮转,避免日志文件过大
### 🛠️ 常见部署问题排查
**问题 1:容器启动失败**
```bash
# 检查容器状态
docker compose ps
# 查看详细错误日志
docker compose logs katelyatv
# 常见原因:端口被占用、环境变量配置错误、镜像拉取失败
```
**问题 2Redis 连接失败**
```bash
# 检查 Redis 容器状态
docker compose exec katelyatv-redis redis-cli ping
# 检查网络连通性
docker compose exec katelyatv ping katelyatv-redis
# 验证环境变量
docker compose exec katelyatv env | grep REDIS
```
**问题 3Upstash Redis 连接问题**
```bash
# 验证 Upstash 配置
curl -H "Authorization: Bearer YOUR_TOKEN" YOUR_UPSTASH_URL/ping
# 检查环境变量格式
echo $UPSTASH_URL # 应该是 https://xxx.upstash.io
echo $UPSTASH_TOKEN # 应该是长字符串令牌
```
**问题 4Cloudflare D1 初始化失败**
- 确保在 D1 控制台中正确执行了所有 SQL 语句
- 检查数据库绑定名称是否为 `DB`
- 验证环境变量 `NEXT_PUBLIC_STORAGE_TYPE=d1`
**问题 5Vercel 部署问题**
- 检查环境变量是否正确设置
- 确保 `config.json` 文件格式正确
- 查看 Vercel 部署日志中的错误信息
## 🔄 自动同步最近更改
建议在 fork 的仓库中启用本仓库自带的 GitHub Actions 自动同步功能(见 `.github/workflows/sync.yml`)。
如需手动同步主仓库更新,也可以使用 GitHub 官方的 [Sync fork](https://docs.github.com/cn/github/collaborating-with-issues-and-pull-requests/syncing-a-fork) 功能。
## ⚙️ 环境变量
### 📋 变量说明表
| 变量 | 说明 | 可选值 | 默认值 |
| --------------------------- | ----------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| USERNAME | redis 部署时的管理员账号 | 任意字符串 | (空) |
| PASSWORD | 默认部署时为唯一访问密码,redis 部署时为管理员密码 | 任意字符串 | (空) |
| SITE_NAME | 站点名称 | 任意字符串 | KatelyaTV |
| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 |
| NEXT_PUBLIC_STORAGE_TYPE | 播放记录/收藏的存储方式 | localstorage、redis、d1、upstash | localstorage |
| REDIS_URL | redis 连接 url,若 NEXT_PUBLIC_STORAGE_TYPE 为 redis 则必填 | 连接 url | 空 |
| UPSTASH_URL | upstash redis 连接 url | 连接 url | 空 |
| UPSTASH_TOKEN | upstash redis 连接 token | 连接 token | 空 |
| NEXT_PUBLIC_ENABLE_REGISTER | 是否开放注册,仅在非 localstorage 部署时生效 | true / false | false |
| NEXT_PUBLIC_SEARCH_MAX_PAGE | 搜索接口可拉取的最大页数 | 1-50 | 5 |
| NEXT_PUBLIC_IMAGE_PROXY | 默认的浏览器端图片代理 | url prefix | (空) |
| NEXT_PUBLIC_DOUBAN_PROXY | 默认的浏览器端豆瓣数据代理 | url prefix | (空) |
### 🔧 配置验证
**部署后可通过以下方式验证环境变量是否生效:**
1. **访问服务状态页**`http://your-domain/api/server-config`
2. **检查管理员面板**:使用管理员账号登录后访问 `/admin`
3. **查看容器日志**
```bash
# Docker 单容器
docker logs katelyatv
# Docker Compose
docker compose logs katelyatv
```
## 配置说明
所有可自定义项集中在根目录的 `config.json` 中:
编辑 `config.json` 文件
```json
{
@@ -1184,214 +240,132 @@ echo $UPSTASH_TOKEN # 应该是长字符串令牌
"name": "示例资源站",
"detail": "https://example.com"
}
// ...更多站点
}
}
```
- `cache_time`:接口缓存时间(秒)。
- `api_site`:你可以增删或替换任何资源站,字段说明:
- `key`:唯一标识,保持小写字母/数字。
- `api`:资源站提供的 `vod` JSON API 根地址。
- `name`:在人机界面中展示的名称。
- `detail`:(可选)部分无法通过 API 获取剧集详情的站点,需要提供网页详情根 URL,用于爬取。
---
KatelyaTV 支持标准的苹果 CMS V10 API 格式。
## 📱 高级功能
修改后 **无需重新构建**,服务会在启动时读取一次。
### TVBox 兼容
## 👨‍💼 管理员配置
支持 TVBox 配置接口,可以将视频源导入到各种电视盒子应用:
**该特性目前仅支持通过非 localstorage 存储的部署方式使用**
- **配置地址**`https://your-domain.com/api/tvbox?format=json`
- **详细说明**:查看 [TVBox 配置指南](docs/TVBOX.md)
支持在运行时动态变更服务配置
### 跳过片头片尾
设置环境变量 USERNAME 和 PASSWORD 即为站长用户,站长可设置用户为管理员
智能跳过片头片尾功能:
站长或管理员访问 `/admin` 即可进行管理员配置
- 🎯 自动检测已设置的跳过片段
- ⚙️ 手动设置跳过时间段(精确到秒)
- 🔄 支持多设备同步(需配置 Redis/D1/Upstash
### 🔧 视频源配置管理
### AndroidTV 支持
管理员界面提供了完整的视频源配置管理功能
配合 [OrionTV](https://github.com/zimplexing/OrionTV) 在 Android TV 上使用
#### 📤 导出配置
1. 在 OrionTV 中填入 KatelyaTV 部署地址
2. 输入设置的 PASSWORD
3. 即可在电视上观看
- **一键导出**:点击"📤 导出配置"按钮,系统会自动生成符合标准格式的 `config.json` 文件
- **自动格式化**:导出的配置文件包含所有已启用的视频源,格式完全符合项目要求
- **本地保存**:配置文件会自动下载到浏览器的下载文件夹,文件名包含日期标记
---
#### 📂 导入配置
## 🛠️ 管理与维护
- **文件选择**:点击"📂 导入配置"按钮,选择本地的 `.json` 配置文件
- **格式验证**:系统会自动验证配置文件格式,确保数据正确性
- **批量导入**:支持一次性导入多个视频源,显示详细的导入结果
- **错误提示**:如果导入过程中出现错误,会显示具体的错误信息
### 升级更新
#### 📋 支持的配置格式
```bash
# Docker 升级
docker compose pull && docker compose up -d
```json
{
"cache_time": 7200,
"api_site": {
"source_key": {
"api": "https://example.com/api.php/provide/vod",
"name": "视频源名称",
"detail": "https://example.com" // 可选
}
}
}
# 查看服务状态
docker compose ps
# 查看日志
docker compose logs -f
```
#### ✨ 其他管理功能
### 数据备份
- **拖拽排序**:支持通过拖拽调整视频源的优先级顺序
- **启用/禁用**:可以临时禁用某个视频源而不删除配置
- **实时生效**:所有配置修改都会立即生效,无需重启服务
```bash
# Redis 数据备份
docker compose exec redis redis-cli BGSAVE
> **💡 提示**:导入的配置会永久保存在数据库中,不会因为浏览器刷新而丢失。这比直接修改 `config.json` 文件更加可靠和方便。
# Kvrocks 数据备份
docker run --rm \
-v katelyatv_kvrocks-data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/kvrocks-backup.tar.gz /data
```
## 📱 AndroidTV 使用
### 常见问题
目前该项目可以配合 [OrionTV](https://github.com/zimplexing/OrionTV) 在 Android TV 上使用,可以直接作为 OrionTV 后端
| 问题 | 解决方案 |
| ---------------- | ------------------------------------------- |
| 无法访问 | 检查端口 3000 是否开放 |
| 密码错误 | 检查 PASSWORD 环境变量 |
| Redis 连接失败 | 检查 Redis 容器状态和网络 |
| Kvrocks 认证错误 | 查看 [详细文档](docs/KVROCKS_DEPLOYMENT.md) |
### 🆕 v0.5.0-katelya 修复说明
---
**修复了 OrionTV 客户端无法播放的问题**:
## 🔒 安全提醒
- **✅ 新增 CORS 支持**:为所有 API 路由添加了跨域请求头部,解决 OrionTV 客户端访问问题
- **✅ 修复认证拦截**:调整了中间件配置,确保 OrionTV 必需的 API 路由不被认证系统拦截
- **✅ 兼容性优化**:优化了搜索、详情、图片代理等关键 API 的响应头部
### 强烈建议
**如果你之前遇到"OrionTV 显示了资源但点击无法播放"的问题,现在应该已经解决了!**
- **设置密码**:避免公开访问,防范法律风险
- **个人使用**:请勿公开分享实例链接
- **遵守法律**:确保使用行为符合当地法规
### 📱 OrionTV 配置方法
1. **下载 OrionTV 客户端**:在 Android TV 上安装 OrionTV 应用
2. **配置 API 地址**:在 OrionTV 中填入你的 KatelyaTV 部署地址
3. **输入密码**:填写你设置的 PASSWORD 环境变量
4. **测试播放**:尝试搜索和播放视频
### 🔍 故障排除
如果还有播放问题,请检查:
- 确保你的 KatelyaTV 版本是 v0.5.0-katelya 或更新版本
- 确认已正确配置视频源(参考本文档的配置文件说明)
- 检查网络连接和防火墙设置
- 确保密码配置正确
暂时收藏夹与播放记录和网页端隔离,后续会支持同步用户数据
## 🗓️ Roadmap
- [x] 深色模式
- [x] 持久化存储
- [x] 多账户
- [x] 观看历史记录
- [x] PWA 支持
- [x] 豆瓣集成
- [ ] 弹幕系统
- [ ] 字幕支持
- [ ] 下载功能
- [ ] 社交分享
## ⚠️ 安全与隐私提醒
### 强烈建议设置密码保护
为了您的安全和避免潜在的法律风险,我们**强烈建议**在部署时设置密码保护:
- **避免公开访问**:不设置密码的实例任何人都可以访问,可能被恶意利用
- **防范版权风险**:公开的视频搜索服务可能面临版权方的投诉举报
- **保护个人隐私**:设置密码可以限制访问范围,保护您的使用记录
### 部署建议
1. **设置环境变量 `PASSWORD`**:为您的实例设置一个强密码
2. **仅供个人使用**:请勿将您的实例链接公开分享或传播
3. **遵守当地法律**:请确保您的使用行为符合当地法律法规
### 重要声明
### 免责声明
- 本项目仅供学习和个人使用
- 请勿将部署的实例用于商业用途或公开服务
- 如因公开分享导致的任何法律问题,用户需自行承担责任
- 项目开发者不对用户的使用行为承担任何法律责任
- 请勿用于商业用途或公开服务
- 用户需对使用行为承担法律责任
---
## 🤝 贡献与支持
### 技术栈
- **前端**Next.js 14, TypeScript, Tailwind CSS
- **播放器**ArtPlayer, HLS.js
- **存储**Redis, Kvrocks, Cloudflare D1, Upstash
- **部署**Docker, Vercel, Cloudflare Pages
### Star History
[![Star History Chart](https://api.star-history.com/svg?repos=katelya77/KatelyaTV&type=Date)](https://star-history.com/#katelya77/KatelyaTV&Date)
### 支持项目
如果这个项目对您有帮助,欢迎给个 ⭐ Star!
<div align="center">
<img src="public/wechat.jpg" alt="微信支付" width="200">
<br>
<strong>请开发者喝杯咖啡 ☕</strong>
</div>
---
## 📄 License
[MIT](LICENSE) © 2025 KatelyaTV & Contributors
## ⭐ Star History
## 🙏 致谢
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=katelya77/KatelyaTV&type=Date)](https://star-history.com/#katelya77/KatelyaTV&Date)
</div>
## 💖 支持项目
如果这个项目对您有帮助,欢迎给个 ⭐️ Star 支持一下!
您也可以通过以下方式支持项目的持续开发:
<div align="center">
### 请开发者喝杯咖啡 ☕
<table>
<tr>
<td align="center">
<img src="public/wechat.jpg" alt="微信支付" width="200">
<br>
<strong>微信支付</strong>
</td>
</tr>
</table>
> 💝 感谢您的支持!您的捐赠将用于项目的持续维护和功能改进。
</div>
## 推荐配置文件说明
### 🎯 为什么需要配置文件?
为了项目的长期稳定运行和合规性,我们根据用户社区的建议,将内置的视频源移除。这样做的好处包括:
- **🛡️ 降低法律风险**:避免项目因内置资源问题而受到影响
- **⚡ 提升加载速度**:减少应用本体大小,提高启动速度
- **🔧 更灵活配置**:用户可以根据需要选择最适合的资源站
- **📈 长期维护性**:确保项目能够持续健康发展
### 📥 获取推荐配置
我们为用户精心准备了一个经过测试和优化的配置文件:
**📂 配置文件下载链接**: [https://www.mediafire.com/file/xl3yo7la2ci378w/config.json/file](https://www.mediafire.com/file/xl3yo7la2ci378w/config.json/file)
### ✨ 配置文件特点
-**经过充分测试**:所有资源站均经过可用性验证
-**响应速度优化**:优选响应快速的资源接口
- 🎬 **内容丰富**:覆盖电影、电视剧、综艺、动漫等多种类型
- 🔄 **定期更新**:我们会根据可用性定期更新推荐配置
### 🛡️ 使用声明
- 提供的配置文件仅供学习交流和技术测试使用
- 所有资源均来源于公开的网络接口,请用户自行判断使用的合法性
- 我们不对任何第三方资源的内容质量或合法性承担责任
- 强烈建议用户仅使用合法、正版的影视内容
- [LibreTV](https://github.com/LibreSpark/LibreTV) — 项目启发
- [LunaTV](https://github.com/MoonTechLab/LunaTV) — 原始项目基础
- [ArtPlayer](https://github.com/zhw2590582/ArtPlayer) — 强大的网页视频播放器
- 感谢所有贡献者和支持者
---
## 🙏 致谢
- [ts-nextjs-tailwind-starter](https://github.com/theodorusclarence/ts-nextjs-tailwind-starter) — 项目最初基于该脚手架。
- [LibreTV](https://github.com/LibreSpark/LibreTV) — 由此启发,站在巨人的肩膀上。
- [LunaTV-原 MoonTV](https://github.com/MoonTechLab/LunaTV) — 原始项目与作者社区,感谢原作奠定坚实基础。
- [ArtPlayer](https://github.com/zhw2590582/ArtPlayer) — 提供强大的网页视频播放器。
- [HLS.js](https://github.com/video-dev/hls.js) — 实现 HLS 流媒体在浏览器中的播放支持。
- 感谢所有提供免费影视接口的站点。
<div align="center">
<p>❤️ Made with love by KatelyaTV Community</p>
</div>
+371
View File
@@ -0,0 +1,371 @@
<div align="center">
<img src="public/logo.png" alt="KatelyaTV Logo" width="128" />
<h1>KatelyaTV</h1>
<p><strong>跨平台 · 聚合搜索 · 即开即用 · 自托管影视聚合播放器</strong></p>
<p>基于 <code>Next.js 14</code> · <code>TypeScript</code> · <code>Tailwind CSS</code> · 多源聚合 / 播放记录 / 收藏同步 / 跳过片头片尾 / PWA</p>
<p>
<a href="#-快速开始">🚀 快速开始</a> ·
<a href="#-功能特性">✨ 功能特性</a> ·
<a href="#-部署方案">📋 部署方案</a> ·
<a href="#-配置说明">⚙️ 配置说明</a>
</p>
</div>
---
## 📰 项目声明
本项目自「MoonTV」演进而来,为其二创/继承版本,持续维护与改进功能与体验。保留并致谢原作者与社区贡献者。
> **🔔 重要变更**:应用户社区建议,为确保项目长期稳定运行和合规性,内置视频源已移除。现需要用户自行配置资源站以使用完整功能。我们提供了经过测试的推荐配置文件,让您快速上手使用。
---
## ✨ 功能特性
### 🎬 核心功能
- **🔍 聚合搜索**:整合多个影视资源站,一键搜索全网内容
- **📺 高清播放**:基于 ArtPlayer 的强大播放器,支持多种格式
- **⏭️ 智能跳过**:自动检测并跳过片头片尾,手动设置跳过时间段
- **🎯 断点续播**:自动记录播放进度,跨设备同步观看位置
- **📱 响应式设计**:完美适配手机、平板、电脑各种屏幕
### 💾 数据管理
- **⭐ 收藏功能**:收藏喜欢的影视作品,支持跨设备同步
- **📖 播放历史**:自动记录观看历史,快速找回看过的内容
- **👥 多用户支持**:独立的用户系统,每个用户独享个人数据
- **🔄 数据同步**:支持多种存储后端(LocalStorage、Redis、D1、Upstash
### 🚀 部署特性
- **🐳 Docker 一键部署**:提供完整的 Docker 镜像,开箱即用
- **☁️ 多平台支持**Vercel、Docker、Cloudflare Pages 全兼容
- **🔧 灵活配置**:支持自定义资源站、代理设置、主题配置
- **📱 PWA 支持**:可安装为桌面/手机应用
- **📺 TVBox 兼容**:支持 TVBox 配置接口
---
## 🚀 快速开始
### 推荐方案选择
| 用户类型 | 推荐方案 | 特点 |
| ----------- | ---------------- | -------------------- |
| 🆕 新手用户 | Docker 单容器 | 最简单,5 分钟部署 |
| 👥 多人使用 | Vercel + Upstash | 免费,支持多用户 |
| 🏠 自托管 | Docker + Redis | 功能完整,数据可控 |
| 🏢 生产环境 | Docker + Kvrocks | 高可靠性,零丢失风险 |
---
## 📋 部署方案
### 方案一:Docker 单容器(推荐新手)
**适合场景**:个人使用,最简单的部署方式
```bash
# 一键启动
docker run -d \
--name katelyatv \
-p 3000:3000 \
--env PASSWORD=your_password \
--restart unless-stopped \
ghcr.io/katelya77/katelyatv:latest
# 访问 http://localhost:3000
```
**自定义配置文件(可选)**
```bash
# 挂载配置文件
docker run -d \
--name katelyatv \
-p 3000:3000 \
--env PASSWORD=your_password \
-v ./config.json:/app/config.json:ro \
--restart unless-stopped \
ghcr.io/katelya77/katelyatv:latest
```
### 方案二:Docker + Redis(多用户推荐)
**适合场景**:家庭/团队使用,支持多用户和数据同步
```bash
# 下载配置文件
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/docker-compose.redis.yml
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/.env.redis.example
cp .env.redis.example .env
# 编辑环境变量
nano .env
# 启动服务
docker compose -f docker-compose.redis.yml up -d
```
**重要环境变量配置**
```bash
# 存储类型
NEXT_PUBLIC_STORAGE_TYPE=redis
# 管理员账号
USERNAME=admin
PASSWORD=your_admin_password
# Redis配置
REDIS_URL=redis://katelyatv-redis:6379
# 开启用户注册
NEXT_PUBLIC_ENABLE_REGISTER=true
```
### 方案三:Docker + Kvrocks(高可靠性)
**适合场景**:生产环境,需要极高的数据可靠性
```bash
# 下载配置文件
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/docker-compose.kvrocks.yml
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/.env.kvrocks.example
cp .env.kvrocks.example .env
# 编辑环境变量
nano .env
# 启动服务(无密码版本)
docker compose -f docker-compose.kvrocks.yml up -d
# 如需密码认证版本,使用:
# docker compose -f docker-compose.kvrocks.auth.yml up -d
```
**Kvrocks 优势**
- 🛡️ **极高可靠性**:基于 RocksDB,数据持久化到磁盘
-**性能优异**:完全兼容 Redis 协议,性能更佳
- 💾 **节省内存**:数据存储在磁盘,内存使用量大幅降低
> 详细部署指南请查看:[Kvrocks 部署文档](docs/KVROCKS_DEPLOYMENT.md)
### 方案四:Vercel + Upstash(免费推荐)
**适合场景**:无服务器,免费部署,支持多用户
1. **Fork 仓库**Fork [KatelyaTV](https://github.com/katelya77/KatelyaTV) 到你的 GitHub
2. **部署到 Vercel**
- 登录 [Vercel](https://vercel.com/),导入你的仓库
- 添加环境变量:`PASSWORD=your_password`
- 点击 Deploy
3. **配置多用户(可选)**
```bash
# 创建 Upstash Redis 数据库
# 在 Vercel 中添加环境变量:
NEXT_PUBLIC_STORAGE_TYPE=upstash
UPSTASH_URL=https://xxx.upstash.io
UPSTASH_TOKEN=your_token
NEXT_PUBLIC_ENABLE_REGISTER=true
USERNAME=admin
PASSWORD=admin_password
```
### 方案五:Cloudflare + D1(技术爱好者)
**适合场景**:全球 CDN 加速,免费但配置稍复杂
```bash
# 下载配置文件
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/wrangler.toml
curl -O https://raw.githubusercontent.com/katelya77/KatelyaTV/main/.env.cloudflare.example
# 创建 D1 数据库
wrangler d1 create katelyatv-db
wrangler d1 execute katelyatv-db --file=./scripts/d1-init.sql
# 部署
wrangler pages deploy
```
---
## ⚙️ 配置说明
### 🔧 环境变量
| 变量名 | 说明 | 默认值 |
| ----------------------------- | ---------------- | ------------ |
| `PASSWORD` | 访问密码(必填) | 无 |
| `USERNAME` | 管理员用户名 | 无 |
| `SITE_NAME` | 站点名称 | KatelyaTV |
| `NEXT_PUBLIC_STORAGE_TYPE` | 存储类型 | localstorage |
| `REDIS_URL` | Redis 连接地址 | 无 |
| `NEXT_PUBLIC_ENABLE_REGISTER` | 开启用户注册 | false |
### 📁 视频源配置
> **重要**:为保障项目合规性,需要配置视频源才能正常使用。
#### 方法一:使用推荐配置(推荐)
1. 下载配置文件:
- [基础版 config.json](https://www.mediafire.com/file/xl3yo7la2ci378w/config.json/file)
- [Plus 版(94 个片源)](https://www.mediafire.com/file/fbpk1mlupxp3u3v/configplus.json/file)
2. 配置方式:
- **Docker**:挂载配置文件 `-v ./config.json:/app/config.json:ro`
- **Vercel**:替换仓库中的 `config.json` 文件内容
- **管理员界面**:登录后台 `/admin` 导入配置
#### 方法二:手动配置
编辑 `config.json` 文件:
```json
{
"cache_time": 7200,
"api_site": {
"example": {
"api": "https://example.com/api.php/provide/vod",
"name": "示例资源站",
"detail": "https://example.com"
}
}
}
```
---
## 📱 高级功能
### TVBox 兼容
支持 TVBox 配置接口,可以将视频源导入到各种电视盒子应用:
- **配置地址**`https://your-domain.com/api/tvbox?format=json`
- **详细说明**:查看 [TVBox 配置指南](docs/TVBOX.md)
### 跳过片头片尾
智能跳过片头片尾功能:
- 🎯 自动检测已设置的跳过片段
- ⚙️ 手动设置跳过时间段(精确到秒)
- 🔄 支持多设备同步(需配置 Redis/D1/Upstash
### AndroidTV 支持
配合 [OrionTV](https://github.com/zimplexing/OrionTV) 在 Android TV 上使用:
1. 在 OrionTV 中填入 KatelyaTV 部署地址
2. 输入设置的 PASSWORD
3. 即可在电视上观看
---
## 🛠️ 管理与维护
### 升级更新
```bash
# Docker 升级
docker compose pull && docker compose up -d
# 查看服务状态
docker compose ps
# 查看日志
docker compose logs -f
```
### 数据备份
```bash
# Redis 数据备份
docker compose exec redis redis-cli BGSAVE
# Kvrocks 数据备份
docker run --rm \
-v katelyatv_kvrocks-data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/kvrocks-backup.tar.gz /data
```
### 常见问题
| 问题 | 解决方案 |
| ---------------- | ------------------------------------------- |
| 无法访问 | 检查端口 3000 是否开放 |
| 密码错误 | 检查 PASSWORD 环境变量 |
| Redis 连接失败 | 检查 Redis 容器状态和网络 |
| Kvrocks 认证错误 | 查看 [详细文档](docs/KVROCKS_DEPLOYMENT.md) |
---
## 🔒 安全提醒
### 强烈建议
- **设置密码**:避免公开访问,防范法律风险
- **个人使用**:请勿公开分享实例链接
- **遵守法律**:确保使用行为符合当地法规
### 免责声明
- 本项目仅供学习和个人使用
- 请勿用于商业用途或公开服务
- 用户需对使用行为承担法律责任
---
## 🤝 贡献与支持
### 技术栈
- **前端**Next.js 14, TypeScript, Tailwind CSS
- **播放器**ArtPlayer, HLS.js
- **存储**Redis, Kvrocks, Cloudflare D1, Upstash
- **部署**Docker, Vercel, Cloudflare Pages
### Star History
[![Star History Chart](https://api.star-history.com/svg?repos=katelya77/KatelyaTV&type=Date)](https://star-history.com/#katelya77/KatelyaTV&Date)
### 支持项目
如果这个项目对您有帮助,欢迎给个 ⭐ Star!
<div align="center">
<img src="public/wechat.jpg" alt="微信支付" width="200">
<br>
<strong>请开发者喝杯咖啡 ☕</strong>
</div>
---
## 📄 License
[MIT](LICENSE) © 2025 KatelyaTV & Contributors
## 🙏 致谢
- [LibreTV](https://github.com/LibreSpark/LibreTV) — 项目启发
- [LunaTV](https://github.com/MoonTechLab/LunaTV) — 原始项目基础
- [ArtPlayer](https://github.com/zhw2590582/ArtPlayer) — 强大的网页视频播放器
- 感谢所有贡献者和支持者
---
<div align="center">
<p>❤️ Made with love by KatelyaTV Community</p>
</div>
-51
View File
@@ -1,51 +0,0 @@
## 🎉 KatelyaTV v0.6.0-katelya
### ✨ 主要更新
#### 📺 TVBox 集成优化
- **新增用户菜单中的"TVBox 配置"按钮** - 提供便捷的配置入口
- **TVBox API 无需认证** - 解决客户端无法登录的问题,现在可直接使用配置链接
- **优化用户体验** - 支持一键复制配置 URL,直接在 TVBox 应用中使用
#### ☁️ Cloudflare Pages 完全支持
- **修复 Edge Runtime 兼容性** - 解决部署失败问题
- **重构 API 架构** - 使用 Edge Runtime 兼容的配置读取方式
- **生产环境稳定性提升** - 确保 Cloudflare Pages 部署成功
#### 🔧 技术改进
- 修复代码风格问题(ESLint 导入排序)
- 优化中间件配置,确保安全性
- 提升构建过程稳定性
### 📱 使用方式
**TVBox 配置 URL**(无需登录):
- JSON 格式:`https://your-domain.com/api/tvbox?format=json`
- Base64 格式:`https://your-domain.com/api/tvbox?format=base64`
**访问配置页面**
1. 登录后点击右上角用户头像
2. 选择"TVBox 配置"
3. 复制配置链接到 TVBox 应用
### 🌐 部署兼容性
- ✅ Cloudflare Pages(推荐)
- ✅ Vercel
- ✅ Docker
- ✅ 传统服务器
### 🔄 升级说明
- **向后兼容**:现有配置和数据完全兼容
- **推荐操作**:重新部署以获取 Cloudflare Pages 优化
- **新功能**:TVBox 配置功能可选使用
---
**重要提示**:本版本主要解决了 TVBox 客户端集成和 Cloudflare Pages 部署的关键问题,建议所有用户升级。
-149
View File
@@ -1,149 +0,0 @@
# 🎉 KatelyaTV v0.5.0-katelya
> **重大更新**:智能跳过片头片尾功能 + 多平台兼容性增强
## ✨ 主要新增功能
### 🎬 智能跳过片头片尾系统
- **批量设置**:支持同时配置片头片尾跳过时间
- **智能检测**:自动识别片头片尾时间点
- **时间格式**:直观的"分:秒"格式输入(如 1:30
- **自动跳转**:支持自动跳到下一集功能
- **浮动界面**:美观的跳过提示,不遮挡视频内容
- **倒计时显示**:5秒跳过倒计时提醒
- **全存储支持**LocalStorage、Redis、D1、Upstash 全兼容
### 🔧 技术架构优化
- **统一构建工具**:全面切换到 pnpm,提升构建速度 50%+
- **多平台兼容**:完美支持 Cloudflare Pages、Docker、Vercel
- **Edge Runtime**Cloudflare Pages 使用 Edge Runtime 优化
- **自动转换**:Docker 部署时自动转换为 Node.js Runtime
### 🗄️ 数据库增强
- **新增表结构**`skip_configs` 表用于存储跳过配置
- **索引优化**:完整的数据库索引提升查询性能
- **迁移文档**:提供现有数据库的迁移指南
## 🔄 改进与修复
### 📦 构建系统
- 统一使用 pnpm 包管理器
- 优化 Cloudflare Pages 构建配置
- 修复 GitHub Actions 工作流语法错误
- 更新所有仓库引用到新的 katelya77/KatelyaTV
### 🎨 用户界面
- 跳过配置界面重新设计
- 支持批量设置片头片尾
- 修复界面重叠问题
- 优化时间输入体验
### 🛠️ 开发体验
- 修复 ESLint 错误
- 清理无用配置文件
- 优化版本检查机制
- 完善 Docker 兼容性测试
## 🚀 部署指南
### Cloudflare Pages(推荐)
```bash
# 构建命令
pnpm pages:build
# 输出目录
.vercel/output/static
```
### Docker 部署
```bash
docker pull ghcr.io/katelya77/katelyatv:v0.5.0-katelya
docker run -d --name katelyatv -p 3000:3000 \
--env PASSWORD=your_password \
ghcr.io/katelya77/katelyatv:v0.5.0-katelya
```
### Vercel 部署
```bash
# 构建命令
pnpm run build
```
## 📋 环境变量
| 变量 | 说明 | 默认值 |
| ------------------------ | ---------- | ------------ |
| PASSWORD | 访问密码 | 必填 |
| NEXT_PUBLIC_STORAGE_TYPE | 存储类型 | localstorage |
| USERNAME | 管理员账号 | 空 |
## 🆕 新功能使用说明
### 跳过片头片尾设置
1. 在播放页面点击"跳过设置"按钮
2. 选择"批量设置"模式
3. 输入片头时间(如:1:30
4. 输入片尾时间(如:1:30
5. 开启"自动跳过"和"自动下一集"
6. 保存设置
### 智能检测功能
- 系统会根据播放行为自动学习片头片尾时间
- 支持自动识别常见的片头片尾模式
- 提供5秒倒计时,可手动取消跳过
## 🔧 技术升级
### 构建工具统一
- 所有平台统一使用 pnpm
- 构建速度提升 2-3 倍
- 磁盘空间节省 50%+
### 多平台兼容
- **Cloudflare Pages**: 使用 Edge Runtime,全球CDN加速
- **Docker**: 自动转换 Runtime,支持多架构
- **Vercel**: 优化构建配置,快速部署
## 🔗 相关资源
- [项目文档](https://github.com/katelya77/KatelyaTV#readme)
- [问题反馈](https://github.com/katelya77/KatelyaTV/issues)
- [功能讨论](https://github.com/katelya77/KatelyaTV/discussions)
- [贡献指南](https://github.com/katelya77/KatelyaTV/blob/main/CONTRIBUTING.md)
## 🙏 致谢
感谢所有用户的反馈和建议,特别是跳过片头片尾功能的需求。本版本致力于提供更智能、更便捷的观影体验。
## 📈 版本对比
| 功能 | v0.4.0 | v0.5.0 |
| ------------ | ------ | ------ |
| 跳过片头片尾 | ❌ | ✅ |
| 批量设置 | ❌ | ✅ |
| 智能检测 | ❌ | ✅ |
| 自动下一集 | ❌ | ✅ |
| pnpm 构建 | ❌ | ✅ |
| Edge Runtime | ❌ | ✅ |
| 多存储支持 | ✅ | ✅ |
| Docker 部署 | ✅ | ✅ |
---
**注意**: 本项目仅供学习和个人使用,请遵守当地法律法规。
**发布日期**: 2025年9月2日
**版本标签**: v0.5.0-katelya
-101
View File
@@ -1,101 +0,0 @@
# KatelyaTV v0.6.0-katelya 发布记录
## 🎉 重大更新
### 📺 TVBox 集成优化
- **用户界面改进**
- 在用户菜单中新增"TVBox 配置"按钮,提供便捷的配置入口
- 优化配置页面用户体验,支持一键复制配置链接
- 新增电视图标标识,界面更加直观
- **认证机制优化**
- **重要变更**TVBox API (`/api/tvbox`) 现已开放无需认证访问
- 解决 TVBox 客户端无法登录的根本问题
- 支持直接在 TVBox 应用中使用配置链接,无需预先登录
- 确保其他管理 API 仍受认证保护,维护系统安全
### 🔧 技术优化
#### Cloudflare Pages 部署支持
- **Edge Runtime 全面兼容**
- 修复所有 API 路由的 Edge Runtime 配置问题
- 解决 Cloudflare Pages 部署失败的核心问题
- 重构文件系统访问逻辑,使用 `getConfig()` 替代 `fs.readFileSync`
- 确保生产环境部署稳定性
#### 代码质量提升
- **ESLint 规则优化**
- 修复导入排序问题,确保代码风格一致性
- 解决所有编译时警告和错误
- 提升代码可维护性和团队协作效率
### 🌐 部署兼容性
-**Cloudflare Pages** - 完全支持,Edge Runtime 兼容
-**Vercel** - 自动适配,零配置部署
-**Docker** - 容器化部署,跨平台兼容
-**传统服务器** - Node.js 运行时,稳定运行
### 📱 TVBox 使用指南
#### 配置方式
1. **通过用户菜单**
- 登录 KatelyaTV 网站
- 点击右上角用户头像
- 选择"TVBox 配置"
- 复制配置链接到 TVBox 应用
2. **直接访问**
- JSON 格式:`https://your-domain.com/api/tvbox?format=json`
- Base64 格式:`https://your-domain.com/api/tvbox?format=base64`
#### 支持的客户端
- TVBox 官方版本
- TVBox 开源版本
- 影视仓
- 其他兼容 TVBox 标准的应用
### 🔄 迁移指南
从 v0.5.x 升级到 v0.6.0
1. **无需数据迁移**:配置数据完全兼容
2. **新功能可选**:现有功能保持不变
3. **推荐操作**:重新部署以获取 Cloudflare Pages 优化
### 🐛 修复的问题
- 修复 Cloudflare Pages 部署时的 Edge Runtime 配置错误
- 解决 TVBox API 认证导致的访问失败问题
- 修复代码导入排序导致的 ESLint 警告
- 优化构建过程,减少 Windows 开发环境的权限警告
### ⚠️ 重要说明
- **安全考虑**:TVBox API 开放访问不会泄露敏感信息,配置数据主要包含公开的视频源地址
- **向后兼容**:现有用户的所有功能和数据保持不变
- **推荐升级**:建议所有用户升级以获得更好的 TVBox 集成体验
---
## 🚀 下一版本预告
- 更多视频源集成
- 播放性能优化
- 移动端体验改进
- 更多第三方客户端支持
---
**发布日期**2025 年 9 月 3 日
**版本标签**v0.6.0-katelya
**兼容性**:向下兼容 v0.5.x 所有功能
-123
View File
@@ -1,123 +0,0 @@
# 🎬 智能跳过功能使用指南
## ✨ 功能特色
### 🚀 全新智能跳过体验
- **🎯 分:秒格式输入** - 更符合观影习惯,如 `2:10` 表示 2 分 10 秒
- **⚡ 自动跳过** - 无需手动点击,到达设定时间自动跳转
- **🎭 智能片尾** - 可设置从指定时间直接跳转下一集
- **🎨 优化布局** - 悬浮式配置显示,不与内容重叠
## 📱 使用方法
### 1. 开启跳过设置
在播放页面,点击标题右侧的 **"跳过设置"** 按钮
### 2. 智能配置模式(推荐)
#### 全局开关
-**启用自动跳过** - 到达时间自动跳转,无需手动点击
-**片尾自动播放下一集** - 片尾时显示倒计时,自动播放下一集
#### 片头设置
- **开始时间**: `0:00` (通常从视频开始)
- **结束时间**: `2:10` (跳过 2 分 10 秒的片头)
#### 片尾设置
- **开始时间**: `19:20` (从 19 分 20 秒开始检测片尾)
- **结束时间**: 留空 (直接跳转下一集) 或 `20:50` (跳过片尾到此时间)
### 3. 支持的时间格式
- **分:秒格式**: `2:10`, `19:20`, `1:30.5`
- **秒数格式**: `130`, `1160`, `90.5`
- **自动识别**: 系统会自动识别并转换格式
## 🎯 使用场景示例
### 场景一:跳过片头
```
片头设置:
开始时间: 0:00
结束时间: 2:10
效果: 视频开始播放时,自动从0秒跳转到2分10秒
```
### 场景二:片尾直接下一集
```
片尾设置:
开始时间: 19:20
结束时间: 留空
效果: 播放到19分20秒时,显示倒计时,自动播放下一集
```
### 场景三:跳过片头+片尾
```
片头: 0:00 → 2:10 (跳过片头)
片尾: 19:20 → 留空 (直接下一集)
效果: 完全自动化的观影体验
```
## 🎨 界面说明
### 播放时显示
- **倒计时器**: 片尾时屏幕中央显示"X 秒后自动播放下一集"
- **跳过提示**: 自动跳过时显示"自动跳过片头/片尾"
- **取消按钮**: 可随时取消自动操作
### 配置显示
- **悬浮卡片**: 右下角显示当前跳过配置
- **状态标识**: 显示自动跳过状态
- **快速修改**: 点击卡片可快速修改配置
## ⚙️ 高级功能
### 精确设置
- 支持小数点精度: `90.5`
- 支持多段跳过: 可设置多个片头/片尾段落
- 智能检测: 自动识别当前播放时间是否在跳过区间
### 数据同步
- **LocalStorage**: 单设备本地存储
- **云端同步**: 支持 Redis、D1、Upstash 跨设备同步
- **实时更新**: 配置修改后立即生效
## 🔧 故障排除
### 常见问题
1. **时间格式错误**: 确保使用 `分:秒` 格式,如 `2:10`
2. **配置不生效**: 检查是否开启了"启用自动跳过"开关
3. **重叠显示**: 新版本已修复,配置卡片不会与内容重叠
### 兼容性
- ✅ 支持所有部署方式 (Docker, Vercel, Cloudflare Pages)
- ✅ 支持所有存储后端 (LocalStorage, Redis, D1, Upstash)
- ✅ 支持桌面和移动设备
## 💡 使用技巧
1. **首次设置**: 建议先观看一遍内容,记录片头片尾时间
2. **批量配置**: 使用智能配置模式一次性设置片头和片尾
3. **个性化**: 不同类型的内容可以设置不同的跳过规则
4. **测试验证**: 设置后可以快进到设定时间测试效果
---
🎉 **享受更流畅的观影体验!**
+1 -1
View File
@@ -1 +1 @@
20250903203337
20250904151930
+63
View File
@@ -0,0 +1,63 @@
version: '3.8'
services:
# KatelyaTV 应用服务
katelyatv:
image: ghcr.io/katelya77/katelyatv:latest
ports:
- "3000:3000"
environment:
# 数据库配置 - 使用 Kvrocks (带密码)
NEXT_PUBLIC_STORAGE_TYPE: kvrocks
KVROCKS_URL: redis://kvrocks:6666
KVROCKS_PASSWORD: ${KVROCKS_PASSWORD}
KVROCKS_DATABASE: 0
# 站点访问密码配置
PASSWORD: ${PASSWORD:-}
# 其他必要的环境变量
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
depends_on:
- kvrocks
restart: unless-stopped
networks:
- katelyatv-network
# Kvrocks 数据库服务 (带密码认证)
kvrocks:
image: apache/kvrocks:latest
ports:
- "6666:6666"
environment:
# Kvrocks 配置
KVROCKS_BIND: 0.0.0.0
KVROCKS_PORT: 6666
KVROCKS_DIR: /var/lib/kvrocks/data
KVROCKS_LOG_LEVEL: info
# 设置密码
KVROCKS_REQUIREPASS: ${KVROCKS_PASSWORD}
volumes:
# 持久化数据存储
- kvrocks-data:/var/lib/kvrocks/data
# 挂载配置文件
- ./docker/kvrocks/kvrocks.auth.conf:/etc/kvrocks/kvrocks.conf:ro
restart: unless-stopped
networks:
- katelyatv-network
healthcheck:
test: ["CMD", "redis-cli", "-h", "localhost", "-p", "6666", "-a", "${KVROCKS_PASSWORD}", "ping"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
# Kvrocks 数据卷
kvrocks-data:
driver: local
networks:
katelyatv-network:
driver: bridge
+60
View File
@@ -0,0 +1,60 @@
version: '3.8'
services:
# KatelyaTV 应用服务(本地构建版本)
katelyatv:
build: .
ports:
- "3000:3000"
environment:
# 数据库配置 - 使用 Kvrocks
NEXT_PUBLIC_STORAGE_TYPE: kvrocks
KVROCKS_URL: redis://kvrocks:6666
KVROCKS_PASSWORD: ${KVROCKS_PASSWORD:-}
KVROCKS_DATABASE: 0
# 其他必要的环境变量
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
depends_on:
- kvrocks
restart: unless-stopped
networks:
- katelyatv-network
# Kvrocks 数据库服务
kvrocks:
image: apache/kvrocks:latest
ports:
- "6666:6666"
environment:
# Kvrocks 配置
KVROCKS_BIND: 0.0.0.0
KVROCKS_PORT: 6666
KVROCKS_DIR: /var/lib/kvrocks/data
KVROCKS_LOG_LEVEL: info
# 可选:设置密码
KVROCKS_REQUIREPASS: ${KVROCKS_PASSWORD:-}
volumes:
# 持久化数据存储
- kvrocks-data:/var/lib/kvrocks/data
# 可选:挂载配置文件
- ./docker/kvrocks/kvrocks.conf:/etc/kvrocks/kvrocks.conf:ro
restart: unless-stopped
networks:
- katelyatv-network
healthcheck:
test: ["CMD", "redis-cli", "-h", "localhost", "-p", "6666", "ping"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
# Kvrocks 数据卷
kvrocks-data:
driver: local
networks:
katelyatv-network:
driver: bridge
+5 -4
View File
@@ -3,7 +3,7 @@ version: '3.8'
services:
# KatelyaTV 应用服务
katelyatv:
build: .
image: ghcr.io/katelya77/katelyatv:latest
ports:
- "3000:3000"
environment:
@@ -13,6 +13,9 @@ services:
KVROCKS_PASSWORD: ${KVROCKS_PASSWORD:-}
KVROCKS_DATABASE: 0
# 站点访问密码配置
PASSWORD: ${PASSWORD:-}
# 其他必要的环境变量
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
@@ -33,12 +36,10 @@ services:
KVROCKS_PORT: 6666
KVROCKS_DIR: /var/lib/kvrocks/data
KVROCKS_LOG_LEVEL: info
# 可选:设置密码
KVROCKS_REQUIREPASS: ${KVROCKS_PASSWORD:-}
volumes:
# 持久化数据存储
- kvrocks-data:/var/lib/kvrocks/data
# 可选:挂载配置文件
# 挂载配置文件
- ./docker/kvrocks/kvrocks.conf:/etc/kvrocks/kvrocks.conf:ro
restart: unless-stopped
networks:
+53
View File
@@ -0,0 +1,53 @@
version: '3.8'
services:
# KatelyaTV 应用服务
katelyatv:
image: ghcr.io/katelya77/katelyatv:latest
ports:
- "3000:3000"
environment:
# 数据库配置 - 使用 Redis
NEXT_PUBLIC_STORAGE_TYPE: redis
REDIS_URL: redis://katelyatv-redis:6379
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
REDIS_DATABASE: 0
# 站点访问密码配置
PASSWORD: ${PASSWORD:-}
# 其他必要的环境变量
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
depends_on:
- katelyatv-redis
restart: unless-stopped
networks:
- katelyatv-network
# Redis 数据库服务
katelyatv-redis:
image: redis:7-alpine
container_name: katelyatv-redis
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
# 持久化数据存储
- katelyatv-redis-data:/data
restart: unless-stopped
networks:
- katelyatv-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
volumes:
# Redis 数据卷
katelyatv-redis-data:
driver: local
networks:
katelyatv-network:
driver: bridge
+50
View File
@@ -0,0 +1,50 @@
# Kvrocks 配置文件 (带密码认证)
# 基于 RocksDB 的 Redis 协议兼容存储引擎
# 网络配置
bind 0.0.0.0
port 6666
# 数据存储配置
dir /var/lib/kvrocks/data
# 日志配置
log-level info
log-dir /var/lib/kvrocks/logs
# 性能优化配置
# RocksDB 配置
rocksdb.max_open_files 4096
rocksdb.max_background_jobs 4
rocksdb.max_write_buffer_number 4
rocksdb.write_buffer_size 64MB
# 压缩配置
rocksdb.compression snappy
# 内存配置
max-memory 512MB
# 安全配置 - 启用密码认证
# 密码将通过环境变量 KVROCKS_REQUIREPASS 设置
requirepass ${KVROCKS_REQUIREPASS}
# 持久化配置
# Kvrocks 基于 RocksDB,天然支持持久化,无需额外配置
# 网络超时配置
timeout 300
# 客户端连接配置
tcp-keepalive 300
tcp-backlog 511
# 慢查询日志
slowlog-log-slower-than 10000
slowlog-max-len 128
# 数据库数量
databases 16
# 备份配置
save ""
+2
View File
@@ -26,6 +26,8 @@ rocksdb.compression snappy
max-memory 512MB
# 安全配置
# 默认不设置密码(适合开发环境)
# 如需启用密码,请取消注释下行并设置密码
# requirepass your_password_here
# 持久化配置
+27
View File
@@ -48,6 +48,33 @@ Kvrocks 是一个分布式键值数据库,兼容 Redis 协议,基于 RocksDB
### 3. 运维简单
- **免维护**:无需定期备份,数据自动持久化
## 🔧 快速部署
### 无密码部署(开发环境)
```bash
# 1. 设置环境变量
cp .env.kvrocks.example .env
# 编辑 .env,不设置 KVROCKS_PASSWORD
# 2. 启动服务
docker-compose -f docker-compose.kvrocks.yml up -d
```
### 密码认证部署(生产环境)
```bash
# 1. 设置环境变量
cp .env.kvrocks.example .env
# 编辑 .env,设置 KVROCKS_PASSWORD=your_secure_password
# 2. 启动服务
docker-compose -f docker-compose.kvrocks.auth.yml up -d
```
📖 **详细部署指南**:请参考 [KVROCKS_DEPLOYMENT.md](./KVROCKS_DEPLOYMENT.md)
- **监控简单**:提供标准 Redis 监控接口
- **迁移容易**:完全兼容 Redis 客户端和工具
+186
View File
@@ -0,0 +1,186 @@
# Kvrocks 部署指南
本文档介绍如何使用 Docker + Kvrocks 部署 KatelyaTV。
## 🚀 快速开始
### 方案一:无密码部署(推荐用于开发环境)
1. **准备环境变量文件**
```bash
# 复制环境变量示例文件
cp .env.kvrocks.example .env
# 编辑环境变量
nano .env
```
2. **环境变量配置**
```bash
# 数据库配置
NEXT_PUBLIC_STORAGE_TYPE=kvrocks
KVROCKS_URL=redis://kvrocks:6666
# 不设置密码
# KVROCKS_PASSWORD=
KVROCKS_DATABASE=0
# 应用配置
NEXTAUTH_SECRET=your_nextauth_secret_here
NEXTAUTH_URL=http://localhost:3000
```
3. **启动服务**
```bash
docker-compose -f docker-compose.kvrocks.yml up -d
```
### 方案二:密码认证部署(推荐用于生产环境)
1. **准备环境变量文件**
```bash
# 复制环境变量示例文件
cp .env.kvrocks.example .env
# 编辑环境变量
nano .env
```
2. **环境变量配置**
```bash
# 数据库配置
NEXT_PUBLIC_STORAGE_TYPE=kvrocks
KVROCKS_URL=redis://kvrocks:6666
# 设置强密码
KVROCKS_PASSWORD=your_secure_password_here
KVROCKS_DATABASE=0
# 应用配置
NEXTAUTH_SECRET=your_nextauth_secret_here
NEXTAUTH_URL=http://localhost:3000
```
3. **启动服务**
```bash
docker-compose -f docker-compose.kvrocks.auth.yml up -d
```
## 🔧 故障排除
### 问题 1:密码认证错误
```
❌ Kvrocks Client Error: [Error]: ERR Client sent AUTH, but no password is set
```
**解决方案:**
- 确保使用正确的 docker-compose 文件
- 检查环境变量 `KVROCKS_PASSWORD` 的设置
- 无密码部署使用:`docker-compose.kvrocks.yml`
- 密码认证部署使用:`docker-compose.kvrocks.auth.yml`
### 问题 2:连接超时
```
❌ Failed to connect to Kvrocks: connect ECONNREFUSED
```
**解决方案:**
1. 检查 Kvrocks 服务是否正常启动
```bash
docker-compose logs kvrocks
```
2. 检查网络连接
```bash
docker-compose exec katelyatv ping kvrocks
```
3. 检查端口映射
```bash
docker-compose ps
```
### 问题 3:数据持久化问题
**解决方案:**
1. 确保数据卷正确挂载
```bash
docker volume ls | grep kvrocks
```
2. 检查数据目录权限
```bash
docker-compose exec kvrocks ls -la /var/lib/kvrocks/data
```
## 📊 健康检查
### 检查服务状态
```bash
# 查看所有服务状态
docker-compose ps
# 查看日志
docker-compose logs -f
# 检查 Kvrocks 连接
docker-compose exec kvrocks redis-cli -p 6666 ping
```
### 性能监控
```bash
# 查看 Kvrocks 信息
docker-compose exec kvrocks redis-cli -p 6666 info
# 查看内存使用
docker-compose exec kvrocks redis-cli -p 6666 info memory
# 查看连接数
docker-compose exec kvrocks redis-cli -p 6666 info clients
```
## 🔒 安全建议
1. **生产环境必须设置密码**
2. **定期备份数据**
3. **限制网络访问**
4. **监控日志异常**
## 📁 文件结构
```
project/
├── docker-compose.kvrocks.yml # 无密码部署配置
├── docker-compose.kvrocks.auth.yml # 密码认证部署配置
├── .env.kvrocks.example # 环境变量示例
├── docker/
│ └── kvrocks/
│ ├── kvrocks.conf # 无密码配置文件
│ └── kvrocks.auth.conf # 密码认证配置文件
└── .env # 实际环境变量(需要创建)
```
## 🆘 获取帮助
如果遇到问题,请:
1. 检查日志:`docker-compose logs -f`
2. 验证环境变量:`docker-compose config`
3. 重启服务:`docker-compose restart`
4. 重新构建:`docker-compose up -d --force-recreate`
+1 -1
View File
@@ -1 +1 @@
if(!self.define){let e,s={};const n=(n,a)=>(n=new URL(n+".js",a).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didnt register its module`);return e}));self.define=(a,c)=>{const i=e||("document"in self?document.currentScript.src:"")||location.href;if(s[i])return;let t={};const r=e=>n(e,i),o={module:{uri:i},exports:t,require:r};s[i]=Promise.all(a.map(e=>o[e]||r(e))).then(e=>(c(...e),t))}}define(["./workbox-e9849328"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"ed4223c3f11fa5b30e75397a730aadec"},{url:"/_next/static/YxNDbDLH8EDPR3w9YWGxN/_buildManifest.js",revision:"046380ae5bc74b46b6d5eac3eed65355"},{url:"/_next/static/YxNDbDLH8EDPR3w9YWGxN/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/110-4d8fbe2ce6008c90.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/154-de4a84fd5b2e0100.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/29-0844689411ca7d55.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/459-6bec40a8423cc309.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/51b697cb-f464f3017ac1ea30.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/682-d1dca8d17a3a8e6f.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/900-fb094d8873768e88.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/967-217cdcb80ae3beeb.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/998-568996670b543597.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/app/_not-found/page-ac328df06cf68f14.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/app/admin/page-d05d4621a6953d54.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/app/config/page-11f6321397ad65b1.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/app/douban/page-2d0023184aa37aff.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/app/layout-bd0bfbfdb401e15f.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/app/login/page-6d62f8fe1814a4fb.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/app/page-6a58e37ab3250691.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/app/play/page-cbcfbf4a92cde119.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/app/search/page-63fe30b91e0539a7.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/app/tvbox/page-3a990d4dba7ad091.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/app/warning/page-11cba4cf9332a238.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/c72274ce-06682d6fc8197e6d.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/da9543df-bf6da1a431d8604f.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/framework-6e06c675866dc992.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/main-app-dbd320e104e1a5dc.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/main-ef3a79fcb73d32d2.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/pages/_app-792b631a362c29e1.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/pages/_error-9fde6601392a2a99.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-17170f1d90853b2d.js",revision:"YxNDbDLH8EDPR3w9YWGxN"},{url:"/_next/static/css/23100062f5d4aac0.css",revision:"23100062f5d4aac0"},{url:"/_next/static/css/27265468060ffa3a.css",revision:"27265468060ffa3a"},{url:"/_next/static/css/275ed64cc4367444.css",revision:"275ed64cc4367444"},{url:"/_next/static/media/26a46d62cd723877-s.woff2",revision:"befd9c0fdfa3d8a645d5f95717ed6420"},{url:"/_next/static/media/55c55f0601d81cf3-s.woff2",revision:"43828e14271c77b87e3ed582dbff9f74"},{url:"/_next/static/media/581909926a08bbc8-s.woff2",revision:"f0b86e7c24f455280b8df606b89af891"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/97e0cb1ae144a2a9-s.woff2",revision:"e360c61c5bd8d90639fd4503c829c2dc"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/favicon.ico",revision:"c5de6e56c5664adda146825f75ea6ecf"},{url:"/icons/icon-192x192.png",revision:"4a56c090828a1ad254c903c7aec0389d"},{url:"/icons/icon-256x256.png",revision:"f6409eb1a001f754121e3a8281c0319c"},{url:"/icons/icon-384x384.png",revision:"f6efc3e357b9ffdf4e0d8c14b2ed0ac1"},{url:"/icons/icon-512x512.png",revision:"9c008cbbeb6a576fe07bb1284a83f4d2"},{url:"/logo.png",revision:"40de611b143c47c6291c7bdad2c959ca"},{url:"/manifest.json",revision:"7bd3dabc1cfbfe40f09577efca223d31"},{url:"/robots.txt",revision:"e2b2cd8514443456bc6fb9d77b3b1f3e"},{url:"/screenshot1.png",revision:"10572bfcea54dc93ac4c5f7c9057fc98"},{url:"/screenshot2.png",revision:"f815a8990973a221899976867365c239"},{url:"/screenshot3.png",revision:"49709e96345dfeeab1d8083821d4b44e"},{url:"/screenshot4.png",revision:"a76c751e41e37556048a487e4f8b8b1c"},{url:"/wechat.jpg",revision:"d0f601311802667cd6ca5a37dc69bfa7"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:a})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")});
if(!self.define){let e,s={};const n=(n,t)=>(n=new URL(n+".js",t).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didnt register its module`);return e}));self.define=(t,a)=>{const c=e||("document"in self?document.currentScript.src:"")||location.href;if(s[c])return;let i={};const r=e=>n(e,c),o={module:{uri:c},exports:i,require:r};s[c]=Promise.all(t.map(e=>o[e]||r(e))).then(e=>(a(...e),i))}}define(["./workbox-e9849328"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"7db9e4ef70cbcab3780d6c680dd662a2"},{url:"/_next/static/_tNEn3OI_gHK8Eg73JHHK/_buildManifest.js",revision:"046380ae5bc74b46b6d5eac3eed65355"},{url:"/_next/static/_tNEn3OI_gHK8Eg73JHHK/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/110-9df7e37d43792a8e.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/154-de4a84fd5b2e0100.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/29-0844689411ca7d55.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/459-6bec40a8423cc309.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/51b697cb-f464f3017ac1ea30.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/682-d1dca8d17a3a8e6f.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/900-fb094d8873768e88.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/967-217cdcb80ae3beeb.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/998-568996670b543597.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/app/_not-found/page-ac328df06cf68f14.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/app/admin/page-d05d4621a6953d54.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/app/config/page-11f6321397ad65b1.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/app/douban/page-2d0023184aa37aff.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/app/layout-bd0bfbfdb401e15f.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/app/login/page-1638e1d936c78592.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/app/page-6a58e37ab3250691.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/app/play/page-586b6c5a6381cf6d.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/app/search/page-63fe30b91e0539a7.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/app/tvbox/page-3a990d4dba7ad091.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/app/warning/page-11cba4cf9332a238.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/c72274ce-06682d6fc8197e6d.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/da9543df-bf6da1a431d8604f.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/framework-6e06c675866dc992.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/main-459a10fe41fde25d.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/main-app-dbd320e104e1a5dc.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/pages/_app-792b631a362c29e1.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/pages/_error-9fde6601392a2a99.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-17170f1d90853b2d.js",revision:"_tNEn3OI_gHK8Eg73JHHK"},{url:"/_next/static/css/00661c2d88d90da0.css",revision:"00661c2d88d90da0"},{url:"/_next/static/css/23100062f5d4aac0.css",revision:"23100062f5d4aac0"},{url:"/_next/static/css/275ed64cc4367444.css",revision:"275ed64cc4367444"},{url:"/_next/static/media/26a46d62cd723877-s.woff2",revision:"befd9c0fdfa3d8a645d5f95717ed6420"},{url:"/_next/static/media/55c55f0601d81cf3-s.woff2",revision:"43828e14271c77b87e3ed582dbff9f74"},{url:"/_next/static/media/581909926a08bbc8-s.woff2",revision:"f0b86e7c24f455280b8df606b89af891"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/97e0cb1ae144a2a9-s.woff2",revision:"e360c61c5bd8d90639fd4503c829c2dc"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/favicon.ico",revision:"c5de6e56c5664adda146825f75ea6ecf"},{url:"/icons/icon-192x192.png",revision:"4a56c090828a1ad254c903c7aec0389d"},{url:"/icons/icon-256x256.png",revision:"f6409eb1a001f754121e3a8281c0319c"},{url:"/icons/icon-384x384.png",revision:"f6efc3e357b9ffdf4e0d8c14b2ed0ac1"},{url:"/icons/icon-512x512.png",revision:"9c008cbbeb6a576fe07bb1284a83f4d2"},{url:"/logo.png",revision:"40de611b143c47c6291c7bdad2c959ca"},{url:"/manifest.json",revision:"7bd3dabc1cfbfe40f09577efca223d31"},{url:"/robots.txt",revision:"e2b2cd8514443456bc6fb9d77b3b1f3e"},{url:"/screenshot1.png",revision:"10572bfcea54dc93ac4c5f7c9057fc98"},{url:"/screenshot2.png",revision:"f815a8990973a221899976867365c239"},{url:"/screenshot3.png",revision:"49709e96345dfeeab1d8083821d4b44e"},{url:"/screenshot4.png",revision:"a76c751e41e37556048a487e4f8b8b1c"},{url:"/wechat.jpg",revision:"d0f601311802667cd6ca5a37dc69bfa7"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:t})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")});
+17
View File
@@ -0,0 +1,17 @@
module.exports = {
env: {
node: true,
es6: true,
},
extends: ['eslint:recommended'],
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {
'no-console': 'off', // 允许在脚本中使用 console
'no-unused-vars': 'off', // 暂时忽略未使用变量
'@typescript-eslint/no-var-requires': 'off', // 允许 require
'import/no-import-module-exports': 'off',
},
};
+301
View File
@@ -0,0 +1,301 @@
#!/usr/bin/env node
/**
* KatelyaTV 全方案部署状态检查脚本
* 检查所有部署方案的配置文件和环境是否完整
*/
const fs = require('fs');
const path = require('path');
console.log('🔍 KatelyaTV 部署配置检查开始...\n');
// 检查结果统计
let checkResults = {
total: 0,
passed: 0,
failed: 0,
warnings: 0,
errors: []
};
// 辅助函数
function logCheck(name, status, message = '') {
checkResults.total++;
if (status === 'PASS') {
checkResults.passed++;
console.log(`${name}: PASS ${message}`);
} else if (status === 'WARN') {
checkResults.warnings++;
console.log(`⚠️ ${name}: WARN ${message}`);
} else {
checkResults.failed++;
console.log(`${name}: FAIL ${message}`);
checkResults.errors.push(`${name}: ${message}`);
}
}
function fileExists(filePath) {
try {
return fs.existsSync(filePath);
} catch (error) {
return false;
}
}
function readJsonFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
} catch (error) {
return null;
}
}
// 检查1Docker 部署配置
function checkDockerConfigs() {
console.log('🐳 检查 Docker 部署配置...');
const dockerConfigs = [
{
name: 'Docker + Redis 配置',
files: ['docker-compose.redis.yml', '.env.redis.example']
},
{
name: 'Docker + Kvrocks 配置(无密码)',
files: ['docker-compose.kvrocks.yml', '.env.kvrocks.example']
},
{
name: 'Docker + Kvrocks 配置(密码认证)',
files: ['docker-compose.kvrocks.auth.yml']
},
{
name: 'Docker + Kvrocks 本地构建配置',
files: ['docker-compose.kvrocks.local.yml']
}
];
for (const config of dockerConfigs) {
let allFilesExist = true;
let missingFiles = [];
for (const file of config.files) {
if (!fileExists(file)) {
allFilesExist = false;
missingFiles.push(file);
}
}
if (allFilesExist) {
logCheck(config.name, 'PASS', '所有配置文件存在');
} else {
logCheck(config.name, 'FAIL', `缺失文件: ${missingFiles.join(', ')}`);
}
}
}
// 检查2Cloudflare 部署配置
function checkCloudflareConfigs() {
console.log('\n☁️ 检查 Cloudflare 部署配置...');
const cloudflareFiles = [
'wrangler.toml',
'.env.cloudflare.example',
'scripts/d1-init.sql'
];
for (const file of cloudflareFiles) {
if (fileExists(file)) {
logCheck(`Cloudflare 配置文件 ${file}`, 'PASS', '文件存在');
} else {
logCheck(`Cloudflare 配置文件 ${file}`, 'FAIL', '文件不存在');
}
}
// 检查 wrangler.toml 内容
if (fileExists('wrangler.toml')) {
const content = fs.readFileSync('wrangler.toml', 'utf8');
if (content.includes('d1_databases') && content.includes('pages:build')) {
logCheck('wrangler.toml 内容', 'PASS', '包含必要配置');
} else {
logCheck('wrangler.toml 内容', 'WARN', '可能缺少部分配置');
}
}
}
// 检查3Vercel 部署配置
function checkVercelConfigs() {
console.log('\n▲ 检查 Vercel 部署配置...');
const vercelFile = 'vercel.json';
if (fileExists(vercelFile)) {
logCheck('Vercel 配置文件', 'PASS', 'vercel.json 存在');
const vercelConfig = readJsonFile(vercelFile);
if (vercelConfig) {
if (vercelConfig.build && vercelConfig.build.env) {
logCheck('Vercel 构建配置', 'PASS', '包含环境变量配置');
} else {
logCheck('Vercel 构建配置', 'WARN', '可能缺少构建环境配置');
}
}
} else {
logCheck('Vercel 配置文件', 'FAIL', 'vercel.json 不存在');
}
}
// 检查4:环境变量示例文件
function checkEnvExamples() {
console.log('\n⚙️ 检查环境变量示例文件...');
const envFiles = [
'.env.example',
'.env.redis.example',
'.env.kvrocks.example',
'.env.cloudflare.example'
];
for (const envFile of envFiles) {
if (fileExists(envFile)) {
const content = fs.readFileSync(envFile, 'utf8');
const hasStorageType = content.includes('NEXT_PUBLIC_STORAGE_TYPE');
const hasAuthConfig = content.includes('NEXTAUTH_SECRET');
if (hasStorageType && hasAuthConfig) {
logCheck(`环境变量文件 ${envFile}`, 'PASS', '包含必要配置');
} else {
logCheck(`环境变量文件 ${envFile}`, 'WARN', '可能缺少部分配置');
}
} else {
logCheck(`环境变量文件 ${envFile}`, 'FAIL', '文件不存在');
}
}
}
// 检查5package.json 脚本
function checkPackageScripts() {
console.log('\n📦 检查 package.json 构建脚本...');
const packageJson = readJsonFile('package.json');
if (packageJson && packageJson.scripts) {
const requiredScripts = [
'dev',
'build',
'start',
'pages:build', // Cloudflare Pages
'lint'
];
for (const script of requiredScripts) {
if (packageJson.scripts[script]) {
logCheck(`package.json 脚本 ${script}`, 'PASS', '脚本存在');
} else {
logCheck(`package.json 脚本 ${script}`, 'WARN', '脚本不存在或未配置');
}
}
} else {
logCheck('package.json', 'FAIL', '文件不存在或格式错误');
}
}
// 检查6Kvrocks 配置文件
function checkKvrocksConfigs() {
console.log('\n🏪 检查 Kvrocks 配置文件...');
const kvrocksConfigs = [
'docker/kvrocks/kvrocks.conf',
'docker/kvrocks/kvrocks.auth.conf'
];
for (const configFile of kvrocksConfigs) {
if (fileExists(configFile)) {
const content = fs.readFileSync(configFile, 'utf8');
const hasBasicConfig = content.includes('bind') && content.includes('port');
if (hasBasicConfig) {
logCheck(`Kvrocks 配置 ${path.basename(configFile)}`, 'PASS', '包含基本配置');
} else {
logCheck(`Kvrocks 配置 ${path.basename(configFile)}`, 'WARN', '可能缺少基本配置');
}
} else {
logCheck(`Kvrocks 配置 ${path.basename(configFile)}`, 'FAIL', '文件不存在');
}
}
}
// 检查7:文档文件
function checkDocumentation() {
console.log('\n📚 检查文档文件...');
const docFiles = [
'README.md',
'docs/KVROCKS.md',
'docs/KVROCKS_DEPLOYMENT.md',
'docs/TVBOX.md',
'KVROCKS_FIX_REPORT.md'
];
for (const docFile of docFiles) {
if (fileExists(docFile)) {
logCheck(`文档文件 ${docFile}`, 'PASS', '文件存在');
} else {
logCheck(`文档文件 ${docFile}`, 'WARN', '文件不存在');
}
}
}
// 主检查函数
async function runChecks() {
try {
await checkDockerConfigs();
await checkCloudflareConfigs();
await checkVercelConfigs();
await checkEnvExamples();
await checkPackageScripts();
await checkKvrocksConfigs();
await checkDocumentation();
} catch (error) {
console.error('检查执行出错:', error);
checkResults.failed++;
checkResults.errors.push(`检查执行出错: ${error.message}`);
}
// 输出检查结果
console.log('\n' + '='.repeat(60));
console.log('📊 部署配置检查结果汇总:');
console.log(` 总计: ${checkResults.total} 项检查`);
console.log(` 通过: ${checkResults.passed} 项 ✅`);
console.log(` 警告: ${checkResults.warnings} 项 ⚠️`);
console.log(` 失败: ${checkResults.failed} 项 ❌`);
if (checkResults.failed > 0) {
console.log('\n🚨 失败的检查项:');
checkResults.errors.forEach((error, index) => {
console.log(` ${index + 1}. ${error}`);
});
}
if (checkResults.warnings > 0) {
console.log('\n⚠️ 警告说明:');
console.log(' - 警告项目不影响基本功能,但建议完善');
console.log(' - 可能影响特定部署方案或高级功能');
}
if (checkResults.failed === 0) {
console.log('\n🎉 所有必要配置文件检查通过!');
console.log(' 您可以选择以下任意部署方案:');
console.log(' 1. 🐳 Docker + Redis (docker-compose.redis.yml)');
console.log(' 2. 🏪 Docker + Kvrocks (docker-compose.kvrocks.yml)');
console.log(' 3. ☁️ Cloudflare Pages + D1 (wrangler.toml)');
console.log(' 4. ▲ Vercel + Upstash (vercel.json)');
}
console.log('='.repeat(60));
// 退出代码
process.exit(checkResults.failed > 0 ? 1 : 0);
}
// 运行检查
runChecks().catch(console.error);
+109
View File
@@ -0,0 +1,109 @@
-- D1 数据库初始化脚本
-- 用于创建 KatelyaTV 所需的数据表
-- 用户表
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 播放记录表
CREATE TABLE IF NOT EXISTS play_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
record_key TEXT NOT NULL,
video_url TEXT,
current_time REAL DEFAULT 0,
duration REAL DEFAULT 0,
episode_index INTEGER DEFAULT 0,
episode_url TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE (user_id, record_key)
);
-- 收藏表
CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
favorite_key TEXT NOT NULL,
title TEXT,
cover_url TEXT,
rating REAL,
year TEXT,
area TEXT,
category TEXT,
actors TEXT,
director TEXT,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE (user_id, favorite_key)
);
-- 搜索历史表
CREATE TABLE IF NOT EXISTS search_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
keyword TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- 跳过配置表
CREATE TABLE IF NOT EXISTS skip_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
config_key TEXT NOT NULL,
start_time INTEGER DEFAULT 0,
end_time INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE (user_id, config_key)
);
-- 管理员配置表
CREATE TABLE IF NOT EXISTS admin_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_key TEXT UNIQUE NOT NULL,
config_value TEXT,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 插入默认管理员配置
INSERT OR IGNORE INTO admin_configs (config_key, config_value, description) VALUES
('site_name', 'KatelyaTV', '站点名称'),
('site_description', '高性能影视播放平台', '站点描述'),
('enable_register', 'true', '是否允许用户注册'),
('max_users', '100', '最大用户数量'),
('cache_ttl', '3600', '缓存时间(秒)');
-- 创建索引以提高查询性能
CREATE INDEX IF NOT EXISTS idx_play_records_user_id ON play_records(user_id);
CREATE INDEX IF NOT EXISTS idx_play_records_record_key ON play_records(record_key);
CREATE INDEX IF NOT EXISTS idx_favorites_user_id ON favorites(user_id);
CREATE INDEX IF NOT EXISTS idx_search_history_user_id ON search_history(user_id);
CREATE INDEX IF NOT EXISTS idx_skip_configs_user_id ON skip_configs(user_id);
-- 创建视图以简化查询
CREATE VIEW IF NOT EXISTS user_stats AS
SELECT
u.id,
u.username,
COUNT(DISTINCT pr.id) as play_count,
COUNT(DISTINCT f.id) as favorite_count,
COUNT(DISTINCT sh.id) as search_count,
u.created_at
FROM users u
LEFT JOIN play_records pr ON u.id = pr.user_id
LEFT JOIN favorites f ON u.id = f.user_id
LEFT JOIN search_history sh ON u.id = sh.user_id
GROUP BY u.id, u.username, u.created_at;
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env node
/**
* Kvrocks 部署测试脚本
* 用于验证 Docker + Kvrocks 部署是否正常工作
*/
const { createClient } = require('redis');
const { spawn } = require('child_process');
const fs = require('fs');
// 配置
const TEST_CONFIG = {
KVROCKS_URL: process.env.KVROCKS_URL || 'redis://localhost:6666',
KVROCKS_PASSWORD: process.env.KVROCKS_PASSWORD,
KVROCKS_DATABASE: parseInt(process.env.KVROCKS_DATABASE || '0'),
TEST_TIMEOUT: 30000, // 30秒超时
};
console.log('🧪 Kvrocks 部署测试开始...\n');
// 测试结果统计
let testResults = {
total: 0,
passed: 0,
failed: 0,
errors: []
};
// 辅助函数
function logTest(name, status, message = '') {
testResults.total++;
if (status === 'PASS') {
testResults.passed++;
console.log(`${name}: PASS ${message}`);
} else {
testResults.failed++;
console.log(`${name}: FAIL ${message}`);
testResults.errors.push(`${name}: ${message}`);
}
}
// 测试1:检查 Docker Compose 文件
async function testDockerComposeFiles() {
console.log('📁 测试 Docker Compose 配置文件...');
const files = [
'docker-compose.kvrocks.yml',
'docker-compose.kvrocks.auth.yml'
];
for (const file of files) {
try {
if (fs.existsSync(file)) {
const content = fs.readFileSync(file, 'utf8');
if (content.includes('kvrocks:') && content.includes('katelyatv:')) {
logTest(`Docker Compose 文件 ${file}`, 'PASS', '配置正确');
} else {
logTest(`Docker Compose 文件 ${file}`, 'FAIL', '配置缺失');
}
} else {
logTest(`Docker Compose 文件 ${file}`, 'FAIL', '文件不存在');
}
} catch (error) {
logTest(`Docker Compose 文件 ${file}`, 'FAIL', error.message);
}
}
}
// 测试2:检查环境变量配置
async function testEnvironmentConfig() {
console.log('\n🔧 测试环境变量配置...');
// 检查必需的环境变量
const requiredVars = ['NEXT_PUBLIC_STORAGE_TYPE'];
const optionalVars = ['KVROCKS_PASSWORD', 'NEXTAUTH_SECRET'];
for (const varName of requiredVars) {
if (process.env[varName]) {
logTest(`环境变量 ${varName}`, 'PASS', `值: ${process.env[varName]}`);
} else {
logTest(`环境变量 ${varName}`, 'FAIL', '未设置');
}
}
for (const varName of optionalVars) {
if (process.env[varName]) {
logTest(`环境变量 ${varName}`, 'PASS', '已设置');
} else {
logTest(`环境变量 ${varName}`, 'PASS', '未设置(可选)');
}
}
// 检查存储类型
if (process.env.NEXT_PUBLIC_STORAGE_TYPE === 'kvrocks') {
logTest('存储类型配置', 'PASS', 'kvrocks');
} else {
logTest('存储类型配置', 'FAIL', `期望 kvrocks,实际 ${process.env.NEXT_PUBLIC_STORAGE_TYPE}`);
}
}
// 测试3Kvrocks 连接测试
async function testKvrocksConnection() {
console.log('\n🔌 测试 Kvrocks 连接...');
let client;
try {
// 构建客户端配置
const clientConfig = {
url: TEST_CONFIG.KVROCKS_URL,
database: TEST_CONFIG.KVROCKS_DATABASE,
socket: {
connectTimeout: 5000,
},
};
// 只有当密码存在且不为空时才添加密码配置
if (TEST_CONFIG.KVROCKS_PASSWORD && TEST_CONFIG.KVROCKS_PASSWORD.trim() !== '') {
clientConfig.password = TEST_CONFIG.KVROCKS_PASSWORD;
console.log('🔐 使用密码认证连接');
} else {
console.log('🔓 无密码认证连接');
}
client = createClient(clientConfig);
// 连接
await client.connect();
logTest('Kvrocks 连接', 'PASS', '连接成功');
// 测试 PING
const pong = await client.ping();
if (pong === 'PONG') {
logTest('Kvrocks PING', 'PASS', 'PONG');
} else {
logTest('Kvrocks PING', 'FAIL', `响应: ${pong}`);
}
// 测试基本操作
const testKey = 'test:' + Date.now();
const testValue = 'test-value-' + Math.random();
await client.set(testKey, testValue);
const getValue = await client.get(testKey);
if (getValue === testValue) {
logTest('Kvrocks 读写操作', 'PASS', '数据一致');
} else {
logTest('Kvrocks 读写操作', 'FAIL', `期望 ${testValue},实际 ${getValue}`);
}
// 清理测试数据
await client.del(testKey);
// 测试数据库信息
const info = await client.info();
if (info.includes('kvrocks_version')) {
const version = info.match(/kvrocks_version:([^\r\n]+)/)?.[1];
logTest('Kvrocks 版本信息', 'PASS', `版本: ${version}`);
} else {
logTest('Kvrocks 版本信息', 'FAIL', '无法获取版本信息');
}
} catch (error) {
logTest('Kvrocks 连接', 'FAIL', error.message);
} finally {
if (client && client.isOpen) {
await client.quit();
}
}
}
// 测试4Docker 服务状态检查
async function testDockerServices() {
console.log('\n🐳 测试 Docker 服务状态...');
return new Promise((resolve) => {
const docker = spawn('docker-compose', ['ps'], { stdio: 'pipe' });
let output = '';
docker.stdout.on('data', (data) => {
output += data.toString();
});
docker.on('close', (code) => {
if (code === 0) {
if (output.includes('kvrocks') && output.includes('Up')) {
logTest('Docker Kvrocks 服务', 'PASS', '服务运行中');
} else {
logTest('Docker Kvrocks 服务', 'FAIL', '服务未运行');
}
if (output.includes('katelyatv') && output.includes('Up')) {
logTest('Docker KatelyaTV 服务', 'PASS', '服务运行中');
} else {
logTest('Docker KatelyaTV 服务', 'FAIL', '服务未运行或未启动');
}
} else {
logTest('Docker 服务检查', 'FAIL', 'docker-compose 命令执行失败');
}
resolve();
});
docker.on('error', (error) => {
logTest('Docker 服务检查', 'FAIL', `Docker 未安装或不可用: ${error.message}`);
resolve();
});
});
}
// 主测试函数
async function runTests() {
console.log(`🏗️ 测试配置:`);
console.log(` Kvrocks URL: ${TEST_CONFIG.KVROCKS_URL}`);
console.log(` 密码认证: ${TEST_CONFIG.KVROCKS_PASSWORD ? '是' : '否'}`);
console.log(` 数据库: ${TEST_CONFIG.KVROCKS_DATABASE}`);
console.log('');
try {
await testDockerComposeFiles();
await testEnvironmentConfig();
await testDockerServices();
await testKvrocksConnection();
} catch (error) {
console.error('测试执行出错:', error);
testResults.failed++;
testResults.errors.push(`测试执行出错: ${error.message}`);
}
// 输出测试结果
console.log('\n' + '='.repeat(50));
console.log('📊 测试结果汇总:');
console.log(` 总计: ${testResults.total} 项测试`);
console.log(` 通过: ${testResults.passed} 项 ✅`);
console.log(` 失败: ${testResults.failed} 项 ❌`);
if (testResults.failed > 0) {
console.log('\n🚨 失败的测试项:');
testResults.errors.forEach((error, index) => {
console.log(` ${index + 1}. ${error}`);
});
console.log('\n💡 解决建议:');
console.log(' 1. 检查 Docker 服务是否正常启动');
console.log(' 2. 验证环境变量配置是否正确');
console.log(' 3. 确认网络连接是否正常');
console.log(' 4. 查看详细部署指南: docs/KVROCKS_DEPLOYMENT.md');
} else {
console.log('\n🎉 所有测试通过!Kvrocks 部署正常工作。');
}
console.log('='.repeat(50));
// 退出代码
process.exit(testResults.failed > 0 ? 1 : 0);
}
// 运行测试
runTests().catch(console.error);
+122
View File
@@ -0,0 +1,122 @@
/* eslint-disable no-console, @typescript-eslint/no-explicit-any */
/**
* 验证 Kvrocks 密码处理修复
* 模拟用户反馈的错误场景
*/
// 模拟用户的环境变量设置
process.env.NEXT_PUBLIC_STORAGE_TYPE = 'kvrocks';
process.env.KVROCKS_URL = 'redis://kvrocks:6666';
process.env.KVROCKS_PASSWORD = ''; // 用户设置了空密码,这是问题所在
process.env.KVROCKS_DATABASE = '0';
// 模拟 Redis 客户端创建函数
function createClient(config) {
console.log('🔧 创建 Redis 客户端配置:', JSON.stringify(config, null, 2));
if (config.password === '') {
console.log('❌ 检测到空密码,这会导致认证错误!');
return {
connect: () => Promise.reject(new Error('ERR Client sent AUTH, but no password is set')),
isOpen: false
};
} else if (config.password === undefined) {
console.log('✅ 无密码配置,正常连接');
return {
connect: () => Promise.resolve(),
isOpen: true
};
} else {
console.log('✅ 有效密码配置,正常连接');
return {
connect: () => Promise.resolve(),
isOpen: true
};
}
}
// 使用修复后的客户端创建逻辑
function getKvrocksClient() {
const kvrocksUrl = process.env.KVROCKS_URL || 'redis://localhost:6666';
const kvrocksPassword = process.env.KVROCKS_PASSWORD;
const kvrocksDatabase = parseInt(process.env.KVROCKS_DATABASE || '0');
console.log('🏪 Initializing Kvrocks client...');
console.log('🔗 Kvrocks URL:', kvrocksUrl);
console.log('🔑 Password configured:', kvrocksPassword ? 'Yes' : 'No');
console.log('🔑 Password value:', JSON.stringify(kvrocksPassword));
// 构建客户端配置
const clientConfig = {
url: kvrocksUrl,
database: kvrocksDatabase,
socket: {
connectTimeout: 10000,
},
};
// 只有当密码存在且不为空时才添加密码配置
if (kvrocksPassword && kvrocksPassword.trim() !== '') {
clientConfig.password = kvrocksPassword;
console.log('🔐 Using password authentication');
} else {
console.log('🔓 No password authentication (connecting without password)');
}
return createClient(clientConfig);
}
async function testScenarios() {
console.log('🧪 测试不同密码配置场景\n');
// 场景1:用户的问题场景 - 空字符串密码
console.log('📝 场景1:用户问题场景(空字符串密码)');
console.log('环境变量: KVROCKS_PASSWORD=""');
process.env.KVROCKS_PASSWORD = '';
try {
const client = getKvrocksClient();
await client.connect();
console.log('✅ 场景1通过:无认证错误\n');
} catch (error) {
console.log('❌ 场景1失败:', error.message, '\n');
}
// 场景2:未设置密码
console.log('📝 场景2:未设置密码');
console.log('环境变量: KVROCKS_PASSWORD=undefined');
delete process.env.KVROCKS_PASSWORD;
try {
const client = getKvrocksClient();
await client.connect();
console.log('✅ 场景2通过:无认证错误\n');
} catch (error) {
console.log('❌ 场景2失败:', error.message, '\n');
}
// 场景3:有效密码
console.log('📝 场景3:有效密码');
console.log('环境变量: KVROCKS_PASSWORD="validpassword"');
process.env.KVROCKS_PASSWORD = 'validpassword';
try {
const client = getKvrocksClient();
await client.connect();
console.log('✅ 场景3通过:正常密码认证\n');
} catch (error) {
console.log('❌ 场景3失败:', error.message, '\n');
}
// 场景4:只有空格的密码
console.log('📝 场景4:只有空格的密码');
console.log('环境变量: KVROCKS_PASSWORD=" "');
process.env.KVROCKS_PASSWORD = ' ';
try {
const client = getKvrocksClient();
await client.connect();
console.log('✅ 场景4通过:空格密码被正确处理\n');
} catch (error) {
console.log('❌ 场景4失败:', error.message, '\n');
}
}
testScenarios().catch(console.error);
+17 -1
View File
@@ -21,8 +21,16 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { ChevronDown, ChevronUp, Settings, Users, Video } from 'lucide-react';
import {
ChevronDown,
ChevronUp,
Settings,
Tv,
Users,
Video,
} from 'lucide-react';
import { GripVertical } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { Suspense, useCallback, useEffect, useState } from 'react';
import Swal from 'sweetalert2';
@@ -1609,6 +1617,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
};
function AdminPageClient() {
const router = useRouter();
const [config, setConfig] = useState<AdminConfig | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -1728,6 +1737,13 @@ function AdminPageClient() {
</button>
)}
<button
onClick={() => router.push('/config')}
className='px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded-md transition-colors flex items-center gap-1'
>
<Tv size={14} />
<span>TVBox </span>
</button>
</div>
{/* 站点配置标签 */}
+28 -14
View File
@@ -69,20 +69,34 @@ export async function GET(request: NextRequest) {
wallpaper: `${baseUrl}/screenshot1.png`, // 使用项目截图作为壁纸
// 影视源配置
sites: sourceConfigs.map((source) => ({
key: source.key || source.name,
name: source.name,
type: 0, // 影视源
api: source.api,
searchable: 1, // 可搜索
quickSearch: 1, // 支持快速搜索
filterable: 1, // 支持分类筛选
ext: source.detail || '', // 详情页地址作为扩展参数
timeout: 30, // 30秒超时
categories: [
"电影", "电视剧", "综艺", "动漫", "纪录片", "短剧"
]
})),
sites: sourceConfigs.map((source) => {
// 更智能的type判断逻辑:
// 1. 如果api地址包含 "/provide/vod" 且不包含 "at/xml",则认为是JSON类型 (type=1)
// 2. 如果api地址包含 "at/xml",则认为是XML类型 (type=0)
// 3. 如果api地址以 ".json" 结尾,则认为是JSON类型 (type=1)
// 4. 其他情况默认为JSON类型 (type=1),因为现在大部分都是JSON
let type = 1; // 默认为JSON类型
const apiLower = source.api.toLowerCase();
if (apiLower.includes('at/xml') || apiLower.endsWith('.xml')) {
type = 0; // XML类型
}
return {
key: source.key || source.name,
name: source.name,
type: type, // 使用智能判断的type
api: source.api,
searchable: 1, // 可搜索
quickSearch: 1, // 支持快速搜索
filterable: 1, // 支持分类筛选
ext: source.detail || '', // 详情页地址作为扩展参数
timeout: 30, // 30秒超时
categories: [
"电影", "电视剧", "综艺", "动漫", "纪录片", "短剧"
]
};
}),
// 解析源配置(添加一些常用的解析源)
parses: [
+137 -12
View File
@@ -20,6 +20,7 @@ import { DoubanItem } from '@/lib/types';
import CapsuleSwitch from '@/components/CapsuleSwitch';
import ContinueWatching from '@/components/ContinueWatching';
import PageLayout from '@/components/PageLayout';
import PaginatedRow from '@/components/PaginatedRow';
import { useSite } from '@/components/SiteProvider';
import VideoCard from '@/components/VideoCard';
@@ -81,6 +82,21 @@ function HomeClient() {
const [loading, setLoading] = useState(true);
const { announcement } = useSite();
// 分页状态管理
const [moviePage, setMoviePage] = useState(0);
const [tvShowPage, setTvShowPage] = useState(0);
const [varietyShowPage, setVarietyShowPage] = useState(0);
const [loadingMore, setLoadingMore] = useState({
movies: false,
tvShows: false,
varietyShows: false,
});
const [hasMoreData, setHasMoreData] = useState({
movies: true,
tvShows: true,
varietyShows: true,
});
const [showAnnouncement, setShowAnnouncement] = useState(false);
// 检查公告弹窗状态
@@ -147,6 +163,100 @@ function HomeClient() {
fetchDoubanData();
}, []);
// 加载更多电影
const loadMoreMovies = async () => {
if (loadingMore.movies || !hasMoreData.movies) return;
setLoadingMore(prev => ({ ...prev, movies: true }));
try {
const nextPage = moviePage + 1;
const moviesData = await getDoubanCategories({
kind: 'movie',
category: '热门',
type: '全部',
pageStart: nextPage * 20,
pageLimit: 20,
});
if (moviesData.code === 200 && moviesData.list.length > 0) {
setHotMovies(prev => [...prev, ...moviesData.list]);
setMoviePage(nextPage);
// 如果返回的数据少于请求的数量,说明没有更多数据了
if (moviesData.list.length < 20) {
setHasMoreData(prev => ({ ...prev, movies: false }));
}
} else {
setHasMoreData(prev => ({ ...prev, movies: false }));
}
} catch (error) {
// 静默处理错误
} finally {
setLoadingMore(prev => ({ ...prev, movies: false }));
}
};
// 加载更多剧集
const loadMoreTvShows = async () => {
if (loadingMore.tvShows || !hasMoreData.tvShows) return;
setLoadingMore(prev => ({ ...prev, tvShows: true }));
try {
const nextPage = tvShowPage + 1;
const tvShowsData = await getDoubanCategories({
kind: 'tv',
category: 'tv',
type: 'tv',
pageStart: nextPage * 20,
pageLimit: 20,
});
if (tvShowsData.code === 200 && tvShowsData.list.length > 0) {
setHotTvShows(prev => [...prev, ...tvShowsData.list]);
setTvShowPage(nextPage);
if (tvShowsData.list.length < 20) {
setHasMoreData(prev => ({ ...prev, tvShows: false }));
}
} else {
setHasMoreData(prev => ({ ...prev, tvShows: false }));
}
} catch (error) {
// 静默处理错误
} finally {
setLoadingMore(prev => ({ ...prev, tvShows: false }));
}
};
// 加载更多综艺
const loadMoreVarietyShows = async () => {
if (loadingMore.varietyShows || !hasMoreData.varietyShows) return;
setLoadingMore(prev => ({ ...prev, varietyShows: true }));
try {
const nextPage = varietyShowPage + 1;
const varietyShowsData = await getDoubanCategories({
kind: 'tv',
category: 'show',
type: 'show',
pageStart: nextPage * 20,
pageLimit: 20,
});
if (varietyShowsData.code === 200 && varietyShowsData.list.length > 0) {
setHotVarietyShows(prev => [...prev, ...varietyShowsData.list]);
setVarietyShowPage(nextPage);
if (varietyShowsData.list.length < 20) {
setHasMoreData(prev => ({ ...prev, varietyShows: false }));
}
} else {
setHasMoreData(prev => ({ ...prev, varietyShows: false }));
}
} catch (error) {
// 静默处理错误
} finally {
setLoadingMore(prev => ({ ...prev, varietyShows: false }));
}
};
// 处理收藏数据更新的函数
const updateFavoriteItems = async (allFavorites: Record<string, Favorite>) => {
const allPlayRecords = await getAllPlayRecords();
@@ -291,7 +401,12 @@ function HomeClient() {
<ChevronRight className='w-4 h-4 ml-1' />
</Link>
</div>
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4'>
<PaginatedRow
itemsPerPage={10}
onLoadMore={loadMoreMovies}
hasMoreData={hasMoreData.movies}
isLoading={loadingMore.movies}
>
{loading
? // 加载状态显示灰色占位数据 (显示10个,2行x5列)
Array.from({ length: 10 }).map((_, index) => (
@@ -305,8 +420,8 @@ function HomeClient() {
<div className='mt-2 h-4 bg-purple-200 rounded animate-pulse dark:bg-purple-800'></div>
</div>
))
: // 显示真实数据,只显示前10个实现2行布局
hotMovies.slice(0, 10).map((movie, index) => (
: // 显示真实数据
hotMovies.map((movie, index) => (
<div
key={index}
className='w-full'
@@ -322,7 +437,7 @@ function HomeClient() {
/>
</div>
))}
</div>
</PaginatedRow>
</section>
{/* 热门剧集 */}
@@ -339,7 +454,12 @@ function HomeClient() {
<ChevronRight className='w-4 h-4 ml-1' />
</Link>
</div>
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4'>
<PaginatedRow
itemsPerPage={10}
onLoadMore={loadMoreTvShows}
hasMoreData={hasMoreData.tvShows}
isLoading={loadingMore.tvShows}
>
{loading
? // 加载状态显示灰色占位数据 (显示10个,2行x5列)
Array.from({ length: 10 }).map((_, index) => (
@@ -353,8 +473,8 @@ function HomeClient() {
<div className='mt-2 h-4 bg-purple-200 rounded animate-pulse dark:bg-purple-800'></div>
</div>
))
: // 显示真实数据,只显示前10个实现2行布局
hotTvShows.slice(0, 10).map((show, index) => (
: // 显示真实数据
hotTvShows.map((show, index) => (
<div
key={index}
className='w-full'
@@ -369,7 +489,7 @@ function HomeClient() {
/>
</div>
))}
</div>
</PaginatedRow>
</section>
{/* 热门综艺 */}
@@ -386,7 +506,12 @@ function HomeClient() {
<ChevronRight className='w-4 h-4 ml-1' />
</Link>
</div>
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4'>
<PaginatedRow
itemsPerPage={10}
onLoadMore={loadMoreVarietyShows}
hasMoreData={hasMoreData.varietyShows}
isLoading={loadingMore.varietyShows}
>
{loading
? // 加载状态显示灰色占位数据 (显示10个,2行x5列)
Array.from({ length: 10 }).map((_, index) => (
@@ -400,8 +525,8 @@ function HomeClient() {
<div className='mt-2 h-4 bg-purple-200 rounded animate-pulse dark:bg-purple-800'></div>
</div>
))
: // 显示真实数据,只显示前10个实现2行布局
hotVarietyShows.slice(0, 10).map((show, index) => (
: // 显示真实数据
hotVarietyShows.map((show, index) => (
<div
key={index}
className='w-full'
@@ -416,7 +541,7 @@ function HomeClient() {
/>
</div>
))}
</div>
</PaginatedRow>
</section>
{/* 首页底部 Logo */}
+4 -2
View File
@@ -737,12 +737,14 @@ function PlayPageClient() {
// ---------------------------------------------------------------------------
// 处理集数切换
const handleEpisodeChange = (episodeNumber: number) => {
if (episodeNumber >= 0 && episodeNumber < totalEpisodes) {
// episodeNumber是显示的集数(从1开始),需要转换为索引(从0开始)
const episodeIndex = episodeNumber - 1;
if (episodeIndex >= 0 && episodeIndex < totalEpisodes) {
// 在更换集数前保存当前播放进度
if (artPlayerRef.current && artPlayerRef.current.paused) {
saveCurrentPlayProgress();
}
setCurrentEpisodeIndex(episodeNumber);
setCurrentEpisodeIndex(episodeIndex);
}
};
+8 -3
View File
@@ -321,7 +321,7 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
</div>
{/* 集数网格 */}
<div className='grid grid-cols-[repeat(auto-fill,minmax(40px,1fr))] auto-rows-[40px] gap-x-3 gap-y-3 overflow-y-auto h-full pb-4'>
<div className='grid grid-cols-[repeat(auto-fill,minmax(48px,1fr))] justify-center gap-2 overflow-y-auto pb-4'>
{(() => {
const len = currentEnd - currentStart + 1;
const episodes = Array.from({ length: len }, (_, i) =>
@@ -333,13 +333,18 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
return (
<button
key={episodeNumber}
onClick={() => handleEpisodeClick(episodeNumber)}
className={`h-10 flex items-center justify-center text-sm font-medium rounded-md transition-all duration-200
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleEpisodeClick(episodeNumber);
}}
className={`w-full h-10 flex items-center justify-center text-sm font-medium rounded-md transition-all duration-200 cursor-pointer
${
isActive
? 'bg-green-500 text-white shadow-lg shadow-green-500/25 dark:bg-green-600'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 hover:scale-105 dark:bg-white/10 dark:text-gray-300 dark:hover:bg-white/20'
}`.trim()}
type="button"
>
{episodeNumber}
</button>
+138
View File
@@ -0,0 +1,138 @@
'use client';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { useId, useMemo, useState } from 'react';
interface PaginatedRowProps {
children: React.ReactNode[];
itemsPerPage?: number;
className?: string;
onLoadMore?: () => Promise<void>; // 新增:加载更多数据的回调函数
hasMoreData?: boolean; // 新增:是否还有更多数据可加载
isLoading?: boolean; // 新增:是否正在加载中
}
export default function PaginatedRow({
children,
itemsPerPage = 10,
className = '',
onLoadMore,
hasMoreData = true,
isLoading = false,
}: PaginatedRowProps) {
const [startIndex, setStartIndex] = useState(0);
const [isHovered, setIsHovered] = useState(false);
const uniqueId = useId(); // 为每个实例生成唯一ID
// 获取当前显示的项目 - 支持无限向前浏览
const currentItems = useMemo(() => {
const endIndex = startIndex + itemsPerPage;
// 如果超出范围,循环显示
if (endIndex <= children.length) {
return children.slice(startIndex, endIndex);
} else {
// 当超出范围时,从头开始循环
const firstPart = children.slice(startIndex);
const secondPart = children.slice(0, endIndex - children.length);
return [...firstPart, ...secondPart];
}
}, [children, startIndex, itemsPerPage]);
// 向前翻页 - 禁止超出第一页
const handlePrevPage = () => {
setStartIndex((prev) => {
const newIndex = prev - itemsPerPage;
return newIndex < 0 ? 0 : newIndex; // 不允许小于0
});
};
// 向后翻页 - 支持动态加载更多数据
const handleNextPage = async () => {
const newIndex = startIndex + itemsPerPage;
// 如果即将超出当前数据范围,且有更多数据可加载,且有加载回调函数
if (newIndex >= children.length && hasMoreData && onLoadMore && !isLoading) {
try {
await onLoadMore(); // 加载更多数据
// 加载完成后,直接设置到下一页
setStartIndex(newIndex);
} catch (error) {
// 静默处理加载错误,保持用户体验
}
} else if (newIndex < children.length) {
// 如果还在当前数据范围内,直接翻页
setStartIndex(newIndex);
} else {
// 如果没有更多数据可加载,循环回到第一页
setStartIndex(0);
}
};
// 检查是否可以向前翻页
const canGoPrev = startIndex > 0;
// 检查是否可以向后翻页:有更多数据或者当前不在最后一页
const canGoNext = children.length > itemsPerPage && (startIndex + itemsPerPage < children.length || hasMoreData || startIndex + itemsPerPage >= children.length);
// 如果没有足够的内容需要分页,就不显示按钮
const needsPagination = children.length > itemsPerPage;
return (
<div
className={`relative ${className}`}
data-paginated-row={uniqueId}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* 内容区域 - 移除group类以避免悬停效果冲突 */}
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 relative'>
{currentItems}
{/* 改进的导航按钮 - 仅在容器悬停时显示 */}
{needsPagination && (
<>
{/* 左箭头按钮 - 只有不在第一页时才显示 */}
{canGoPrev && (
<button
onClick={handlePrevPage}
className={`absolute -left-12 z-20 w-10 h-10 bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 rounded-full shadow-lg hover:shadow-xl flex items-center justify-center transition-all duration-300 hover:scale-110 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900 ${
isHovered ? 'opacity-100' : 'opacity-0'
}`}
style={{
// 确保按钮在两行中间
top: 'calc(50% - 20px)',
}}
aria-label='上一页'
>
<ChevronLeft className='w-5 h-5 text-white' />
</button>
)}
{/* 右箭头按钮 - 总是显示,支持动态加载 */}
{canGoNext && (
<button
onClick={handleNextPage}
disabled={isLoading}
className={`absolute -right-12 z-20 w-10 h-10 bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 rounded-full shadow-lg hover:shadow-xl flex items-center justify-center transition-all duration-300 hover:scale-110 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900 disabled:opacity-50 disabled:cursor-not-allowed ${
isHovered ? 'opacity-100' : 'opacity-0'
}`}
style={{
// 确保按钮在两行中间
top: 'calc(50% - 20px)',
}}
aria-label={isLoading ? '加载中...' : '下一页'}
>
{isLoading ? (
<div className='w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin' />
) : (
<ChevronRight className='w-5 h-5 text-white' />
)}
</button>
)}
</>
)}
</div>
{/* 移除页码指示器 - 不再需要 */}
</div>
);
}
+86 -14
View File
@@ -43,7 +43,8 @@ export default function SkipController({
const [batchSettings, setBatchSettings] = useState({
openingStart: '0:00', // 片头开始时间(分:秒格式)
openingEnd: '1:30', // 片头结束时间(分:秒格式,90秒=1分30秒)
endingStart: '20:00', // 片尾开始时间(分:秒格式)
endingMode: 'remaining', // 片尾模式:'remaining'(剩余时间) 或 'absolute'(绝对时间)
endingStart: '2:00', // 片尾开始时间(剩余时间模式:还剩多少时间开始倒计时;绝对时间模式:从视频开始多长时间)
endingEnd: '', // 片尾结束时间(可选,空表示直接跳转下一集)
autoSkip: true, // 自动跳过开关
autoNextEpisode: true, // 自动下一集开关
@@ -300,30 +301,57 @@ export default function SkipController({
if (batchSettings.endingStart) {
const endingStartSeconds = timeToSeconds(batchSettings.endingStart);
// 根据模式计算实际的开始时间
let actualStartSeconds: number;
if (batchSettings.endingMode === 'remaining') {
// 剩余时间模式:从视频总长度减去剩余时间
actualStartSeconds = duration - endingStartSeconds;
} else {
// 绝对时间模式:使用输入的时间
actualStartSeconds = endingStartSeconds;
}
// 确保开始时间在有效范围内
if (actualStartSeconds < 0) {
actualStartSeconds = 0;
} else if (actualStartSeconds >= duration) {
alert(`片尾开始时间超出视频长度(总长:${secondsToTime(duration)}`);
return;
}
// 如果没有设置结束时间,则直接跳转到下一集
if (!batchSettings.endingEnd || batchSettings.endingEnd.trim() === '') {
// 直接从指定时间跳转下一集
segments.push({
start: endingStartSeconds,
start: actualStartSeconds,
end: duration, // 设置为视频总长度
type: 'ending',
title: '片尾跳转下一集',
title: batchSettings.endingMode === 'remaining'
? `剩余${batchSettings.endingStart}时跳转下一集`
: '片尾跳转下一集',
autoSkip: batchSettings.autoSkip,
autoNextEpisode: batchSettings.autoNextEpisode,
});
} else {
let actualEndSeconds: number;
const endingEndSeconds = timeToSeconds(batchSettings.endingEnd);
if (endingStartSeconds >= endingEndSeconds) {
if (batchSettings.endingMode === 'remaining') {
actualEndSeconds = duration - endingEndSeconds;
} else {
actualEndSeconds = endingEndSeconds;
}
if (actualStartSeconds >= actualEndSeconds) {
alert('片尾开始时间必须小于结束时间');
return;
}
segments.push({
start: endingStartSeconds,
end: endingEndSeconds,
start: actualStartSeconds,
end: actualEndSeconds,
type: 'ending',
title: '片尾',
title: batchSettings.endingMode === 'remaining' ? '片尾(剩余时间模式)' : '片尾',
autoSkip: batchSettings.autoSkip,
autoNextEpisode: batchSettings.autoNextEpisode,
});
@@ -352,7 +380,8 @@ export default function SkipController({
setBatchSettings({
openingStart: '0:00',
openingEnd: '1:30',
endingStart: '20:00',
endingMode: 'remaining',
endingStart: '2:00',
endingEnd: '',
autoSkip: true,
autoNextEpisode: true,
@@ -363,7 +392,7 @@ export default function SkipController({
console.error('保存跳过配置失败:', err);
alert('保存失败,请重试');
}
}, [batchSettings, duration, source, id, title, onSettingModeChange, timeToSeconds]);
}, [batchSettings, duration, source, id, title, onSettingModeChange, timeToSeconds, secondsToTime]);
// 删除跳过片段
const handleDeleteSegment = useCallback(
@@ -558,18 +587,60 @@ export default function SkipController({
🎭
</h4>
{/* 片尾模式选择 */}
<div>
<label className="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">
</label>
<div className="flex gap-4">
<label className="flex items-center">
<input
type="radio"
name="endingMode"
value="remaining"
checked={batchSettings.endingMode === 'remaining'}
onChange={(e) => setBatchSettings({...batchSettings, endingMode: e.target.value})}
className="mr-2"
/>
</label>
<label className="flex items-center">
<input
type="radio"
name="endingMode"
value="absolute"
checked={batchSettings.endingMode === 'absolute'}
onChange={(e) => setBatchSettings({...batchSettings, endingMode: e.target.value})}
className="mr-2"
/>
</label>
</div>
<p className="text-xs text-gray-500 mt-1">
{batchSettings.endingMode === 'remaining'
? '基于剩余时间倒计时(如:还剩2分钟时开始)'
: '基于播放时间(如:播放到第20分钟时开始)'
}
</p>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
(:)
{batchSettings.endingMode === 'remaining' ? '剩余时间 (分:秒)' : '开始时间 (分:秒)'}
</label>
<input
type="text"
value={batchSettings.endingStart}
onChange={(e) => setBatchSettings({...batchSettings, endingStart: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="20:00"
placeholder={batchSettings.endingMode === 'remaining' ? '2:00' : '20:00'}
/>
<p className="text-xs text-gray-500 mt-1"></p>
<p className="text-xs text-gray-500 mt-1">
{batchSettings.endingMode === 'remaining'
? '当剩余时间达到此值时开始倒计时'
: '从视频开始播放此时间后开始检测片尾'
}
</p>
</div>
<div>
@@ -615,7 +686,8 @@ export default function SkipController({
setBatchSettings({
openingStart: '0:00',
openingEnd: '1:30',
endingStart: '20:00',
endingMode: 'remaining',
endingStart: '2:00',
endingEnd: '',
autoSkip: true,
autoNextEpisode: true,
@@ -691,7 +763,7 @@ export default function SkipController({
{/* 管理已有片段 - 优化布局避免重叠 */}
{skipConfig && skipConfig.segments && skipConfig.segments.length > 0 && !isSettingMode && (
<div className="fixed bottom-4 right-4 z-[9998] max-w-sm bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 animate-fade-in">
<div className="fixed bottom-4 left-4 z-[9998] max-w-sm bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 animate-fade-in">
<div className="p-3">
<h4 className="font-medium mb-2 text-gray-900 dark:text-gray-100 text-sm flex items-center">
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+1 -15
View File
@@ -2,7 +2,7 @@
'use client';
import { KeyRound, LogOut, Settings, Shield, Tv, User, X } from 'lucide-react';
import { KeyRound, LogOut, Settings, Shield, User, X } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
@@ -149,11 +149,6 @@ export const UserMenu: React.FC = () => {
router.push('/admin');
};
const handleTVBoxConfig = () => {
setIsOpen(false);
router.push('/config');
};
const handleChangePassword = () => {
setIsOpen(false);
setIsChangePasswordOpen(true);
@@ -368,15 +363,6 @@ export const UserMenu: React.FC = () => {
<span className='font-medium'></span>
</button>
{/* TVBox配置按钮 */}
<button
onClick={handleTVBoxConfig}
className='w-full px-3 py-2 text-left flex items-center gap-2.5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-sm'
>
<Tv className='w-4 h-4 text-gray-500 dark:text-gray-400' />
<span className='font-medium'>TVBox配置</span>
</button>
{/* 管理面板按钮 */}
{showAdminPanel && (
<button
+1 -1
View File
@@ -228,7 +228,7 @@ export default function VideoCard({
const configs = {
playrecord: {
showSourceName: true,
showProgress: true,
showProgress: false,
showPlayButton: true,
showHeart: true,
showCheckCircle: true,
+14 -3
View File
@@ -351,10 +351,11 @@ export function getKvrocksClient(): RedisClientType {
console.log('🏪 Initializing Kvrocks client...');
console.log('🔗 Kvrocks URL:', kvrocksUrl.replace(/\/\/.*@/, '//***:***@'));
console.log('🔑 Password configured:', kvrocksPassword ? 'Yes' : 'No');
kvrocksClient = createClient({
// 构建客户端配置
const clientConfig: any = {
url: kvrocksUrl,
password: kvrocksPassword,
database: kvrocksDatabase,
socket: {
connectTimeout: 10000, // 10秒连接超时
@@ -364,7 +365,17 @@ export function getKvrocksClient(): RedisClientType {
return delay;
},
},
});
};
// 只有当密码存在且不为空时才添加密码配置
if (kvrocksPassword && kvrocksPassword.trim() !== '') {
clientConfig.password = kvrocksPassword;
console.log('🔐 Using password authentication');
} else {
console.log('🔓 No password authentication (connecting without password)');
}
kvrocksClient = createClient(clientConfig);
kvrocksClient.on('error', (err) => {
console.error('❌ Kvrocks Client Error:', err);
+1 -1
View File
@@ -2,7 +2,7 @@
'use client';
const CURRENT_VERSION = '20250903203337';
const CURRENT_VERSION = '20250904151930';
// 版本检查结果枚举
export enum UpdateStatus {
+53
View File
@@ -0,0 +1,53 @@
name = "katelyatv"
compatibility_date = "2024-09-01"
[env.production]
name = "katelyatv"
[env.production.vars]
# 存储类型配置
NEXT_PUBLIC_STORAGE_TYPE = "d1"
# 站点配置
NEXT_PUBLIC_SITE_NAME = "KatelyaTV"
NEXT_PUBLIC_SITE_DESCRIPTION = "高性能影视播放平台"
# NextAuth 配置
NEXTAUTH_URL = "https://your-domain.pages.dev"
# 图片代理配置
IMAGE_PROXY_ENABLED = "true"
# 缓存配置
CACHE_TTL = "3600"
# CORS 配置
CORS_ORIGIN = "*"
# Rate Limiting 配置
RATE_LIMIT_MAX = "100"
RATE_LIMIT_WINDOW = "60000"
# 健康检查配置
HEALTH_CHECK_ENABLED = "true"
HEALTH_CHECK_INTERVAL = "30"
# 日志配置
LOG_LEVEL = "info"
LOG_FORMAT = "json"
# 生产环境标识
NODE_ENV = "production"
[[env.production.d1_databases]]
binding = "DB"
database_name = "katelyatv-db"
database_id = "your-d1-database-id-here"
[build]
command = "pnpm pages:build"
environment = { NODE_VERSION = "18" }
[[build.environment_variables]]
name = "NPM_FLAGS"
value = "--prefix=/opt/buildhome/.asdf/installs/nodejs/18.17.1/.npm"