<?php namespace App\Models; use App\Events\AttachmentSoldToClientEvent; use App\Events\NewPictureReleasedEvent; use App\Events\NewFileReleasedEvent; use App\Http\Resources\PictureOrVideoResource; use App\Jobs\SendMailsToFansWhenModelPostsContent; use App\Traits\SimpleErrorsHandling; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\UploadedFile; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Storage; use Intervention\Image\Facades\Image; use Lakshmaji\Thumbnail\Facade\Thumbnail; /** * Class Attachment * * @property User $model * @property mixed created_at * * @package App */ class Attachment extends Model { use SimpleErrorsHandling; /** Directories for files */ const DIR_NAME_ATTACHMENT_THUMBS = 'public/attachment_thumbs'; const DIR_NAME_ATTACHMENT_FILES = 'model_attachments'; // storage/app/model_attachments/ /** Max size of the Model's uploaded */ const MAX_FILE_SIZE_IN_BYTES = 1073741824; // 1 GB, acc. to clients request 8.01.2020 /** Default attachment thumb for the case on some reasons thumb from attachment will not be generated */ const DEFAULT_THUMB_FILE_NAME = 'default_attachment_thumb.svg'; /** @var array $attributes */ protected $attributes = [ 'file_name' => '', 'thumb_file' => 'public/attachment_thumbs/'.self::DEFAULT_THUMB_FILE_NAME, 'title' => null, 'description' => null, 'price' => null, 'tags' => null, 'visible_for_fans' => null, ]; /** * @var array */ protected $fillable = [ 'user_id', 'title', 'description', 'price', 'tags', 'video_duration', 'price', 'visible_for_fans' ]; /** * @var array */ protected $guarded = [ 'file_name', 'mime_type', ]; public static function boot(){ parent::boot(); static::creating(function (Attachment $attachment){ $attachment->calculateDuration(); $attachment->makeThumb(); }); } public function getTypeAttribute() { return explode('/', $this->mime_type)[0] === 'video' ? 'video' : 'picture'; } /** * Relation to the Model * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function model() { return $this->belongsTo(User::class, 'user_id'); } /** * Relation to the Model's Video Buyings * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function attachmentBuyings() { return $this->hasMany( FanAttachmentBuying::class, 'attachment_id', 'id'); } public static function storeFile(Request $request): Attachment { $newFile = new self(); $newFile->fillFromRequiest($request); $model = $newFile->model; \Log::debug($model->login . ' published '. $newFile->type . ' ' . $newFile->title); if($newFile->type === 'video') { $fullPath = $newFile->getFilePath(); $command = "ffmpeg -i " . $fullPath . " -movflags faststart -acodec copy -vcodec copy " . $fullPath; exec($command, $output); } if($newFile->price > 0) { event(new NewFileReleasedEvent( ['twitterMessageEvent' => TwitterMessageText::EVENT_NEW_FILE_RELEASED], $newFile->model)); SendMailsToFansWhenModelPostsContent::dispatch($newFile->model, __('backend.' . $newFile->type), $newFile->title); } return $newFile; } private function fillFromRequiest($request) { $user = Auth::user() ?? User::find($request->modelId); $fileName = 'model_' . $user->id . uniqid('', true); $file = $request->file('file'); $path = Storage::putFileAs( self::DIR_NAME_ATTACHMENT_FILES, $file, $fileName . '.' . $file->getClientOriginalExtension() ); $this->file_name = $path; $this->user_id = $user->id; $this->title = $request->title; $this->description = $request->description; $this->price = $request->price; $this->tags = implode(', ', $request->input('tags', [])); $this->visible_for_fans = $request->visible_for_fans; $this->mime_type = $file->getClientMimeType(); $this->type = explode('/', $file->getClientMimeType())[0] === 'video' ? 'video': 'picture'; $this->is_approved = (int)$request->price !== 0; $this->save(); return $this; } public function makeThumb() { if( ! Storage::disk('local')->exists(self::DIR_NAME_ATTACHMENT_THUMBS) ) { Storage::disk('local')->makeDirectory(self::DIR_NAME_ATTACHMENT_THUMBS); } if(!Storage::disk('local')->exists($this->file_name)) { $this->thumb_file = 'public/attachment_thumbs/'.self::DEFAULT_THUMB_FILE_NAME; return ; } if ($this->type === 'video') { $this->makeVideoThumb(); } else { $this->makePictureThumb(); } } /** * To make the video thumb and store it to video_thumbs in public directory * @comment need to change access mode for thumbs directory and for watermark (to 777) */ protected function makeVideoThumb() { $ifFFmpegInstalled = trim(shell_exec('ffmpeg -version')); if ( $ifFFmpegInstalled !== '' && $this->file_name ) { $user = Auth::user() ?? $this->model; // file type is video // set storage path to store the file (image generated for a given video) $thumbnail_path = storage_path().'/app/' . self::DIR_NAME_ATTACHMENT_THUMBS; $video_path = storage_path().'/app/' . $this->file_name; $timestamp = now()->timestamp; // set thumbnail image name $thumbnail_image = (Auth::guest() ? 'guest' : $user->id).'_'.$timestamp.".jpg"; // set the thumbnail image "palyback" video button $water_mark = storage_path().'/app/'. self::DIR_NAME_ATTACHMENT_FILES .'/watermark/p.png'; // get video length and process it // assign the value to time_to_image (which will get screenshot of video at that specified seconds) // @my_todo: not realized in owners library Lakshmaji\Thumbnail\ //$time_to_image = floor(($data['video_length'])/2); $time_to_image = 2; // seconds $thumbnail_status = Thumbnail::getThumbnail($video_path,$thumbnail_path,$thumbnail_image,$time_to_image); if( $thumbnail_status ) { $this->thumb_file = self::DIR_NAME_ATTACHMENT_THUMBS .'/'. $thumbnail_image; //echo "Thumbnail generated"; $thumbPath = storage_path() . '/app/' . $this->thumb_file; Image::make($thumbPath) ->fit(config(' thumbnail.dimensions.width', 240), config(' thumbnail.dimensions.width', 320)) ->blur(50) ->save($thumbPath); } else { //echo "thumbnail generation has failed"; } }else{ $this->thumb_file = self::DIR_NAME_ATTACHMENT_THUMBS .'/'.self::DEFAULT_THUMB_FILE_NAME; } } /** * To make the picture thumb and store it to picture_thumbs in public directory * @comment need to change access mode for thumbs directory and for watermark (to 777) * */ public function makePictureThumb(): void { $user = Auth::user(); // file type is picture // set storage path to store the file (image generated for a given picture) $thumbnail_path = Storage::disk('local')->path(self::DIR_NAME_ATTACHMENT_THUMBS); $picture_path = Storage::disk('local')->path($this->file_name); $timestamp = now()->timestamp; $extension = Image::make($picture_path)->extension; // set thumbnail image name $thumbnail_image = (Auth::user()->id?? 'guest').'_'.$timestamp.'.'.$extension; // set the thumbnail image "palyback" picture button $water_mark = storage_path().'/app/'. self::DIR_NAME_ATTACHMENT_FILES .'/watermark/p.png'; Image::make($picture_path) ->fit(config(' thumbnail.dimensions.width', 240), config(' thumbnail.dimensions.width', 320)) ->blur(50) ->save("$thumbnail_path/$thumbnail_image"); $this->thumb_file = self::DIR_NAME_ATTACHMENT_THUMBS .'/'. $thumbnail_image; } /** * Store the MIME type of attachment file * @param $mimeType */ public function setMimeType( $mimeType ) { $this->mime_type = $mimeType; } /** * Returns the MIME type of file * @return string */ public function getMimeType() { if ( is_null($this->mime_type) && !is_null($this->file_name) ) { $this->mime_type = Storage::disk('local')->mimeType($this->file_name); $this->save(); } return $this->mime_type; } /** * Remove attachment file from storage, info about it from DB and thumb file if it exists * @return bool|null */ public function deleteAttachmentFile( $deleteWithTheThumbnail = true ) { $result = Storage::disk('local')->delete($this->file_name); // except the default attachment thumb (for local environment without ffmpeg) if ( $result && $deleteWithTheThumbnail && $this->thumb_file && $this->thumb_file != self::DIR_NAME_ATTACHMENT_THUMBS .'/'.self::DEFAULT_THUMB_FILE_NAME ) { $result = Storage::disk('local')->delete($this->thumb_file); } return $result ? $this->delete() : $result; } /** * Get the url for the Attachment thumb image * @return \Illuminate\Contracts\Routing\UrlGenerator|string */ public function getThumbImageLink() { return $this->thumb_file ? url( Storage::url($this->thumb_file) ) : url( self::DEFAULT_THUMB_FILE_NAME); } /** * Get full path to the attachment file * @return null|string */ public function getFilePath() { return $this->file_name ? Storage::disk('local')->path($this->file_name): null; } /** * Get info about Attachment in array to display it on front-end * @param array $purchasedAttachmentsArray * @return array */ public function getInfoForFrontEnd(array $purchasedAttachmentsArray = [] ) { return [ 'id' => $this->id, 'title' => $this->getShortTitleName(), 'thumb' => $this->getThumbImageLink(), 'price' => $this->price, 'duration' => $this->getDuration(), 'payed' => in_array($this->id, $purchasedAttachmentsArray), //'downloadUrl' => route('') 'type' => $this->type, 'is_approved' => $this->is_approved, ]; } /** * Get info about Attachment in array to display it on front-end * @param array $purchasedAttachmentsArray * @return array */ public function getExtendedInfoForFrontEnd( $purchasedAttachmentsArray = [] ) { $isPurchased = in_array($this->id, (array)$purchasedAttachmentsArray); /** @var User $model */ $model = $this->model; $modelProfile = $model->userModelProfile; return [ 'id' => $this->id, 'added_at' => $this->created_at, 'model_id' => $this->user_id, 'title' => $this->getShortTitleName(), 'thumb' => $this->getThumbImageLink(), 'price' => $this->price, 'payed' => $isPurchased, 'downloadUrl' => ($isPurchased || (int)$this->price === 0) ? route('download-file', ['file_id' => $this->id]) : null, 'watchUrl' => ($isPurchased || (int)$this->price === 0) ? route('attachment-source', ['attachment' => $this->id]) : null, 'mimeType' => $this->getMimeType(), 'duration' => $this->getDuration(), 'modelAvatar' => $modelProfile->getAvatarLink(), 'profileName' => $modelProfile->getProfileName(), 'profileUrl' => $modelProfile->getProfileLink(), 'type' => $this->type, 'visibleForFans' => $this->visible_for_fans, 'is_approved' => $this->is_approved, ]; } /** * Get array for Admin Attachments page * @return array */ public function getExtendedInfoForAdmin() { /** @var User $model */ $model = $this->model; $modelProfile = $model->userModelProfile; return [ 'id' => $this->id, 'model_id' => $this->user_id, 'title' => $this->getShortTitleName(), 'thumb' => $this->getThumbImageLink(), 'price' => $this->price, 'payed' => $this->attachmentBuyings->count(), 'downloadUrl' => route('download-file', ['file_id' => $this->id]), 'watchUrl' => route('attachment-source', ['attachment' => $this->id]), 'mimeType' => $this->getMimeType(), 'duration' => $this->getDuration(), 'modelAvatar' => $modelProfile->getAvatarLink(), 'profileName' => $modelProfile->getProfileName(), 'profileUrl' => $modelProfile->getProfileLink(), 'added_at' => $this->created_at, 'type' => $this->type, 'visibleForFans' => $this->visible_for_fans, 'is_approved' => $this->is_approved, ]; } /** * Method to buy this Attachment by specified Fan * @param User $fan * @return bool */ public function buyThisAttachment( User $fan ) { /** @var User $model */ $model = $this->model; $newTransaction = new TokensTransaction(); if ( $newTransaction->makeTransaction($fan, $model, (integer)$this->price, TokensTransaction::TRANSACTION_TYPE_BUY_ATTACHMENT) ) { $newAttachmentBuying = new FanAttachmentBuying(); $newAttachmentBuying->saveAttachmentBuying( $newTransaction , $this->id); event( new AttachmentSoldToClientEvent(['twitterMessageEvent' => TwitterMessageText::EVENT_VIDEO_SOLD_TO_CLIENT], $model) ); return true; }else{ $this->errors = $newTransaction->errors; } return false; } /** * Make the simple Attachment output to the browser */ public function simpleOutput() { header("Content-Length:" . Storage::size($this->file_name) ); header("Content-Type: " . $this->getMimeType() ); echo Storage::get( $this->file_name ); } /** * Calculate the video duration * @return false|null|string */ public function calculateDuration() { if($this->type !== 'video') { $this->video_duration = 'picture'; return 'picture'; } $realFilePath = $this->getFilePath(); if ( File::exists( $realFilePath ) ) { $getID3 = new \getID3; $file = $getID3->analyze( $realFilePath ); $this->video_duration = $file['playtime_seconds'] ?? 0 ; return $this->video_duration; } return 0; } /** * Get the video duration * @param string $format * @return false|null|string */ public function getDuration($format = 'i:s' ) { if($this->type === 'picture'){ return 'picture'; } return date($format, (float)($this->video_duration ?? $this->calculateDuration())); } /** * Get the file size converted to the string * @return string */ public function getFileSizeToString() { $fileSize = Storage::size($this->file_name); return self::convertBytesFileSize( $fileSize ); } /** * Returns the file size in string format (with kB, MB units) * @param $file * @return string */ public static function fileSizeToString( $file ) { $fileSize = $file->getSize(); return self::convertBytesFileSize( $fileSize ); } /** * Return converted file size (from bytes to kB , MB) * @param null $fileSize * @return string */ public static function convertBytesFileSize( $fileSize = null ) { $fileSize = is_null($fileSize) ? self::MAX_FILE_SIZE_IN_BYTES : $fileSize; if ( $fileSize > 1000000 ) { return round($fileSize/1000000, 0, PHP_ROUND_HALF_DOWN). ' MB'; } elseif ($fileSize > 1000) { return round($fileSize/1000, 0, PHP_ROUND_HALF_DOWN). ' kB'; } else { return $fileSize.' bytes'; } } /** * Get the file short name * @return string */ public function getFileShortName() { $fileName = basename($this->file_name); $dotPosition = strpos($fileName, '.'); if ( $dotPosition < 7 ) { return $fileName; } $extension = mb_substr($fileName, $dotPosition+1); return mb_substr($fileName, 0, 7) . '...' . $extension; } /** * Returns the file name for ajax response * @param $file * @return string */ public static function fileShortName(UploadedFile $file ) { $fileName = $file->getClientOriginalName(); $dotPosition = strpos($fileName, '.'); if ( $dotPosition < 7 ) { return $fileName; } return mb_substr($fileName, 0, 7) . '...' . $file->getClientOriginalExtension(); } /** * Get all available attachments * @param array $excludedAttachments * @return Builder|static[] */ public static function getAll($excludedAttachments = [] ) { if ( !empty($excludedAttachments) ) { $query = self::whereNotIn('attachments.id', $excludedAttachments)->with('model'); }else{ $query = self::query()->with('model'); } $query ->where('is_approved', '=', 1) ->with(['model', 'model.userModelProfile']) ->whereHas('model', function($query) { $query->where([ ['validation', '=', User::VALIDATION_CONFIRMED], ['status', '=', User::STATUS_ENABLED], ]); }) ->orderBy('attachments.created_at', 'desc'); return $query; } /** * Get the attachment available for the Fan by subscription on the Model * @param User $fan * @param array $excludedAttachments * @return mixed */ public static function getAttachmentsOfSubscribedModels(User $fan, array $excludedAttachments = [] ) { /** @var int $fanId */ $fanId = $fan->id; $query = self::join('tokens_models_subscriptions', 'tokens_models_subscriptions.model_id', '=', 'attachments.user_id') ->where([ ['tokens_models_subscriptions.fan_id', '=', $fanId], ['tokens_models_subscriptions.expire_at', '>', now()], ]); return !empty($excludedAttachments) ? $query->whereNotIn('id', $excludedAttachments)->get() : $query->get(); } /** * Method to search among the Attachments by specific text * @param $stringToSearch * @return mixed */ public static function searchAllByText( $stringToSearch ) { return self::selectRaw('attachments.*') ->where('tags', 'like', '%'.$stringToSearch.'%') ->join('user_model_profiles', 'user_model_profiles.user_id', '=', 'attachments.user_id') ->orWhere('description', 'like', '%'.$stringToSearch.'%') ->orWhere('title', 'like', '%'.$stringToSearch.'%') ->orWhere('user_model_profiles.artistic_name', 'like', '%'.$stringToSearch.'%') ->with(['model', 'model.userModelProfile']) ->get(); } /** * Get the merged collection of purchased or subscribed Model's attachments for specified fan (if was passed) * @param User|null $fan * @return array|Collection */ public static function getForFanPurchasedOrSubscribedAttachments(User $fan = null ) { $mergedCollection = []; if ( $fan ) { $attachmentsPurchasedByFan = $fan->fansAttachments; $purchasedAttachmentIdsArray = $attachmentsPurchasedByFan->pluck('id')->toArray(); $attachmentsOfSubscribedModels = Attachment::getAttachmentsOfSubscribedModels( $fan, $purchasedAttachmentIdsArray ); $mergedCollection = $attachmentsOfSubscribedModels->merge( $attachmentsPurchasedByFan ); // remove that relation to avoid Auth::user() serialization error $fan->unsetRelation('fansAttachments'); } return $mergedCollection; } /** * Get the collection of the Model's Attachments by specified ids * * @param array $idsArray * @return mixed */ public static function getByIds($idsArray = []) { return self::whereIn('id', $idsArray) ->with(['model', 'model.userModelProfile']) ->get() ; } /** * Get array of the Fan's purchased or subscribed Model's Attachments * * @param User $fan * @return Collection */ public static function getIdsArrayOfPurchasedOrSubscribedAttachmentsForFan(User $fan = null ) { $array = []; if ( $fan ) { $query = DB::select( DB::raw(' SELECT fab.attachment_id attachment_id FROM fan_attachment_buyings fab WHERE fab.fan_id = ? UNION ( SELECT a.id FROM attachments a INNER JOIN tokens_models_subscriptions tms ON tms.model_id = a.user_id WHERE tms.fan_id = ? AND tms.expire_at > ? AND tms.is_free = ? and a.visible_for_fans = ? ) '), [ $fan->id, $fan->id, now(), false, 1]); return collect($query)->pluck('attachment_id'); } return collect([]); } /** * Convert the attachments collection to the array for the Front-end extended format * @param $attachments * @param array|null|Collection $purchasedAttachmentsIdsArray * @return Collection */ public static function getInfoInArrayForFrontEnd($attachments, $purchasedAttachmentsIdsArray = [] ) { $resultAttachmentArray = []; /** @var Attachment $attachment */ foreach ($attachments as $attachment) { // if ( env('APP_DEBUG') === false ) { // $attachment->model->userModelProfile->setRelation('user', $attachment->model); // } /** @var User $editor */ $editor = Auth::user(); if ( !Auth::guest() && $editor->role() === User::ROLE_ADMIN ) { $resultAttachmentArray[] = $attachment->getExtendedInfoForAdmin(); }else{ $resultAttachmentArray[] = $attachment->getExtendedInfoForFrontEnd( $purchasedAttachmentsIdsArray ); } } return collect($resultAttachmentArray); } /** * @param $attachmentsData * @return AnonymousResourceCollection */ public static function prepareDataForFrontend($attachmentsData) { return PictureOrVideoResource::collection( $attachmentsData ); } /** * Get Models attachment in array with extended data * @param $attachment * @param User|null $fan * @return AnonymousResourceCollection */ public static function getInfoInArrayForFrontendForFan($attachment, User $fan = null ) { $purchasedAndSubscribedAttachmentsIds = Attachment::getIdsArrayOfPurchasedOrSubscribedAttachmentsForFan( $fan ); session()->put('purchasedAttachmentsForFan '. ($fan->id ?? ''), $purchasedAndSubscribedAttachmentsIds); return PictureOrVideoResource::collection( $attachment ); } /** * Returns the short version of the Attachment title (of full if it's length less than 26 signs) * @return mixed */ protected function getShortTitleName() { return strlen($this->title) > 26 ? mb_substr($this->title,0, 26) . '...' : $this->title; } public function isPurchased() { return (bool)$this->attachmentBuyings()->count(); } /** * Check if user can view attachment * @return bool */ public function canUserViewAttachment(): bool { /** @var User $fan */ $fan = Auth::user(); if(!$fan){ return false; } $modelId = $this->user_id; return $fan->fansAttachments->contains($this) || $fan->id === $modelId || $fan->role() === User::ROLE_ADMIN || TokensModelsSubscription::checkFanHasSubscriptionOnModel($fan->id, $modelId) || (int)$this->price === 0; } public function getPendingFiles() { return $this->where('is_approved', '=', 0)->get(); } public static function getPendingFilesCount() { return self::where('is_approved', '=', 0)->count(); } }