Skip to content

Instantly share code, notes, and snippets.

@theinizio
Created September 17, 2020 16:28
Show Gist options
  • Save theinizio/a60c74cd724517f59edb64e352875cbb to your computer and use it in GitHub Desktop.
Save theinizio/a60c74cd724517f59edb64e352875cbb to your computer and use it in GitHub Desktop.
<?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();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment