mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-03-03 01:37:32 +08:00
feat: support editting live info
This commit is contained in:
@@ -3959,6 +3959,7 @@ const LiveSourceConfig = ({
|
|||||||
const { isLoading, withLoading } = useLoadingState();
|
const { isLoading, withLoading } = useLoadingState();
|
||||||
const [liveSources, setLiveSources] = useState<LiveDataSource[]>([]);
|
const [liveSources, setLiveSources] = useState<LiveDataSource[]>([]);
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
const [editingLiveSource, setEditingLiveSource] = useState<LiveDataSource | null>(null);
|
||||||
const [orderChanged, setOrderChanged] = useState(false);
|
const [orderChanged, setOrderChanged] = useState(false);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [newLiveSource, setNewLiveSource] = useState<LiveDataSource>({
|
const [newLiveSource, setNewLiveSource] = useState<LiveDataSource>({
|
||||||
@@ -4087,6 +4088,27 @@ const LiveSourceConfig = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditLiveSource = () => {
|
||||||
|
if (!editingLiveSource || !editingLiveSource.name || !editingLiveSource.url) return;
|
||||||
|
withLoading('editLiveSource', async () => {
|
||||||
|
await callLiveSourceApi({
|
||||||
|
action: 'edit',
|
||||||
|
key: editingLiveSource.key,
|
||||||
|
name: editingLiveSource.name,
|
||||||
|
url: editingLiveSource.url,
|
||||||
|
ua: editingLiveSource.ua,
|
||||||
|
epg: editingLiveSource.epg,
|
||||||
|
});
|
||||||
|
setEditingLiveSource(null);
|
||||||
|
}).catch(() => {
|
||||||
|
console.error('操作失败', 'edit', editingLiveSource);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEdit = () => {
|
||||||
|
setEditingLiveSource(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDragEnd = (event: any) => {
|
const handleDragEnd = (event: any) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
if (!over || active.id === over.id) return;
|
if (!over || active.id === over.id) return;
|
||||||
@@ -4180,13 +4202,22 @@ const LiveSourceConfig = ({
|
|||||||
{!liveSource.disabled ? '禁用' : '启用'}
|
{!liveSource.disabled ? '禁用' : '启用'}
|
||||||
</button>
|
</button>
|
||||||
{liveSource.from !== 'config' && (
|
{liveSource.from !== 'config' && (
|
||||||
<button
|
<>
|
||||||
onClick={() => handleDelete(liveSource.key)}
|
<button
|
||||||
disabled={isLoading(`deleteLiveSource_${liveSource.key}`)}
|
onClick={() => setEditingLiveSource(liveSource)}
|
||||||
className={`${buttonStyles.roundedSecondary} ${isLoading(`deleteLiveSource_${liveSource.key}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
|
disabled={isLoading(`editLiveSource_${liveSource.key}`)}
|
||||||
>
|
className={`${buttonStyles.roundedPrimary} ${isLoading(`editLiveSource_${liveSource.key}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
删除
|
>
|
||||||
</button>
|
编辑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(liveSource.key)}
|
||||||
|
disabled={isLoading(`deleteLiveSource_${liveSource.key}`)}
|
||||||
|
className={`${buttonStyles.roundedSecondary} ${isLoading(`deleteLiveSource_${liveSource.key}`) ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -4290,6 +4321,103 @@ const LiveSourceConfig = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 编辑直播源表单 */}
|
||||||
|
{editingLiveSource && (
|
||||||
|
<div className='p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 space-y-4'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<h5 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||||
|
编辑直播源: {editingLiveSource.name}
|
||||||
|
</h5>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelEdit}
|
||||||
|
className='text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200'
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||||||
|
<div>
|
||||||
|
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||||
|
名称
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={editingLiveSource.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditingLiveSource((prev) => prev ? ({ ...prev, name: e.target.value }) : null)
|
||||||
|
}
|
||||||
|
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||||
|
Key (不可编辑)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={editingLiveSource.key}
|
||||||
|
disabled
|
||||||
|
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||||
|
M3U 地址
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={editingLiveSource.url}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditingLiveSource((prev) => prev ? ({ ...prev, url: e.target.value }) : null)
|
||||||
|
}
|
||||||
|
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||||
|
节目单地址(选填)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={editingLiveSource.epg}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditingLiveSource((prev) => prev ? ({ ...prev, epg: e.target.value }) : null)
|
||||||
|
}
|
||||||
|
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className='block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1'>
|
||||||
|
自定义 UA(选填)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={editingLiveSource.ua}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditingLiveSource((prev) => prev ? ({ ...prev, ua: e.target.value }) : null)
|
||||||
|
}
|
||||||
|
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-end space-x-2'>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelEdit}
|
||||||
|
className={buttonStyles.secondary}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleEditLiveSource}
|
||||||
|
disabled={!editingLiveSource.name || !editingLiveSource.url || isLoading('editLiveSource')}
|
||||||
|
className={`${!editingLiveSource.name || !editingLiveSource.url || isLoading('editLiveSource') ? buttonStyles.disabled : buttonStyles.success}`}
|
||||||
|
>
|
||||||
|
{isLoading('editLiveSource') ? '保存中...' : '保存'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 直播源表格 */}
|
{/* 直播源表格 */}
|
||||||
<div className='border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto relative' data-table="live-source-list">
|
<div className='border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto relative' data-table="live-source-list">
|
||||||
<table className='min-w-full divide-y divide-gray-200 dark:divide-gray-700'>
|
<table className='min-w-full divide-y divide-gray-200 dark:divide-gray-700'>
|
||||||
|
|||||||
@@ -102,6 +102,34 @@ export async function POST(request: NextRequest) {
|
|||||||
disableSource.disabled = true;
|
disableSource.disabled = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'edit':
|
||||||
|
// 编辑直播源
|
||||||
|
const editSource = config.LiveConfig.find((l) => l.key === key);
|
||||||
|
if (!editSource) {
|
||||||
|
return NextResponse.json({ error: '直播源不存在' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置文件中的直播源不允许编辑
|
||||||
|
if (editSource.from === 'config') {
|
||||||
|
return NextResponse.json({ error: '不能编辑配置文件中的直播源' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新字段(除了 key 和 from)
|
||||||
|
editSource.name = name as string;
|
||||||
|
editSource.url = url as string;
|
||||||
|
editSource.ua = ua || '';
|
||||||
|
editSource.epg = epg || '';
|
||||||
|
|
||||||
|
// 刷新频道数
|
||||||
|
try {
|
||||||
|
const nums = await refreshLiveChannels(editSource);
|
||||||
|
editSource.channelNumber = nums;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新直播源失败:', error);
|
||||||
|
editSource.channelNumber = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'sort':
|
case 'sort':
|
||||||
// 排序直播源
|
// 排序直播源
|
||||||
const { order } = body;
|
const { order } = body;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export async function GET(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 });
|
return NextResponse.json({ error: 'Failed to fetch', message: response.statusText }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = response.headers.get('Content-Type');
|
const contentType = response.headers.get('Content-Type');
|
||||||
@@ -43,6 +43,6 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
return NextResponse.json({ success: true, type: 'm3u8' }, { status: 200 });
|
return NextResponse.json({ success: true, type: 'm3u8' }, { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 });
|
return NextResponse.json({ error: 'Failed to fetch', message: error }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,8 +39,9 @@ export async function GET(request: Request) {
|
|||||||
return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 });
|
return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get('Content-Type') || '';
|
||||||
// rewrite m3u8
|
// rewrite m3u8
|
||||||
if (response.headers.get('Content-Type')?.includes('application/vnd.apple.mpegurl')) {
|
if (contentType.toLowerCase().includes('mpegurl')) {
|
||||||
// 获取最终的响应URL(处理重定向后的URL)
|
// 获取最终的响应URL(处理重定向后的URL)
|
||||||
const finalUrl = response.url;
|
const finalUrl = response.url;
|
||||||
const m3u8Content = await response.text();
|
const m3u8Content = await response.text();
|
||||||
@@ -52,7 +53,7 @@ export async function GET(request: Request) {
|
|||||||
const modifiedContent = rewriteM3U8Content(m3u8Content, baseUrl, request, allowCORS);
|
const modifiedContent = rewriteM3U8Content(m3u8Content, baseUrl, request, allowCORS);
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.set('Content-Type', 'application/vnd.apple.mpegurl');
|
headers.set('Content-Type', contentType);
|
||||||
headers.set('Access-Control-Allow-Origin', '*');
|
headers.set('Access-Control-Allow-Origin', '*');
|
||||||
headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
headers.set('Access-Control-Allow-Headers', 'Content-Type, Range, Origin, Accept');
|
headers.set('Access-Control-Allow-Headers', 'Content-Type, Range, Origin, Accept');
|
||||||
|
|||||||
Reference in New Issue
Block a user