小雅对id进行base58
This commit is contained in:
@@ -140,6 +140,7 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const { XiaoyaClient } = await import('@/lib/xiaoya.client');
|
const { XiaoyaClient } = await import('@/lib/xiaoya.client');
|
||||||
const { getXiaoyaMetadata, getXiaoyaEpisodes } = await import('@/lib/xiaoya-metadata');
|
const { getXiaoyaMetadata, getXiaoyaEpisodes } = await import('@/lib/xiaoya-metadata');
|
||||||
|
const { base58Decode, base58Encode } = await import('@/lib/utils');
|
||||||
|
|
||||||
const client = new XiaoyaClient(
|
const client = new XiaoyaClient(
|
||||||
xiaoyaConfig.ServerURL,
|
xiaoyaConfig.ServerURL,
|
||||||
@@ -148,27 +149,42 @@ export async function GET(request: NextRequest) {
|
|||||||
xiaoyaConfig.Token
|
xiaoyaConfig.Token
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 对id进行base58解码得到真实路径
|
||||||
|
let decodedPath: string;
|
||||||
|
try {
|
||||||
|
decodedPath = base58Decode(id);
|
||||||
|
console.log('[xiaoya] 解码路径:', decodedPath);
|
||||||
|
} catch (decodeError) {
|
||||||
|
console.error('[xiaoya] Base58解码失败:', decodeError);
|
||||||
|
throw new Error('无效的视频ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证解码后的路径
|
||||||
|
if (!decodedPath || decodedPath.trim() === '') {
|
||||||
|
throw new Error('解码后的路径为空');
|
||||||
|
}
|
||||||
|
|
||||||
// 获取元数据
|
// 获取元数据
|
||||||
const metadata = await getXiaoyaMetadata(
|
const metadata = await getXiaoyaMetadata(
|
||||||
client,
|
client,
|
||||||
id, // id 就是视频路径
|
decodedPath, // 使用解码后的路径
|
||||||
config.SiteConfig.TMDBApiKey,
|
config.SiteConfig.TMDBApiKey,
|
||||||
config.SiteConfig.TMDBProxy
|
config.SiteConfig.TMDBProxy
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取集数列表
|
// 获取集数列表
|
||||||
const episodes = await getXiaoyaEpisodes(client, id);
|
const episodes = await getXiaoyaEpisodes(client, decodedPath);
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
source: 'xiaoya',
|
source: 'xiaoya',
|
||||||
source_name: '小雅',
|
source_name: '小雅',
|
||||||
id: id,
|
id: id, // 保持编码后的id
|
||||||
title: metadata.title,
|
title: metadata.title,
|
||||||
poster: metadata.poster || '',
|
poster: metadata.poster || '',
|
||||||
year: metadata.year || '',
|
year: metadata.year || '',
|
||||||
douban_id: 0,
|
douban_id: 0,
|
||||||
desc: metadata.plot || '',
|
desc: metadata.plot || '',
|
||||||
episodes: episodes.map(ep => `/api/xiaoya/play?path=${encodeURIComponent(ep.path)}`),
|
episodes: episodes.map(ep => `/api/xiaoya/play?path=${encodeURIComponent(base58Encode(ep.path))}`),
|
||||||
episodes_titles: episodes.map(ep => ep.title),
|
episodes_titles: episodes.map(ep => ep.title),
|
||||||
subtitles: [],
|
subtitles: [],
|
||||||
proxyMode: false,
|
proxyMode: false,
|
||||||
@@ -176,6 +192,7 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
return NextResponse.json(result);
|
return NextResponse.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[xiaoya] 获取详情失败:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: (error as Error).message },
|
{ error: (error as Error).message },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ async function getFinalUrl(url: string, maxRedirects = 5): Promise<string> {
|
|||||||
/**
|
/**
|
||||||
* GET /api/xiaoya/play?path=<path>
|
* GET /api/xiaoya/play?path=<path>
|
||||||
* 获取小雅视频的播放链接(优先使用视频预览流,失败时降级到直连)
|
* 获取小雅视频的播放链接(优先使用视频预览流,失败时降级到直连)
|
||||||
|
* path参数为base58编码的路径
|
||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -69,12 +70,16 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const path = searchParams.get('path');
|
const encodedPath = searchParams.get('path');
|
||||||
|
|
||||||
if (!path) {
|
if (!encodedPath) {
|
||||||
return NextResponse.json({ error: '缺少参数' }, { status: 400 });
|
return NextResponse.json({ error: '缺少参数' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 对path进行base58解码
|
||||||
|
const { base58Decode } = await import('@/lib/utils');
|
||||||
|
const path = base58Decode(encodedPath);
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const xiaoyaConfig = config.XiaoyaConfig;
|
const xiaoyaConfig = config.XiaoyaConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useEffect, useState, useRef, useMemo } from 'react';
|
|||||||
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
import VideoCard from '@/components/VideoCard';
|
import VideoCard from '@/components/VideoCard';
|
||||||
|
import { base58Encode } from '@/lib/utils';
|
||||||
|
|
||||||
type LibrarySourceType = 'openlist' | 'emby' | 'xiaoya' | `emby:${string}` | `emby_${string}`;
|
type LibrarySourceType = 'openlist' | 'emby' | 'xiaoya' | `emby:${string}` | `emby_${string}`;
|
||||||
|
|
||||||
@@ -692,8 +693,9 @@ export default function PrivateLibraryPage() {
|
|||||||
key={item.path}
|
key={item.path}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isVideoFile) {
|
if (isVideoFile) {
|
||||||
// 视频文件:直接播放
|
// 视频文件:直接播放,对path进行base58编码
|
||||||
router.push(`/play?source=xiaoya&id=${encodeURIComponent(item.path)}&title=${encodeURIComponent(title)}`);
|
const encodedPath = base58Encode(item.path);
|
||||||
|
router.push(`/play?source=xiaoya&id=${encodeURIComponent(encodedPath)}&title=${encodeURIComponent(title)}`);
|
||||||
} else {
|
} else {
|
||||||
// 文件夹:进入浏览
|
// 文件夹:进入浏览
|
||||||
setXiaoyaPath(item.path);
|
setXiaoyaPath(item.path);
|
||||||
@@ -792,7 +794,11 @@ export default function PrivateLibraryPage() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={file.path}
|
key={file.path}
|
||||||
onClick={() => router.push(`/play?source=xiaoya&id=${encodeURIComponent(file.path)}&title=${encodeURIComponent(title)}`)}
|
onClick={() => {
|
||||||
|
// 对path进行base58编码
|
||||||
|
const encodedPath = base58Encode(file.path);
|
||||||
|
router.push(`/play?source=xiaoya&id=${encodeURIComponent(encodedPath)}&title=${encodeURIComponent(title)}`);
|
||||||
|
}}
|
||||||
className='flex items-center gap-2 p-3 bg-gray-100 dark:bg-gray-800 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-left'
|
className='flex items-center gap-2 p-3 bg-gray-100 dark:bg-gray-800 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-left'
|
||||||
>
|
>
|
||||||
<svg className='w-5 h-5 text-green-600' fill='currentColor' viewBox='0 0 20 20'>
|
<svg className='w-5 h-5 text-green-600' fill='currentColor' viewBox='0 0 20 20'>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
||||||
import he from 'he';
|
import he from 'he';
|
||||||
import Hls from 'hls.js';
|
import Hls from 'hls.js';
|
||||||
|
import bs58 from 'bs58';
|
||||||
|
|
||||||
function getDoubanImageProxyConfig(): {
|
function getDoubanImageProxyConfig(): {
|
||||||
proxyType:
|
proxyType:
|
||||||
@@ -330,3 +331,43 @@ export function cleanHtmlTags(text: string): string {
|
|||||||
// 使用 he 库解码 HTML 实体
|
// 使用 he 库解码 HTML 实体
|
||||||
return he.decode(cleanedText);
|
return he.decode(cleanedText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字符串编码为 Base58
|
||||||
|
* @param str 要编码的字符串
|
||||||
|
* @returns Base58 编码后的字符串
|
||||||
|
*/
|
||||||
|
export function base58Encode(str: string): string {
|
||||||
|
if (!str) return '';
|
||||||
|
|
||||||
|
// 在浏览器环境中使用 TextEncoder
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const bytes = encoder.encode(str);
|
||||||
|
return bs58.encode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 Node.js 环境中使用 Buffer
|
||||||
|
const buffer = Buffer.from(str, 'utf-8');
|
||||||
|
return bs58.encode(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Base58 字符串解码为原始字符串
|
||||||
|
* @param encoded Base58 编码的字符串
|
||||||
|
* @returns 解码后的原始字符串
|
||||||
|
*/
|
||||||
|
export function base58Decode(encoded: string): string {
|
||||||
|
if (!encoded) return '';
|
||||||
|
|
||||||
|
const bytes = bs58.decode(encoded);
|
||||||
|
|
||||||
|
// 在浏览器环境中使用 TextDecoder
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
return decoder.decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 Node.js 环境中使用 Buffer
|
||||||
|
return Buffer.from(bytes).toString('utf-8');
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ export interface XiaoyaMetadata {
|
|||||||
* 从文件夹名提取 TMDb ID 和年份
|
* 从文件夹名提取 TMDb ID 和年份
|
||||||
* 格式: "标题 (年份) {tmdb-id}"
|
* 格式: "标题 (年份) {tmdb-id}"
|
||||||
*/
|
*/
|
||||||
function parseFolderName(folderName: string): {
|
function parseFolderName(folderName: string | undefined): {
|
||||||
title?: string;
|
title?: string;
|
||||||
year?: string;
|
year?: string;
|
||||||
tmdbId?: number;
|
tmdbId?: number;
|
||||||
} {
|
} {
|
||||||
|
if (!folderName || typeof folderName !== 'string') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
const match = folderName.match(/^(.+?)\s*\((\d{4})\)\s*\{tmdb-(\d+)\}$/);
|
const match = folderName.match(/^(.+?)\s*\((\d{4})\)\s*\{tmdb-(\d+)\}$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
return {
|
return {
|
||||||
@@ -85,7 +88,18 @@ export async function getXiaoyaMetadata(
|
|||||||
tmdbProxy?: string
|
tmdbProxy?: string
|
||||||
): Promise<XiaoyaMetadata> {
|
): Promise<XiaoyaMetadata> {
|
||||||
const pathParts = videoPath.split('/').filter(Boolean);
|
const pathParts = videoPath.split('/').filter(Boolean);
|
||||||
const isInSeasonDir = /season\s*\d+/i.test(pathParts[pathParts.length - 2]);
|
|
||||||
|
// 验证路径格式
|
||||||
|
if (pathParts.length < 2) {
|
||||||
|
throw new Error(`无效的视频路径格式: ${videoPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInSeasonDir = pathParts.length >= 2 && /season\s*\d+/i.test(pathParts[pathParts.length - 2]);
|
||||||
|
|
||||||
|
// 验证路径长度是否足够
|
||||||
|
if (isInSeasonDir && pathParts.length < 3) {
|
||||||
|
throw new Error(`Season目录路径格式不正确: ${videoPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 确定元数据目录
|
// 确定元数据目录
|
||||||
const metadataDir = isInSeasonDir
|
const metadataDir = isInSeasonDir
|
||||||
@@ -94,6 +108,11 @@ export async function getXiaoyaMetadata(
|
|||||||
|
|
||||||
const folderName = pathParts[isInSeasonDir ? pathParts.length - 3 : pathParts.length - 2];
|
const folderName = pathParts[isInSeasonDir ? pathParts.length - 3 : pathParts.length - 2];
|
||||||
|
|
||||||
|
// 验证 folderName 是否有效
|
||||||
|
if (!folderName) {
|
||||||
|
throw new Error(`无法从路径中提取文件夹名: ${videoPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 优先级 1: 从文件夹名提取 TMDb ID
|
// 优先级 1: 从文件夹名提取 TMDb ID
|
||||||
const folderInfo = parseFolderName(folderName);
|
const folderInfo = parseFolderName(folderName);
|
||||||
if (folderInfo.tmdbId) {
|
if (folderInfo.tmdbId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user