mirror of
https://github.com/openvk/openvk
synced 2025-04-24 00:53:00 +03:00
Draft some music API methods
This commit is contained in:
parent
b3a00c3813
commit
1e453c7fbb
4 changed files with 673 additions and 33 deletions
|
@ -1,22 +1,491 @@
|
||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
namespace openvk\VKAPI\Handlers;
|
namespace openvk\VKAPI\Handlers;
|
||||||
|
use Chandler\Database\DatabaseConnection;
|
||||||
|
use openvk\Web\Models\Entities\Audio as AEntity;
|
||||||
|
use openvk\Web\Models\Repositories\Audios;
|
||||||
|
use openvk\Web\Models\Repositories\Clubs;
|
||||||
|
use openvk\Web\Models\Repositories\Util\EntityStream;
|
||||||
|
|
||||||
final class Audio extends VKAPIRequestHandler
|
final class Audio extends VKAPIRequestHandler
|
||||||
{
|
{
|
||||||
function get(): object
|
private function toSafeAudioStruct(?AEntity $audio, ?string $hash = NULL, bool $need_user = false): object
|
||||||
{
|
{
|
||||||
$serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"];
|
if(!$audio || !$audio->canBeViewedBy($this->getUser()))
|
||||||
|
$this->fail(201, "Access denied to audio(" . $audio->getPrettyId() . ")");
|
||||||
|
|
||||||
return (object) [
|
# рофлан ебало
|
||||||
"count" => 1,
|
$privApi = $hash && $GLOBALS["csrfCheck"];
|
||||||
"items" => [(object) [
|
$audioObj = $audio->toVkApiStruct($this->getUser());
|
||||||
"id" => 1,
|
if(!$privApi) {
|
||||||
"owner_id" => 1,
|
$audioObj->manifest = false;
|
||||||
"artist" => "В ОВК ПОКА НЕТ МУЗЫКИ",
|
$audioObj->keys = false;
|
||||||
"title" => "ЖДИТЕ :)))",
|
}
|
||||||
"duration" => 22,
|
|
||||||
"url" => $serverUrl . "/assets/packages/static/openvk/audio/nomusic.mp3"
|
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");
|
||||||
|
} else if(!is_null($genre_id)) {
|
||||||
|
$genre_str = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL;
|
||||||
|
if(!$genre_str)
|
||||||
|
$this->fail(8, "Invalid genre ID $genre_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getById(string $audios, ?string $hash = NULL, int $need_user = 0): object
|
||||||
|
{
|
||||||
|
$this->requireUser();
|
||||||
|
|
||||||
|
$audioIds = array_unique(explode(",", $audios));
|
||||||
|
if(sizeof($audioIds) === 1) {
|
||||||
|
$descriptor = explode("_", $audioIds[0]);
|
||||||
|
if(sizeof($descriptor) === 1)
|
||||||
|
$audio = (new Audios)->get((int) $descriptor[0]);
|
||||||
|
else if(sizeof($descriptor) === 2)
|
||||||
|
$audio = (new Audios)->getByOwnerAndVID((int) $descriptor[0], (int) $descriptor[1]);
|
||||||
|
else
|
||||||
|
$this->fail(8, "Invalid audio $descriptor");
|
||||||
|
|
||||||
|
return (object) [
|
||||||
|
"count" => 1,
|
||||||
|
"items" => [
|
||||||
|
$this->toSafeAudioStruct($audio, $hash, (bool) $need_user),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
} else if(sizeof($audioIds) > 32) {
|
||||||
|
$this->fail(1980, "Can't get more than 32 audios at once");
|
||||||
|
}
|
||||||
|
|
||||||
|
$audios = [];
|
||||||
|
foreach($audioIds as $id)
|
||||||
|
$audios[] = $this->getById($id, $hash)->items[0];
|
||||||
|
|
||||||
|
return (object) [
|
||||||
|
"count" => sizeof($audios),
|
||||||
|
"items" => $audios,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO stub
|
||||||
|
function getRecommendations(): object
|
||||||
|
{
|
||||||
|
return (object) [
|
||||||
|
"count" => 0,
|
||||||
|
"items" => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
else if($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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCount(int $owner_id, int $uploaded_only = 0): int
|
||||||
|
{
|
||||||
|
if($owner_id < 0) {
|
||||||
|
$owner_id *= -1;
|
||||||
|
$group = (new Clubs)->get($owner_id);
|
||||||
|
if(!$group)
|
||||||
|
$this->fail(0404, "Group not found");
|
||||||
|
|
||||||
|
return (new Audios)->getClubCollectionSize($group);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id);
|
||||||
|
if(!$user)
|
||||||
|
$this->fail(0404, "User not found");
|
||||||
|
|
||||||
|
if($uploaded_only) {
|
||||||
|
return DatabaseConnection::i()->getContext()->table("audios")
|
||||||
|
->where([
|
||||||
|
"deleted" => false,
|
||||||
|
"owner" => $owner_id,
|
||||||
|
])->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (new Audios)->getUserCollectionSize($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(int $owner_id = 0, int $album_id = 0, ?string $audio_ids = NULL, 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();
|
||||||
|
|
||||||
|
if($album_id != 0)
|
||||||
|
$this->fail(10, "album_id is not supported");
|
||||||
|
|
||||||
|
$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(!is_null($audio_ids)) {
|
||||||
|
$audio_ids = explode(",", $audio_ids);
|
||||||
|
if(!$audio_ids)
|
||||||
|
$this->fail(10, "Audio::get@L0d186:explode(string): Unknown error");
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
$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,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLyrics(int $lyrics_id): object
|
||||||
|
{
|
||||||
|
$this->requireUser();
|
||||||
|
|
||||||
|
$audio = (new Audios)->get($lyrics_id);
|
||||||
|
if(!$audio || !$audio->getLyrics())
|
||||||
|
$this->fail(0404, "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()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function beacon(int $aid, ?int $gid = NULL): int
|
||||||
|
{
|
||||||
|
$this->requireUser();
|
||||||
|
|
||||||
|
$audio = (new Audios)->get($aid);
|
||||||
|
if(!$audio)
|
||||||
|
$this->fail(0404, "Not Found");
|
||||||
|
else if(!$audio->canBeViewedBy($this->getUser()))
|
||||||
|
$this->fail(201, "Insufficient permissions to listen this audio");
|
||||||
|
|
||||||
|
$group = NULL;
|
||||||
|
if(!is_null($group)) {
|
||||||
|
$group = (new Clubs)->get($gid);
|
||||||
|
if(!$group)
|
||||||
|
$this->fail(0404, "Not Found");
|
||||||
|
else if(!$group->canBeModifiedBy($this->getUser()))
|
||||||
|
$this->fail(203, "Insufficient rights to this group");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $audio->listen($group ?? $this->getUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBroadcast(string $audio, string $target_ids): array
|
||||||
|
{
|
||||||
|
$this->requireUser();
|
||||||
|
|
||||||
|
[$owner, $aid] = explode("_", $audio);
|
||||||
|
$song = (new Audios)->getByOwnerAndVID($owner, $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(0404, "Not Found");
|
||||||
|
else if(!$group->canBeModifiedBy($this->getUser()))
|
||||||
|
$this->fail(203, "Insufficient rights to this group");
|
||||||
|
|
||||||
|
$ids[] = $id;
|
||||||
|
$this->beacon($song->getId(), $id * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
$dbContext = DatabaseConnection::i()->getContext();
|
||||||
|
$entityIds = [];
|
||||||
|
$query = $dbContext->table("subscriptions")->select("model, target")
|
||||||
|
->where("follower", $this->getUser()->getId());
|
||||||
|
|
||||||
|
if($filter != "all")
|
||||||
|
$query = $query->where("model = ?", "openvk\\Web\\Models\\Entities\\" . ($filter == "groups" ? "Club" : "User"));
|
||||||
|
|
||||||
|
foreach($query as $_rel) {
|
||||||
|
$id = $_rel->target;
|
||||||
|
if($_rel->model === "openvk\\Web\\Models\\Entities\\Club")
|
||||||
|
$id *= -1;
|
||||||
|
|
||||||
|
$entityIds[] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$audioIds = [];
|
||||||
|
$threshold = $active === 0 ? 3600 : 120;
|
||||||
|
foreach($entityIds as $ent) {
|
||||||
|
$lastListen = $dbContext->table("audio_listens")->where("entity", $ent)
|
||||||
|
->where("time >= ?", time() - $threshold)->fetch();
|
||||||
|
if(!$lastListen)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$audio = (new Audios)->get($lastListen->audio);
|
||||||
|
$audioIds[$ent] = $this->toSafeAudioStruct($audio, $hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
foreach($audioIds as $ent => $audio) {
|
||||||
|
$entity = ($ent < 0 ? (new Groups($this->getUser())) : (new Users($this->getUser())))
|
||||||
|
->get((string) abs($ent));
|
||||||
|
|
||||||
|
$entity->status_audio = $audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (object) [
|
||||||
|
"count" => sizeof($items),
|
||||||
|
"items" => $items,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
$audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id);
|
||||||
|
if(!$audio)
|
||||||
|
$this->fail(0404, "Not Found");
|
||||||
|
else if(!$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);
|
||||||
|
} else if(!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->save();
|
||||||
|
|
||||||
|
return $lyrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(int $audio_id, int $owner_id, ?int $group_id = NULL, ?int $album_id = NULL): string
|
||||||
|
{
|
||||||
|
$this->requireUser();
|
||||||
|
|
||||||
|
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(0404, "Invalid group_id");
|
||||||
|
else if(!$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(0404, "Not found");
|
||||||
|
else if(!$audio->canBeViewedBy($this->getUser()))
|
||||||
|
$this->fail(201, "Access denied to audio(owner=$owner_id, vid=$audio_id)");
|
||||||
|
|
||||||
|
$audio->add($to);
|
||||||
|
|
||||||
|
return $audio->getPrettyId();
|
||||||
|
}
|
||||||
|
|
||||||
|
function delete(int $audio_id, int $owner_id, ?int $group_id = NULL): int
|
||||||
|
{
|
||||||
|
$this->requireUser();
|
||||||
|
|
||||||
|
$from = $this->getUser();
|
||||||
|
if(!is_null($group_id)) {
|
||||||
|
$group = (new Clubs)->get($group_id);
|
||||||
|
if(!$group)
|
||||||
|
$this->fail(0404, "Invalid group_id");
|
||||||
|
else if(!$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(0404, "Not found");
|
||||||
|
|
||||||
|
$audio->remove($from);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,35 @@ class Audio extends Media
|
||||||
'Blues','Big Band','Classic Rock','Chorus','Country','Easy Listening','Dance','Acoustic','Disco','Humour','Funk','Speech','Grunge','Chanson','Hip-Hop','Opera','Jazz','Chamber Music','Metal','Sonata','New Age','Symphony','Oldies','Booty Bass','Other','Primus','Pop','Porn Groove','R&B','Satire','Rap','Slow Jam','Reggae','Club','Rock','Tango','Techno','Samba','Industrial','Folklore','Alternative','Ballad','Ska','Power Ballad','Death Metal','Rhythmic Soul','Pranks','Freestyle','Soundtrack','Duet','Euro-Techno','Punk Rock','Ambient','Drum Solo','Trip-Hop','A Cappella','Vocal','Euro-House','Jazz+Funk','Dance Hall','Fusion','Goa','Trance','Drum & Bass','Classical','Club-House','Instrumental','Hardcore','Acid','Terror','House','Indie','Game','BritPop','Sound Clip','Negerpunk','Gospel','Polsk Punk','Noise','Beat','AlternRock','Christian Gangsta Rap','Bass','Heavy Metal','Soul','Black Metal','Punk','Crossover','Space','Contemporary Christian','Meditative','Christian Rock','Instrumental Pop','Merengue','Instrumental Rock','Salsa','Ethnic','Thrash Metal','Gothic','Anime','Darkwave','JPop','Techno-Industrial','Synthpop','Electronic','Abstract','Pop-Folk','Art Rock','Eurodance','Baroque','Dream','Bhangra','Southern Rock','Big Beat','Comedy','Breakbeat','Cult','Chillout','Gangsta Rap','Downtempo','Top 40','Dub','Christian Rap','EBM','Pop / Funk','Eclectic','Jungle','Electro','Native American','Electroclash','Cabaret','Emo','New Wave','Experimental','Psychedelic','Garage','Rave','Global','Showtunes','IDM','Trailer','Illbient','Lo-Fi','Industro-Goth','Tribal','Jam Band','Acid Punk','Krautrock','Acid Jazz','Leftfield','Polka','Lounge','Retro','Math Rock','Musical','New Romantic','Rock & Roll','Nu-Breakz','Hard Rock','Post-Punk','Folk','Post-Rock','Folk-Rock','Psytrance','National Folk','Shoegaze','Swing','Space Rock','Fast Fusion','Trop Rock','Bebob','World Music','Latin','Neoclassical','Revival','Audiobook','Celtic','Audio Theatre','Bluegrass','Neue Deutsche Welle','Avantgarde','Podcast','Gothic Rock','Indie Rock','Progressive Rock','G-Funk','Psychedelic Rock','Dubstep','Symphonic Rock','Garage Rock','Slow Rock','Psybient','Psychobilly','Touhou'
|
'Blues','Big Band','Classic Rock','Chorus','Country','Easy Listening','Dance','Acoustic','Disco','Humour','Funk','Speech','Grunge','Chanson','Hip-Hop','Opera','Jazz','Chamber Music','Metal','Sonata','New Age','Symphony','Oldies','Booty Bass','Other','Primus','Pop','Porn Groove','R&B','Satire','Rap','Slow Jam','Reggae','Club','Rock','Tango','Techno','Samba','Industrial','Folklore','Alternative','Ballad','Ska','Power Ballad','Death Metal','Rhythmic Soul','Pranks','Freestyle','Soundtrack','Duet','Euro-Techno','Punk Rock','Ambient','Drum Solo','Trip-Hop','A Cappella','Vocal','Euro-House','Jazz+Funk','Dance Hall','Fusion','Goa','Trance','Drum & Bass','Classical','Club-House','Instrumental','Hardcore','Acid','Terror','House','Indie','Game','BritPop','Sound Clip','Negerpunk','Gospel','Polsk Punk','Noise','Beat','AlternRock','Christian Gangsta Rap','Bass','Heavy Metal','Soul','Black Metal','Punk','Crossover','Space','Contemporary Christian','Meditative','Christian Rock','Instrumental Pop','Merengue','Instrumental Rock','Salsa','Ethnic','Thrash Metal','Gothic','Anime','Darkwave','JPop','Techno-Industrial','Synthpop','Electronic','Abstract','Pop-Folk','Art Rock','Eurodance','Baroque','Dream','Bhangra','Southern Rock','Big Beat','Comedy','Breakbeat','Cult','Chillout','Gangsta Rap','Downtempo','Top 40','Dub','Christian Rap','EBM','Pop / Funk','Eclectic','Jungle','Electro','Native American','Electroclash','Cabaret','Emo','New Wave','Experimental','Psychedelic','Garage','Rave','Global','Showtunes','IDM','Trailer','Illbient','Lo-Fi','Industro-Goth','Tribal','Jam Band','Acid Punk','Krautrock','Acid Jazz','Leftfield','Polka','Lounge','Retro','Math Rock','Musical','New Romantic','Rock & Roll','Nu-Breakz','Hard Rock','Post-Punk','Folk','Post-Rock','Folk-Rock','Psytrance','National Folk','Shoegaze','Swing','Space Rock','Fast Fusion','Trop Rock','Bebob','World Music','Latin','Neoclassical','Revival','Audiobook','Celtic','Audio Theatre','Bluegrass','Neue Deutsche Welle','Avantgarde','Podcast','Gothic Rock','Indie Rock','Progressive Rock','G-Funk','Psychedelic Rock','Dubstep','Symphonic Rock','Garage Rock','Slow Rock','Psybient','Psychobilly','Touhou'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Taken from: https://web.archive.org/web/20220322153107/https://dev.vk.com/reference/objects/audio-genres
|
||||||
|
const vkGenres = [
|
||||||
|
"Rock" => 1,
|
||||||
|
"Pop" => 2,
|
||||||
|
"Rap" => 3,
|
||||||
|
"Hip-Hop" => 3, # VK API lists №3 as Rap & Hip-Hop, but these genres are distinct in OpenVK
|
||||||
|
"Easy Listening" => 4,
|
||||||
|
"House" => 5,
|
||||||
|
"Dance" => 5,
|
||||||
|
"Instrumental" => 6,
|
||||||
|
"Metal" => 7,
|
||||||
|
"Alternative" => 21,
|
||||||
|
"Dubstep" => 8,
|
||||||
|
"Jazz" => 1001,
|
||||||
|
"Blues" => 1001,
|
||||||
|
"Drum & Bass" => 10,
|
||||||
|
"Trance" => 11,
|
||||||
|
"Chanson" => 12,
|
||||||
|
"Ethnic" => 13,
|
||||||
|
"Acoustic" => 14,
|
||||||
|
"Vocal" => 14,
|
||||||
|
"Reggae" => 15,
|
||||||
|
"Classical" => 16,
|
||||||
|
"Indie Pop" => 17,
|
||||||
|
"Speech" => 19,
|
||||||
|
"Disco" => 22,
|
||||||
|
"Other" => 18,
|
||||||
|
];
|
||||||
|
|
||||||
private function fileLength(string $filename): int
|
private function fileLength(string $filename): int
|
||||||
{
|
{
|
||||||
if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe"))
|
if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe"))
|
||||||
|
@ -118,6 +147,11 @@ class Audio extends Media
|
||||||
return $this->getTitle() . " - " . $this->getPerformer();
|
return $this->getTitle() . " - " . $this->getPerformer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getGenre(): ?string
|
||||||
|
{
|
||||||
|
return $this->getRecord()->genre;
|
||||||
|
}
|
||||||
|
|
||||||
function getLyrics(): ?string
|
function getLyrics(): ?string
|
||||||
{
|
{
|
||||||
return $this->getRecord()->lyrics ?? NULL;
|
return $this->getRecord()->lyrics ?? NULL;
|
||||||
|
@ -153,17 +187,16 @@ class Audio extends Media
|
||||||
|
|
||||||
function getOriginalURL(bool $force = false): string
|
function getOriginalURL(bool $force = false): string
|
||||||
{
|
{
|
||||||
$disallowed = OPENVK_ROOT_CONF["openvk"]["preferences"]["music"]["exposeOriginalURLs"] && !$force;
|
$disallowed = !OPENVK_ROOT_CONF["openvk"]["preferences"]["music"]["exposeOriginalURLs"] && !$force;
|
||||||
if(!$this->isAvailable() || $disallowed)
|
if(!$this->isAvailable() || $disallowed)
|
||||||
return ovk_scheme(true)
|
return ovk_scheme(true)
|
||||||
. $_SERVER["HTTP_HOST"] . ":"
|
. $_SERVER["HTTP_HOST"] . ":"
|
||||||
. $_SERVER["HTTP_PORT"]
|
. $_SERVER["HTTP_PORT"]
|
||||||
. "/assets/packages/static/openvk/audio/nomusic.mp3";
|
. "/assets/packages/static/openvk/audio/nomusic.mp3";
|
||||||
|
|
||||||
$key = bin2hex($this->getRecord()->token);
|
$key = bin2hex($this->getRecord()->token);
|
||||||
$garbage = sha1((string) time());
|
|
||||||
|
|
||||||
return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3?tk=$garbage";
|
return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKeys(): array
|
function getKeys(): array
|
||||||
|
@ -173,6 +206,11 @@ class Audio extends Media
|
||||||
return $keys;
|
return $keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAnonymous(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function isExplicit(): bool
|
function isExplicit(): bool
|
||||||
{
|
{
|
||||||
return (bool) $this->getRecord()->explicit;
|
return (bool) $this->getRecord()->explicit;
|
||||||
|
@ -249,21 +287,29 @@ class Audio extends Media
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function listen(User $user): bool
|
function listen($entity): bool
|
||||||
{
|
{
|
||||||
|
$entityId = $entity->getId();
|
||||||
|
if($entity instanceof Club)
|
||||||
|
$entityId *= -1;
|
||||||
|
|
||||||
$listensTable = DatabaseConnection::i()->getContext()->table("audio_listens");
|
$listensTable = DatabaseConnection::i()->getContext()->table("audio_listens");
|
||||||
$lastListen = $listensTable->where([
|
$lastListen = $listensTable->where([
|
||||||
"user" => $user->getId(),
|
"entity" => $entityId,
|
||||||
"audio" => $this->getId(),
|
"audio" => $this->getId(),
|
||||||
])->fetch();
|
])->fetch();
|
||||||
|
|
||||||
if(!$lastListen || (time() - $lastListen->time >= 900)) {
|
if(!$lastListen || (time() - $lastListen->time >= $this->getLength())) {
|
||||||
$listensTable->insert([
|
$listensTable->insert([
|
||||||
"user" => $user->getId(),
|
"entity" => $entityId,
|
||||||
"audio" => $this->getId(),
|
"audio" => $this->getId(),
|
||||||
"time" => time(),
|
"time" => time(),
|
||||||
]);
|
]);
|
||||||
$this->stateChanges("listens", $this->getListens() + 1);
|
|
||||||
|
if($entity instanceof User) {
|
||||||
|
$this->stateChanges("listens", $this->getListens() + 1);
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -275,10 +321,84 @@ class Audio extends Media
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns compatible with VK API 4.x, 5.x structure.
|
||||||
|
*
|
||||||
|
* Always sets album(_id) to NULL at this time.
|
||||||
|
* If genre is not present in VK genre list, fallbacks to "Other".
|
||||||
|
* The url and manifest properties will be set to false if the audio can't be played (processing, removed).
|
||||||
|
*
|
||||||
|
* Aside from standard VK properties, this method will also return some OVK extended props:
|
||||||
|
* 1. added - Is in the library of $user?
|
||||||
|
* 2. editable - Can be edited by $user?
|
||||||
|
* 3. withdrawn - Removed due to copyright request?
|
||||||
|
* 4. ready - Can be played at this time?
|
||||||
|
* 5. genre_str - Full name of genre, NULL if it's undefined
|
||||||
|
* 6. manifest - URL to MPEG-DASH manifest
|
||||||
|
* 7. keys - ClearKey DRM keys
|
||||||
|
* 8. explicit - Marked as NSFW?
|
||||||
|
* 9. searchable - Can be found via search?
|
||||||
|
* 10. unique_id - Unique ID of audio
|
||||||
|
*
|
||||||
|
* @notice that in case if exposeOriginalURLs is set to false in config, "url" will always contain link to nomusic.mp3,
|
||||||
|
* unless $forceURLExposure is set to true.
|
||||||
|
*
|
||||||
|
* @notice may trigger db flush if the audio is not processed yet, use with caution on unsaved models.
|
||||||
|
*
|
||||||
|
* @param ?User $user user, relative to whom "added", "editable" will be set
|
||||||
|
* @param bool $forceURLExposure force set "url" regardless of config
|
||||||
|
*/
|
||||||
|
function toVkApiStruct(?User $user = NULL, bool $forceURLExposure = false): object
|
||||||
|
{
|
||||||
|
$obj = (object) [];
|
||||||
|
$obj->unique_id = base64_encode((string) $this->getId());
|
||||||
|
$obj->id = $obj->aid = $this->getVirtualId();
|
||||||
|
$obj->artist = $this->getPerformer();
|
||||||
|
$obj->title = $this->getTitle();
|
||||||
|
$obj->duration = $this->getLength();
|
||||||
|
$obj->album_id = $obj->album = NULL; # i forgor to implement
|
||||||
|
$obj->url = false;
|
||||||
|
$obj->manifest = false;
|
||||||
|
$obj->keys = false;
|
||||||
|
$obj->genre_id = $obj->genre = self::vkGenres[$this->getGenre() ?? ""] ?? 18; # return Other if no match
|
||||||
|
$obj->genre_str = $this->getGenre();
|
||||||
|
$obj->owner_id = $this->getOwner()->getId();
|
||||||
|
if($this->getOwner() instanceof Club)
|
||||||
|
$obj->owner_id *= -1;
|
||||||
|
|
||||||
|
$obj->lyrics = NULL;
|
||||||
|
if(!is_null($this->getLyrics()))
|
||||||
|
$obj->lyrics = $this->getId();
|
||||||
|
|
||||||
|
$obj->added = $user && $this->isInLibraryOf($user);
|
||||||
|
$obj->editable = $user && $this->canBeModifiedBy($user);
|
||||||
|
$obj->searchable = !$this->isUnlisted();
|
||||||
|
$obj->explicit = $this->isExplicit();
|
||||||
|
$obj->withdrawn = $this->isWithdrawn();
|
||||||
|
$obj->ready = $this->isAvailable() && !$obj->withdrawn;
|
||||||
|
if($obj->ready) {
|
||||||
|
$obj->url = $this->getOriginalURL($forceURLExposure);
|
||||||
|
$obj->manifest = $this->getURL();
|
||||||
|
$obj->keys = $this->getKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOwner(int $oid): void
|
||||||
|
{
|
||||||
|
# WARNING: API implementation won't be able to handle groups like that, don't remove
|
||||||
|
if($oid <= 0)
|
||||||
|
throw new \OutOfRangeException("Only users can be owners of audio!");
|
||||||
|
|
||||||
|
$this->stateChanges("owner", $oid);
|
||||||
|
}
|
||||||
|
|
||||||
function setGenre(string $genre): void
|
function setGenre(string $genre): void
|
||||||
{
|
{
|
||||||
if(!in_array($genre, Audio::genres)) {
|
if(!in_array($genre, Audio::genres)) {
|
||||||
$this->stateChanges("genre", NULL);
|
$this->stateChanges("genre", NULL);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->stateChanges("genre", $genre);
|
$this->stateChanges("genre", $genre);
|
||||||
|
@ -311,4 +431,15 @@ class Audio extends Media
|
||||||
function setSegment_Size(int $len): void {
|
function setSegment_Size(int $len): void {
|
||||||
throw new \LogicException("Changing length is not supported.");
|
throw new \LogicException("Changing length is not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function delete(bool $softly = true): void
|
||||||
|
{
|
||||||
|
$ctx = DatabaseConnection::i()->getContext();
|
||||||
|
$ctx->table("audio_relations")->where("audio", $this->getId())
|
||||||
|
->delete();
|
||||||
|
$ctx->table("audio_listens")->where("audio", $this->getId())
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
parent::delete($softly);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -15,6 +15,10 @@ class Audios
|
||||||
const ORDER_NEW = 0;
|
const ORDER_NEW = 0;
|
||||||
const ORDER_POPULAR = 1;
|
const ORDER_POPULAR = 1;
|
||||||
|
|
||||||
|
const VK_ORDER_NEW = 0;
|
||||||
|
const VK_ORDER_LENGTH = 1;
|
||||||
|
const VK_ORDER_POPULAR = 2;
|
||||||
|
|
||||||
function __construct()
|
function __construct()
|
||||||
{
|
{
|
||||||
$this->context = DatabaseConnection::i()->getContext();
|
$this->context = DatabaseConnection::i()->getContext();
|
||||||
|
@ -42,10 +46,10 @@ class Audios
|
||||||
return new Audio($audio);
|
return new Audio($audio);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getByEntityID(int $entity, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
|
function getByEntityID(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable
|
||||||
{
|
{
|
||||||
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
|
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
|
||||||
$iter = $this->rels->where("entity", $entity)->page($page, $perPage);
|
$iter = $this->rels->where("entity", $entity)->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset);
|
||||||
foreach($iter as $rel) {
|
foreach($iter as $rel) {
|
||||||
$audio = $this->get($rel->audio);
|
$audio = $this->get($rel->audio);
|
||||||
if(!$audio || $audio->isDeleted()) {
|
if(!$audio || $audio->isDeleted()) {
|
||||||
|
@ -59,12 +63,12 @@ class Audios
|
||||||
|
|
||||||
function getByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
|
function getByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
|
||||||
{
|
{
|
||||||
return $this->getByEntityID($user->getId(), $page, $perPage, $deleted);
|
return $this->getByEntityID($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
|
function getByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
|
||||||
{
|
{
|
||||||
return $this->getByEntityID($club->getId() * -1, $page, $perPage, $deleted);
|
return $this->getByEntityID($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserCollectionSize(User $user): int
|
function getUserCollectionSize(User $user): int
|
||||||
|
@ -87,7 +91,7 @@ class Audios
|
||||||
return new EntityStream("Audio", $search);
|
return new EntityStream("Audio", $search);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGlobal(int $order): EntityStream
|
function getGlobal(int $order, ?string $genreId = NULL): EntityStream
|
||||||
{
|
{
|
||||||
$search = $this->audios->where([
|
$search = $this->audios->where([
|
||||||
"deleted" => 0,
|
"deleted" => 0,
|
||||||
|
@ -95,15 +99,24 @@ class Audios
|
||||||
"withdrawn" => 0,
|
"withdrawn" => 0,
|
||||||
])->order($order == Audios::ORDER_NEW ? "created DESC" : "listens DESC");
|
])->order($order == Audios::ORDER_NEW ? "created DESC" : "listens DESC");
|
||||||
|
|
||||||
|
if(!is_null($genreId))
|
||||||
|
$search = $search->where("genre", $genreId);
|
||||||
|
|
||||||
return new EntityStream("Audio", $search);
|
return new EntityStream("Audio", $search);
|
||||||
}
|
}
|
||||||
|
|
||||||
function search(string $query): EntityStream
|
function search(string $query, int $sortMode = 0, bool $performerOnly = false, bool $withLyrics = false): EntityStream
|
||||||
{
|
{
|
||||||
|
$columns = $performerOnly ? "performer" : "performer, name";
|
||||||
|
$order = (["created", "length", "listens"][$sortMode] ?? "") . "DESC";
|
||||||
|
|
||||||
$search = $this->audios->where([
|
$search = $this->audios->where([
|
||||||
"unlisted" => 0,
|
"unlisted" => 0,
|
||||||
"deleted" => 0,
|
"deleted" => 0,
|
||||||
])->where("MATCH (performer, name) AGAINST (? WITH QUERY EXPANSION)", $query);
|
])->where("MATCH ($columns) AGAINST (? WITH QUERY EXPANSION)", $query)->order($order);
|
||||||
|
|
||||||
|
if($withLyrics)
|
||||||
|
$search = $search->where("lyrics IS NOT NULL");
|
||||||
|
|
||||||
return new EntityStream("Audio", $search);
|
return new EntityStream("Audio", $search);
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,33 @@ function ovk_proc_strtr(string $string, int $length = 0): string
|
||||||
return $newString . ($string !== $newString ? "…" : ""); #if cut hasn't happened, don't append "..."
|
return $newString . ($string !== $newString ? "…" : ""); #if cut hasn't happened, don't append "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function knuth_shuffle(Traversable $arr, int $seed): array
|
||||||
|
{
|
||||||
|
$data = is_array($arr) ? $arr : iterator_to_array($arr);
|
||||||
|
$retVal = [];
|
||||||
|
$ind = [];
|
||||||
|
$count = sizeof($data);
|
||||||
|
|
||||||
|
srand($seed, MT_RAND_PHP);
|
||||||
|
|
||||||
|
for($i = 0; $i < $count; ++$i)
|
||||||
|
$ind[$i] = 0;
|
||||||
|
|
||||||
|
for($i = 0; $i < $count; ++$i) {
|
||||||
|
do {
|
||||||
|
$index = rand() % $count;
|
||||||
|
} while($ind[$index] != 0);
|
||||||
|
|
||||||
|
$ind[$index] = 1;
|
||||||
|
$retVal[$i] = $data[$index];
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reseed
|
||||||
|
srand(hexdec(bin2hex(openssl_random_pseudo_bytes(4))));
|
||||||
|
|
||||||
|
return $retVal;
|
||||||
|
}
|
||||||
|
|
||||||
function bmask(int $input, array $options = []): Bitmask
|
function bmask(int $input, array $options = []): Bitmask
|
||||||
{
|
{
|
||||||
return new Bitmask($input, $options["length"] ?? 1, $options["mappings"] ?? []);
|
return new Bitmask($input, $options["length"] ?? 1, $options["mappings"] ?? []);
|
||||||
|
|
Loading…
Reference in a new issue