fail(0o404, "Audio not found"); } elseif (!$audio->canBeViewedBy($this->getUser())) { $this->fail(201, "Access denied to audio(" . $audio->getPrettyId() . ")"); } # рофлан ебало $privApi = $hash && $GLOBALS["csrfCheck"]; $audioObj = $audio->toVkApiStruct($this->getUser()); if (!$privApi) { $audioObj->manifest = false; $audioObj->keys = false; } if ($need_user) { $user = (new \openvk\Web\Models\Repositories\Users())->get($audio->getOwner()->getId()); $audioObj->user = (object) [ "id" => $user->getId(), "photo" => $user->getAvatarUrl(), "name" => $user->getCanonicalName(), "name_gen" => $user->getCanonicalName(), ]; } return $audioObj; } private function streamToResponse(EntityStream $es, int $offset, int $count, ?string $hash = null): object { $items = []; foreach ($es->offsetLimit($offset, $count) as $audio) { $items[] = $this->toSafeAudioStruct($audio, $hash); } return (object) [ "count" => sizeof($items), "items" => $items, ]; } private function validateGenre(?string& $genre_str, ?int $genre_id): void { if (!is_null($genre_str)) { if (!in_array($genre_str, AEntity::genres)) { $this->fail(8, "Invalid genre_str"); } } elseif (!is_null($genre_id)) { $genre_str = array_flip(AEntity::vkGenres)[$genre_id] ?? null; if (!$genre_str) { $this->fail(8, "Invalid genre ID $genre_id"); } } } private function audioFromAnyId(string $id): ?AEntity { $descriptor = explode("_", $id); if (sizeof($descriptor) === 1) { if (ctype_digit($descriptor[0])) { $audio = (new Audios())->get((int) $descriptor[0]); } else { $aid = base64_decode($descriptor[0], true); if (!$aid) { $this->fail(8, "Invalid audio $id"); } $audio = (new Audios())->get((int) $aid); } } elseif (sizeof($descriptor) === 2) { $audio = (new Audios())->getByOwnerAndVID((int) $descriptor[0], (int) $descriptor[1]); } else { $this->fail(8, "Invalid audio $id"); } return $audio; } public function getById(string $audios, ?string $hash = null, int $need_user = 0): object { $this->requireUser(); $audioIds = array_unique(explode(",", $audios)); if (sizeof($audioIds) === 1) { $audio = $this->audioFromAnyId($audioIds[0]); return (object) [ "count" => 1, "items" => [ $this->toSafeAudioStruct($audio, $hash, (bool) $need_user), ], ]; } elseif (sizeof($audioIds) > 6000) { $this->fail(1980, "Can't get more than 6000 audios at once"); } $audios = []; foreach ($audioIds as $id) { $audios[] = $this->getById($id, $hash)->items[0]; } return (object) [ "count" => sizeof($audios), "items" => $audios, ]; } public function isLagtrain(string $audio_id): int { $this->requireUser(); $audio = $this->audioFromAnyId($audio_id); if (!$audio) { $this->fail(0o404, "Audio not found"); } # Possible information disclosure risks are acceptable :D return (int) (strpos($audio->getName(), "Lagtrain") !== false); } // TODO stub public function getRecommendations(): object { return (object) [ "count" => 0, "items" => [], ]; } public function getPopular(?int $genre_id = null, ?string $genre_str = null, int $offset = 0, int $count = 100, ?string $hash = null): object { $this->requireUser(); $this->validateGenre($genre_str, $genre_id); $results = (new Audios())->getGlobal(Audios::ORDER_POPULAR, $genre_str); return $this->streamToResponse($results, $offset, $count, $hash); } public function getFeed(?int $genre_id = null, ?string $genre_str = null, int $offset = 0, int $count = 100, ?string $hash = null): object { $this->requireUser(); $this->validateGenre($genre_str, $genre_id); $results = (new Audios())->getGlobal(Audios::ORDER_NEW, $genre_str); return $this->streamToResponse($results, $offset, $count, $hash); } public function search(string $q, int $auto_complete = 0, int $lyrics = 0, int $performer_only = 0, int $sort = 2, int $search_own = 0, int $offset = 0, int $count = 30, ?string $hash = null): object { $this->requireUser(); if (($auto_complete + $search_own) != 0) { $this->fail(10, "auto_complete and search_own are not supported"); } elseif ($count > 300 || $count < 1) { $this->fail(8, "count is invalid: $count"); } $results = (new Audios())->search($q, $sort, (bool) $performer_only, (bool) $lyrics); return $this->streamToResponse($results, $offset, $count, $hash); } public function getCount(int $owner_id, int $uploaded_only = 0): int { $this->requireUser(); if ($owner_id < 0) { $owner_id *= -1; $group = (new Clubs())->get($owner_id); if (!$group) { $this->fail(0o404, "Group not found"); } return (new Audios())->getClubCollectionSize($group); } $user = (new \openvk\Web\Models\Repositories\Users())->get($owner_id); if (!$user) { $this->fail(0o404, "User not found"); } if (!$user->getPrivacyPermission("audios.read", $this->getUser())) { $this->fail(15, "Access denied"); } if ($uploaded_only) { return DatabaseConnection::i()->getContext()->table("audios") ->where([ "deleted" => false, "owner" => $owner_id, ])->count(); } return (new Audios())->getUserCollectionSize($user); } public function get(int $owner_id = 0, int $album_id = 0, string $audio_ids = '', int $need_user = 1, int $offset = 0, int $count = 100, int $uploaded_only = 0, int $need_seed = 0, ?string $shuffle_seed = null, int $shuffle = 0, ?string $hash = null): object { $this->requireUser(); $shuffleSeed = null; $shuffleSeedStr = null; if ($shuffle == 1) { if (!$shuffle_seed) { if ($need_seed == 1) { $shuffleSeed = openssl_random_pseudo_bytes(6); $shuffleSeedStr = base64_encode($shuffleSeed); $shuffleSeed = hexdec(bin2hex($shuffleSeed)); } else { $hOffset = ((int) date("i") * 60) + (int) date("s"); $thisHour = time() - $hOffset; $shuffleSeed = $thisHour + $this->getUser()->getId(); $shuffleSeedStr = base64_encode(hex2bin(dechex($shuffleSeed))); } } else { $shuffleSeed = hexdec(bin2hex(base64_decode($shuffle_seed))); $shuffleSeedStr = $shuffle_seed; } } if ($album_id != 0) { $album = (new Audios())->getPlaylist($album_id); if (!$album) { $this->fail(0o404, "album_id invalid"); } elseif (!$album->canBeViewedBy($this->getUser())) { $this->fail(600, "Can't open this album for reading"); } $songs = []; $list = $album->getAudios($offset, $count, $shuffleSeed); foreach ($list as $song) { $songs[] = $this->toSafeAudioStruct($song, $hash, $need_user == 1); } $response = (object) [ "count" => sizeof($songs), "items" => $songs, ]; if (!is_null($shuffleSeed)) { $response->shuffle_seed = $shuffleSeedStr; } return $response; } if (!empty($audio_ids)) { $audio_ids = explode(",", $audio_ids); if (!$audio_ids) { $this->fail(10, "Audio::get@L0d186:explode(string): Unknown error"); } elseif (sizeof($audio_ids) < 1) { $this->fail(8, "Invalid audio_ids syntax"); } if (!is_null($shuffleSeed)) { $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); } $obj = $this->getById(implode(",", $audio_ids), $hash, $need_user); if (!is_null($shuffleSeed)) { $obj->shuffle_seed = $shuffleSeedStr; } return $obj; } $dbCtx = DatabaseConnection::i()->getContext(); if ($uploaded_only == 1) { if ($owner_id <= 0) { $this->fail(8, "uploaded_only can only be used with owner_id > 0"); } $user = (new \openvk\Web\Models\Repositories\Users())->get($owner_id); if (!$user) { $this->fail(0o602, "Invalid user"); } if (!$user->getPrivacyPermission("audios.read", $this->getUser())) { $this->fail(15, "Access denied: this user chose to hide his audios"); } if (!is_null($shuffleSeed)) { $audio_ids = []; $query = $dbCtx->table("audios")->select("virtual_id")->where([ "owner" => $owner_id, "deleted" => 0, ]); foreach ($query as $res) { $audio_ids[] = $res->virtual_id; } $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); $audio_ids = array_slice($audio_ids, $offset, $count); $audio_q = ""; # audio.getById query foreach ($audio_ids as $aid) { $audio_q .= ",$owner_id" . "_$aid"; } $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); $obj->shuffle_seed = $shuffleSeedStr; return $obj; } $res = (new Audios())->getByUploader((new \openvk\Web\Models\Repositories\Users())->get($owner_id)); return $this->streamToResponse($res, $offset, $count, $hash, $need_user); } $query = $dbCtx->table("audio_relations")->select("audio")->where("entity", $owner_id); if (!is_null($shuffleSeed)) { $audio_ids = []; foreach ($query as $aid) { $audio_ids[] = $aid->audio; } $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); $audio_ids = array_slice($audio_ids, $offset, $count); $audio_q = ""; foreach ($audio_ids as $aid) { $audio_q .= ",$aid"; } $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); $obj->shuffle_seed = $shuffleSeedStr; return $obj; } $items = []; if ($owner_id > 0) { $user = (new \openvk\Web\Models\Repositories\Users())->get($owner_id); if (!$user) { $this->fail(50, "Invalid user"); } if (!$user->getPrivacyPermission("audios.read", $this->getUser())) { $this->fail(15, "Access denied: this user chose to hide his audios"); } } $audios = (new Audios())->getByEntityID($owner_id, $offset, $count); foreach ($audios as $audio) { $items[] = $this->toSafeAudioStruct($audio, $hash, $need_user == 1); } return (object) [ "count" => sizeof($items), "items" => $items, ]; } public function getLyrics(int $lyrics_id): object { $this->requireUser(); $audio = (new Audios())->get($lyrics_id); if (!$audio || !$audio->getLyrics()) { $this->fail(0o404, "Not found"); } if (!$audio->canBeViewedBy($this->getUser())) { $this->fail(201, "Access denied to lyrics"); } return (object) [ "lyrics_id" => $lyrics_id, "text" => preg_replace("%\r\n?%", "\n", $audio->getLyrics()), ]; } public function beacon(int $aid, ?int $gid = null): int { $this->requireUser(); $this->willExecuteWriteAction(); $audio = (new Audios())->get($aid); if (!$audio) { $this->fail(0o404, "Not Found"); } elseif (!$audio->canBeViewedBy($this->getUser())) { $this->fail(201, "Insufficient permissions to listen this audio"); } $group = null; if (!is_null($gid)) { $group = (new Clubs())->get($gid); if (!$group) { $this->fail(0o404, "Not Found"); } elseif (!$group->canBeModifiedBy($this->getUser())) { $this->fail(203, "Insufficient rights to this group"); } } return (int) $audio->listen($group ?? $this->getUser()); } public function setBroadcast(string $audio, string $target_ids): array { $this->requireUser(); [$owner, $aid] = explode("_", $audio); $song = (new Audios())->getByOwnerAndVID((int) $owner, (int) $aid); $ids = []; foreach (explode(",", $target_ids) as $id) { $id = (int) $id; if ($id > 0) { if ($id != $this->getUser()->getId()) { $this->fail(600, "Can't listen on behalf of $id"); } else { $ids[] = $id; $this->beacon($song->getId()); continue; } } $group = (new Clubs())->get($id * -1); if (!$group) { $this->fail(0o404, "Not Found"); } elseif (!$group->canBeModifiedBy($this->getUser())) { $this->fail(203, "Insufficient rights to this group"); } $ids[] = $id; $this->beacon($song ? $song->getId() : 0, $id * -1); } return $ids; } public function getBroadcastList(string $filter = "all", int $active = 0, ?string $hash = null): object { $this->requireUser(); if (!in_array($filter, ["all", "friends", "groups"])) { $this->fail(8, "Invalid filter $filter"); } $broadcastList = $this->getUser()->getBroadcastList($filter); $items = []; foreach ($broadcastList as $res) { $struct = $res->toVkApiStruct(); $status = $res->getCurrentAudioStatus(); $struct->status_audio = $status ? $this->toSafeAudioStruct($status) : null; $items[] = $struct; } return (object) [ "count" => sizeof($items), "items" => $items, ]; } public function edit(int $owner_id, int $audio_id, ?string $artist = null, ?string $title = null, ?string $text = null, ?int $genre_id = null, ?string $genre_str = null, int $no_search = 0): int { $this->requireUser(); $this->willExecuteWriteAction(); $audio = (new Audios())->getByOwnerAndVID($owner_id, $audio_id); if (!$audio) { $this->fail(0o404, "Not Found"); } elseif (!$audio->canBeModifiedBy($this->getUser())) { $this->fail(201, "Insufficient permissions to edit this audio"); } if (!is_null($genre_id)) { $genre = array_flip(AEntity::vkGenres)[$genre_id] ?? null; if (!$genre) { $this->fail(8, "Invalid genre ID $genre_id"); } $audio->setGenre($genre); } elseif (!is_null($genre_str)) { if (!in_array($genre_str, AEntity::genres)) { $this->fail(8, "Invalid genre ID $genre_str"); } $audio->setGenre($genre_str); } $lyrics = 0; if (!is_null($text)) { $audio->setLyrics($text); $lyrics = $audio->getId(); } if (!is_null($artist)) { $audio->setPerformer($artist); } if (!is_null($title)) { $audio->setName($title); } $audio->setSearchability(!((bool) $no_search)); $audio->setEdited(time()); $audio->save(); return $lyrics; } public function add(int $audio_id, int $owner_id, ?int $group_id = null, ?int $album_id = null): string { $this->requireUser(); $this->willExecuteWriteAction(); if (!is_null($album_id)) { $this->fail(10, "album_id not implemented"); } // TODO get rid of dups $to = $this->getUser(); if (!is_null($group_id)) { $group = (new Clubs())->get($group_id); if (!$group) { $this->fail(0o404, "Invalid group_id"); } elseif (!$group->canBeModifiedBy($this->getUser())) { $this->fail(203, "Insufficient rights to this group"); } $to = $group; } $audio = (new Audios())->getByOwnerAndVID($owner_id, $audio_id); if (!$audio) { $this->fail(0o404, "Not found"); } elseif (!$audio->canBeViewedBy($this->getUser())) { $this->fail(201, "Access denied to audio(owner=$owner_id, vid=$audio_id)"); } try { $audio->add($to); } catch (\OverflowException $ex) { $this->fail(300, "Album is full"); } return $audio->getPrettyId(); } public function delete(int $audio_id, int $owner_id, ?int $group_id = null): int { $this->requireUser(); $this->willExecuteWriteAction(); $from = $this->getUser(); if (!is_null($group_id)) { $group = (new Clubs())->get($group_id); if (!$group) { $this->fail(0o404, "Invalid group_id"); } elseif (!$group->canBeModifiedBy($this->getUser())) { $this->fail(203, "Insufficient rights to this group"); } $from = $group; } $audio = (new Audios())->getByOwnerAndVID($owner_id, $audio_id); if (!$audio) { $this->fail(0o404, "Not found"); } $audio->remove($from); return 1; } public function restore(int $audio_id, int $owner_id, ?int $group_id = null, ?string $hash = null): object { $this->requireUser(); $vid = $this->add($audio_id, $owner_id, $group_id); return $this->getById($vid, $hash)->items[0]; } public function getAlbums(int $owner_id = 0, int $offset = 0, int $count = 50, int $drop_private = 1): object { $this->requireUser(); $owner_id = $owner_id == 0 ? $this->getUser()->getId() : $owner_id; $playlists = []; if ($owner_id > 0 && $owner_id != $this->getUser()->getId()) { $user = (new \openvk\Web\Models\Repositories\Users())->get($owner_id); if (!$user->getPrivacyPermission("audios.read", $this->getUser())) { $this->fail(50, "Access to playlists denied"); } } foreach ((new Audios())->getPlaylistsByEntityId($owner_id, $offset, $count) as $playlist) { if (!$playlist->canBeViewedBy($this->getUser())) { if ($drop_private == 1) { continue; } $playlists[] = null; continue; } $playlists[] = $playlist->toVkApiStruct($this->getUser()); } return (object) [ "count" => sizeof($playlists), "items" => $playlists, ]; } public function searchAlbums(string $query = '', int $offset = 0, int $limit = 25, int $drop_private = 0, int $order = 0, int $from_me = 0): object { $this->requireUser(); $playlists = []; $params = []; $order_str = (['id', 'length', 'listens'][$order] ?? 'id'); if ($from_me === 1) { $params['from_me'] = $this->getUser()->getId(); } $search = (new Audios())->findPlaylists($query, $params, ['type' => $order_str, 'invert' => false]); foreach ($search->offsetLimit($offset, $limit) as $playlist) { if (!$playlist->canBeViewedBy($this->getUser())) { if ($drop_private == 0) { $playlists[] = null; } continue; } $playlists[] = $playlist->toVkApiStruct($this->getUser()); } return (object) [ "count" => $search->size(), "items" => $playlists, ]; } public function addAlbum(string $title, ?string $description = null, int $group_id = 0): int { $this->requireUser(); $this->willExecuteWriteAction(); $group = null; if ($group_id != 0) { $group = (new Clubs())->get($group_id); if (!$group) { $this->fail(0o404, "Invalid group_id"); } elseif (!$group->canBeModifiedBy($this->getUser())) { $this->fail(600, "Insufficient rights to this group"); } } $album = new Playlist(); $album->setName($title); if (!is_null($group)) { $album->setOwner($group_id * -1); } else { $album->setOwner($this->getUser()->getId()); } if (!is_null($description)) { $album->setDescription($description); } $album->save(); if (!is_null($group)) { $album->bookmark($group); } else { $album->bookmark($this->getUser()); } return $album->getId(); } public function editAlbum(int $album_id, ?string $title = null, ?string $description = null): int { $this->requireUser(); $this->willExecuteWriteAction(); $album = (new Audios())->getPlaylist($album_id); if (!$album) { $this->fail(0o404, "Album not found"); } elseif (!$album->canBeModifiedBy($this->getUser())) { $this->fail(600, "Insufficient rights to this album"); } if (!is_null($title)) { $album->setName($title); } if (!is_null($description)) { $album->setDescription($description); } $album->setEdited(time()); $album->save(); return (int) !(!$title && !$description); } public function deleteAlbum(int $album_id): int { $this->requireUser(); $this->willExecuteWriteAction(); $album = (new Audios())->getPlaylist($album_id); if (!$album) { $this->fail(0o404, "Album not found"); } elseif (!$album->canBeModifiedBy($this->getUser())) { $this->fail(600, "Insufficient rights to this album"); } $album->delete(); return 1; } public function moveToAlbum(int $album_id, string $audio_ids): int { $this->requireUser(); $this->willExecuteWriteAction(); $album = (new Audios())->getPlaylist($album_id); if (!$album) { $this->fail(0o404, "Album not found"); } elseif (!$album->canBeModifiedBy($this->getUser())) { $this->fail(600, "Insufficient rights to this album"); } $audios = []; $audio_ids = array_unique(explode(",", $audio_ids)); if (sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) { $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); } foreach ($audio_ids as $audio_id) { $audio = $this->audioFromAnyId($audio_id); if (!$audio) { continue; } elseif (!$audio->canBeViewedBy($this->getUser())) { continue; } $audios[] = $audio; } if (sizeof($audios) < 1) { return 0; } $res = 1; try { foreach ($audios as $audio) { $res = min($res, (int) $album->add($audio)); } } catch (\OutOfBoundsException $ex) { return 0; } return $res; } public function removeFromAlbum(int $album_id, string $audio_ids): int { $this->requireUser(); $this->willExecuteWriteAction(); $album = (new Audios())->getPlaylist($album_id); if (!$album) { $this->fail(0o404, "Album not found"); } elseif (!$album->canBeModifiedBy($this->getUser())) { $this->fail(600, "Insufficient rights to this album"); } $audios = []; $audio_ids = array_unique(explode(",", $audio_ids)); if (sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) { $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); } foreach ($audio_ids as $audio_id) { $audio = $this->audioFromAnyId($audio_id); if (!$audio) { continue; } elseif ($audio->canBeViewedBy($this->getUser())) { continue; } $audios[] = $audio; } if (sizeof($audios) < 1) { return 0; } foreach ($audios as $audio) { $album->remove($audio); } return 1; } public function copyToAlbum(int $album_id, string $audio_ids): int { return $this->moveToAlbum($album_id, $audio_ids); } public function bookmarkAlbum(int $id): int { $this->requireUser(); $this->willExecuteWriteAction(); $album = (new Audios())->getPlaylist($id); if (!$album) { $this->fail(0o404, "Not found"); } if (!$album->canBeViewedBy($this->getUser())) { $this->fail(600, "Access error"); } return (int) $album->bookmark($this->getUser()); } public function unBookmarkAlbum(int $id): int { $this->requireUser(); $this->willExecuteWriteAction(); $album = (new Audios())->getPlaylist($id); if (!$album) { $this->fail(0o404, "Not found"); } if (!$album->canBeViewedBy($this->getUser())) { $this->fail(600, "Access error"); } return (int) $album->unbookmark($this->getUser()); } }