refactor: Update file URL generation logic for video and thumbnail paths
This commit is contained in:
@@ -118,20 +118,32 @@ export class CloudStorageService {
|
||||
}
|
||||
logger.info(`[CloudStorage] Local file cleanup completed`);
|
||||
|
||||
// Update video record to point to cloud storage URLs
|
||||
// Update video record to point to cloud storage URLs with sign information
|
||||
if (videoData.id && deletedFiles.length > 0) {
|
||||
try {
|
||||
const storageService = await import("./storageService");
|
||||
const updates: any = {};
|
||||
|
||||
// Get file list from Openlist to retrieve sign information
|
||||
const fileUrls = await this.getFileUrlsWithSign(
|
||||
config,
|
||||
videoData.videoFilename,
|
||||
videoData.thumbnailFilename
|
||||
);
|
||||
|
||||
// Update video path if video was deleted
|
||||
if (videoData.videoFilename) {
|
||||
updates.videoPath = `/cloud/videos/${videoData.videoFilename}`;
|
||||
if (videoData.videoFilename && fileUrls.videoUrl) {
|
||||
updates.videoPath = fileUrls.videoUrl;
|
||||
}
|
||||
|
||||
// Update thumbnail path if thumbnail was deleted
|
||||
if (videoData.thumbnailFilename) {
|
||||
updates.thumbnailPath = `/cloud/images/${videoData.thumbnailFilename}`;
|
||||
if (fileUrls.thumbnailUrl) {
|
||||
updates.thumbnailPath = fileUrls.thumbnailUrl;
|
||||
} else if (fileUrls.thumbnailThumbUrl) {
|
||||
// Use thumb URL if file doesn't exist but thumb is available
|
||||
updates.thumbnailPath = fileUrls.thumbnailThumbUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
@@ -329,4 +341,139 @@ export class CloudStorageService {
|
||||
private static sanitizeFilename(filename: string): string {
|
||||
return filename.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file URLs with sign information from Openlist
|
||||
* Returns URLs in format: https://域名/d/上传路径/文件名?sign=xxx
|
||||
*/
|
||||
private static async getFileUrlsWithSign(
|
||||
config: CloudDriveConfig,
|
||||
videoFilename?: string,
|
||||
thumbnailFilename?: string
|
||||
): Promise<{
|
||||
videoUrl?: string;
|
||||
thumbnailUrl?: string;
|
||||
thumbnailThumbUrl?: string;
|
||||
}> {
|
||||
try {
|
||||
// Extract base URL from apiUrl (remove /api/fs/put)
|
||||
const apiBaseUrl = config.apiUrl.replace("/api/fs/put", "");
|
||||
const listUrl = `${apiBaseUrl}/api/fs/list`;
|
||||
|
||||
// Normalize upload path
|
||||
const normalizedUploadPath = config.uploadPath.replace(/\\/g, "/");
|
||||
const uploadPath = normalizedUploadPath.startsWith("/")
|
||||
? normalizedUploadPath
|
||||
: `/${normalizedUploadPath}`;
|
||||
|
||||
// Call api/fs/list to get file list with sign information
|
||||
const response = await axios.post(
|
||||
listUrl,
|
||||
{
|
||||
path: uploadPath,
|
||||
password: "",
|
||||
page: 1,
|
||||
per_page: 0,
|
||||
refresh: false,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: config.token,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data?.code !== 200 || !response.data?.data?.content) {
|
||||
logger.error(
|
||||
`[CloudStorage] Failed to get file list: ${JSON.stringify(
|
||||
response.data
|
||||
)}`
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
const files = response.data.data.content;
|
||||
const result: {
|
||||
videoUrl?: string;
|
||||
thumbnailUrl?: string;
|
||||
thumbnailThumbUrl?: string;
|
||||
} = {};
|
||||
|
||||
// Extract domain from apiBaseUrl
|
||||
// If apiBaseUrl is like https://example.com/api/fs/put, then apiBaseUrl will be https://example.com
|
||||
// Use this as the base domain for building file URLs
|
||||
const domain = apiBaseUrl;
|
||||
|
||||
// Find video file
|
||||
if (videoFilename) {
|
||||
const videoFile = files.find(
|
||||
(file: any) => file.name === videoFilename
|
||||
);
|
||||
if (videoFile && videoFile.sign) {
|
||||
// Build URL: https://域名/d/上传路径/文件名?sign=xxx
|
||||
// Only encode the filename, not the path
|
||||
const encodedFilename = encodeURIComponent(videoFilename);
|
||||
result.videoUrl = `${domain}/d${uploadPath}/${encodedFilename}?sign=${encodeURIComponent(
|
||||
videoFile.sign
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Find thumbnail file
|
||||
if (thumbnailFilename) {
|
||||
const thumbnailFile = files.find(
|
||||
(file: any) => file.name === thumbnailFilename
|
||||
);
|
||||
if (thumbnailFile) {
|
||||
// Prefer file URL with sign if available
|
||||
if (thumbnailFile.sign) {
|
||||
// Build URL: https://域名/d/上传路径/文件名?sign=xxx
|
||||
const encodedFilename = encodeURIComponent(thumbnailFilename);
|
||||
result.thumbnailUrl = `${domain}/d${uploadPath}/${encodedFilename}?sign=${encodeURIComponent(
|
||||
thumbnailFile.sign
|
||||
)}`;
|
||||
}
|
||||
// If file doesn't have sign but has thumb URL, use thumb URL
|
||||
// Also check if no thumbnail file exists but video file has thumb
|
||||
if (thumbnailFile.thumb) {
|
||||
// Use thumb URL and modify resolution
|
||||
// Replace width=176&height=176 with width=1280&height=720
|
||||
let thumbUrl = thumbnailFile.thumb;
|
||||
thumbUrl = thumbUrl.replace(
|
||||
/width=\d+[&\\u0026]height=\d+/,
|
||||
"width=1280&height=720"
|
||||
);
|
||||
// Also handle \u0026 encoding
|
||||
thumbUrl = thumbUrl.replace(/\\u0026/g, "&");
|
||||
result.thumbnailThumbUrl = thumbUrl;
|
||||
}
|
||||
} else {
|
||||
// Thumbnail file not found, check if video file has thumb
|
||||
if (videoFilename) {
|
||||
const videoFile = files.find(
|
||||
(file: any) => file.name === videoFilename
|
||||
);
|
||||
if (videoFile && videoFile.thumb) {
|
||||
// Use video file's thumb URL and modify resolution
|
||||
let thumbUrl = videoFile.thumb;
|
||||
thumbUrl = thumbUrl.replace(
|
||||
/width=\d+[&\\u0026]height=\d+/,
|
||||
"width=1280&height=720"
|
||||
);
|
||||
thumbUrl = thumbUrl.replace(/\\u0026/g, "&");
|
||||
result.thumbnailThumbUrl = thumbUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`[CloudStorage] Failed to get file URLs with sign:`,
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ const CollectionCard: React.FC<CollectionCardProps> = ({ collection, videos }) =
|
||||
|
||||
const getThumbnailSrc = (video: Video) => {
|
||||
return video.thumbnailPath
|
||||
? `${BACKEND_URL}${video.thumbnailPath}`
|
||||
? (video.thumbnailPath.startsWith("http://") || video.thumbnailPath.startsWith("https://")
|
||||
? video.thumbnailPath
|
||||
: `${BACKEND_URL}${video.thumbnailPath}`)
|
||||
: video.thumbnailUrl;
|
||||
};
|
||||
|
||||
|
||||
@@ -100,7 +100,9 @@ const VideosTable: React.FC<VideosTableProps> = ({
|
||||
|
||||
const getThumbnailSrc = (video: Video) => {
|
||||
if (video.thumbnailPath) {
|
||||
return `${BACKEND_URL}${video.thumbnailPath}`;
|
||||
return video.thumbnailPath.startsWith("http://") || video.thumbnailPath.startsWith("https://")
|
||||
? video.thumbnailPath
|
||||
: `${BACKEND_URL}${video.thumbnailPath}`;
|
||||
}
|
||||
return video.thumbnailUrl || 'https://via.placeholder.com/120x90?text=No+Thumbnail';
|
||||
};
|
||||
|
||||
@@ -84,7 +84,9 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
|
||||
// Use local thumbnail if available, otherwise fall back to the original URL
|
||||
const thumbnailSrc = video.thumbnailPath
|
||||
? `${BACKEND_URL}${video.thumbnailPath}`
|
||||
? (video.thumbnailPath.startsWith("http://") || video.thumbnailPath.startsWith("https://")
|
||||
? video.thumbnailPath
|
||||
: `${BACKEND_URL}${video.thumbnailPath}`)
|
||||
: video.thumbnailUrl;
|
||||
|
||||
// Handle author click
|
||||
@@ -207,7 +209,9 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
<Box
|
||||
component="video"
|
||||
ref={videoRef}
|
||||
src={`${BACKEND_URL}${video.videoPath}`}
|
||||
src={video.videoPath.startsWith("http://") || video.videoPath.startsWith("https://")
|
||||
? video.videoPath
|
||||
: `${BACKEND_URL}${video.videoPath}`}
|
||||
muted
|
||||
autoPlay
|
||||
playsInline
|
||||
|
||||
@@ -62,7 +62,9 @@ const SidebarThumbnail: React.FC<{ video: Video }> = ({ video }) => {
|
||||
// The image is always rendered but hidden until loaded
|
||||
}}
|
||||
onLoad={() => setIsImageLoaded(true)}
|
||||
image={`${BACKEND_URL}${video.thumbnailPath}`}
|
||||
image={video.thumbnailPath && (video.thumbnailPath.startsWith("http://") || video.thumbnailPath.startsWith("https://"))
|
||||
? video.thumbnailPath
|
||||
: `${BACKEND_URL}${video.thumbnailPath}`}
|
||||
alt={video.title}
|
||||
onError={(e) => {
|
||||
setIsImageLoaded(true);
|
||||
|
||||
@@ -55,7 +55,11 @@ const VideoInfo: React.FC<VideoInfoProps> = ({
|
||||
{(video.videoPath || video.sourceUrl) && (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={video.videoPath ? `${BACKEND_URL}${video.videoPath}` : video.sourceUrl}
|
||||
src={video.videoPath
|
||||
? (video.videoPath.startsWith("http://") || video.videoPath.startsWith("https://")
|
||||
? video.videoPath
|
||||
: `${BACKEND_URL}${video.videoPath}`)
|
||||
: video.sourceUrl}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '1px',
|
||||
|
||||
@@ -49,7 +49,9 @@ const VideoMetadata: React.FC<VideoMetadataProps> = ({
|
||||
fontSize: { xs: '0.75rem', sm: '0.875rem' }
|
||||
}}
|
||||
>
|
||||
<a href={`${BACKEND_URL}${video.videoPath}`} download style={{ color: theme.palette.primary.main, textDecoration: 'none', display: 'flex', alignItems: 'center' }}>
|
||||
<a href={video.videoPath && (video.videoPath.startsWith("http://") || video.videoPath.startsWith("https://"))
|
||||
? video.videoPath
|
||||
: `${BACKEND_URL}${video.videoPath}`} download style={{ color: theme.palette.primary.main, textDecoration: 'none', display: 'flex', alignItems: 'center' }}>
|
||||
<Download sx={{ mr: 0.5, fontSize: { xs: '0.875rem', sm: '1rem' } }} />
|
||||
<strong>{t('download')}</strong>
|
||||
</a>
|
||||
|
||||
@@ -61,7 +61,11 @@ export const useVideoResolution = (video: Video) => {
|
||||
}
|
||||
|
||||
// Set the video source
|
||||
const fullVideoUrl = video.videoPath ? `${BACKEND_URL}${video.videoPath}` : video.sourceUrl;
|
||||
const fullVideoUrl = video.videoPath
|
||||
? (video.videoPath.startsWith("http://") || video.videoPath.startsWith("https://")
|
||||
? video.videoPath
|
||||
: `${BACKEND_URL}${video.videoPath}`)
|
||||
: video.sourceUrl;
|
||||
if (videoElement.src !== fullVideoUrl) {
|
||||
videoElement.src = fullVideoUrl;
|
||||
videoElement.load(); // Force reload
|
||||
|
||||
@@ -539,7 +539,11 @@ const VideoPlayer: React.FC = () => {
|
||||
{/* Main Content Column */}
|
||||
<Grid size={{ xs: 12, lg: 8 }}>
|
||||
<VideoControls
|
||||
src={`${BACKEND_URL}${video.videoPath || video.sourceUrl}`}
|
||||
src={video.videoPath
|
||||
? (video.videoPath.startsWith("http://") || video.videoPath.startsWith("https://")
|
||||
? video.videoPath
|
||||
: `${BACKEND_URL}${video.videoPath}`)
|
||||
: video.sourceUrl}
|
||||
autoPlay={autoPlay}
|
||||
autoLoop={autoLoop}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
|
||||
@@ -95,3 +95,20 @@ export const generateTimestamp = (): string => {
|
||||
const seconds = String(now.getSeconds()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get full URL for a file path
|
||||
* If path is already a full URL (starts with http:// or https://), return it as is
|
||||
* Otherwise, prepend BACKEND_URL
|
||||
*/
|
||||
export const getFileUrl = (path: string | null | undefined, backendUrl: string): string | undefined => {
|
||||
if (!path) return undefined;
|
||||
|
||||
// Check if path is already a full URL
|
||||
if (path.startsWith("http://") || path.startsWith("https://")) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Otherwise, prepend backend URL
|
||||
return `${backendUrl}${path}`;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user