From 791c36416d4787fdc403f2851e529930ef153f21 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Tue, 21 Nov 2023 20:14:38 +0300 Subject: [PATCH] Add something related with videos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Теперь видосы работают как аудио, пользователи могут добавлять и удалять видео из коллекции. Но, правда, после обновления пользователи потеряют все свои видео, потом подумаю как исправить - Ещё теперь видео можно загружать в группу, жесть. И на странице группы теперь показывается 2 случайных видео из группы - Возможно, исправлена загрузка видео под виндовс (а может я её сломал) - У видосов теперь сохраняется ширина и высота, а так же длина - У прикреплённого видео рядом с названием показывается его длина - Видео теперь размещаются в masonry layout. Если помимо видео у поста есть другие фотографии или другие видео, то показывается только обложка видео и кнопка проигрывания - В класс video в api добавлена поддержка просмотра видеозаписей из групп --- VKAPI/Handlers/Video.php | 17 +- Web/Models/Entities/Club.php | 13 ++ .../Entities/Traits/TAttachmentHost.php | 7 +- Web/Models/Entities/Video.php | 166 ++++++++++++++++-- Web/Models/Repositories/Videos.php | 61 ++++++- Web/Models/shell/processVideo.ps1 | 8 +- Web/Presenters/GroupPresenter.php | 4 +- Web/Presenters/VideosPresenter.php | 29 ++- Web/Presenters/templates/Group/View.xml | 26 +++ Web/Presenters/templates/Videos/List.xml | 8 +- Web/Presenters/templates/Videos/Upload.xml | 4 +- .../templates/components/attachment.xml | 42 +++-- .../templates/components/comment.xml | 2 +- .../components/post/microblogpost.xml | 2 +- .../templates/components/post/oldpost.xml | 2 +- Web/Util/Makima/Makima.php | 2 +- Web/static/css/main.css | 29 +++ Web/static/img/video_controls.png | Bin 0 -> 2466 bytes install/sqls/00043-better-videos.sql | 10 ++ locales/ru.strings | 2 + 20 files changed, 365 insertions(+), 69 deletions(-) create mode 100644 Web/static/img/video_controls.png create mode 100644 install/sqls/00043-better-videos.sql diff --git a/VKAPI/Handlers/Video.php b/VKAPI/Handlers/Video.php index 5d0967c7..b2124615 100755 --- a/VKAPI/Handlers/Video.php +++ b/VKAPI/Handlers/Video.php @@ -36,21 +36,22 @@ final class Video extends VKAPIRequestHandler ]; } else { if ($owner_id > 0) - $user = (new UsersRepo)->get($owner_id); + $owner = (new UsersRepo)->get($owner_id); else - $this->fail(1, "Not implemented"); + $owner = (new ClubsRepo)->get(abs($owner_id)); - if(!$user->getPrivacyPermission('videos.read', $this->getUser())) { + if(!$owner) + $this->fail(20, "Invalid user"); + + if($owner_id > 0 && !$owner->getPrivacyPermission('videos.read', $this->getUser())) $this->fail(20, "Access denied: this user chose to hide his videos"); - } - $videos = (new VideosRepo)->getByUser($user, $offset + 1, $count); - $videosCount = (new VideosRepo)->getUserVideosCount($user); + $videos = (new VideosRepo)->getByEntityId($owner_id, $offset, $count); + $videosCount = (new VideosRepo)->getVideosCountByEntityId($owner_id); $items = []; - foreach ($videos as $video) { + foreach ($videos as $video) $items[] = $video->getApiStructure($this->getUser()); - } return (object) [ "count" => $videosCount, diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 5324efb6..8d908011 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -414,6 +414,11 @@ class Club extends RowModel return (bool) $this->getRecord()->everyone_can_upload_audios; } + function isEveryoneCanUploadVideos(): bool + { + return false; + } + function canUploadAudio(?User $user): bool { if(!$user) @@ -422,6 +427,14 @@ class Club extends RowModel return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user); } + function canUploadVideo(?User $user): bool + { + if(!$user) + return NULL; + + return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user); + } + function getAudiosCollectionSize() { return (new \openvk\Web\Models\Repositories\Audios)->getClubCollectionSize($this); diff --git a/Web/Models/Entities/Traits/TAttachmentHost.php b/Web/Models/Entities/Traits/TAttachmentHost.php index db814cce..7d7c6eb6 100644 --- a/Web/Models/Entities/Traits/TAttachmentHost.php +++ b/Web/Models/Entities/Traits/TAttachmentHost.php @@ -1,6 +1,6 @@ getChildren(); + $children = iterator_to_array($this->getChildren()); $skipped = $photos = $result = []; foreach($children as $child) { - if($child instanceof Photo) { + if($child instanceof Photo || $child instanceof Video && $child->getDimensions()) { $photos[] = $child; continue; } @@ -68,6 +68,7 @@ trait TAttachmentHost "height" => $height . "px", "tiles" => $result, "extras" => $skipped, + "count" => sizeof($children), ]; } diff --git a/Web/Models/Entities/Video.php b/Web/Models/Entities/Video.php index fa54392e..aa6c0bd7 100644 --- a/Web/Models/Entities/Video.php +++ b/Web/Models/Entities/Video.php @@ -4,6 +4,7 @@ use openvk\Web\Util\Shell\Shell; use openvk\Web\Util\Shell\Exceptions\{ShellUnavailableException, UnknownCommandException}; use openvk\Web\Models\VideoDrivers\VideoDriver; use Nette\InvalidStateException as ISE; +use Chandler\Database\DatabaseConnection; define("VIDEOS_FRIENDLY_ERROR", "Uploads are disabled on this instance :<", false); @@ -34,10 +35,25 @@ class Video extends Media if(sizeof($durations[1]) === 0) throw new \DomainException("$filename does not contain any meaningful video streams"); - foreach($durations[1] as $duration) - if(floatval($duration) < 1.0) + $length = 0; + foreach($durations[1] as $duration) { + $duration = floatval($duration); + if($duration < 1.0) throw new \DomainException("$filename does not contain any meaningful video streams"); + else + $length = max($length, $duration); + } + + $this->stateChanges("length", (int) round($length, 0, PHP_ROUND_HALF_EVEN)); + preg_match('%width=([0-9\.]++)%', $streams, $width); + preg_match('%height=([0-9\.]++)%', $streams, $height); + + if(!empty($width) && !empty($height)) { + $this->stateChanges("width", $width[1]); + $this->stateChanges("width", $height[1]); + } + try { if(!is_dir($dirId = dirname($this->pathFromHash($hash)))) mkdir($dirId); @@ -45,7 +61,11 @@ class Video extends Media $dir = $this->getBaseDir(); $ext = Shell::isPowershell() ? "ps1" : "sh"; $cmd = Shell::isPowershell() ? "powershell" : "bash"; - Shell::$cmd(__DIR__ . "/../shell/processVideo.$ext", OPENVK_ROOT, $filename, $dir, $hash)->start(); #async :DDD + + if($cmd == "bash") + Shell::$cmd(__DIR__ . "/../shell/processVideo.$ext", OPENVK_ROOT, $filename, $dir, $hash)->start(); # async :DDD + else + Shell::$cmd(__DIR__ . "/../shell/processVideo.$ext", OPENVK_ROOT, $filename, $dir, $hash)->execute($err); # под виндой только execute } catch(ShellUnavailableException $suex) { exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "Shell is unavailable" : VIDEOS_FRIENDLY_ERROR); } catch(UnknownCommandException $ucex) { @@ -118,11 +138,12 @@ class Video extends Media function getApiStructure(?User $user = NULL): object { $fromYoutube = $this->getType() == Video::TYPE_EMBED; + $dimensions = $this->getDimensions(); $res = (object)[ "type" => "video", "video" => [ "can_comment" => 1, - "can_like" => 1, // we don't h-have wikes in videos + "can_like" => 1, "can_repost" => 1, "can_subscribe" => 1, "can_add_to_faves" => 0, @@ -130,7 +151,7 @@ class Video extends Media "comments" => $this->getCommentsCount(), "date" => $this->getPublicationTime()->timestamp(), "description" => $this->getDescription(), - "duration" => 0, // я хуй знает как получить длину видео + "duration" => $this->getLength(), "image" => [ [ "url" => $this->getThumbnailURL(), @@ -139,8 +160,8 @@ class Video extends Media "with_padding" => 1 ] ], - "width" => 640, - "height" => 480, + "width" => $dimensions ? NULL : $dimensions[0], + "height" => $dimensions ? NULL : $dimensions[1], "id" => $this->getVirtualId(), "owner_id" => $this->getOwner()->getId(), "user_id" => $this->getOwner()->getId(), @@ -194,10 +215,7 @@ class Video extends Media function isDeleted(): bool { - if ($this->getRecord()->deleted == 1) - return TRUE; - else - return FALSE; + return $this->getRecord()->deleted == 1; } function deleteVideo(): void @@ -205,6 +223,8 @@ class Video extends Media $this->setDeleted(1); $this->unwire(); $this->save(); + + $ctx->table("video_relations")->where("video", $this->getId())->delete(); } static function fastMake(int $owner, string $name = "Unnamed Video.ogv", string $description = "", array $file, bool $unlisted = true, bool $anon = false): Video @@ -221,7 +241,7 @@ class Video extends Media $video->setFile($file); $video->setUnlisted($unlisted); $video->save(); - + return $video; } @@ -243,4 +263,126 @@ class Video extends Media return $res; } + + function isInLibraryOf($entity): bool + { + return sizeof(DatabaseConnection::i()->getContext()->table("video_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "video" => $this->getId(), + ])) != 0; + } + + function add($entity): bool + { + if($this->isInLibraryOf($entity)) + return false; + + $entityId = $entity->getId() * ($entity instanceof Club ? -1 : 1); + $audioRels = DatabaseConnection::i()->getContext()->table("video_relations"); + + $audioRels->insert([ + "entity" => $entityId, + "video" => $this->getId(), + ]); + + return true; + } + + function remove($entity): bool + { + if(!$this->isInLibraryOf($entity)) + return false; + + DatabaseConnection::i()->getContext()->table("video_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "video" => $this->getId(), + ])->delete(); + + return true; + } + + function getLength() + { + return $this->getRecord()->length; + } + + function getFormattedLength(): string + { + $len = $this->getLength(); + if(!$len) return "00:00"; + + $mins = floor($len / 60); + $secs = $len - ($mins * 60); + + return ( + str_pad((string) $mins, 2, "0", STR_PAD_LEFT) + . ":" . + str_pad((string) $secs, 2, "0", STR_PAD_LEFT) + ); + } + + function fillDimensions() + { + $hash = $this->getRecord()->hash; + $path = $this->pathFromHash($hash); + + if(!file_exists($path)) { + $this->stateChanges("width", 0); + $this->stateChanges("height", 0); + $this->stateChanges("length", 0); + + $this->save(); + + return false; + } + + $streams = Shell::ffprobe("-i", $path, "-show_streams", "-select_streams v", "-loglevel error")->execute($error); + + $durations = []; + + preg_match_all('%duration=([0-9\.]++)%', $streams, $durations); + + $length = 0; + foreach($durations[1] as $duration) { + $duration = floatval($duration); + if($duration < 1.0) + continue; + else + $length = max($length, $duration); + } + + $this->stateChanges("length", (int) round($length, 0, PHP_ROUND_HALF_EVEN)); + + preg_match('%width=([0-9\.]++)%', $streams, $width); + preg_match('%height=([0-9\.]++)%', $streams, $height); + + #exit(var_dump($path)); + if(!empty($width) && !empty($height)) { + $this->stateChanges("width", $width[1]); + $this->stateChanges("height", $height[1]); + } + + $this->save(); + + return true; + } + + function getDimensions() + { + if($this->getType() == Video::TYPE_EMBED) return NULL; + + $width = $this->getRecord()->width; + $height = $this->getRecord()->height; + if(!$width) $this->fillDimensions(); + + return $width != 0 ? [$width, $height] : NULL; + } + + function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("video_relations")->where("video", $this->getId())->delete(); + + parent::delete($softly); + } } diff --git a/Web/Models/Repositories/Videos.php b/Web/Models/Repositories/Videos.php index 2d41c3f9..93097e7c 100644 --- a/Web/Models/Repositories/Videos.php +++ b/Web/Models/Repositories/Videos.php @@ -1,6 +1,6 @@ context = DatabaseConnection::i()->getContext(); $this->videos = $this->context->table("videos"); + $this->rels = $this->context->table("video_relations"); } function get(int $id): ?Video @@ -33,17 +35,66 @@ class Videos return new Video($videos); } + + function getByEntityId(int $entity, int $offset = 0, ?int $limit = NULL): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->rels->where("entity", $entity)->limit($limit, $offset)->order("id DESC"); + foreach($iter as $rel) { + $vid = $this->get($rel->video); + if(!$vid || $vid->isDeleted()) { + continue; + } + + yield $vid; + } + } + + function getVideosCountByEntityId(int $id) + { + return sizeof($this->rels->where("entity", $id)); + } function getByUser(User $user, int $page = 1, ?int $perPage = NULL): \Traversable { - $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; - foreach($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])->page($page, $perPage)->order("created DESC") as $video) - yield new Video($video); + return $this->getByEntityId($user->getId(), ($perPage * ($page - 1)), $perPage); + } + + function getByClub(Club $club, int $page = 1, ?int $perPage = NULL): \Traversable + { + return $this->getByEntityId($club->getRealId(), ($perPage * ($page - 1)), $perPage); } function getUserVideosCount(User $user): int { - return sizeof($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])); + return sizeof($this->rels->where("entity", $user->getId())); + } + + function getRandomTwoVideosByEntityId(int $id): Array + { + $iter = $this->rels->where("entity", $id); + $ids = []; + + foreach($iter as $it) + $ids[] = $it->video; + + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + + $ids = knuth_shuffle($ids, $shuffleSeed); + $ids = array_slice($ids, 0, 3); + $videos = []; + + foreach($ids as $id) { + $video = $this->get((int)$id); + + if(!$video || $video->isDeleted()) + continue; + + $videos[] = $video; + } + + return $videos; } function find(string $query = "", array $pars = [], string $sort = "id"): Util\EntityStream diff --git a/Web/Models/shell/processVideo.ps1 b/Web/Models/shell/processVideo.ps1 index f2cc9918..4e752487 100644 --- a/Web/Models/shell/processVideo.ps1 +++ b/Web/Models/shell/processVideo.ps1 @@ -9,12 +9,12 @@ $temp2 = [System.IO.Path]::GetTempFileName() $shell = Get-WmiObject Win32_process -filter "ProcessId = $PID" $shell.SetPriority(16384) +Remove-Item $temp Move-Item $file $temp # video stub logic was implicitly deprecated, so we start processing at once -ffmpeg -i $temp -ss 00:00:01.000 -vframes 1 "$dir$hashT/$hash.gif" -ffmpeg -i $temp -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y $temp2 +ffmpeg -i $temp -ss 00:00:01.000 -y -vframes 1 "$dir$hashT/$hash.gif" +ffmpeg -i $temp -c:v libx264 -f mp4 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y $temp2 -Move-Item $temp2 "$dir$hashT/$hash.mp4" -Remove-Item $temp +Move-Item $temp2 "$dir$hashT/$hash.mp4" -Force Remove-Item $temp2 diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index beeede13..b2ec52aa 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -3,7 +3,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{Club, Photo, Post}; use Nette\InvalidStateException; use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; -use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios, Posts}; +use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios, Posts, Videos}; use Chandler\Security\Authenticator; final class GroupPresenter extends OpenVKPresenter @@ -31,6 +31,8 @@ final class GroupPresenter extends OpenVKPresenter $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); $this->template->topics = (new Topics)->getLastTopics($club, 3); $this->template->topicsCount = (new Topics)->getClubTopicsCount($club); + $this->template->videos = (new Videos)->getRandomTwoVideosByEntityId($club->getRealId()); + $this->template->videosCount = (new Videos)->getVideosCountByEntityId($club->getRealId()); $this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($club->getRealId()); $this->template->audiosCount = (new Audios)->getClubCollectionSize($club); } diff --git a/Web/Presenters/VideosPresenter.php b/Web/Presenters/VideosPresenter.php index 6f92891c..a30d7051 100644 --- a/Web/Presenters/VideosPresenter.php +++ b/Web/Presenters/VideosPresenter.php @@ -1,7 +1,7 @@ users->get($id); - if(!$user) $this->notFound(); - if(!$user->getPrivacyPermission('videos.read', $this->user->identity ?? NULL)) + if($id > 0) + $owner = $this->users->get($id); + else + $owner = (new Clubs)->get(abs($id)); + + if(!$owner) $this->notFound(); + if($id > 0 && !$owner->getPrivacyPermission('videos.read', $this->user->identity ?? NULL)) $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); - $this->template->user = $user; - $this->template->videos = $this->videos->getByUser($user, (int) ($this->queryParam("p") ?? 1)); - $this->template->count = $this->videos->getUserVideosCount($user); + $this->template->owner = $owner; + $this->template->canUpload = $id > 0 ? $id == $this->user->id : $owner->canBeModifiedBy($this->user->identity); + $this->template->videos = $id > 0 ? $this->videos->getByUser($owner, (int) ($this->queryParam("p") ?? 1)) : $this->videos->getByClub($owner, (int) ($this->queryParam("p") ?? 1)); + $this->template->count = $this->videos->getVideosCountByEntityId($owner->getRealId()); $this->template->paginatorConf = (object) [ "count" => $this->template->count, "page" => (int) ($this->queryParam("p") ?? 1), @@ -60,6 +65,14 @@ final class VideosPresenter extends OpenVKPresenter if(OPENVK_ROOT_CONF['openvk']['preferences']['videos']['disableUploading']) $this->flashFail("err", tr("error"), tr("video_uploads_disabled")); + if($this->queryParam("gid")) { + $club = (new Clubs)->get((int)$this->queryParam("gid")); + + if(!$club || !$club->canUploadVideo($this->user->identity)) + $this->notFound(); + } + + $this->template->owner = $club ?: $this->user->identity; if($_SERVER["REQUEST_METHOD"] === "POST") { if(!empty($this->postParam("name"))) { $video = new Video; @@ -82,6 +95,8 @@ final class VideosPresenter extends OpenVKPresenter } $video->save(); + + $video->add(!isset($club) ? $this->user->identity : $club); $this->redirect("/video" . $video->getPrettyId()); } else { diff --git a/Web/Presenters/templates/Group/View.xml b/Web/Presenters/templates/Group/View.xml index 7d2066c2..f45fe73a 100644 --- a/Web/Presenters/templates/Group/View.xml +++ b/Web/Presenters/templates/Group/View.xml @@ -299,6 +299,32 @@ +