Skip to content

Commit e567ee7

Browse files
authored
Merge branch 'senshinya:main' into main
2 parents 7f22bc8 + 247a156 commit e567ee7

File tree

5 files changed

+96
-76
lines changed

5 files changed

+96
-76
lines changed

README.md

Lines changed: 19 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -102,50 +102,14 @@ docker run -d --name moontv -p 3000:3000 ghcr.io/senshinya/moontv:latest
102102

103103
访问 `http://服务器 IP:3000` 即可。
104104

105-
#### 2. docker-compose 示例
106105

107-
```yaml
108-
version: '3.9'
109-
services:
110-
moontv:
111-
image: ghcr.io/senshinya/moontv:latest
112-
container_name: moontv
113-
restart: unless-stopped
114-
ports:
115-
- '3000:3000'
116-
environment:
117-
- PASSWORD=your_password
118-
# 如需自定义配置,可挂载文件
119-
# volumes:
120-
# - ./config.json:/app/config.json:ro
121-
```
122-
123-
执行:
124-
125-
```bash
126-
docker compose up -d
127-
```
128-
129-
随后同样访问 `http://服务器 IP:3000`
130-
131-
### **请勿使用 Pull Bot 自动同步**
132-
133-
Pull Bot 会反复触发无效的 PR 和垃圾邮件,严重干扰项目维护。作者可能会直接拉黑所有 Pull Bot 自动发起的同步请求的仓库所有者。
134-
135-
**推荐做法:**
136-
137-
建议在 fork 的仓库中启用本仓库自带的 GitHub Actions 自动同步功能(见 `.github/workflows/sync.yml`)。
138-
139-
如需手动同步主仓库更新,也可以使用 GitHub 官方的 [Sync fork](https://docs.github.com/cn/github/collaborating-with-issues-and-pull-requests/syncing-a-fork) 功能。
140-
141-
## Compose 最佳实践
106+
## Docker Compose 最佳实践
142107

143108
若你使用 docker compose 部署,以下是一些 compose 示例
144109

145110
### local storage 版本
146111

147112
```yaml
148-
version: '3.9'
149113
services:
150114
moontv:
151115
image: ghcr.io/senshinya/moontv:latest
@@ -163,30 +127,44 @@ services:
163127
### Redis 版本(推荐,多账户数据隔离,跨设备同步)
164128

165129
```yaml
166-
version: '3.9'
167130
services:
168-
moontv:
131+
moontv-core:
169132
image: ghcr.io/senshinya/moontv:latest
170133
container_name: moontv
171134
restart: unless-stopped
172135
ports:
173136
- '3000:3000'
174137
environment:
175138
- NEXT_PUBLIC_STORAGE_TYPE=redis
176-
- REDIS_URL=redis://redis:6379
139+
- REDIS_URL=redis://moontv-redis:6379
177140
- NEXT_PUBLIC_ENABLE_REGISTER=true # 首次部署请设置该变量,注册初始账户后可关闭
141+
networks:
142+
- moontv-network
143+
depends_on:
144+
- moontv-redis
178145
# 如需自定义配置,可挂载文件
179146
# volumes:
180147
# - ./config.json:/app/config.json:ro
181-
redis:
148+
moontv-redis:
182149
image: redis
183150
container_name: moontv-redis
184151
restart: unless-stopped
152+
networks:
153+
- moontv-network
185154
# 如需持久化
186155
# volumes:
187156
# - ./data:/data
157+
networks:
158+
moontv-network:
159+
driver: bridge
188160
```
189161
162+
## 自动同步最近更改
163+
164+
建议在 fork 的仓库中启用本仓库自带的 GitHub Actions 自动同步功能(见 `.github/workflows/sync.yml`)。
165+
166+
如需手动同步主仓库更新,也可以使用 GitHub 官方的 [Sync fork](https://docs.github.com/cn/github/collaborating-with-issues-and-pull-requests/syncing-a-fork) 功能。
167+
190168
## 环境变量
191169

192170
| 变量 | 说明 | 可选值 | 默认值 |

src/app/api/searchhistory/route.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,22 @@ export const runtime = 'edge';
1010
const HISTORY_LIMIT = 20;
1111

1212
/**
13-
* GET /api/searchhistory
13+
* GET /api/searchhistory?user=<username>
1414
* 返回 string[]
1515
*/
16-
export async function GET() {
16+
export async function GET(request: NextRequest) {
1717
try {
18-
const history = await db.getSearchHistory();
18+
const { searchParams } = new URL(request.url);
19+
const user = searchParams.get('user')?.trim();
20+
21+
if (!user) {
22+
return NextResponse.json(
23+
{ error: 'User parameter is required' },
24+
{ status: 400 }
25+
);
26+
}
27+
28+
const history = await db.getSearchHistory(user);
1929
return NextResponse.json(history, { status: 200 });
2030
} catch (err) {
2131
console.error('获取搜索历史失败', err);
@@ -28,23 +38,32 @@ export async function GET() {
2838

2939
/**
3040
* POST /api/searchhistory
31-
* body: { keyword: string }
41+
* body: { keyword: string, user: string }
3242
*/
3343
export async function POST(request: NextRequest) {
3444
try {
3545
const body = await request.json();
3646
const keyword: string = body.keyword?.trim();
47+
const user: string = body.user?.trim();
48+
3749
if (!keyword) {
3850
return NextResponse.json(
3951
{ error: 'Keyword is required' },
4052
{ status: 400 }
4153
);
4254
}
4355

44-
await db.addSearchHistory(keyword);
56+
if (!user) {
57+
return NextResponse.json(
58+
{ error: 'User parameter is required' },
59+
{ status: 400 }
60+
);
61+
}
62+
63+
await db.addSearchHistory(user, keyword);
4564

4665
// 再次获取最新列表,确保客户端与服务端同步
47-
const history = await db.getSearchHistory();
66+
const history = await db.getSearchHistory(user);
4867
return NextResponse.json(history.slice(0, HISTORY_LIMIT), { status: 200 });
4968
} catch (err) {
5069
console.error('添加搜索历史失败', err);
@@ -56,17 +75,25 @@ export async function POST(request: NextRequest) {
5675
}
5776

5877
/**
59-
* DELETE /api/searchhistory
78+
* DELETE /api/searchhistory?user=<username>&keyword=<kw>
6079
*
6180
* 1. 不带 keyword -> 清空全部搜索历史
6281
* 2. 带 keyword=<kw> -> 删除单条关键字
6382
*/
6483
export async function DELETE(request: NextRequest) {
6584
try {
6685
const { searchParams } = new URL(request.url);
86+
const user = searchParams.get('user')?.trim();
6787
const kw = searchParams.get('keyword')?.trim();
6888

69-
await db.deleteSearchHistory(kw || undefined);
89+
if (!user) {
90+
return NextResponse.json(
91+
{ error: 'User parameter is required' },
92+
{ status: 400 }
93+
);
94+
}
95+
96+
await db.deleteSearchHistory(user, kw || undefined);
7097

7198
return NextResponse.json({ success: true }, { status: 200 });
7299
} catch (err) {

src/components/VideoCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export default function VideoCard({
149149
};
150150

151151
const hideCheckCircle = from === 'favorites' || from === 'search';
152-
const alwaysShowHeart = from === 'favorites';
152+
const alwaysShowHeart = from !== 'favorites';
153153

154154
return (
155155
<Link
@@ -254,7 +254,7 @@ export default function VideoCard({
254254
</div>
255255
)}
256256
{/* 搜索非聚合 - 集数圆形展示框 */}
257-
{from === 'search' && (
257+
{episodes && episodes > 1 && !currentEpisode && (
258258
<div className='absolute top-2 right-2 w-4 h-4 sm:w-7 sm:h-7 rounded-full bg-green-500/90 dark:bg-green-600/90 flex items-center justify-center shadow-md text-[0.55rem] sm:text-xs'>
259259
<span className='text-white font-bold leading-none'>
260260
{episodes}

src/lib/db.client.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,10 @@ export async function getSearchHistory(): Promise<string[]> {
206206
// 如果配置为使用数据库,则从后端 API 获取
207207
if (STORAGE_TYPE !== 'localstorage') {
208208
try {
209-
return fetchFromApi<string[]>('/api/searchhistory');
209+
const user = getUsername();
210+
return fetchFromApi<string[]>(
211+
`/api/searchhistory?user=${encodeURIComponent(user ?? '')}`
212+
);
210213
} catch (err) {
211214
console.error('获取搜索历史失败:', err);
212215
return [];
@@ -240,12 +243,13 @@ export async function addSearchHistory(keyword: string): Promise<void> {
240243
// 数据库模式
241244
if (STORAGE_TYPE !== 'localstorage') {
242245
try {
246+
const user = getUsername();
243247
await fetch('/api/searchhistory', {
244248
method: 'POST',
245249
headers: {
246250
'Content-Type': 'application/json',
247251
},
248-
body: JSON.stringify({ keyword: trimmed }),
252+
body: JSON.stringify({ keyword: trimmed, user: user ?? '' }),
249253
});
250254
} catch (err) {
251255
console.error('保存搜索历史失败:', err);
@@ -276,7 +280,8 @@ export async function clearSearchHistory(): Promise<void> {
276280
// 数据库模式
277281
if (STORAGE_TYPE !== 'localstorage') {
278282
try {
279-
await fetch('/api/searchhistory', {
283+
const user = getUsername();
284+
await fetch(`/api/searchhistory?user=${encodeURIComponent(user ?? '')}`, {
280285
method: 'DELETE',
281286
});
282287
} catch (err) {
@@ -300,9 +305,15 @@ export async function deleteSearchHistory(keyword: string): Promise<void> {
300305
// 数据库模式
301306
if (STORAGE_TYPE !== 'localstorage') {
302307
try {
303-
await fetch(`/api/searchhistory?keyword=${encodeURIComponent(trimmed)}`, {
304-
method: 'DELETE',
305-
});
308+
const user = getUsername();
309+
await fetch(
310+
`/api/searchhistory?user=${encodeURIComponent(
311+
user ?? ''
312+
)}&keyword=${encodeURIComponent(trimmed)}`,
313+
{
314+
method: 'DELETE',
315+
}
316+
);
306317
} catch (err) {
307318
console.error('删除搜索历史失败:', err);
308319
}

src/lib/db.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ export interface IStorage {
5858
checkUserExist(userName: string): Promise<boolean>;
5959

6060
// 搜索历史相关
61-
getSearchHistory(): Promise<string[]>;
62-
addSearchHistory(keyword: string): Promise<void>;
63-
deleteSearchHistory(keyword?: string): Promise<void>;
61+
getSearchHistory(userName: string): Promise<string[]>;
62+
addSearchHistory(userName: string, keyword: string): Promise<void>;
63+
deleteSearchHistory(userName: string, keyword?: string): Promise<void>;
6464

6565
// 用户列表
6666
getAllUsers(): Promise<string[]>;
@@ -184,26 +184,30 @@ class RedisStorage implements IStorage {
184184
}
185185

186186
// ---------- 搜索历史 ----------
187-
private shKey = 'moontv:search_history';
187+
private shKey(user: string) {
188+
return `u:${user}:sh`; // u:username:sh
189+
}
188190

189-
async getSearchHistory(): Promise<string[]> {
190-
return (await this.client.lRange(this.shKey, 0, -1)) as string[];
191+
async getSearchHistory(userName: string): Promise<string[]> {
192+
return (await this.client.lRange(this.shKey(userName), 0, -1)) as string[];
191193
}
192194

193-
async addSearchHistory(keyword: string): Promise<void> {
195+
async addSearchHistory(userName: string, keyword: string): Promise<void> {
196+
const key = this.shKey(userName);
194197
// 先去重
195-
await this.client.lRem(this.shKey, 0, keyword);
198+
await this.client.lRem(key, 0, keyword);
196199
// 插入到最前
197-
await this.client.lPush(this.shKey, keyword);
200+
await this.client.lPush(key, keyword);
198201
// 限制最大长度
199-
await this.client.lTrim(this.shKey, 0, SEARCH_HISTORY_LIMIT - 1);
202+
await this.client.lTrim(key, 0, SEARCH_HISTORY_LIMIT - 1);
200203
}
201204

202-
async deleteSearchHistory(keyword?: string): Promise<void> {
205+
async deleteSearchHistory(userName: string, keyword?: string): Promise<void> {
206+
const key = this.shKey(userName);
203207
if (keyword) {
204-
await this.client.lRem(this.shKey, 0, keyword);
208+
await this.client.lRem(key, 0, keyword);
205209
} else {
206-
await this.client.del(this.shKey);
210+
await this.client.del(key);
207211
}
208212
}
209213

@@ -371,16 +375,16 @@ export class DbManager {
371375
}
372376

373377
// ---------- 搜索历史 ----------
374-
async getSearchHistory(): Promise<string[]> {
375-
return this.storage.getSearchHistory();
378+
async getSearchHistory(userName: string): Promise<string[]> {
379+
return this.storage.getSearchHistory(userName);
376380
}
377381

378-
async addSearchHistory(keyword: string): Promise<void> {
379-
await this.storage.addSearchHistory(keyword);
382+
async addSearchHistory(userName: string, keyword: string): Promise<void> {
383+
await this.storage.addSearchHistory(userName, keyword);
380384
}
381385

382-
async deleteSearchHistory(keyword?: string): Promise<void> {
383-
await this.storage.deleteSearchHistory(keyword);
386+
async deleteSearchHistory(userName: string, keyword?: string): Promise<void> {
387+
await this.storage.deleteSearchHistory(userName, keyword);
384388
}
385389

386390
// 获取全部用户名

0 commit comments

Comments
 (0)