Skip to content

Commit 91a89e8

Browse files
committed
Fix issue #72 follow-ups and IPTV source handling
1 parent 35f7f4d commit 91a89e8

23 files changed

Lines changed: 451 additions & 152 deletions

app/api/iptv/route.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,28 @@ export const runtime = 'edge';
99

1010
export async function GET(request: NextRequest) {
1111
const url = request.nextUrl.searchParams.get('url');
12+
const customUa = request.nextUrl.searchParams.get('ua');
13+
const customReferer = request.nextUrl.searchParams.get('referer');
1214

1315
if (!url) {
1416
return NextResponse.json({ error: 'Missing url parameter' }, { status: 400 });
1517
}
1618

1719
try {
20+
const parsedUrl = new URL(url);
21+
let refererOrigin = `${parsedUrl.protocol}//${parsedUrl.host}`;
22+
if (customReferer) {
23+
try {
24+
refererOrigin = new URL(customReferer).origin;
25+
} catch {
26+
refererOrigin = `${parsedUrl.protocol}//${parsedUrl.host}`;
27+
}
28+
}
1829
const response = await fetch(url, {
1930
headers: {
20-
'User-Agent': 'Mozilla/5.0 (compatible; KVideo/1.0)',
31+
'User-Agent': customUa || 'Mozilla/5.0 (compatible; KVideo/1.0)',
32+
...(customReferer ? { 'Referer': customReferer } : {}),
33+
'Origin': refererOrigin,
2134
},
2235
});
2336

app/iptv/page.tsx

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* IPTV Page - Live TV channel viewer with M3U source management
55
*/
66

7-
import { useState, useEffect } from 'react';
7+
import { useState, useEffect, useMemo } from 'react';
88
import { useIPTVStore } from '@/lib/store/iptv-store';
99
import { IPTVSourceManager } from '@/components/iptv/IPTVSourceManager';
1010
import { IPTVChannelGrid } from '@/components/iptv/IPTVChannelGrid';
@@ -15,12 +15,35 @@ import Link from 'next/link';
1515
import type { M3UChannel } from '@/lib/utils/m3u-parser';
1616

1717
export default function IPTVPage() {
18-
const { sources, cachedChannels, cachedGroups, cachedChannelsBySource, refreshSources, isLoading, lastRefreshed } = useIPTVStore();
18+
const { sources, cachedChannels, cachedChannelsBySource, refreshSources, isLoading, lastRefreshed } = useIPTVStore();
1919
const [activeChannel, setActiveChannel] = useState<M3UChannel | null>(null);
2020
const [showManager, setShowManager] = useState(false);
2121

22-
const canManageSources = hasPermission('source_management');
22+
const canManageSources = hasPermission('iptv_source_management');
2323
const canAccessIPTV = hasPermission('iptv_access');
24+
const canUseBuiltinSources = hasPermission('iptv_builtin_sources');
25+
const visibleSources = useMemo(
26+
() => sources.filter((source) => canUseBuiltinSources || source.kind !== 'builtin'),
27+
[sources, canUseBuiltinSources]
28+
);
29+
const visibleSourceIds = useMemo(() => new Set(visibleSources.map((source) => source.id)), [visibleSources]);
30+
const visibleChannels = useMemo(
31+
() => cachedChannels.filter((channel) => !channel.sourceId || visibleSourceIds.has(channel.sourceId)),
32+
[cachedChannels, visibleSourceIds]
33+
);
34+
const visibleGroups = useMemo(
35+
() => Array.from(new Set(visibleChannels.map((channel) => channel.group).filter(Boolean))).sort() as string[],
36+
[visibleChannels]
37+
);
38+
const visibleChannelsBySource = useMemo(
39+
() =>
40+
Object.fromEntries(
41+
visibleSources
42+
.map((source) => [source.id, cachedChannelsBySource[source.id]])
43+
.filter(([, data]) => !!data)
44+
),
45+
[visibleSources, cachedChannelsBySource]
46+
);
2447

2548
// If auth is configured and user doesn't have iptv_access, show access denied
2649
if (!canAccessIPTV && getSession()) {
@@ -65,7 +88,7 @@ export default function IPTVPage() {
6588
直播
6689
</h1>
6790
<p className="text-sm text-[var(--text-color-secondary)]">
68-
{cachedChannels.length > 0 ? `${cachedChannels.length} 个频道` : 'IPTV 直播频道'}
91+
{visibleChannels.length > 0 ? `${visibleChannels.length} 个频道` : 'IPTV 直播频道'}
6992
</p>
7093
</div>
7194
</div>
@@ -101,12 +124,12 @@ export default function IPTVPage() {
101124
{!isLoading && (
102125
<div className="bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-sm)] p-6">
103126
<IPTVChannelGrid
104-
channels={cachedChannels}
105-
groups={cachedGroups}
127+
channels={visibleChannels}
128+
groups={visibleGroups}
106129
onSelect={setActiveChannel}
107130
activeChannel={activeChannel}
108-
channelsBySource={cachedChannelsBySource}
109-
sources={sources}
131+
channelsBySource={visibleChannelsBySource}
132+
sources={visibleSources}
110133
/>
111134
</div>
112135
)}
@@ -117,10 +140,10 @@ export default function IPTVPage() {
117140
<IPTVPlayer
118141
channel={activeChannel}
119142
onClose={() => setActiveChannel(null)}
120-
channels={cachedChannels}
143+
channels={visibleChannels}
121144
onChannelChange={setActiveChannel}
122-
channelsBySource={cachedChannelsBySource}
123-
sources={sources}
145+
channelsBySource={visibleChannelsBySource}
146+
sources={visibleSources}
124147
/>
125148
)}
126149
</div>

app/player/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,9 @@ function PlayerContent() {
448448
poster={videoData.vod_pic}
449449
type={videoData.type_name}
450450
year={videoData.vod_year}
451+
sourceMap={Object.fromEntries(
452+
(groupedSources.length > 0 ? groupedSources : [{ id: videoId, source }]).map((item) => [item.source, item.id])
453+
)}
451454
size={20}
452455
isPremium={isPremium}
453456
/>

components/PasswordGate.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { Lock } from 'lucide-react';
1313
*/
1414
function syncIPTVSources(rawValue: string) {
1515
const iptvStore = useIPTVStore.getState();
16-
const existingUrls = new Set(iptvStore.sources.map(s => s.url));
1716

1817
let entries: { name: string; url: string }[] = [];
1918

@@ -34,12 +33,7 @@ function syncIPTVSources(rawValue: string) {
3433
}
3534
}
3635

37-
// Add new sources that don't already exist
38-
for (const entry of entries) {
39-
if (!existingUrls.has(entry.url)) {
40-
iptvStore.addSource(entry.name || '直播源', entry.url);
41-
}
42-
}
36+
iptvStore.syncBuiltinSources(entries);
4337
}
4438

4539
/**

components/favorites/FavoriteButton.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface FavoriteButtonProps {
1818
type?: string;
1919
year?: string;
2020
remarks?: string;
21+
sourceMap?: Record<string, string | number>;
2122
className?: string;
2223
size?: number;
2324
showTooltip?: boolean;
@@ -33,6 +34,7 @@ export const FavoriteButton = memo<FavoriteButtonProps>(({
3334
type,
3435
year,
3536
remarks,
37+
sourceMap,
3638
className = '',
3739
size = 20,
3840
showTooltip = true,
@@ -61,11 +63,12 @@ export const FavoriteButton = memo<FavoriteButtonProps>(({
6163
type,
6264
year,
6365
remarks,
66+
sourceMap,
6467
});
6568
setIsFav(newState);
6669

6770
setTimeout(() => setIsAnimating(false), 300);
68-
}, [videoId, source, title, poster, sourceName, type, year, remarks, toggleFavorite]);
71+
}, [videoId, source, title, poster, sourceName, type, year, remarks, sourceMap, toggleFavorite]);
6972

7073
return (
7174
<button

components/favorites/FavoritesItem.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import { Icons } from '@/components/ui/Icon';
77
import { formatDate } from '@/lib/utils/format-utils';
8+
import { getSourceName } from '@/lib/utils/source-names';
9+
import { storeGroupedSources } from '@/lib/utils/grouped-sources-cache';
810
import type { FavoriteItem } from '@/lib/types';
911

1012
interface FavoritesItemProps {
@@ -20,6 +22,18 @@ export function FavoritesItem({ item, onRemove, isPremium = false }: FavoritesIt
2022
source: item.source,
2123
title: item.title,
2224
});
25+
if (item.sourceMap && Object.keys(item.sourceMap).length > 1) {
26+
const groupData = Object.entries(item.sourceMap).map(([sourceName, videoId]) => ({
27+
id: videoId,
28+
source: sourceName,
29+
sourceName: getSourceName(sourceName),
30+
pic: item.poster,
31+
}));
32+
const cacheKey = storeGroupedSources(groupData);
33+
if (cacheKey) {
34+
params.set('gs', cacheKey);
35+
}
36+
}
2337
if (isPremium) {
2438
params.set('premium', '1');
2539
}

components/history/HistoryItem.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function HistoryItem({ item, onRemove, isPremium = false }: HistoryItemPr
3232
id: videoId,
3333
source: sourceName,
3434
sourceName: getSourceName(sourceName),
35+
pic: item.poster,
3536
}));
3637
const cacheKey = storeGroupedSources(groupData);
3738
if (cacheKey) {
@@ -101,6 +102,7 @@ export function HistoryItem({ item, onRemove, isPremium = false }: HistoryItemPr
101102
title={item.title}
102103
poster={item.poster}
103104
remarks={episodeText}
105+
sourceMap={item.sourceMap}
104106
size={14}
105107
className="!p-1.5 !bg-transparent !border-0 !shadow-none hover:!bg-[var(--glass-bg)]"
106108
showTooltip={false}

0 commit comments

Comments
 (0)