refactor: Update file URL generation logic for video and thumbnail paths

This commit is contained in:
Peifan Li
2025-12-18 15:59:36 -05:00
parent fc86017167
commit ebe02d35bd
10 changed files with 201 additions and 13 deletions

View File

@@ -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 {};
}
}
}

View File

@@ -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;
};

View File

@@ -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';
};

View File

@@ -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

View File

@@ -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);

View File

@@ -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',

View File

@@ -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>

View File

@@ -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

View File

@@ -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}

View File

@@ -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}`;
};