From e1de07284e85229aa9b66736447a1261e4e77013 Mon Sep 17 00:00:00 2001 From: Celestora Date: Wed, 23 Mar 2022 20:27:12 +0200 Subject: [PATCH] Add playlist model --- VKAPI/Handlers/Audio.php | 4 +- Web/Models/Entities/Audio.php | 2 + Web/Models/Entities/MediaCollection.php | 72 +++++++++++-- Web/Models/Entities/Playlist.php | 130 ++++++++++++++++++++++++ Web/Models/Repositories/Audios.php | 63 +++++++++++- Web/Presenters/AudioPresenter.php | 2 - 6 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 Web/Models/Entities/Playlist.php diff --git a/VKAPI/Handlers/Audio.php b/VKAPI/Handlers/Audio.php index 9f6d3003..3012c8f8 100644 --- a/VKAPI/Handlers/Audio.php +++ b/VKAPI/Handlers/Audio.php @@ -320,7 +320,7 @@ final class Audio extends VKAPIRequestHandler $this->requireUser(); [$owner, $aid] = explode("_", $audio); - $song = (new Audios)->getByOwnerAndVID($owner, $aid); + $song = (new Audios)->getByOwnerAndVID((int) $owner, (int) $aid); $ids = []; foreach(explode(",", $target_ids) as $id) { $id = (int) $id; @@ -338,7 +338,7 @@ final class Audio extends VKAPIRequestHandler if(!$group) $this->fail(0404, "Not Found"); else if(!$group->canBeModifiedBy($this->getUser())) - $this->fail(203, "Insufficient rights to this group"); + $this->fail(203,"Insufficient rights to this group"); $ids[] = $id; $this->beacon($song->getId(), $id * -1); diff --git a/Web/Models/Entities/Audio.php b/Web/Models/Entities/Audio.php index 73ffb8f4..158e74bf 100644 --- a/Web/Models/Entities/Audio.php +++ b/Web/Models/Entities/Audio.php @@ -444,6 +444,8 @@ class Audio extends Media ->delete(); $ctx->table("audio_listens")->where("audio", $this->getId()) ->delete(); + $ctx->table("playlist_relations")->where("media", $this->getId()) + ->delete(); parent::delete($softly); } diff --git a/Web/Models/Entities/MediaCollection.php b/Web/Models/Entities/MediaCollection.php index 05f3835c..b96aaea0 100644 --- a/Web/Models/Entities/MediaCollection.php +++ b/Web/Models/Entities/MediaCollection.php @@ -17,7 +17,17 @@ abstract class MediaCollection extends RowModel protected $specialNames = []; - private $relations; + protected $relations; + + /** + * Maximum amount of items Collection can have + */ + const MAX_ITEMS = INF; + + /** + * Maximum amount of Collections with same "owner" allowed + */ + const MAX_COUNT = INF; function __construct(?ActiveRow $ar = NULL) { @@ -70,18 +80,29 @@ abstract class MediaCollection extends RowModel } abstract function getCoverURL(): ?string; - - function fetch(int $page = 1, ?int $perPage = NULL): \Traversable + + function fetchClassic(int $offset = 0, ?int $limit = NULL): \Traversable { - $related = $this->getRecord()->related("$this->relTableName.collection")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE)->order("media ASC"); + $related = $this->getRecord()->related("$this->relTableName.collection") + ->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset) + ->order("media ASC"); + foreach($related as $rel) { $media = $rel->ref($this->entityTableName, "media"); if(!$media) continue; - + yield new $this->entityClassName($media); } } + + function fetch(int $page = 1, ?int $perPage = NULL): \Traversable + { + $page = max(1, $page); + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + + return $this->fetchClassic($perPage * ($page - 1), $perPage); + } function size(): int { @@ -110,7 +131,7 @@ abstract class MediaCollection extends RowModel { return $this->getRecord()->special_type !== 0; } - + function add(RowModel $entity): bool { $this->entitySuitable($entity); @@ -118,6 +139,10 @@ abstract class MediaCollection extends RowModel if(!$this->allowDuplicates) if($this->has($entity)) return false; + + if(self::MAX_ITEMS != INF) + if(sizeof($this->relations->where("collection", $this->getId())) > self::MAX_ITEMS) + throw new \OutOfBoundsException("Collection is full"); $this->relations->insert([ "collection" => $this->getId(), @@ -127,14 +152,14 @@ abstract class MediaCollection extends RowModel return true; } - function remove(RowModel $entity): void + function remove(RowModel $entity): bool { $this->entitySuitable($entity); - $this->relations->where([ + return $this->relations->where([ "collection" => $this->getId(), "media" => $entity->getId(), - ])->delete(); + ])->delete() > 0; } function has(RowModel $entity): bool @@ -148,6 +173,33 @@ abstract class MediaCollection extends RowModel return !is_null($rel); } - + + function save(): void + { + $thisTable = DatabaseConnection::i()->getContext()->table($this->tableName); + if(self::MAX_COUNT != INF) + if(isset($this->changes["owner"])) + if(sizeof($thisTable->where("owner", $this->changes["owner"])) > self::MAX_COUNT) + throw new \OutOfBoundsException("Maximum amount of collections"); + + if(is_null($this->getRecord())) + if(!isset($this->changes["created"])) + $this->stateChanges("created", time()); + else + $this->stateChanges("edited", time()); + + parent::save(); + } + + function delete(bool $softly = true): void + { + if(!$softly) { + $this->relations->where("collection", $this->getId()) + ->delete(); + } + + parent::delete($softly); + } + use Traits\TOwnable; } diff --git a/Web/Models/Entities/Playlist.php b/Web/Models/Entities/Playlist.php new file mode 100644 index 00000000..5c439166 --- /dev/null +++ b/Web/Models/Entities/Playlist.php @@ -0,0 +1,130 @@ +importTable = DatabaseConnection::i()->getContext()->table("playlist_imports"); + } + + function getCoverURL(): ?string + { + return NULL; + } + + function getAudios(int $offset = 0, ?int $limit = NULL, ?int $shuffleSeed = NULL): \Traversable + { + if(!$shuffleSeed) + return $this->fetchClassic($offset, $limit); + + $ids = []; + foreach($this->relations->select("media AS i")->where("collection", $this->getId()) as $rel) + $ids[] = $rel->i; + + $ids = knuth_shuffle($ids, $shuffleSeed); + $ids = array_slice($ids, $offset, $limit ?? OPENVK_DEFAULT_PER_PAGE); + foreach($ids as $id) + yield (new Audios)->get($id); + } + + function add(RowModel $audio): bool + { + if($res = parent::add($audio)) { + $this->stateChanges("length", $this->getRecord()->length + $audio->getLength()); + $this->save(); + } + + return $res; + } + + function remove(RowModel $audio): bool + { + if($res = parent::remove($audio)) { + $this->stateChanges("length", $this->getRecord()->length - $audio->getLength()); + $this->save(); + } + + return $res; + } + + function isBookmarkedBy(RowModel $entity): bool + { + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + return !is_null($this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->fetch()); + } + + function bookmark(RowModel $entity): bool + { + if($this->isBookmarkedBy($entity)) + return false; + + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + if($this->importTable->where("entity", $id)->count > self::MAX_COUNT) + throw new \OutOfBoundsException("Maximum amount of playlists"); + + $this->importTable->insert([ + "entity" => $id, + "playlist" => $this->getId(), + ]); + + return true; + } + + function unbookmark(RowModel $entity): bool + { + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + $count = $this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->delete(); + + return $count > 0; + } + + function setLength(): void + { + throw new \LogicException("Can't set length of playlist manually"); + } + + function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("playlist_imports")->where("playlist", $this->getId()) + ->delete(); + + parent::delete($softly); + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/Audios.php b/Web/Models/Repositories/Audios.php index f2fa0062..614e9512 100644 --- a/Web/Models/Repositories/Audios.php +++ b/Web/Models/Repositories/Audios.php @@ -3,6 +3,7 @@ namespace openvk\Web\Models\Repositories; use Chandler\Database\DatabaseConnection; use openvk\Web\Models\Entities\Audio; use openvk\Web\Models\Entities\Club; +use openvk\Web\Models\Entities\Playlist; use openvk\Web\Models\Entities\User; use openvk\Web\Models\Repositories\Util\EntityStream; @@ -11,6 +12,8 @@ class Audios private $context; private $audios; private $rels; + private $playlists; + private $playlistImports; const ORDER_NEW = 0; const ORDER_POPULAR = 1; @@ -24,6 +27,9 @@ class Audios $this->context = DatabaseConnection::i()->getContext(); $this->audios = $this->context->table("audios"); $this->rels = $this->context->table("audio_relations"); + + $this->playlists = $this->context->table("playlists"); + $this->playlistImports = $this->context->table("playlist_imports"); } function get(int $id): ?Audio @@ -35,6 +41,15 @@ class Audios return new Audio($audio); } + private function getPlaylist(int $id): ?Playlist + { + $playlist = $this->playlists->get($id); + if(!$playlist) + return NULL; + + return new Playlist($playlist); + } + function getByOwnerAndVID(int $owner, int $vId): ?Audio { $audio = $this->audios->where([ @@ -48,8 +63,8 @@ class Audios function getByEntityID(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable { - $perPage ??= OPENVK_DEFAULT_PER_PAGE; - $iter = $this->rels->where("entity", $entity)->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset); + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->rels->where("entity", $entity)->limit($limit, $offset); foreach($iter as $rel) { $audio = $this->get($rel->audio); if(!$audio || $audio->isDeleted()) { @@ -61,6 +76,21 @@ class Audios } } + function getPlaylistsByEntityId(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->playlistImports->where("entity", $entity)->limit($limit, $offset); + foreach($iter as $rel) { + $playlist = $this->getPlaylist($rel->playlist); + if(!$playlist || $playlist->isDeleted()) { + $deleted++; + continue; + } + + yield $playlist; + } + } + function getByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable { return $this->getByEntityID($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted); @@ -71,6 +101,16 @@ class Audios return $this->getByEntityID($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted); } + function getPlaylistsByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getPlaylistsByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted); + } + function getUserCollectionSize(User $user): int { return sizeof($this->rels->where("entity", $user->getId())); @@ -81,6 +121,16 @@ class Audios return sizeof($this->rels->where("entity", $club->getId() * -1)); } + function getUserPlaylistsCount(User $user): int + { + return sizeof($this->playlistImports->where("entity", $user->getId())); + } + + function getClubPlaylistsCount(Club $club): int + { + return sizeof($this->playlistImports->where("entity", $club->getId() * -1)); + } + function getByUploader(User $user): EntityStream { $search = $this->audios->where([ @@ -120,4 +170,13 @@ class Audios return new EntityStream("Audio", $search); } + + function searchPlaylists(string $query): EntityStream + { + $search = $this->audios->where([ + "deleted" => 0, + ])->where("MATCH (title, description) AGAINST (? IN BOOLEAN MODE)", $query); + + return new EntityStream("Playlist", $search); + } } \ No newline at end of file diff --git a/Web/Presenters/AudioPresenter.php b/Web/Presenters/AudioPresenter.php index f1371c9b..05b1c4e8 100644 --- a/Web/Presenters/AudioPresenter.php +++ b/Web/Presenters/AudioPresenter.php @@ -1,8 +1,6 @@