Files
2025-06-13 09:44:18 +08:00

495 lines
17 KiB
PHP

<?php
namespace App\Helpers\FirstParty\MediaEngine;
use App\Models\Media;
use App\Models\MediaCollection;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class MediaEngine
{
const LOCAL_TEMP_DISK = 'local_temp';
const USER_UPLOADED = 'user_uploaded';
const USER_RENDERED = 'user_rendered';
const USER = 'user';
public static function getMediaCloudUrl($media)
{
// Check for uploaded_url first (URL-based media)
if (! empty($media->uploaded_url)) {
return $media->uploaded_url;
}
// Fallback to storage disk URL (file-based media)
return Storage::disk($media->disk)->url($media->file_path.$media->file_name);
}
public static function loadMediaToLocalTemp($uuid)
{
$media = self::getMediaByUuid($uuid);
$result = [
'media' => $media,
'uuid' => $uuid,
'temp_path' => null,
'temp' => null,
];
if (! $media) {
return $result;
}
$tempPath = 'temp_'.$media->file_name;
// Handle URL-based media
if (! empty($media->uploaded_url)) {
if (! Storage::disk(self::LOCAL_TEMP_DISK)->exists($tempPath)) {
$fileContent = @file_get_contents($media->uploaded_url);
if ($fileContent !== false) {
if (Storage::disk(self::LOCAL_TEMP_DISK)->put($tempPath, $fileContent)) {
$result['temp_path'] = $tempPath;
$result['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
}
}
} else {
$result['temp_path'] = $tempPath;
$result['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
}
return $result;
}
// Handle file-based media (existing behavior)
if (! Storage::disk($media->disk)->exists($media->file_path.$media->file_name)) {
return $result;
}
$fileContent = Storage::disk($media->disk)->get($media->file_path.$media->file_name);
if (! Storage::disk(self::LOCAL_TEMP_DISK)->exists($tempPath)) {
if (Storage::disk(self::LOCAL_TEMP_DISK)->put($tempPath, $fileContent)) {
$result['temp_path'] = $tempPath;
$result['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
}
} else {
$result['temp_path'] = $tempPath;
$result['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
}
return $result;
}
public static function loadMediasToLocalTemp(array $uuids)
{
$medias = self::getMediaByUuids($uuids);
$result = [];
foreach ($medias as $media) {
if (! $media) {
continue; // Skip invalid media instead of returning early
}
$tempPath = 'temp_'.$media->file_name;
$singleResult = [
'media' => $media,
'uuid' => $media->uuid,
'temp_path' => null,
'temp' => null,
'cloud_url' => self::getMediaCloudUrl($media),
];
// Handle URL-based media
if (! empty($media->uploaded_url)) {
if (Storage::disk(self::LOCAL_TEMP_DISK)->exists($tempPath)) {
// File already exists in temp, just set the paths
$singleResult['temp_path'] = $tempPath;
$singleResult['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
} else {
// File needs to be downloaded to temp
$fileContent = @file_get_contents($media->uploaded_url);
if ($fileContent !== false) {
if (Storage::disk(self::LOCAL_TEMP_DISK)->put($tempPath, $fileContent)) {
$singleResult['temp_path'] = $tempPath;
$singleResult['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
}
}
}
$result[] = $singleResult;
continue;
}
// Handle file-based media (existing behavior)
// Check if file exists in cloud storage
if (! Storage::disk($media->disk)->exists($media->file_path.$media->file_name)) {
$result[] = $singleResult; // Add to results but with null temp paths
continue;
}
// Check if already loaded in local temp
if (Storage::disk(self::LOCAL_TEMP_DISK)->exists($tempPath)) {
// File already exists in temp, just set the paths
$singleResult['temp_path'] = $tempPath;
$singleResult['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
} else {
// File needs to be loaded to temp
$fileContent = Storage::disk($media->disk)->get($media->file_path.$media->file_name);
if (Storage::disk(self::LOCAL_TEMP_DISK)->put($tempPath, $fileContent)) {
$singleResult['temp_path'] = $tempPath;
$singleResult['temp'] = Storage::disk(self::LOCAL_TEMP_DISK)->path($tempPath);
}
}
$result[] = $singleResult;
}
return $result;
}
public static function deleteMediaFromLocalTemp($uuid)
{
$result = self::loadMediaToLocalTemp($uuid);
if ($result['temp_path']) {
Storage::disk(self::LOCAL_TEMP_DISK)->delete($result['temp_path']);
}
return [
'media' => $result['media'],
'uuid' => $uuid,
'temp_path' => null,
'temp' => null,
];
}
public static function deleteMediasFromLocalTemp(array $uuids)
{
$results = self::loadMediasToLocalTemp($uuids);
$deletedResults = [];
foreach ($results as $result) {
if ($result['temp_path']) {
Storage::disk(self::LOCAL_TEMP_DISK)->delete($result['temp_path']);
}
$deletedResults[] = [
'media' => $result['media'],
'uuid' => $result['uuid'],
'temp_path' => null,
'temp' => null,
];
}
return $deletedResults;
}
/**
* Add a new media file to the specified media collection.
*
* Example: $newMedia = MediaEngine::addMedia(
* 'bgm_collection',
* 'bgm',
* 'user_upload',
* 'web_app',
* $fileContent, // File content or null
* $url, // URL or null
* 'save_file', // Mode: 'save_file'|'download'|'save_url'
* 'background_music.mp3', // Optional - auto-generated if null
* 'r2',
* 'name of file',
* $user_id,
* $mimeType // Optional MIME type
* );
*/
public static function addMedia(
string $mediaCollectionKey,
string $mediaType,
string $mediaSource,
string $mediaProvider,
?string $fileContent = null,
?string $url = null,
?string $mode = 'save_file',
?string $fileName = null,
string $disk = 'r2',
?string $name = null,
?int $userId = null,
?string $mimeType = null
) {
$mediaCollection = MediaCollection::where('key', $mediaCollectionKey)->first();
if (! $mediaCollection) {
throw new \InvalidArgumentException("Media collection with key '{$mediaCollectionKey}' not found.");
}
$config = config("platform.media.{$mediaCollectionKey}");
// Smart mode detection for backward compatibility
if ($mode === 'save_file') {
// If mode is default but we have URL and no fileContent, switch to save_url for backward compatibility
if (! empty($url) && empty($fileContent)) {
$mode = 'save_url';
}
}
// Validate mode parameter
$validModes = ['save_file', 'download', 'save_url'];
if (! in_array($mode, $validModes)) {
throw new \InvalidArgumentException("Invalid mode '{$mode}'. Valid modes are: ".implode(', ', $validModes));
}
// Validate required parameters based on mode
if ($mode === 'save_file' && empty($fileContent)) {
throw new \InvalidArgumentException("fileContent is required when mode is 'save_file'.");
}
if (($mode === 'download' || $mode === 'save_url') && empty($url)) {
throw new \InvalidArgumentException("url is required when mode is '{$mode}'.");
}
// Auto-generate fileName if not provided
if (empty($fileName)) {
if (! empty($url)) {
// Extract filename from URL
$pathInfo = pathinfo(parse_url($url, PHP_URL_PATH));
$fileName = $pathInfo['basename'] ?? null;
// If no filename in URL, generate one
if (empty($fileName) || $fileName === '/') {
$extension = ! empty($config['extension']) ? $config['extension'] : 'file';
$fileName = $mediaType.'_'.epoch_now_timestamp().'.'.$extension;
}
} else {
// Generate filename for file-based media
$extension = ! empty($config['extension']) ? $config['extension'] : 'file';
$fileName = $mediaType.'_'.epoch_now_timestamp().'.'.$extension;
}
}
// Handle different modes
if ($mode === 'save_url') {
// URL-based media handling - just save URL reference
$adjustedFileName = $config['prefix'].epoch_now_timestamp().'-'.$fileName;
// Auto-detect MIME type from URL if not provided
if (empty($mimeType)) {
$fileDetails = self::getFileDetailsbyUrl($url);
$mimeType = $fileDetails->mimetype;
// Fallback to config mime if detection fails
if (empty($mimeType) || $mimeType === 'application/octet-stream') {
$mimeType = $config['mime'];
}
}
$media = new Media([
'uuid' => Str::uuid(),
'media_collection_id' => $mediaCollection->id,
'user_id' => $userId,
'media_type' => $mediaType,
'media_source' => $mediaSource,
'media_provider' => $mediaProvider,
'mime_type' => $mimeType,
'file_name' => $adjustedFileName,
'file_path' => $config['location'], // Keep for consistency
'disk' => $disk,
'name' => $name,
'uploaded_url' => $url,
]);
$media->save();
return $media;
}
// For 'save_file' and 'download' modes, we need to store file content
if ($mode === 'download') {
// Download content from URL
$fileContent = @file_get_contents($url);
if ($fileContent === false) {
throw new \RuntimeException("Failed to download file from URL: {$url}");
}
}
// File-based media handling (for both 'save_file' and 'download' modes)
if (empty($fileContent)) {
throw new \InvalidArgumentException('fileContent is required for file storage.');
}
// Adjust fileName with prefix, postfix, and ensure extension
$adjustedFileName = $config['prefix'].epoch_now_timestamp().'-'.$fileName;
// Construct file path
$filePath = $config['location'];
// Store the file
$stored = Storage::disk($disk)->put($filePath.$adjustedFileName, $fileContent);
if (! $stored) {
throw new \RuntimeException("Failed to store file: {$filePath}");
}
// Get filetype
$detectedMimeType = null;
$tempFile = tempnam(sys_get_temp_dir(), 'mime_');
try {
file_put_contents($tempFile, $fileContent);
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$detectedMimeType = finfo_file($finfo, $tempFile);
finfo_close($finfo);
} finally {
// This ensures the file is deleted even if an exception occurs
if (file_exists($tempFile)) {
unlink($tempFile);
}
}
if (is_empty($detectedMimeType)) {
$detectedMimeType = $config['mime'];
}
$media = new Media([
'uuid' => Str::uuid(),
'media_collection_id' => $mediaCollection->id,
'user_id' => $userId,
'media_type' => $mediaType,
'media_source' => $mediaSource,
'media_provider' => $mediaProvider,
'mime_type' => $detectedMimeType,
'file_name' => $adjustedFileName,
'file_path' => $filePath,
'disk' => $disk,
'name' => $name,
'uploaded_url' => null,
]);
$media->save();
return $media;
}
private static function ensureUniqueFileName($fileName, $disk, $location)
{
$uniqueFileName = $fileName;
$counter = 1;
while (Storage::disk($disk)->exists($location.$uniqueFileName)) {
$info = pathinfo($fileName);
$uniqueFileName = $info['filename'].'-'.$counter.'.'.$info['extension'];
$counter++;
}
return $uniqueFileName;
}
public static function getMediaByUuid($uuid)
{
return Media::where('uuid', $uuid)->first();
}
public static function getMediaByUuids(array $uuids)
{
return Media::whereIn('uuid', $uuids)->get();
}
public static function getMediasByCollectionKey($key)
{
$collection = MediaCollection::where('key', $key)->first();
if (! $collection) {
return collect();
}
return $collection->media;
}
public static function getCollectionKeyByOwnerMediaType($owner_type, $media_type)
{
$mediaConfig = config('platform.media');
foreach ($mediaConfig as $key => $item) {
if ($item['owner_type'] == $owner_type && $item['media_type'] == $media_type) {
return $key;
}
}
}
/**
* Get file details from a URL including filename, extension and MIME type
*
* @param string $url The URL of the file
* @return object Object containing filename, extension and MIME type
*/
public static function getFileDetailsbyUrl($url)
{
// Create an empty result object
$result = new \stdClass;
// Parse the URL to extract the filename
$pathInfo = pathinfo(parse_url($url, PHP_URL_PATH));
// Set the filename and extension
$result->filename = $pathInfo['filename'] ?? '';
$result->extension = $pathInfo['extension'] ?? '';
// Initialize the MIME type as unknown
$result->mimetype = 'application/octet-stream';
// Try to get the real MIME type using fileinfo
try {
// Create a temporary file
$tempFile = tempnam(sys_get_temp_dir(), 'file_');
// Get the file content from URL
$fileContent = @file_get_contents($url);
if ($fileContent !== false) {
// Write the content to the temporary file
file_put_contents($tempFile, $fileContent);
// Create a finfo object
$finfo = new \finfo(FILEINFO_MIME_TYPE);
// Get the MIME type
$result->mimetype = $finfo->file($tempFile);
// Clean up the temporary file
@unlink($tempFile);
}
} catch (\Exception $e) {
// If there's an error, try to determine MIME type from extension
$commonMimeTypes = [
// Images
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
// Audio
'mp3' => 'audio/mpeg',
'wav' => 'audio/wav',
'ogg' => 'audio/ogg',
'm4a' => 'audio/mp4',
'flac' => 'audio/flac',
// Video
'mp4' => 'video/mp4',
'webm' => 'video/webm',
'avi' => 'video/x-msvideo',
'mov' => 'video/quicktime',
'mkv' => 'video/x-matroska',
];
if (! empty($result->extension) && isset($commonMimeTypes[strtolower($result->extension)])) {
$result->mimetype = $commonMimeTypes[strtolower($result->extension)];
}
}
return $result;
}
}