Merge branch 'master' into newapiz

This commit is contained in:
lalka2018 2023-11-14 19:30:12 +03:00 committed by GitHub
commit 21dee75411
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
107 changed files with 9975 additions and 548 deletions

View file

@ -30,7 +30,7 @@ If you want, you can add your instance to the list above so that people can regi
1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler)
* PHP 8.1 is supported too, however it was not tested carefully, so be aware.
* PHP 8 is still being tested; the functionality of the engine on this version of PHP is not yet guaranteed.
2. Install MySQL-compatible database.

View file

@ -30,7 +30,7 @@ _[English](README.md)_
1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler)
* PHP 8 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать (UPD: он не работает).
* PHP 8 пока ещё тестируется, работоспособность движка на этой версии PHP пока не гарантируется.
2. Установите MySQL-совместимую базу данных.

View file

@ -1,22 +1,788 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\Audio as AEntity;
use openvk\Web\Models\Entities\Playlist;
use openvk\Web\Models\Repositories\Audios;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Repositories\Util\EntityStream;
final class Audio extends VKAPIRequestHandler
{
function get(): object
private function toSafeAudioStruct(?AEntity $audio, ?string $hash = NULL, bool $need_user = false): object
{
if(!$audio)
$this->fail(0404, "Audio not found");
else if(!$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");
} 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");
}
}
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);
}
} else if(sizeof($descriptor) === 2) {
$audio = (new Audios)->getByOwnerAndVID((int) $descriptor[0], (int) $descriptor[1]);
} else {
$this->fail(8, "Invalid audio $id");
}
return $audio;
}
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),
],
];
} else if(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,
];
}
function isLagtrain(string $audio_id): int
{
$this->requireUser();
$audio = $this->audioFromAnyId($audio_id);
if(!$audio)
$this->fail(0404, "Audio not found");
# Possible information disclosure risks are acceptable :D
return (int) (strpos($audio->getName(), "Lagtrain") !== false);
}
// 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
{
$this->requireUser();
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(!$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);
}
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
{
$serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"];
return (object) [
"count" => 1,
"items" => [(object) [
"id" => 1,
"owner_id" => 1,
"artist" => "В ОВК ПОКА НЕТ МУЗЫКИ",
"title" => "ЖДИТЕ :)))",
"duration" => 22,
"url" => $serverUrl . "/assets/packages/static/openvk/audio/nomusic.mp3"
]]
];
$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(0404, "album_id invalid");
else if(!$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");
else if(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(0602, "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,
];
}
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();
$this->willExecuteWriteAction();
$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($gid)) {
$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((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(0404, "Not Found");
else if(!$group->canBeModifiedBy($this->getUser()))
$this->fail(203,"Insufficient rights to this group");
$ids[] = $id;
$this->beacon($song ? $song->getId() : 0, $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");
$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,
];
}
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(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->setEdited(time());
$audio->save();
return $lyrics;
}
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(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)");
try {
$audio->add($to);
} catch(\OverflowException $ex) {
$this->fail(300, "Album is full");
}
return $audio->getPrettyId();
}
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(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];
}
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,
];
}
function searchAlbums(string $query, int $offset = 0, int $limit = 25, int $drop_private = 0): object
{
$this->requireUser();
$playlists = [];
$search = (new Audios)->searchPlaylists($query)->offsetLimit($offset, $limit);
foreach($search as $playlist) {
if(!$playlist->canBeViewedBy($this->getUser())) {
if($drop_private == 0)
$playlists[] = NULL;
continue;
}
$playlists[] = $playlist->toVkApiStruct($this->getUser());
}
return (object) [
"count" => sizeof($playlists),
"items" => $playlists,
];
}
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(0404, "Invalid group_id");
else if(!$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();
}
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(0404, "Album not found");
else if(!$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);
}
function deleteAlbum(int $album_id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "Album not found");
else if(!$album->canBeModifiedBy($this->getUser()))
$this->fail(600, "Insufficient rights to this album");
$album->delete();
return 1;
}
function moveToAlbum(int $album_id, string $audio_ids): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "Album not found");
else if(!$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;
else if(!$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;
}
function removeFromAlbum(int $album_id, string $audio_ids): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "Album not found");
else if(!$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;
else if($audio->canBeViewedBy($this->getUser()))
continue;
$audios[] = $audio;
}
if(sizeof($audios) < 1)
return 0;
foreach($audios as $audio)
$album->remove($audio);
return 1;
}
function copyToAlbum(int $album_id, string $audio_ids): int
{
return $this->moveToAlbum($album_id, $audio_ids);
}
function bookmarkAlbum(int $id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($id);
if(!$album)
$this->fail(0404, "Not found");
if(!$album->canBeViewedBy($this->getUser()))
$this->fail(600, "Access error");
return (int) $album->bookmark($this->getUser());
}
function unBookmarkAlbum(int $id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($id);
if(!$album)
$this->fail(0404, "Not found");
if(!$album->canBeViewedBy($this->getUser()))
$this->fail(600, "Access error");
return (int) $album->unbookmark($this->getUser());
}
}

View file

@ -4,7 +4,7 @@ use openvk\Web\Models\Repositories\Users as UsersRepo;
final class Friends extends VKAPIRequestHandler
{
function get(int $user_id, string $fields = "", int $offset = 0, int $count = 100): object
function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count = 100): object
{
$i = 0;
$offset++;
@ -13,6 +13,14 @@ final class Friends extends VKAPIRequestHandler
$users = new UsersRepo;
$this->requireUser();
if ($user_id == 0) {
$user_id = $this->getUser()->getId();
}
if (is_null($users->get($user_id))) {
$this->fail(100, "One of the parameters specified was missing or invalid");
}
foreach($users->get($user_id)->getFriends($offset, $count) as $friend) {
$friends[$i] = $friend->getId();

View file

@ -292,7 +292,8 @@ final class Groups extends VKAPIRequestHandler
int $topics = NULL,
int $adminlist = NULL,
int $topicsAboveWall = NULL,
int $hideFromGlobalFeed = NULL)
int $hideFromGlobalFeed = NULL,
int $audio = NULL)
{
$this->requireUser();
$this->willExecuteWriteAction();
@ -303,17 +304,22 @@ final class Groups extends VKAPIRequestHandler
if(!$club || !$club->canBeModifiedBy($this->getUser())) $this->fail(15, "You can't modify this group.");
if(!empty($screen_name) && !$club->setShortcode($screen_name)) $this->fail(103, "Invalid shortcode.");
!is_null($title) ? $club->setName($title) : NULL;
!is_null($description) ? $club->setAbout($description) : NULL;
!is_null($screen_name) ? $club->setShortcode($screen_name) : NULL;
!is_null($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL;
!is_null($wall) ? $club->setWall($wall) : NULL;
!is_null($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL;
!is_null($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL;
!is_null($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL;
!is_null($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL;
!empty($title) ? $club->setName($title) : NULL;
!empty($description) ? $club->setAbout($description) : NULL;
!empty($screen_name) ? $club->setShortcode($screen_name) : NULL;
!empty($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL;
!empty($wall) ? $club->setWall($wall) : NULL;
!empty($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL;
!empty($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL;
!empty($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL;
!empty($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL;
in_array($audio, [0, 1]) ? $club->setEveryone_can_upload_audios($audio) : NULL;
$club->save();
try {
$club->save();
} catch(\TypeError $e) {
$this->fail(8, "Nothing changed");
}
return 1;
}
@ -370,7 +376,7 @@ final class Groups extends VKAPIRequestHandler
$arr->items[$i]->can_see_all_posts = 1;
break;
case "can_see_audio":
$arr->items[$i]->can_see_audio = 0;
$arr->items[$i]->can_see_audio = 1;
break;
case "can_write_private_message":
$arr->items[$i]->can_write_private_message = 0;
@ -469,7 +475,7 @@ final class Groups extends VKAPIRequestHandler
"wall" => $club->canPost() == true ? 1 : 0,
"photos" => 1,
"video" => 0,
"audio" => 0,
"audio" => $club->isEveryoneCanUploadAudios() ? 1 : 0,
"docs" => 0,
"topics" => $club->isEveryoneCanCreateTopics() == true ? 1 : 0,
"wiki" => 0,

View file

@ -432,13 +432,11 @@ final class Photos extends VKAPIRequestHandler
if(empty($photo_ids)) {
$album = (new Albums)->getAlbumByOwnerAndId($owner_id, $album_id);
if(!$album->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) {
$this->fail(21, "This user chose to hide his albums.");
}
if(!$album || $album->isDeleted()) {
if(!$album || $album->isDeleted())
$this->fail(21, "Invalid album");
}
if(!$album->getOwner()->getPrivacyPermission('photos.read', $this->getUser()))
$this->fail(21, "This user chose to hide his albums.");
$photos = array_slice(iterator_to_array($album->getPhotos(1, $count + $offset)), $offset);
$res["count"] = sizeof($photos);
@ -456,8 +454,7 @@ final class Photos extends VKAPIRequestHandler
"items" => []
];
foreach($photos as $photo)
{
foreach($photos as $photo) {
$id = explode("_", $photo);
$phot = (new PhotosRepo)->getByOwnerAndVID((int)$id[0], (int)$id[1]);

View file

@ -8,13 +8,23 @@ final class Status extends VKAPIRequestHandler
function get(int $user_id = 0, int $group_id = 0)
{
$this->requireUser();
if($user_id == 0 && $group_id == 0) {
return $this->getUser()->getStatus();
} else {
if($group_id > 0)
$this->fail(501, "Group statuses are not implemented");
else
return (new UsersRepo)->get($user_id)->getStatus();
if($user_id == 0 && $group_id == 0)
$user_id = $this->getUser()->getId();
if($group_id > 0)
$this->fail(501, "Group statuses are not implemented");
else {
$user = (new UsersRepo)->get($user_id);
$audioStatus = $user->getCurrentAudioStatus();
if($audioStatus) {
return [
"status" => $user->getStatus(),
"audio" => $audioStatus->toVkApiStruct(),
];
}
return $user->getStatus();
}
}

View file

@ -37,8 +37,8 @@ final class Users extends VKAPIRequestHandler
} else if($usr->isBanned()) {
$response[$i] = (object)[
"id" => $usr->getId(),
"first_name" => $usr->getFirstName(),
"last_name" => $usr->getLastName(),
"first_name" => $usr->getFirstName(true),
"last_name" => $usr->getLastName(true),
"deactivated" => "banned",
"ban_reason" => $usr->getBanReason()
];
@ -47,8 +47,8 @@ final class Users extends VKAPIRequestHandler
} else {
$response[$i] = (object)[
"id" => $usr->getId(),
"first_name" => $usr->getFirstName(),
"last_name" => $usr->getLastName(),
"first_name" => $usr->getFirstName(true),
"last_name" => $usr->getLastName(true),
"is_closed" => false,
"can_access_closed" => true,
];
@ -96,6 +96,12 @@ final class Users extends VKAPIRequestHandler
case "status":
if($usr->getStatus() != NULL)
$response[$i]->status = $usr->getStatus();
$audioStatus = $usr->getCurrentAudioStatus();
if($audioStatus)
$response[$i]->status_audio = $audioStatus->toVkApiStruct();
break;
case "screen_name":
if($usr->getShortCode() != NULL)
@ -160,6 +166,18 @@ final class Users extends VKAPIRequestHandler
case "interests":
$response[$i]->interests = $usr->getInterests();
break;
case "quotes":
$response[$i]->interests = $usr->getFavoriteQuote();
break;
case "email":
$response[$i]->interests = $usr->getEmail();
break;
case "telegram":
$response[$i]->interests = $usr->getTelegram();
break;
case "about":
$response[$i]->interests = $usr->getDescription();
break;
case "rating":
$response[$i]->rating = $usr->getRating();
break;

View file

@ -16,6 +16,7 @@ use openvk\Web\Models\Repositories\Videos as VideosRepo;
use openvk\Web\Models\Entities\Note;
use openvk\Web\Models\Repositories\Notes as NotesRepo;
use openvk\Web\Models\Repositories\Polls as PollsRepo;
use openvk\Web\Models\Repositories\Audios as AudiosRepo;
final class Wall extends VKAPIRequestHandler
{
@ -59,6 +60,11 @@ final class Wall extends VKAPIRequestHandler
$attachments[] = $attachment->getApiStructure();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Note) {
$attachments[] = $attachment->toVkApiStruct();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) {
$attachments[] = [
"type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser()),
];
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = [];
@ -234,6 +240,11 @@ final class Wall extends VKAPIRequestHandler
$attachments[] = $attachment->getApiStructure();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Note) {
$attachments[] = $attachment->toVkApiStruct();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) {
$attachments[] = [
"type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser())
];
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = [];
@ -458,6 +469,9 @@ final class Wall extends VKAPIRequestHandler
$attachmentType = "note";
elseif(str_contains($attac, "poll"))
$attachmentType = "poll";
elseif(str_contains($attac, "audio"))
$attachmentType = "audio";
else
$this->fail(205, "Unknown attachment type");
@ -499,6 +513,10 @@ final class Wall extends VKAPIRequestHandler
$this->fail(100, "Poll does not exist");
if($attacc->getOwner()->getId() != $this->getUser()->getId())
$this->fail(43, "You do not have access to this poll");
} elseif($attachmentType == "audio") {
$attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
if(!$attacc || $attacc->isDeleted())
$this->fail(100, "Audio does not exist");
$post->attach($attacc);
}
@ -579,6 +597,11 @@ final class Wall extends VKAPIRequestHandler
$attachments[] = $this->getApiPhoto($attachment);
} elseif($attachment instanceof \openvk\Web\Models\Entities\Note) {
$attachments[] = $attachment->toVkApiStruct();
} elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) {
$attachments[] = [
"type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser()),
];
}
}
@ -638,6 +661,9 @@ final class Wall extends VKAPIRequestHandler
$comment = (new CommentsRepo)->get($comment_id); # один хуй айди всех комментов общий
if(!$comment || $comment->isDeleted())
$this->fail(100, "Invalid comment");
$profiles = [];
$attachments = [];
@ -645,6 +671,11 @@ final class Wall extends VKAPIRequestHandler
foreach($comment->getChildren() as $attachment) {
if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$attachments[] = $this->getApiPhoto($attachment);
} elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) {
$attachments[] = [
"type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser()),
];
}
}
@ -736,6 +767,8 @@ final class Wall extends VKAPIRequestHandler
$attachmentType = "photo";
elseif(str_contains($attac, "video"))
$attachmentType = "video";
elseif(str_contains($attac, "audio"))
$attachmentType = "audio";
else
$this->fail(205, "Unknown attachment type");
@ -761,6 +794,12 @@ final class Wall extends VKAPIRequestHandler
if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser()))
$this->fail(11, "Access to video denied");
$comment->attach($attacc);
} elseif($attachmentType == "audio") {
$attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
if(!$attacc || $attacc->isDeleted())
$this->fail(100, "Audio does not exist");
$comment->attach($attacc);
}
}
@ -883,7 +922,7 @@ final class Wall extends VKAPIRequestHandler
return [
"type" => "photo",
"photo" => [
"album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : NULL,
"album_id" => $attachment->getAlbum() ? $attachment->getAlbum()->getId() : 0,
"date" => $attachment->getPublicationTime()->timestamp(),
"id" => $attachment->getVirtualId(),
"owner_id" => $attachment->getOwner()->getId(),

View file

@ -48,7 +48,7 @@ class APIToken extends RowModel
$this->delete();
}
function save(): void
function save(?bool $log = false): void
{
if(is_null($this->getRecord()))
$this->stateChanges("secret", bin2hex(openssl_random_pseudo_bytes(36)));

View file

@ -0,0 +1,469 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Util\Shell\Exceptions\UnknownCommandException;
use openvk\Web\Util\Shell\Shell;
/**
* @method setName(string)
* @method setPerformer(string)
* @method setLyrics(string)
* @method setExplicit(bool)
*/
class Audio extends Media
{
protected $tableName = "audios";
protected $fileExtension = "mpd";
# Taken from winamp :D
const genres = [
'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
{
if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe"))
throw new \Exception();
$error = NULL;
$streams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams a", "-loglevel error")->execute($error);
if($error !== 0)
throw new \DomainException("$filename is not recognized as media container");
else if(empty($streams) || ctype_space($streams))
throw new \DomainException("$filename does not contain any audio streams");
$vstreams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams v", "-loglevel error")->execute($error);
# check if audio has cover (attached_pic)
preg_match("%attached_pic=([0-1])%", $vstreams, $hasCover);
if(!empty($vstreams) && !ctype_space($vstreams) && ((int)($hasCover[1]) !== 1))
throw new \DomainException("$filename is a video");
$durations = [];
preg_match_all('%duration=([0-9\.]++)%', $streams, $durations);
if(sizeof($durations[1]) === 0)
throw new \DomainException("$filename does not contain any meaningful audio streams");
$length = 0;
foreach($durations[1] as $duration) {
$duration = floatval($duration);
if($duration < 1.0 || $duration > 65536.0)
throw new \DomainException("$filename does not contain any meaningful audio streams");
else
$length = max($length, $duration);
}
return (int) round($length, 0, PHP_ROUND_HALF_EVEN);
}
/**
* @throws \Exception
*/
protected function saveFile(string $filename, string $hash): bool
{
$duration = $this->fileLength($filename);
$kid = openssl_random_pseudo_bytes(16);
$key = openssl_random_pseudo_bytes(16);
$tok = openssl_random_pseudo_bytes(28);
$ss = ceil($duration / 15);
$this->stateChanges("kid", $kid);
$this->stateChanges("key", $key);
$this->stateChanges("token", $tok);
$this->stateChanges("segment_size", $ss);
$this->stateChanges("length", $duration);
try {
$args = [
str_replace("enabled", "available", OPENVK_ROOT),
str_replace("enabled", "available", $this->getBaseDir()),
$hash,
$filename,
bin2hex($kid),
bin2hex($key),
bin2hex($tok),
$ss,
];
if(Shell::isPowershell()) {
Shell::powershell("-executionpolicy bypass", "-File", __DIR__ . "/../shell/processAudio.ps1", ...$args)
->start();
} else {
Shell::bash(__DIR__ . "/../shell/processAudio.sh", ...$args) // Pls workkkkk
->start(); // idk, not tested :")
}
# Wait until processAudio will consume the file
$start = time();
while(file_exists($filename))
if(time() - $start > 5)
throw new \RuntimeException("Timed out waiting FFMPEG");
} catch(UnknownCommandException $ucex) {
exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash/pwsh is not installed" : VIDEOS_FRIENDLY_ERROR);
}
return true;
}
function getTitle(): string
{
return $this->getRecord()->name;
}
function getPerformer(): string
{
return $this->getRecord()->performer;
}
function getName(): string
{
return $this->getPerformer() . "" . $this->getTitle();
}
function getGenre(): ?string
{
return $this->getRecord()->genre;
}
function getLyrics(): ?string
{
return !is_null($this->getRecord()->lyrics) ? htmlspecialchars($this->getRecord()->lyrics, ENT_DISALLOWED | ENT_XHTML) : NULL;
}
function getLength(): int
{
return $this->getRecord()->length;
}
function getFormattedLength(): string
{
$len = $this->getLength();
$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 getSegmentSize(): float
{
return $this->getRecord()->segment_size;
}
function getListens(): int
{
return $this->getRecord()->listens;
}
function getOriginalURL(bool $force = false): string
{
$disallowed = !OPENVK_ROOT_CONF["openvk"]["preferences"]["music"]["exposeOriginalURLs"] && !$force;
if(!$this->isAvailable() || $disallowed)
return ovk_scheme(true)
. $_SERVER["HTTP_HOST"] . ":"
. $_SERVER["HTTP_PORT"]
. "/assets/packages/static/openvk/audio/nomusic.mp3";
$key = bin2hex($this->getRecord()->token);
return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3";
}
function getURL(?bool $force = false): string
{
if ($this->isWithdrawn()) return "";
return parent::getURL();
}
function getKeys(): array
{
$keys[bin2hex($this->getRecord()->kid)] = bin2hex($this->getRecord()->key);
return $keys;
}
function isAnonymous(): bool
{
return false;
}
function isExplicit(): bool
{
return (bool) $this->getRecord()->explicit;
}
function isWithdrawn(): bool
{
return (bool) $this->getRecord()->withdrawn;
}
function isUnlisted(): bool
{
return (bool) $this->getRecord()->unlisted;
}
# NOTICE may flush model to DB if it was just processed
function isAvailable(): bool
{
if($this->getRecord()->processed)
return true;
# throttle requests to isAvailable to prevent DoS attack if filesystem is actually an S3 storage
if(time() - $this->getRecord()->checked < 5)
return false;
try {
$fragments = str_replace(".mpd", "_fragments", $this->getFileName());
$original = "original_" . bin2hex($this->getRecord()->token) . ".mp3";
if(file_exists("$fragments/$original")) {
# Original gets uploaded after fragments
$this->stateChanges("processed", 0x01);
return true;
}
} finally {
$this->stateChanges("checked", time());
$this->save();
}
return false;
}
function isInLibraryOf($entity): bool
{
return sizeof(DatabaseConnection::i()->getContext()->table("audio_relations")->where([
"entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1),
"audio" => $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("audio_relations");
if(sizeof($audioRels->where("entity", $entityId)) > 65536)
throw new \OverflowException("Can't have more than 65536 audios in a playlist");
$audioRels->insert([
"entity" => $entityId,
"audio" => $this->getId(),
]);
return true;
}
function remove($entity): bool
{
if(!$this->isInLibraryOf($entity))
return false;
DatabaseConnection::i()->getContext()->table("audio_relations")->where([
"entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1),
"audio" => $this->getId(),
])->delete();
return true;
}
function listen($entity, Playlist $playlist = NULL): bool
{
$listensTable = DatabaseConnection::i()->getContext()->table("audio_listens");
$lastListen = $listensTable->where([
"entity" => $entity->getRealId(),
"audio" => $this->getId(),
])->order("index DESC")->fetch();
if(!$lastListen || (time() - $lastListen->time >= $this->getLength())) {
$listensTable->insert([
"entity" => $entity->getRealId(),
"audio" => $this->getId(),
"time" => time(),
"playlist" => $playlist ? $playlist->getId() : NULL,
]);
if($entity instanceof User) {
$this->stateChanges("listens", ($this->getListens() + 1));
$this->save();
if($playlist) {
$playlist->incrementListens();
$playlist->save();
}
}
$entity->setLast_played_track($this->getId());
$entity->save();
return true;
}
$lastListen->update([
"time" => time(),
]);
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
{
if(!in_array($genre, Audio::genres)) {
$this->stateChanges("genre", NULL);
return;
}
$this->stateChanges("genre", $genre);
}
function setCopyrightStatus(bool $withdrawn = true): void {
$this->stateChanges("withdrawn", $withdrawn);
}
function setSearchability(bool $searchable = true): void {
$this->stateChanges("unlisted", !$searchable);
}
function setToken(string $tok): void {
throw new \LogicException("Changing keys is not supported.");
}
function setKid(string $kid): void {
throw new \LogicException("Changing keys is not supported.");
}
function setKey(string $key): void {
throw new \LogicException("Changing keys is not supported.");
}
function setLength(int $len): void {
throw new \LogicException("Changing length is not supported.");
}
function setSegment_Size(int $len): void {
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();
$ctx->table("playlist_relations")->where("media", $this->getId())
->delete();
parent::delete($softly);
}
}

View file

@ -371,6 +371,29 @@ class Club extends RowModel
{
return $this->getRecord()->alert;
}
function getRealId(): int
{
return $this->getId() * -1;
}
function isEveryoneCanUploadAudios(): bool
{
return (bool) $this->getRecord()->everyone_can_upload_audios;
}
function canUploadAudio(?User $user): bool
{
if(!$user)
return NULL;
return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user);
}
function getAudiosCollectionSize()
{
return (new \openvk\Web\Models\Repositories\Audios)->getClubCollectionSize($this);
}
function toVkApiStruct(?User $user = NULL): object
{
@ -381,26 +404,27 @@ class Club extends RowModel
$res->screen_name = $this->getShortCode();
$res->is_closed = 0;
$res->deactivated = NULL;
$res->is_admin = $this->canBeModifiedBy($user);
$res->is_admin = $user && $this->canBeModifiedBy($user);
if($this->canBeModifiedBy($user)) {
if($user && $this->canBeModifiedBy($user)) {
$res->admin_level = 3;
}
$res->is_member = $this->getSubscriptionStatus($user) ? 1 : 0;
$res->is_member = $user && $this->getSubscriptionStatus($user) ? 1 : 0;
$res->type = "group";
$res->photo_50 = $this->getAvatarUrl("miniscule");
$res->photo_100 = $this->getAvatarUrl("tiny");
$res->photo_200 = $this->getAvatarUrl("normal");
$res->can_create_topic = $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0);
$res->can_create_topic = $user && $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0);
$res->can_post = $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0);
$res->can_post = $user && $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0);
return $res;
}
use Traits\TBackDrops;
use Traits\TSubscribable;
use Traits\TAudioStatuses;
}

View file

@ -131,7 +131,7 @@ class Correspondence
*/
function getPreviewMessage(): ?Message
{
$messages = $this->getMessages(1, NULL, 1);
$messages = $this->getMessages(1, NULL, 1, 0);
return $messages[0] ?? NULL;
}

View file

@ -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(?bool $log = false): 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($log);
}
function delete(bool $softly = true): void
{
if(!$softly) {
$this->relations->where("collection", $this->getId())
->delete();
}
parent::delete($softly);
}
use Traits\TOwnable;
}

View file

@ -54,11 +54,11 @@ class PasswordReset extends RowModel
}
}
function save(): void
function save(?bool $log = false): void
{
$this->stateChanges("key", base64_encode(openssl_random_pseudo_bytes(46)));
$this->stateChanges("timestamp", time());
parent::save();
parent::save($log);
}
}

View file

@ -0,0 +1,256 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection;
use Nette\Database\Table\ActiveRow;
use openvk\Web\Models\Repositories\Audios;
use openvk\Web\Models\Repositories\Photos;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\Photo;
/**
* @method setName(string $name)
* @method setDescription(?string $desc)
*/
class Playlist extends MediaCollection
{
protected $tableName = "playlists";
protected $relTableName = "playlist_relations";
protected $entityTableName = "audios";
protected $entityClassName = 'openvk\Web\Models\Entities\Audio';
protected $allowDuplicates = false;
private $importTable;
const MAX_COUNT = 1000;
const MAX_ITEMS = 10000;
function __construct(?ActiveRow $ar = NULL)
{
parent::__construct($ar);
$this->importTable = DatabaseConnection::i()->getContext()->table("playlist_imports");
}
function getCoverURL(string $size = "normal"): ?string
{
$photo = (new Photos)->get((int) $this->getRecord()->cover_photo_id);
return is_null($photo) ? "/assets/packages/static/openvk/img/song.jpg" : $photo->getURLBySizeId($size);
}
function getLength(): int
{
return $this->getRecord()->length;
}
function getAudios(int $offset = 0, ?int $limit = NULL, ?int $shuffleSeed = NULL): \Traversable
{
if(!$shuffleSeed) {
foreach ($this->fetchClassic($offset, $limit) as $e)
yield $e; # No, I can't return, it will break with []
return;
}
$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 getDescription(): ?string
{
return $this->getRecord()->description;
}
function getDescriptionHTML(): ?string
{
return htmlspecialchars($this->getRecord()->description, ENT_DISALLOWED | ENT_XHTML);
}
function getListens()
{
return $this->getRecord()->listens;
}
function toVkApiStruct(?User $user = NULL): object
{
$oid = $this->getOwner()->getId();
if($this->getOwner() instanceof Club)
$oid *= -1;
return (object) [
"id" => $this->getId(),
"owner_id" => $oid,
"title" => $this->getName(),
"description" => $this->getDescription(),
"size" => $this->size(),
"length" => $this->getLength(),
"created" => $this->getCreationTime()->timestamp(),
"modified" => $this->getEditTime() ? $this->getEditTime()->timestamp() : NULL,
"accessible" => $this->canBeViewedBy($user),
"editable" => $this->canBeModifiedBy($user),
"bookmarked" => $this->isBookmarkedBy($user),
"listens" => $this->getListens(),
"cover_url" => $this->getCoverURL(),
];
}
function setLength(): void
{
throw new \LogicException("Can't set length of playlist manually");
}
function resetLength(): bool
{
$this->stateChanges("length", 0);
return true;
}
function delete(bool $softly = true): void
{
$ctx = DatabaseConnection::i()->getContext();
$ctx->table("playlist_imports")->where("playlist", $this->getId())
->delete();
parent::delete($softly);
}
function hasAudio(Audio $audio): bool
{
$ctx = DatabaseConnection::i()->getContext();
return !is_null($ctx->table("playlist_relations")->where([
"collection" => $this->getId(),
"media" => $audio->getId()
])->fetch());
}
function getCoverPhotoId(): ?int
{
return $this->getRecord()->cover_photo_id;
}
function canBeModifiedBy(User $user): bool
{
if(!$user)
return false;
if($this->getOwner() instanceof User)
return $user->getId() == $this->getOwner()->getId();
else
return $this->getOwner()->canBeModifiedBy($user);
}
function getLengthInMinutes(): int
{
return (int)round($this->getLength() / 60, PHP_ROUND_HALF_DOWN);
}
function fastMakeCover(int $owner, array $file)
{
$cover = new Photo;
$cover->setOwner($owner);
$cover->setDescription("Playlist cover image");
$cover->setFile($file);
$cover->setCreated(time());
$cover->save();
$this->setCover_photo_id($cover->getId());
return $cover;
}
function getURL(): string
{
return "/playlist" . $this->getOwner()->getRealId() . "_" . $this->getId();
}
function incrementListens()
{
$this->stateChanges("listens", ($this->getListens() + 1));
}
function getMetaDescription(): string
{
$length = $this->getLengthInMinutes();
$props = [];
$props[] = tr("audios_count", $this->size());
$props[] = "<span id='listensCount'>" . tr("listens_count", $this->getListens()) . "</span>";
if($length > 0) $props[] = tr("minutes_count", $length);
$props[] = tr("created_playlist") . " " . $this->getPublicationTime();
# if($this->getEditTime()) $props[] = tr("updated_playlist") . " " . $this->getEditTime();
return implode("", $props);
}
}

View file

@ -279,12 +279,12 @@ class Poll extends Attachable
return $poll;
}
function save(): void
function save(?bool $log = false): void
{
if(empty($this->choicesToPersist))
throw new InvalidStateException;
parent::save();
parent::save($log);
foreach($this->choicesToPersist as $option) {
DatabaseConnection::i()->getContext()->table("poll_options")->insert([
"poll" => $this->getId(),

View file

@ -5,7 +5,7 @@ use Nette\Database\Table\ActiveRow;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\Club;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Users, Posts, Photos, Videos, Clubs};
use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Audios, Users, Posts, Photos, Videos, Clubs};
use Chandler\Database\DatabaseConnection as DB;
use Nette\InvalidStateException as ISE;
use Nette\Database\Table\Selection;
@ -74,6 +74,7 @@ class Report extends RowModel
else if ($this->getContentType() == "note") return (new Notes)->get($this->getContentId());
else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId());
else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId());
else if ($this->getContentType() == "audio") return (new Audios)->get($this->getContentId());
else return null;
}

View file

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities\Traits;
use openvk\Web\Models\Repositories\Audios;
use Chandler\Database\DatabaseConnection;
trait TAudioStatuses
{
function isBroadcastEnabled(): bool
{
if($this->getRealId() < 0) return true;
return (bool) $this->getRecord()->audio_broadcast_enabled;
}
function getCurrentAudioStatus()
{
if(!$this->isBroadcastEnabled()) return NULL;
$audioId = $this->getRecord()->last_played_track;
if(!$audioId) return NULL;
$audio = (new Audios)->get($audioId);
if(!$audio || $audio->isDeleted())
return NULL;
$listensTable = DatabaseConnection::i()->getContext()->table("audio_listens");
$lastListen = $listensTable->where([
"entity" => $this->getRealId(),
"audio" => $audio->getId(),
"time >" => (time() - $audio->getLength()) - 10,
])->fetch();
if($lastListen)
return $audio;
return NULL;
}
}

View file

@ -4,6 +4,12 @@ use openvk\Web\Models\Entities\User;
trait TOwnable
{
function canBeViewedBy(?User $user): bool
{
// TODO implement normal check in master
return true;
}
function canBeModifiedBy(User $user): bool
{
if(method_exists($this, "isCreatedBySystem"))

View file

@ -4,7 +4,7 @@ use morphos\Gender;
use openvk\Web\Themes\{Themepack, Themepacks};
use openvk\Web\Util\DateTime;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift};
use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift, Audio};
use openvk\Web\Models\Repositories\{Applications, Bans, Comments, Notes, Posts, Users, Clubs, Albums, Gifts, Notifications, Videos, Photos};
use openvk\Web\Models\Exceptions\InvalidUserNameException;
use Nette\Database\Table\ActiveRow;
@ -190,7 +190,7 @@ class User extends RowModel
function getMorphedName(string $case = "genitive", bool $fullName = true): string
{
$name = $fullName ? ($this->getLastName() . " " . $this->getFirstName()) : $this->getFirstName();
if(!preg_match("%^[А-яё\-]+$%", $name))
if(!preg_match("%[А-яё\-]+$%", $name))
return $name; # name is probably not russian
$inflected = inflectName($name, $case, $this->isFemale() ? Gender::FEMALE : Gender::MALE);
@ -455,6 +455,7 @@ class User extends RowModel
"length" => 1,
"mappings" => [
"photos",
"audios",
"videos",
"messages",
"notes",
@ -462,7 +463,7 @@ class User extends RowModel
"news",
"links",
"poster",
"apps"
"apps",
],
])->get($id);
}
@ -482,6 +483,7 @@ class User extends RowModel
"friends.add",
"wall.write",
"messages.write",
"audios.read",
],
])->get($id);
}
@ -720,8 +722,8 @@ class User extends RowModel
for($i = 0; $i < 10 - $this->get2faBackupCodeCount(); $i++) {
$codes[] = [
owner => $this->getId(),
code => random_int(10000000, 99999999)
"owner" => $this->getId(),
"code" => random_int(10000000, 99999999)
];
}
@ -1010,6 +1012,7 @@ class User extends RowModel
"friends.add",
"wall.write",
"messages.write",
"audios.read",
],
])->set($id, $status)->toInteger());
}
@ -1020,6 +1023,7 @@ class User extends RowModel
"length" => 1,
"mappings" => [
"photos",
"audios",
"videos",
"messages",
"notes",
@ -1027,7 +1031,7 @@ class User extends RowModel
"news",
"links",
"poster",
"apps"
"apps",
],
])->set($id, (int) $status)->toInteger();
@ -1223,6 +1227,11 @@ class User extends RowModel
return $response;
}
function getRealId()
{
return $this->getId();
}
function toVkApiStruct(): object
{
$res = (object) [];
@ -1239,7 +1248,47 @@ class User extends RowModel
return $res;
}
function getAudiosCollectionSize()
{
return (new \openvk\Web\Models\Repositories\Audios)->getUserCollectionSize($this);
}
function getBroadcastList(string $filter = "friends", bool $shuffle = false)
{
$dbContext = DatabaseConnection::i()->getContext();
$entityIds = [];
$query = $dbContext->table("subscriptions")->where("follower", $this->getRealId());
if($filter != "all")
$query = $query->where("model = ?", "openvk\\Web\\Models\\Entities\\" . ($filter == "groups" ? "Club" : "User"));
foreach($query as $_rel) {
$entityIds[] = $_rel->model == "openvk\\Web\\Models\\Entities\\Club" ? $_rel->target * -1 : $_rel->target;
}
if($shuffle) {
$shuffleSeed = openssl_random_pseudo_bytes(6);
$shuffleSeed = hexdec(bin2hex($shuffleSeed));
$entityIds = knuth_shuffle($entityIds, $shuffleSeed);
}
$entityIds = array_slice($entityIds, 0, 10);
$returnArr = [];
foreach($entityIds as $id) {
$entit = $id > 0 ? (new Users)->get($id) : (new Clubs)->get(abs($id));
if($id > 0 && $entit->isDeleted()) continue;
$returnArr[] = $entit;
}
return $returnArr;
}
use Traits\TBackDrops;
use Traits\TSubscribable;
use Traits\TAudioStatuses;
}

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Util\Shell\Shell;
use openvk\Web\Util\Shell\Shell\Exceptions\{ShellUnavailableException, UnknownCommandException};
use openvk\Web\Util\Shell\Exceptions\{ShellUnavailableException, UnknownCommandException};
use openvk\Web\Models\VideoDrivers\VideoDriver;
use Nette\InvalidStateException as ISE;

View file

@ -130,7 +130,7 @@ class Albums
"owner" => $owner,
"id" => $id
])->fetch();
return new Album($album);
return $album ? new Album($album) : NULL;
}
}

View file

@ -0,0 +1,296 @@
<?php declare(strict_types=1);
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;
class Audios
{
private $context;
private $audios;
private $rels;
private $playlists;
private $playlistImports;
private $playlistRels;
const ORDER_NEW = 0;
const ORDER_POPULAR = 1;
const VK_ORDER_NEW = 0;
const VK_ORDER_LENGTH = 1;
const VK_ORDER_POPULAR = 2;
function __construct()
{
$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");
$this->playlistRels = $this->context->table("playlist_relations");
}
function get(int $id): ?Audio
{
$audio = $this->audios->get($id);
if(!$audio)
return NULL;
return new Audio($audio);
}
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([
"owner" => $owner,
"virtual_id" => $vId,
])->fetch();
if(!$audio) return NULL;
return new Audio($audio);
}
function getPlaylistByOwnerAndVID(int $owner, int $vId): ?Playlist
{
$playlist = $this->playlists->where([
"owner" => $owner,
"id" => $vId,
])->fetch();
if(!$playlist) return NULL;
return new Playlist($playlist);
}
function getByEntityID(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable
{
$limit ??= OPENVK_DEFAULT_PER_PAGE;
$iter = $this->rels->where("entity", $entity)->limit($limit, $offset)->order("index DESC");
foreach($iter as $rel) {
$audio = $this->get($rel->audio);
if(!$audio || $audio->isDeleted()) {
$deleted++;
continue;
}
yield $audio;
}
}
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);
}
function getRandomThreeAudiosByEntityId(int $id): Array
{
$iter = $this->rels->where("entity", $id);
$ids = [];
foreach($iter as $it)
$ids[] = $it->audio;
$shuffleSeed = openssl_random_pseudo_bytes(6);
$shuffleSeed = hexdec(bin2hex($shuffleSeed));
$ids = knuth_shuffle($ids, $shuffleSeed);
$ids = array_slice($ids, 0, 3);
$audios = [];
foreach($ids as $id) {
$audio = $this->get((int)$id);
if(!$audio || $audio->isDeleted())
continue;
$audios[] = $audio;
}
return $audios;
}
function getByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
{
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 getCollectionSizeByEntityId(int $id): int
{
return sizeof($this->rels->where("entity", $id));
}
function getUserCollectionSize(User $user): int
{
return sizeof($this->rels->where("entity", $user->getId()));
}
function getClubCollectionSize(Club $club): int
{
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([
"owner" => $user->getId(),
"deleted" => 0,
]);
return new EntityStream("Audio", $search);
}
function getGlobal(int $order, ?string $genreId = NULL): EntityStream
{
$search = $this->audios->where([
"deleted" => 0,
"unlisted" => 0,
"withdrawn" => 0,
])->order($order == Audios::ORDER_NEW ? "created DESC" : "listens DESC");
if(!is_null($genreId))
$search = $search->where("genre", $genreId);
return new EntityStream("Audio", $search);
}
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([
"unlisted" => 0,
"deleted" => 0,
])->where("MATCH ($columns) AGAINST (? WITH QUERY EXPANSION)", $query)->order($order);
if($withLyrics)
$search = $search->where("lyrics IS NOT NULL");
return new EntityStream("Audio", $search);
}
function searchPlaylists(string $query): EntityStream
{
$search = $this->playlists->where([
"deleted" => 0,
])->where("MATCH (`name`, `description`) AGAINST (? IN BOOLEAN MODE)", $query);
return new EntityStream("Playlist", $search);
}
function getNew(): EntityStream
{
return new EntityStream("Audio", $this->audios->where("created >= " . (time() - 259200))->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("created DESC")->limit(25));
}
function getPopular(): EntityStream
{
return new EntityStream("Audio", $this->audios->where("listens > 0")->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("listens DESC")->limit(25));
}
function isAdded(int $user_id, int $audio_id): bool
{
return !is_null($this->rels->where([
"entity" => $user_id,
"audio" => $audio_id
])->fetch());
}
function find(string $query, array $pars = [], string $sort = "id DESC", int $page = 1, ?int $perPage = NULL): \Traversable
{
$query = "%$query%";
$result = $this->audios->where([
"unlisted" => 0,
"deleted" => 0,
]);
$notNullParams = [];
foreach($pars as $paramName => $paramValue)
if($paramName != "before" && $paramName != "after" && $paramName != "only_performers")
$paramValue != NULL ? $notNullParams+=["$paramName" => "%$paramValue%"] : NULL;
else
$paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL;
$nnparamsCount = sizeof($notNullParams);
if($notNullParams["only_performers"] == "1") {
$result->where("performer LIKE ?", $query);
} else {
$result->where("name LIKE ? OR performer LIKE ?", $query, $query);
}
if($nnparamsCount > 0) {
foreach($notNullParams as $paramName => $paramValue) {
switch($paramName) {
case "before":
$result->where("created < ?", $paramValue);
break;
case "after":
$result->where("created > ?", $paramValue);
break;
case "with_lyrics":
$result->where("lyrics IS NOT NULL");
break;
}
}
}
return new Util\EntityStream("Audio", $result->order($sort));
}
function findPlaylists(string $query, int $page = 1, ?int $perPage = NULL): \Traversable
{
$query = "%$query%";
$result = $this->playlists->where("name LIKE ?", $query);
return new Util\EntityStream("Playlist", $result);
}
}

View file

@ -52,7 +52,6 @@ class Messages
$query = file_get_contents(__DIR__ . "/../sql/get-correspondencies-count.tsql");
DatabaseConnection::i()->getConnection()->query(file_get_contents(__DIR__ . "/../sql/mysql-msg-fix.tsql"));
$count = DatabaseConnection::i()->getConnection()->query($query, $id, $class, $id, $class)->fetch()->cnt;
bdump($count);
return $count;
}
}

View file

@ -0,0 +1,39 @@
$ovkRoot = $args[0]
$storageDir = $args[1]
$fileHash = $args[2]
$hashPart = $fileHash.substring(0, 2)
$filename = $args[3]
$audioFile = [System.IO.Path]::GetTempFileName()
$temp = [System.IO.Path]::GetTempFileName()
$keyID = $args[4]
$key = $args[5]
$token = $args[6]
$seg = $args[7]
$shell = Get-WmiObject Win32_process -filter "ProcessId = $PID"
$shell.SetPriority(16384) # because there's no "nice" program in Windows we just set a lower priority for entire tree
Remove-Item $temp
Remove-Item $audioFile
New-Item -ItemType "directory" $temp
New-Item -ItemType "directory" ("$temp/$fileHash" + '_fragments')
New-Item -ItemType "directory" ("$storageDir/$hashPart/$fileHash" + '_fragments')
Set-Location -Path $temp
Move-Item $filename $audioFile
ffmpeg -i $audioFile -f dash -encryption_scheme cenc-aes-ctr -encryption_key $key `
-encryption_kid $keyID -map 0:a -vn -c:a aac -ar 44100 -seg_duration $seg `
-use_timeline 1 -use_template 1 -init_seg_name ($fileHash + '_fragments/0_0.$ext$') `
-media_seg_name ($fileHash + '_fragments/chunk$Number%06d$_$RepresentationID$.$ext$') -adaptation_sets 'id=0,streams=a' `
"$fileHash.mpd"
ffmpeg -i $audioFile -vn -ar 44100 "original_$token.mp3"
Move-Item "original_$token.mp3" ($fileHash + '_fragments')
Get-ChildItem -Path ($fileHash + '_fragments/*') | Move-Item -Destination ("$storageDir/$hashPart/$fileHash" + '_fragments')
Move-Item -Path ("$fileHash.mpd") -Destination "$storageDir/$hashPart"
cd ..
Remove-Item -Recurse $temp
Remove-Item $audioFile

View file

@ -0,0 +1,35 @@
ovkRoot=$1
storageDir=$2
fileHash=$3
hashPart=$(echo $fileHash | cut -c1-2)
filename=$4
audioFile=$(mktemp)
temp=$(mktemp -d)
keyID=$5
key=$6
token=$7
seg=$8
trap 'rm -f "$temp" "$audioFile"' EXIT
mkdir -p "$temp/$fileHash"_fragments
mkdir -p "$storageDir/$hashPart/$fileHash"_fragments
cd "$temp"
mv "$filename" "$audioFile"
ffmpeg -i "$audioFile" -f dash -encryption_scheme cenc-aes-ctr -encryption_key "$key" \
-encryption_kid "$keyID" -map 0 -vn -c:a aac -ar 44100 -seg_duration "$seg" \
-use_timeline 1 -use_template 1 -init_seg_name "$fileHash"_fragments/0_0."\$ext\$" \
-media_seg_name "$fileHash"_fragments/chunk"\$Number"%06d\$_"\$RepresentationID\$"."\$ext\$" -adaptation_sets 'id=0,streams=a' \
"$fileHash.mpd"
ffmpeg -i "$audioFile" -vn -ar 44100 "original_$token.mp3"
mv "original_$token.mp3" "$fileHash"_fragments
mv "$fileHash"_fragments "$storageDir/$hashPart"
mv "$fileHash.mpd" "$storageDir/$hashPart"
cd ..
rm -rf "$temp"
rm -f "$audioFile"

View file

@ -3,7 +3,19 @@ namespace openvk\Web\Presenters;
use Chandler\Database\Log;
use Chandler\Database\Logs;
use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink};
use openvk\Web\Models\Repositories\{Bans, ChandlerGroups, ChandlerUsers, Photos, Posts, Users, Clubs, Videos, Vouchers, Gifts, BannedLinks};
use openvk\Web\Models\Repositories\{Audios,
ChandlerGroups,
ChandlerUsers,
Users,
Clubs,
Util\EntityStream,
Vouchers,
Gifts,
BannedLinks,
Bans,
Photos,
Posts,
Videos};
use Chandler\Database\DatabaseConnection;
final class AdminPresenter extends OpenVKPresenter
@ -14,9 +26,10 @@ final class AdminPresenter extends OpenVKPresenter
private $gifts;
private $bannedLinks;
private $chandlerGroups;
private $audios;
private $logs;
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups)
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups, Audios $audios)
{
$this->users = $users;
$this->clubs = $clubs;
@ -24,8 +37,9 @@ final class AdminPresenter extends OpenVKPresenter
$this->gifts = $gifts;
$this->bannedLinks = $bannedLinks;
$this->chandlerGroups = $chandlerGroups;
$this->audios = $audios;
$this->logs = DatabaseConnection::i()->getContext()->table("ChandlerLogs");
parent::__construct();
}
@ -43,6 +57,15 @@ final class AdminPresenter extends OpenVKPresenter
$count = $repo->find($query)->size();
return $repo->find($query)->page($page, 20);
}
private function searchPlaylists(&$count)
{
$query = $this->queryParam("q") ?? "";
$page = (int) ($this->queryParam("p") ?? 1);
$count = $this->audios->findPlaylists($query)->size();
return $this->audios->findPlaylists($query)->page($page, 20);
}
function onStartup(): void
{
@ -578,6 +601,54 @@ final class AdminPresenter extends OpenVKPresenter
$this->redirect("/admin/users/id" . $user->getId());
}
function renderMusic(): void
{
$this->template->mode = in_array($this->queryParam("act"), ["audios", "playlists"]) ? $this->queryParam("act") : "audios";
if ($this->template->mode === "audios")
$this->template->audios = $this->searchResults($this->audios, $this->template->count);
else
$this->template->playlists = $this->searchPlaylists($this->template->count);
}
function renderEditMusic(int $audio_id): void
{
$audio = $this->audios->get($audio_id);
$this->template->audio = $audio;
try {
$this->template->owner = $audio->getOwner()->getId();
} catch(\Throwable $e) {
$this->template->owner = 1;
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$audio->setName($this->postParam("name"));
$audio->setPerformer($this->postParam("performer"));
$audio->setLyrics($this->postParam("text"));
$audio->setGenre($this->postParam("genre"));
$audio->setOwner((int) $this->postParam("owner"));
$audio->setExplicit(!empty($this->postParam("explicit")));
$audio->setDeleted(!empty($this->postParam("deleted")));
$audio->setWithdrawn(!empty($this->postParam("withdrawn")));
$audio->save();
}
}
function renderEditPlaylist(int $playlist_id): void
{
$playlist = $this->audios->getPlaylist($playlist_id);
$this->template->playlist = $playlist;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$playlist->setName($this->postParam("name"));
$playlist->setDescription($this->postParam("description"));
$playlist->setCover_Photo_Id((int) $this->postParam("photo"));
$playlist->setOwner((int) $this->postParam("owner"));
$playlist->setDeleted(!empty($this->postParam("deleted")));
$playlist->save();
}
}
function renderLogs(): void
{
$filter = [];

View file

@ -0,0 +1,696 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\Audio;
use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Entities\Photo;
use openvk\Web\Models\Entities\Playlist;
use openvk\Web\Models\Repositories\Audios;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Repositories\Users;
final class AudioPresenter extends OpenVKPresenter
{
private $audios;
protected $presenterName = "audios";
const MAX_AUDIO_SIZE = 25000000;
function __construct(Audios $audios)
{
$this->audios = $audios;
}
function renderPopular(): void
{
$this->renderList(NULL, "popular");
}
function renderNew(): void
{
$this->renderList(NULL, "new");
}
function renderList(?int $owner = NULL, ?string $mode = "list"): void
{
$this->template->_template = "Audio/List.xml";
$page = (int)($this->queryParam("p") ?? 1);
$audios = [];
if ($mode === "list") {
$entity = NULL;
if ($owner < 0) {
$entity = (new Clubs)->get($owner * -1);
if (!$entity || $entity->isBanned())
$this->redirect("/audios" . $this->user->id);
$audios = $this->audios->getByClub($entity, $page, 10);
$audiosCount = $this->audios->getClubCollectionSize($entity);
} else {
$entity = (new Users)->get($owner);
if (!$entity || $entity->isDeleted() || $entity->isBanned())
$this->redirect("/audios" . $this->user->id);
if(!$entity->getPrivacyPermission("audios.read", $this->user->identity))
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
$audios = $this->audios->getByUser($entity, $page, 10);
$audiosCount = $this->audios->getUserCollectionSize($entity);
}
if (!$entity)
$this->notFound();
$this->template->owner = $entity;
$this->template->ownerId = $owner;
$this->template->club = $owner < 0 ? $entity : NULL;
$this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id));
$this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity));
} else if ($mode === "new") {
$audios = $this->audios->getNew();
$audiosCount = $audios->size();
} else if ($mode === "playlists") {
if($owner < 0) {
$entity = (new Clubs)->get(abs($owner));
if (!$entity || $entity->isBanned())
$this->redirect("/playlists" . $this->user->id);
$playlists = $this->audios->getPlaylistsByClub($entity, $page, 10);
$playlistsCount = $this->audios->getClubPlaylistsCount($entity);
} else {
$entity = (new Users)->get($owner);
if (!$entity || $entity->isDeleted() || $entity->isBanned())
$this->redirect("/playlists" . $this->user->id);
if(!$entity->getPrivacyPermission("audios.read", $this->user->identity))
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
$playlists = $this->audios->getPlaylistsByUser($entity, $page, 9);
$playlistsCount = $this->audios->getUserPlaylistsCount($entity);
}
$this->template->playlists = iterator_to_array($playlists);
$this->template->playlistsCount = $playlistsCount;
$this->template->owner = $entity;
$this->template->ownerId = $owner;
$this->template->club = $owner < 0 ? $entity : NULL;
$this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id));
$this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity));
} else {
$audios = $this->audios->getPopular();
$audiosCount = $audios->size();
}
// $this->renderApp("owner=$owner");
if ($audios !== []) {
$this->template->audios = iterator_to_array($audios);
$this->template->audiosCount = $audiosCount;
}
$this->template->mode = $mode;
$this->template->page = $page;
if(in_array($mode, ["list", "new", "popular"]) && $this->user->identity)
$this->template->friendsAudios = $this->user->identity->getBroadcastList("all", true);
}
function renderEmbed(int $owner, int $id): void
{
$audio = $this->audios->getByOwnerAndVID($owner, $id);
if(!$audio) {
header("HTTP/1.1 404 Not Found");
exit("<b>" . tr("audio_embed_not_found") . ".</b>");
} else if($audio->isDeleted()) {
header("HTTP/1.1 410 Not Found");
exit("<b>" . tr("audio_embed_deleted") . ".</b>");
} else if($audio->isWithdrawn()) {
header("HTTP/1.1 451 Unavailable for legal reasons");
exit("<b>" . tr("audio_embed_withdrawn") . ".</b>");
} else if(!$audio->canBeViewedBy(NULL)) {
header("HTTP/1.1 403 Forbidden");
exit("<b>" . tr("audio_embed_forbidden") . ".</b>");
} else if(!$audio->isAvailable()) {
header("HTTP/1.1 425 Too Early");
exit("<b>" . tr("audio_embed_processing") . ".</b>");
}
$this->template->audio = $audio;
}
function renderUpload(): void
{
$this->assertUserLoggedIn();
$group = NULL;
$isAjax = $this->postParam("ajax", false) == 1;
if(!is_null($this->queryParam("gid"))) {
$gid = (int) $this->queryParam("gid");
$group = (new Clubs)->get($gid);
if(!$group)
$this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax);
if(!$group->canUploadAudio($this->user->identity))
$this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax);
}
$this->template->group = $group;
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
$upload = $_FILES["blob"];
if(isset($upload) && file_exists($upload["tmp_name"])) {
if($upload["size"] > self::MAX_AUDIO_SIZE)
$this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large"), null, $isAjax);
} else {
$err = !isset($upload) ? 65536 : $upload["error"];
$err = str_pad(dechex($err), 9, "0", STR_PAD_LEFT);
$readableError = tr("error_generic");
switch($upload["error"]) {
default:
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$readableError = tr("file_too_big");
break;
case UPLOAD_ERR_PARTIAL:
$readableError = tr("file_loaded_partially");
break;
case UPLOAD_ERR_NO_FILE:
$readableError = tr("file_not_uploaded");
break;
case UPLOAD_ERR_NO_TMP_DIR:
$readableError = "Missing a temporary folder.";
break;
case UPLOAD_ERR_CANT_WRITE:
case UPLOAD_ERR_EXTENSION:
$readableError = "Failed to write file to disk. ";
break;
}
$this->flashFail("err", tr("error"), $readableError . " " . tr("error_code", $err), null, $isAjax);
}
$performer = $this->postParam("performer");
$name = $this->postParam("name");
$lyrics = $this->postParam("lyrics");
$genre = empty($this->postParam("genre")) ? "Other" : $this->postParam("genre");
$nsfw = ($this->postParam("explicit") ?? "off") === "on";
if(empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) # FQN of audio must not be more than 128 chars
$this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, $isAjax);
$audio = new Audio;
$audio->setOwner($this->user->id);
$audio->setName($name);
$audio->setPerformer($performer);
$audio->setLyrics(empty($lyrics) ? NULL : $lyrics);
$audio->setGenre($genre);
$audio->setExplicit($nsfw);
try {
$audio->setFile($upload);
} catch(\DomainException $ex) {
$e = $ex->getMessage();
$this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large") . " $e.", null, $isAjax);
} catch(\RuntimeException $ex) {
$this->flashFail("err", tr("error"), tr("ffmpeg_timeout"), null, $isAjax);
} catch(\BadMethodCallException $ex) {
$this->flashFail("err", tr("error"), "Загрузка аудио под Linux на данный момент не реализована. Следите за обновлениями: <a href='https://github.com/openvk/openvk/pull/512/commits'>https://github.com/openvk/openvk/pull/512/commits</a>", null, $isAjax);
} catch(\Exception $ex) {
$this->flashFail("err", tr("error"), tr("ffmpeg_not_installed"), null, $isAjax);
}
$audio->save();
$audio->add($group ?? $this->user->identity);
if(!$isAjax)
$this->redirect(is_null($group) ? "/audios" . $this->user->id : "/audios-" . $group->getId());
else {
$redirectLink = "/audios";
if(!is_null($group))
$redirectLink .= $group->getRealId();
else
$redirectLink .= $this->user->id;
$pagesCount = (int)ceil((new Audios)->getCollectionSizeByEntityId(isset($group) ? $group->getRealId() : $this->user->id) / 10);
$redirectLink .= "?p=".$pagesCount;
$this->returnJson([
"success" => true,
"redirect_link" => $redirectLink,
]);
}
}
function renderListen(int $id): void
{
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$this->assertNoCSRF();
if(is_null($this->user))
$this->returnJson(["success" => false]);
$audio = $this->audios->get($id);
if ($audio && !$audio->isDeleted() && !$audio->isWithdrawn()) {
if(!empty($this->postParam("playlist"))) {
$playlist = (new Audios)->getPlaylist((int)$this->postParam("playlist"));
if(!$playlist || $playlist->isDeleted() || !$playlist->canBeViewedBy($this->user->identity) || !$playlist->hasAudio($audio))
$playlist = NULL;
}
$listen = $audio->listen($this->user->identity, $playlist);
$returnArr = ["success" => $listen];
if($playlist)
$returnArr["new_playlists_listens"] = $playlist->getListens();
$this->returnJson($returnArr);
}
$this->returnJson(["success" => false]);
} else {
$this->redirect("/");
}
}
function renderSearch(): void
{
$this->redirect("/search?type=audios");
}
function renderNewPlaylist(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction(true);
$owner = $this->user->id;
if ($this->requestParam("gid")) {
$club = (new Clubs)->get((int) abs((int)$this->requestParam("gid")));
if (!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity))
$this->redirect("/audios" . $this->user->id);
$owner = ($club->getId() * -1);
$this->template->club = $club;
}
$this->template->owner = $owner;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$title = $this->postParam("title");
$description = $this->postParam("description");
$audios = !empty($this->postParam("audios")) ? array_slice(explode(",", $this->postParam("audios")), 0, 1000) : [];
if(empty($title) || iconv_strlen($title) < 1)
$this->flashFail("err", tr("error"), tr("set_playlist_name"));
$playlist = new Playlist;
$playlist->setOwner($owner);
$playlist->setName(substr($title, 0, 125));
$playlist->setDescription(substr($description, 0, 2045));
if($_FILES["cover"]["error"] === UPLOAD_ERR_OK) {
if(!str_starts_with($_FILES["cover"]["type"], "image"))
$this->flashFail("err", tr("error"), tr("not_a_photo"));
try {
$playlist->fastMakeCover($this->user->id, $_FILES["cover"]);
} catch(\Throwable $e) {
$this->flashFail("err", tr("error"), tr("invalid_cover_photo"));
}
}
$playlist->save();
foreach($audios as $audio) {
$audio = $this->audios->get((int)$audio);
if(!$audio || $audio->isDeleted() || !$audio->canBeViewedBy($this->user->identity))
continue;
$playlist->add($audio);
}
$playlist->bookmark(isset($club) ? $club : $this->user->identity);
$this->redirect("/playlist" . $owner . "_" . $playlist->getId());
} else {
if(isset($club)) {
$this->template->audios = iterator_to_array($this->audios->getByClub($club, 1, 10));
$count = (new Audios)->getClubCollectionSize($club);
} else {
$this->template->audios = iterator_to_array($this->audios->getByUser($this->user->identity, 1, 10));
$count = (new Audios)->getUserCollectionSize($this->user->identity);
}
$this->template->pagesCount = ceil($count / 10);
}
}
function renderPlaylistAction(int $id) {
$this->assertUserLoggedIn();
$this->willExecuteWriteAction(true);
$this->assertNoCSRF();
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
$this->redirect("/");
}
$playlist = $this->audios->getPlaylist($id);
if(!$playlist || $playlist->isDeleted())
$this->flashFail("err", "error", tr("invalid_playlist"), null, true);
switch ($this->queryParam("act")) {
case "bookmark":
if(!$playlist->isBookmarkedBy($this->user->identity))
$playlist->bookmark($this->user->identity);
else
$this->flashFail("err", "error", tr("playlist_already_bookmarked"), null, true);
break;
case "unbookmark":
if($playlist->isBookmarkedBy($this->user->identity))
$playlist->unbookmark($this->user->identity);
else
$this->flashFail("err", "error", tr("playlist_not_bookmarked"), null, true);
break;
case "delete":
if($playlist->canBeModifiedBy($this->user->identity)) {
$tmOwner = $playlist->getOwner();
$playlist->delete();
} else
$this->flashFail("err", "error", tr("access_denied"), null, true);
$this->returnJson(["success" => true, "id" => $tmOwner->getRealId()]);
break;
default:
break;
}
$this->returnJson(["success" => true]);
}
function renderEditPlaylist(int $owner_id, int $virtual_id)
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
$playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id);
$page = (int)($this->queryParam("p") ?? 1);
if (!$playlist || $playlist->isDeleted() || !$playlist->canBeModifiedBy($this->user->identity))
$this->notFound();
$this->template->playlist = $playlist;
$this->template->page = $page;
$audios = iterator_to_array($playlist->fetch(1, $playlist->size()));
$this->template->audios = array_slice($audios, 0, 10);
$audiosIds = [];
foreach($audios as $aud)
$audiosIds[] = $aud->getId();
$this->template->audiosIds = implode(",", array_unique($audiosIds)) . ",";
$this->template->ownerId = $owner_id;
$this->template->owner = $playlist->getOwner();
$this->template->pagesCount = $pagesCount = ceil($playlist->size() / 10);
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
$title = $this->postParam("title");
$description = $this->postParam("description");
$new_audios = !empty($this->postParam("audios")) ? explode(",", rtrim($this->postParam("audios"), ",")) : [];
if(empty($title) || iconv_strlen($title) < 1)
$this->flashFail("err", tr("error"), tr("set_playlist_name"));
$playlist->setName(ovk_proc_strtr($title, 125));
$playlist->setDescription(ovk_proc_strtr($description, 2045));
$playlist->setEdited(time());
$playlist->resetLength();
if($_FILES["new_cover"]["error"] === UPLOAD_ERR_OK) {
if(!str_starts_with($_FILES["new_cover"]["type"], "image"))
$this->flashFail("err", tr("error"), tr("not_a_photo"));
try {
$playlist->fastMakeCover($this->user->id, $_FILES["new_cover"]);
} catch(\Throwable $e) {
$this->flashFail("err", tr("error"), tr("invalid_cover_photo"));
}
}
$playlist->save();
DatabaseConnection::i()->getContext()->table("playlist_relations")->where([
"collection" => $playlist->getId()
])->delete();
foreach ($new_audios as $new_audio) {
$audio = (new Audios)->get((int)$new_audio);
if(!$audio || $audio->isDeleted())
continue;
$playlist->add($audio);
}
$this->redirect("/playlist".$playlist->getPrettyId());
}
function renderPlaylist(int $owner_id, int $virtual_id): void
{
$playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id);
$page = (int)($this->queryParam("p") ?? 1);
if (!$playlist || $playlist->isDeleted())
$this->notFound();
$this->template->playlist = $playlist;
$this->template->page = $page;
$this->template->audios = iterator_to_array($playlist->fetch($page, 10));
$this->template->ownerId = $owner_id;
$this->template->owner = $playlist->getOwner();
$this->template->isBookmarked = $this->user->identity && $playlist->isBookmarkedBy($this->user->identity);
$this->template->isMy = $this->user->identity && $playlist->getOwner()->getId() === $this->user->id;
$this->template->canEdit = $this->user->identity && $playlist->canBeModifiedBy($this->user->identity);
}
function renderAction(int $audio_id): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction(true);
$this->assertNoCSRF();
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
$this->redirect("/");
}
$audio = $this->audios->get($audio_id);
if(!$audio || $audio->isDeleted())
$this->flashFail("err", "error", tr("invalid_audio"), null, true);
switch ($this->queryParam("act")) {
case "add":
if($audio->isWithdrawn())
$this->flashFail("err", "error", tr("invalid_audio"), null, true);
if(!$audio->isInLibraryOf($this->user->identity))
$audio->add($this->user->identity);
else
$this->flashFail("err", "error", tr("do_have_audio"), null, true);
break;
case "remove":
if($audio->isInLibraryOf($this->user->identity))
$audio->remove($this->user->identity);
else
$this->flashFail("err", "error", tr("do_not_have_audio"), null, true);
break;
case "remove_club":
$club = (new Clubs)->get((int)$this->postParam("club"));
if(!$club || !$club->canBeModifiedBy($this->user->identity))
$this->flashFail("err", "error", tr("access_denied"), null, true);
if($audio->isInLibraryOf($club))
$audio->remove($club);
else
$this->flashFail("err", "error", tr("group_hasnt_audio"), null, true);
break;
case "add_to_club":
$club = (new Clubs)->get((int)$this->postParam("club"));
if(!$club || !$club->canBeModifiedBy($this->user->identity))
$this->flashFail("err", "error", tr("access_denied"), null, true);
if(!$audio->isInLibraryOf($club))
$audio->add($club);
else
$this->flashFail("err", "error", tr("group_has_audio"), null, true);
break;
case "delete":
if($audio->canBeModifiedBy($this->user->identity))
$audio->delete();
else
$this->flashFail("err", "error", tr("access_denied"), null, true);
break;
case "edit":
$audio = $this->audios->get($audio_id);
if (!$audio || $audio->isDeleted() || $audio->isWithdrawn())
$this->flashFail("err", "error", tr("invalid_audio"), null, true);
if ($audio->getOwner()->getId() !== $this->user->id)
$this->flashFail("err", "error", tr("access_denied"), null, true);
$performer = $this->postParam("performer");
$name = $this->postParam("name");
$lyrics = $this->postParam("lyrics");
$genre = empty($this->postParam("genre")) ? "undefined" : $this->postParam("genre");
$nsfw = (int)($this->postParam("explicit") ?? 0) === 1;
$unlisted = (int)($this->postParam("unlisted") ?? 0) === 1;
if(empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) # FQN of audio must not be more than 128 chars
$this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, true);
$audio->setName($name);
$audio->setPerformer($performer);
$audio->setLyrics(empty($lyrics) ? NULL : $lyrics);
$audio->setGenre($genre);
$audio->setExplicit($nsfw);
$audio->setSearchability($unlisted);
$audio->setEdited(time());
$audio->save();
$this->returnJson(["success" => true, "new_info" => [
"name" => ovk_proc_strtr($audio->getTitle(), 40),
"performer" => ovk_proc_strtr($audio->getPerformer(), 40),
"lyrics" => nl2br($audio->getLyrics() ?? ""),
"lyrics_unformatted" => $audio->getLyrics() ?? "",
"explicit" => $audio->isExplicit(),
"genre" => $audio->getGenre(),
"unlisted" => $audio->isUnlisted(),
]]);
break;
default:
break;
}
$this->returnJson(["success" => true]);
}
function renderPlaylists(int $owner)
{
$this->renderList($owner, "playlists");
}
function renderApiGetContext()
{
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
$this->redirect("/");
}
$ctx_type = $this->postParam("context");
$ctx_id = (int)($this->postParam("context_entity"));
$page = (int)($this->postParam("page") ?? 1);
$perPage = 10;
switch($ctx_type) {
default:
case "entity_audios":
if($ctx_id >= 0) {
$entity = $ctx_id != 0 ? (new Users)->get($ctx_id) : $this->user->identity;
if(!$entity || !$entity->getPrivacyPermission("audios.read", $this->user->identity))
$this->flashFail("err", "Error", "Can't get queue", 80, true);
$audios = $this->audios->getByUser($entity, $page, $perPage);
$audiosCount = $this->audios->getUserCollectionSize($entity);
} else {
$entity = (new Clubs)->get(abs($ctx_id));
if(!$entity || $entity->isBanned())
$this->flashFail("err", "Error", "Can't get queue", 80, true);
$audios = $this->audios->getByClub($entity, $page, $perPage);
$audiosCount = $this->audios->getClubCollectionSize($entity);
}
break;
case "new_audios":
$audios = $this->audios->getNew();
$audiosCount = $audios->size();
break;
case "popular_audios":
$audios = $this->audios->getPopular();
$audiosCount = $audios->size();
break;
case "playlist_context":
$playlist = $this->audios->getPlaylist($ctx_id);
if (!$playlist || $playlist->isDeleted())
$this->flashFail("err", "Error", "Can't get queue", 80, true);
$audios = $playlist->fetch($page, 10);
$audiosCount = $playlist->size();
break;
case "search_context":
$stream = $this->audios->search($this->postParam("query"), 2, $this->postParam("type") === "by_performer");
$audios = $stream->page($page, 10);
$audiosCount = $stream->size();
break;
}
$pagesCount = ceil($audiosCount / $perPage);
# костылёк для получения плееров в пикере аудиозаписей
if((int)($this->postParam("returnPlayers")) === 1) {
$this->template->audios = $audios;
$this->template->page = $page;
$this->template->pagesCount = $pagesCount;
$this->template->count = $audiosCount;
return 0;
}
$audiosArr = [];
foreach($audios as $audio) {
$audiosArr[] = [
"id" => $audio->getId(),
"name" => $audio->getTitle(),
"performer" => $audio->getPerformer(),
"keys" => $audio->getKeys(),
"url" => $audio->getUrl(),
"length" => $audio->getLength(),
"available" => $audio->isAvailable(),
"withdrawn" => $audio->isWithdrawn(),
];
}
$resultArr = [
"success" => true,
"page" => $page,
"perPage" => $perPage,
"pagesCount" => $pagesCount,
"count" => $audiosCount,
"items" => $audiosArr,
];
$this->returnJson($resultArr);
}
}

View file

@ -18,6 +18,8 @@ final class BlobPresenter extends OpenVKPresenter
function renderFile(/*string*/ $dir, string $name, string $format)
{
header("Access-Control-Allow-Origin: *");
$dir = $this->getDirName($dir);
$base = realpath(OPENVK_ROOT . "/storage/$dir");
$path = realpath(OPENVK_ROOT . "/storage/$dir/$name.$format");
@ -37,5 +39,5 @@ final class BlobPresenter extends OpenVKPresenter
readfile($path);
exit;
}
}
}

View file

@ -2,7 +2,7 @@
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, Photo, Video, User, Topic, Post};
use openvk\Web\Models\Entities\Notifications\CommentNotification;
use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos};
use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos, Audios};
final class CommentPresenter extends OpenVKPresenter
{
@ -103,8 +103,27 @@ final class CommentPresenter extends OpenVKPresenter
}
}
}
$audios = [];
if(!empty($this->postParam("audios"))) {
$un = rtrim($this->postParam("audios"), ",");
$arr = explode(",", $un);
if(sizeof($arr) < 11) {
foreach($arr as $dat) {
$ids = explode("_", $dat);
$audio = (new Audios)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
if(!$audio || $audio->isDeleted())
continue;
$audios[] = $audio;
}
}
}
if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1)
if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && sizeof($audios) < 1)
$this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty"));
try {
@ -126,6 +145,9 @@ final class CommentPresenter extends OpenVKPresenter
if(sizeof($videos) > 0)
foreach($videos as $vid)
$comment->attach($vid);
foreach($audios as $audio)
$comment->attach($audio);
if($entity->getOwner()->getId() !== $this->user->identity->getId())
if(($owner = $entity->getOwner()) instanceof User)

View file

@ -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};
use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios};
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->audios = (new Audios)->getRandomThreeAudiosByEntityId($club->getRealId());
$this->template->audiosCount = (new Audios)->getClubCollectionSize($club);
}
$this->template->club = $club;
@ -218,6 +220,7 @@ final class GroupPresenter extends OpenVKPresenter
$club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display"));
$club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1);
$club->setDisplay_Topics_Above_Wall(empty($this->postParam("display_topics_above_wall")) ? 0 : 1);
$club->setEveryone_can_upload_audios(empty($this->postParam("upload_audios")) ? 0 : 1);
$club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed")) ? 0 : 1);
$website = $this->postParam("website") ?? "";

View file

@ -128,7 +128,7 @@ final class MessengerPresenter extends OpenVKPresenter
$messages = [];
$correspondence = new Correspondence($this->user->identity, $correspondent);
foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg) as $message)
foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg, NULL, 0) as $message)
$messages[] = $message->simplify();
header("Content-Type: application/json");

View file

@ -198,6 +198,9 @@ abstract class OpenVKPresenter extends SimplePresenter
{
$user = Authenticator::i()->getUser();
if(!$this->template)
$this->template = new \stdClass;
$this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false;
$this->template->isTimezoned = Session::i()->get("_timezoneOffset");

View file

@ -336,7 +336,10 @@ final class PhotosPresenter extends OpenVKPresenter
if(is_null($this->user) || $this->user->id != $ownerId)
$this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied"));
$redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId;
if(!is_null($album = $photo->getAlbum()))
$redirect = $album->getOwner() instanceof User ? "/id0" : "/club" . $ownerId;
else
$redirect = "/id0";
$photo->isolate();
$photo->delete();

View file

@ -23,7 +23,7 @@ final class ReportPresenter extends OpenVKPresenter
if ($_SERVER["REQUEST_METHOD"] === "POST")
$this->assertNoCSRF();
$act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user"]) ? $this->queryParam("act") : NULL;
$act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"]) ? $this->queryParam("act") : NULL;
if (!$this->queryParam("orig")) {
$this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST");
@ -88,7 +88,7 @@ final class ReportPresenter extends OpenVKPresenter
if(!$id)
exit(json_encode([ "error" => tr("error_segmentation") ]));
if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user"])) {
if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"])) {
if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) {
$report = new Report;
$report->setUser_id($this->user->id);

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{User, Club};
use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Comments, Videos, Applications, Notes};
use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Comments, Videos, Applications, Notes, Audios};
use Chandler\Database\DatabaseConnection;
final class SearchPresenter extends OpenVKPresenter
@ -13,6 +13,7 @@ final class SearchPresenter extends OpenVKPresenter
private $videos;
private $apps;
private $notes;
private $audios;
function __construct(Users $users, Clubs $clubs)
{
@ -23,22 +24,21 @@ final class SearchPresenter extends OpenVKPresenter
$this->videos = new Videos;
$this->apps = new Applications;
$this->notes = new Notes;
$this->audios = new Audios;
parent::__construct();
}
function renderIndex(): void
{
$this->assertUserLoggedIn();
$query = $this->queryParam("query") ?? "";
$type = $this->queryParam("type") ?? "users";
$sorter = $this->queryParam("sort") ?? "id";
$invert = $this->queryParam("invert") == 1 ? "ASC" : "DESC";
$page = (int) ($this->queryParam("p") ?? 1);
$this->willExecuteWriteAction();
if($query != "")
$this->assertUserLoggedIn();
# https://youtu.be/pSAWM5YuXx8
$repos = [
@ -47,7 +47,7 @@ final class SearchPresenter extends OpenVKPresenter
"posts" => "posts",
"comments" => "comments",
"videos" => "videos",
"audios" => "posts",
"audios" => "audios",
"apps" => "apps",
"notes" => "notes"
];
@ -62,7 +62,17 @@ final class SearchPresenter extends OpenVKPresenter
break;
case "rating":
$sort = "rating " . $invert;
break;
break;
case "length":
if($type != "audios") break;
$sort = "length " . $invert;
break;
case "listens":
if($type != "audios") break;
$sort = "listens " . $invert;
break;
}
$parameters = [
@ -86,18 +96,21 @@ final class SearchPresenter extends OpenVKPresenter
"hometown" => $this->queryParam("hometown") != "" ? $this->queryParam("hometown") : NULL,
"before" => $this->queryParam("datebefore") != "" ? strtotime($this->queryParam("datebefore")) : NULL,
"after" => $this->queryParam("dateafter") != "" ? strtotime($this->queryParam("dateafter")) : NULL,
"gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL
"gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL,
"only_performers" => $this->queryParam("only_performers") == "on" ? "1" : NULL,
"with_lyrics" => $this->queryParam("with_lyrics") == "on" ? true : NULL,
];
$repo = $repos[$type] or $this->throwError(400, "Bad Request", "Invalid search entity $type.");
$results = $this->{$repo}->find($query, $parameters, $sort);
$iterator = $results->page($page);
$iterator = $results->page($page, 14);
$count = $results->size();
$this->template->iterator = iterator_to_array($iterator);
$this->template->count = $count;
$this->template->type = $type;
$this->template->page = $page;
$this->template->perPage = 14;
}
}

View file

@ -67,7 +67,7 @@ final class SupportPresenter extends OpenVKPresenter
$this->template->count = $this->tickets->getTicketsCountByUserId($this->user->id);
if($this->template->mode === "list") {
$this->template->page = (int) ($this->queryParam("p") ?? 1);
$this->template->tickets = $this->tickets->getTicketsByUserId($this->user->id, $this->template->page);
$this->template->tickets = iterator_to_array($this->tickets->getTicketsByUserId($this->user->id, $this->template->page));
}
if($this->template->mode === "new")

View file

@ -5,7 +5,7 @@ use openvk\Web\Util\Sms;
use openvk\Web\Themes\Themepacks;
use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification};
use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications, Audios};
use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Util\Validator;
use Chandler\Security\Authenticator;
@ -45,7 +45,10 @@ final class UserPresenter extends OpenVKPresenter
$this->template->videosCount = (new Videos)->getUserVideosCount($user);
$this->template->notes = (new Notes)->getUserNotes($user, 1, 4);
$this->template->notesCount = (new Notes)->getUserNotesCount($user);
$this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($user->getId());
$this->template->audiosCount = (new Audios)->getUserCollectionSize($user);
$this->template->audioStatus = $user->getCurrentAudioStatus();
$this->template->user = $user;
}
}
@ -55,7 +58,7 @@ final class UserPresenter extends OpenVKPresenter
$this->assertUserLoggedIn();
$user = $this->users->get($id);
$page = abs($this->queryParam("p") ?? 1);
$page = abs((int)($this->queryParam("p") ?? 1));
if(!$user)
$this->notFound();
elseif (!$user->getPrivacyPermission('friends.read', $this->user->identity ?? NULL))
@ -169,6 +172,7 @@ final class UserPresenter extends OpenVKPresenter
if ($this->postParam("gender") <= 1 && $this->postParam("gender") >= 0)
$user->setSex($this->postParam("gender"));
$user->setAudio_broadcast_enabled($this->checkbox("broadcast_music"));
if(!empty($this->postParam("phone")) && $this->postParam("phone") !== $user->getPhone()) {
if(!OPENVK_ROOT_CONF["openvk"]["credentials"]["smsc"]["enable"])
@ -241,6 +245,7 @@ final class UserPresenter extends OpenVKPresenter
}
$user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status"));
$user->setAudio_broadcast_enabled($this->postParam("broadcast") == 1);
$user->save();
$this->returnJson([
@ -430,10 +435,11 @@ final class UserPresenter extends OpenVKPresenter
"friends.add",
"wall.write",
"messages.write",
"audios.read",
];
foreach($settings as $setting) {
$input = $this->postParam(str_replace(".", "_", $setting));
$user->setPrivacySetting($setting, min(3, abs($input ?? $user->getPrivacySetting($setting))));
$user->setPrivacySetting($setting, min(3, (int)abs((int)$input ?? $user->getPrivacySetting($setting))));
}
} else if($_GET['act'] === "finance.top-up") {
$token = $this->postParam("key0") . $this->postParam("key1") . $this->postParam("key2") . $this->postParam("key3");
@ -474,6 +480,7 @@ final class UserPresenter extends OpenVKPresenter
} else if($_GET['act'] === "lMenu") {
$settings = [
"menu_bildoj" => "photos",
"menu_muziko" => "audios",
"menu_filmetoj" => "videos",
"menu_mesagoj" => "messages",
"menu_notatoj" => "notes",

View file

@ -233,8 +233,13 @@ final class VKAPIPresenter extends OpenVKPresenter
$this->badMethodCall($object, $method, $parameter->getName());
}
settype($val, $parameter->getType()->getName());
$params[] = $val;
try {
settype($val, $parameter->getType()->getName());
$params[] = $val;
} catch (\Throwable $e) {
// Just ignore the exception, since
// some args are intended for internal use
}
}
define("VKAPI_DECL_VER", $this->requestParam("v") ?? "4.100", false);

View file

@ -3,7 +3,7 @@ namespace openvk\Web\Presenters;
use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos, Audios};
use Chandler\Database\DatabaseConnection;
use Nette\InvalidStateException as ISE;
use Bhaktaraz\RSSGenerator\Item;
@ -154,7 +154,7 @@ final class WallPresenter extends OpenVKPresenter
$this->template->paginatorConf = (object) [
"count" => sizeof($posts),
"page" => (int) ($_GET["p"] ?? 1),
"amount" => sizeof($posts->page((int) ($_GET["p"] ?? 1), $perPage)),
"amount" => $posts->page((int) ($_GET["p"] ?? 1), $perPage)->count(),
"perPage" => $perPage,
];
$this->template->posts = [];
@ -182,7 +182,7 @@ final class WallPresenter extends OpenVKPresenter
$this->template->paginatorConf = (object) [
"count" => $count,
"page" => (int) ($_GET["p"] ?? 1),
"amount" => sizeof($posts),
"amount" => $posts->getRowCount(),
"perPage" => $pPage,
];
foreach($posts as $post)
@ -311,8 +311,27 @@ final class WallPresenter extends OpenVKPresenter
}
}
}
$audios = [];
if(!empty($this->postParam("audios"))) {
$un = rtrim($this->postParam("audios"), ",");
$arr = explode(",", $un);
if(sizeof($arr) < 11) {
foreach($arr as $dat) {
$ids = explode("_", $dat);
$audio = (new Audios)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]);
if(!$audio || $audio->isDeleted())
continue;
$audios[] = $audio;
}
}
}
if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && !$poll && !$note)
if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && sizeof($audios) < 1 && !$poll && !$note)
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
try {
@ -341,6 +360,9 @@ final class WallPresenter extends OpenVKPresenter
if(!is_null($note))
$post->attach($note);
foreach($audios as $audio)
$post->attach($audio);
if($wall > 0 && $wall !== $this->user->identity->getId())
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();

View file

@ -16,6 +16,8 @@
{script "js/node_modules/umbrellajs/umbrella.min.js"}
{script "js/l10n.js"}
{script "js/openvk.cls.js"}
{script "js/node_modules/dashjs/dist/dash.all.min.js"}
{script "js/al_music.js"}
{css "js/node_modules/tippy.js/dist/backdrop.css"}
{css "js/node_modules/tippy.js/dist/border.css"}
@ -122,6 +124,7 @@
<option value="comments">{_s_by_comments}</option>
<option value="videos">{_s_by_videos}</option>
<option value="apps">{_s_by_apps}</option>
<option value="audios">{_s_by_audios}</option>
</select>
</form>
<div class="searchTips" id="srcht" hidden>
@ -140,13 +143,13 @@
<option value="comments" {if str_contains($_SERVER['REQUEST_URI'], "type=comments")}selected{/if}>{_s_by_comments}</option>
<option value="videos" {if str_contains($_SERVER['REQUEST_URI'], "type=videos")}selected{/if}>{_s_by_videos}</option>
<option value="apps" {if str_contains($_SERVER['REQUEST_URI'], "type=apps")}selected{/if}>{_s_by_apps}</option>
<option value="audios" {if str_contains($_SERVER['REQUEST_URI'], "type=audios")}selected{/if}>{_s_by_audios}</option>
</select>
<button class="searchBtn"><span style="color:white;font-weight: 600;font-size:12px;">{_header_search}</span></button>
</form>
<script>
let els = document.querySelectorAll("div.dec")
for(const element of els)
{
for(const element of els) {
element.style.display = "none"
}
</script>
@ -182,6 +185,7 @@
</a>
<a n:if="$thisUser->getLeftMenuItemStatus('photos')" href="/albums{$thisUser->getId()}" class="link">{_my_photos}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('videos')" href="/videos{$thisUser->getId()}" class="link">{_my_videos}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('audios')" href="/audios{$thisUser->getId()}" class="link">{_my_audios}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('messages')" href="/im" class="link">{_my_messages}
<object type="internal/link" n:if="$thisUser->getUnreadMessagesCount() > 0">
(<b>{$thisUser->getUnreadMessagesCount()}</b>)
@ -426,6 +430,12 @@
//]]>
</script>
<script>
window.openvk = {
"audio_genres": {\openvk\Web\Models\Entities\Audio::genres}
}
</script>
{ifset bodyScripts}
{include bodyScripts}
{/ifset}

View file

@ -15,7 +15,7 @@
.navigation-lang .link_new {
display: inline-block;
padding: 25px 25px 20px 25px;
padding: 20px 10px 5px 10px;
text-decoration: none;
border-top: 1px solid #fff;
color: #000;

View file

@ -178,11 +178,29 @@
<h2>{_tour_section_6_title_1|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_6_text_1|noescape}</span></li>
<li><span>{_tour_section_6_text_1|noescape}</span></li>
<li><span>{_tour_section_6_text_2|noescape}</span></li>
<li><span>{_tour_section_6_text_3|noescape}</span></li>
<img src="assets/packages/static/openvk/img/tour/audios.png" width="440">
</ul>
<ul class="listing">
<li><span>{_tour_section_6_text_4|noescape}</span></li>
<img src="assets/packages/static/openvk/img/tour/audios_search.png" width="440">
<li><span>{_tour_section_6_text_5|noescape}</span></li>
<img src="assets/packages/static/openvk/img/tour/audios_upload.png" width="440">
</ul>
<p class="big">{_tour_section_6_bottom_text_1|noescape}</p>
<h2>{_tour_section_6_title_2|noescape}</h2>
<ul class="listing">
<li><span>{_tour_section_6_text_6|noescape}</span></li>
<li><span>{_tour_section_6_text_7|noescape}</span></li>
<img src="assets/packages/static/openvk/img/tour/audios_playlists.png" width="440">
</ul>
<br>
</div>

View file

@ -97,6 +97,9 @@
<li>
<a href="/admin/bannedLinks">{_admin_banned_links}</a>
</li>
<li>
<a href="/admin/music">{_admin_music}</a>
</li>
</ul>
<div class="aui-nav-heading">
<strong>Chandler</strong>

View file

@ -0,0 +1,81 @@
{extends "@layout.xml"}
{block title}
{_edit} {$audio->getName()}
{/block}
{block heading}
{$audio->getName()}
{/block}
{block content}
<div class="aui-tabs horizontal-tabs">
<form class="aui" method="POST">
<div class="field-group">
<label for="id">ID</label>
<input class="text medium-field" type="number" id="id" disabled value="{$audio->getId()}" />
</div>
<div class="field-group">
<label>{_created}</label>
{$audio->getPublicationTime()}
</div>
<div class="field-group">
<label>{_edited}</label>
{$audio->getEditTime() ?? "never"}
</div>
<div class="field-group">
<label for="name">{_name}</label>
<input class="text medium-field" type="text" id="name" name="name" value="{$audio->getTitle()}" />
</div>
<div class="field-group">
<label for="performer">{_performer}</label>
<input class="text medium-field" type="text" id="performer" name="performer" value="{$audio->getPerformer()}" />
</div>
<div class="field-group">
<label for="ext">{_lyrics}</label>
<textarea class="text medium-field" type="text" id="text" name="text" style="resize: vertical;">{$audio->getLyrics()}</textarea>
</div>
<div class="field-group">
<label>{_admin_audio_length}</label>
{$audio->getFormattedLength()}
</div>
<div class="field-group">
<label for="ext">{_genre}</label>
<select class="select medium-field" name="genre">
<option n:foreach='\openvk\Web\Models\Entities\Audio::genres as $genre'
n:attr="selected: $genre == $audio->getGenre()" value="{$genre}">
{$genre}
</option>
</select>
</div>
<div class="field-group">
<label>{_admin_original_file}</label>
<audio controls src="{$audio->getOriginalURL(true)}">
</div>
<hr />
<div class="field-group">
<label for="owner">{_owner}</label>
<input class="text medium-field" type="number" id="owner_id" name="owner" value="{$owner}" />
</div>
<div class="field-group">
<label for="explicit">Explicit</label>
<input class="toggle-large" type="checkbox" id="explicit" name="explicit" value="1" {if $audio->isExplicit()} checked {/if} />
</div>
<div class="field-group">
<label for="deleted">{_deleted}</label>
<input class="toggle-large" type="checkbox" id="deleted" name="deleted" value="1" {if $audio->isDeleted()} checked {/if} />
</div>
<div class="field-group">
<label for="withdrawn">{_withdrawn}</label>
<input class="toggle-large" type="checkbox" id="withdrawn" name="withdrawn" value="1" {if $audio->isWithdrawn()} checked {/if} />
</div>
<hr />
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="{_save}">
</div>
</div>
</form>
</div>
{/block}

View file

@ -0,0 +1,54 @@
{extends "@layout.xml"}
{block title}
{_edit} {$playlist->getName()}
{/block}
{block heading}
{$playlist->getName()}
{/block}
{block content}
<div class="aui-tabs horizontal-tabs">
<form class="aui" method="POST">
<div class="field-group">
<label for="id">ID</label>
<input class="text medium-field" type="number" id="id" disabled value="{$playlist->getId()}" />
</div>
<div class="field-group">
<label for="name">{_name}</label>
<input class="text medium-field" type="text" id="name" name="name" value="{$playlist->getName()}" />
</div>
<div class="field-group">
<label for="ext">{_description}</label>
<textarea class="text medium-field" type="text" id="description" name="description" style="resize: vertical;">{$playlist->getDescription()}</textarea>
</div>
<div class="field-group">
<label for="ext">{_admin_cover_id}</label>
<span id="avatar" class="aui-avatar aui-avatar-project aui-avatar-xlarge">
<span class="aui-avatar-inner">
<img src="{$playlist->getCoverUrl()}" style="object-fit: cover;"></img>
</span>
</span>
<br />
<input class="text medium-field" type="number" id="photo" name="photo" value="{$playlist->getCoverPhotoId()}" />
</div>
<hr />
<div class="field-group">
<label for="owner">{_owner}</label>
<input class="text medium-field" type="number" id="owner_id" name="owner" value="{$playlist->getOwner()->getId()}" />
</div>
<div class="field-group">
<label for="deleted">{_deleted}</label>
<input class="toggle-large" type="checkbox" id="deleted" name="deleted" value="1" {if $playlist->isDeleted()} checked {/if} />
</div>
<hr />
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="button submit" type="submit" value="{_save}">
</div>
</div>
</form>
</div>
{/block}

View file

@ -0,0 +1,135 @@
{extends "@layout.xml"}
{var $search = $mode === "audios"}
{block title}
{_audios}
{/block}
{block heading}
{_audios}
{/block}
{block searchTitle}
{include title}
{/block}
{block content}
<nav class="aui-navgroup aui-navgroup-horizontal">
<div class="aui-navgroup-inner">
<div class="aui-navgroup-primary">
<ul class="aui-nav" resolved="">
<li n:attr="class => $mode === 'audios' ? 'aui-nav-selected' : ''">
<a href="?act=audios">{_audios}</a>
</li>
<li n:attr="class => $mode === 'playlists' ? 'aui-nav-selected' : ''">
<a href="?act=playlists">{_playlists}</a>
</li>
</ul>
</div>
</div>
</nav>
<table class="aui aui-table-list">
{if $mode === "audios"}
{var $audios = iterator_to_array($audios)}
{var $amount = sizeof($audios)}
<thead>
<tr>
<th>ID</th>
<th>{_admin_author}</th>
<th>{_peformer}</th>
<th>{_admin_title}</th>
<th>{_genre}</th>
<th>Explicit</th>
<th>{_withdrawn}</th>
<th>{_deleted}</th>
<th>{_created}</th>
<th>{_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$audios as $audio">
<td>{$audio->getId()}</td>
<td>
{var $owner = $audio->getOwner()}
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$owner->getAvatarUrl('miniscule')}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
</span>
</span>
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
<span n:if="$owner->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
</td>
<td>{$audio->getPerformer()}</td>
<td>{$audio->getTitle()}</td>
<td>{$audio->getGenre()}</td>
<td>{$audio->isExplicit() ? tr("yes") : tr("no")}</td>
<td n:attr="style => $audio->isWithdrawn() ? 'color: red;' : ''">
{$audio->isWithdrawn() ? tr("yes") : tr("no")}
</td>
<td n:attr="style => $audio->isDeleted() ? 'color: red;' : ''">
{$audio->isDeleted() ? tr("yes") : tr("no")}
</td>
<td>{$audio->getPublicationTime()}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/music/{$audio->getId()}/edit">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
</a>
</td>
</tr>
</tbody>
{else}
{var $playlists = iterator_to_array($playlists)}
{var $amount = sizeof($playlists)}
<thead>
<tr>
<th>ID</th>
<th>{_admin_author}</th>
<th>{_name}</th>
<th>{_created_playlist}</th>
<th>{_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$playlists as $playlist">
<td>{$playlist->getId()}</td>
<td>
{var $owner = $playlist->getOwner()}
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$owner->getAvatarUrl('miniscule')}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
</span>
</span>
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
<span n:if="$owner->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span>
</td>
<td>
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$playlist->getCoverURL()}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" />
</span>
</span>
{ovk_proc_strtr($playlist->getName(), 30)}
</td>
<td>{$playlist->getCreationTime()}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/playlist/{$playlist->getId()}/edit">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
</td>
</tr>
</tbody>
{/if}
</table>
<br/>
<div align="right">
{var $isLast = ((10 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="/admin/music?act={($_GET['act'] ?? 'audios')}&p={($_GET['p'] ?? 1) - 1}">&laquo;</a>
<a n:if="$isLast" class="aui-button" href="/admin/music?act={($_GET['act'] ?? 'audios')}&p={($_GET['p'] ?? 1) + 1}">&raquo;</a>
</div>
{/block}

View file

@ -0,0 +1,7 @@
<input type="hidden" name="count" value="{$count}">
<input type="hidden" name="pagesCount" value="{$pagesCount}">
<input type="hidden" name="page" value="{$page}">
{foreach $audios as $audio}
{include "player.xml", audio => $audio, hideButtons => true}
{/foreach}

View file

@ -0,0 +1,95 @@
{extends "../@layout.xml"}
{block title}{_edit_playlist}{/block}
{block header}
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
»
<a href="/audios{$ownerId}">{_audios}</a>
»
<a href="/playlist{$playlist->getPrettyId()}">{_playlist}</a>
»
{_edit_playlist}
{/block}
{block content}
<div class="playlistBlock" style="display: flex;margin-top: 0px;">
<div class="playlistCover">
<a>
<img src="{$playlist->getCoverURL('normal')}" alt="{_playlist_cover}">
</a>
<div class="profile_links" style="width: 139px;">
<a class="profile_link" style="width: 98%;" id="_deletePlaylist" data-id="{$playlist->getId()}">{_delete_playlist}</a>
</div>
</div>
<div style="padding-left: 13px;width:75%">
<div class="playlistInfo">
<input value="{$playlist->getName()}" type="text" name="title" maxlength="125">
</div>
<div class="moreInfo">
<textarea name="description" maxlength="2045" style="margin-top: 11px;">{$playlist->getDescription()}</textarea>
</div>
</div>
</div>
<div style="margin-top: 19px;">
<input id="playlist_query" type="text" style="height: 26px;" placeholder="{_header_search}">
<div class="playlistAudiosContainer editContainer">
<div id="newPlaylistAudios" n:foreach="$audios as $audio">
<div class="playerContainer">
{include "player.xml", audio => $audio, hideButtons => true}
</div>
<div class="attachAudio addToPlaylist" data-id="{$audio->getId()}">
<span>{_remove_from_playlist}</span>
</div>
</div>
</div>
<div class="showMoreAudiosPlaylist" data-page="2" data-playlist="{$playlist->getId()}" n:if="$pagesCount > 1">
{_show_more_audios}
</div>
</div>
<form method="post" id="editPlaylistForm" data-id="{$playlist->getId()}" enctype="multipart/form-data">
<input type="hidden" name="title" maxlength="128" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<textarea style="display:none;" name="description" maxlength="2048" />
<input type="hidden" name="audios">
<input type="file" style="display:none;" name="new_cover" accept=".jpg,.png">
<div style="float:right;margin-top: 8px;">
<button class="button" type="submit">{_save}</button>
</div>
</form>
<script>
document.querySelector("input[name='audios']").value = {$audiosIds}
u("#editPlaylistForm").on("submit", (e) => {
document.querySelector("#editPlaylistForm input[name='title']").value = document.querySelector(".playlistInfo input[name='title']").value
document.querySelector("#editPlaylistForm textarea[name='description']").value = document.querySelector(".playlistBlock textarea[name='description']").value
})
u("#editPlaylistForm input[name='new_cover']").on("change", (e) => {
if(!e.currentTarget.files[0].type.startsWith("image/")) {
fastError(tr("not_a_photo"))
return
}
let image = URL.createObjectURL(e.currentTarget.files[0])
document.querySelector(".playlistCover img").src = image
})
u(".playlistCover img").on("click", (e) => {
document.querySelector("input[name='new_cover']").click()
})
document.querySelector("#editPlaylistForm input[name='new_cover']").value = ""
</script>
{script "js/al_playlists.js"}
{/block}

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
<link rel="icon">
<title>{$audio->getName()}</title>
{css "css/main.css"}
{css "css/audios.css"}
{script "js/node_modules/dashjs/dist/dash.all.min.js"}
{script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/umbrellajs/umbrella.min.js"}
</head>
<body id="audioEmbed">
{include "player.xml", audio => $audio}
{script "js/al_music.js"}
</body>
</html>

View file

@ -0,0 +1,126 @@
{extends "../@layout.xml"}
{block title}
{if $mode == 'list'}
{if $ownerId > 0}
{_audios} {$owner->getMorphedName("genitive", false)}
{else}
{_audios_group}
{/if}
{elseif $mode == 'new'}
{_audio_new}
{elseif $mode == 'popular'}
{_audio_popular}
{else}
{if $ownerId > 0}
{_playlists} {$owner->getMorphedName("genitive", false)}
{else}
{_playlists_group}
{/if}
{/if}
{/block}
{block header}
<div n:if="$mode == 'list'">
<div n:if="$isMy">{_my_audios_small}</div>
<div n:if="!$isMy">
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
»
{_audios}
</div>
</div>
<div n:if="$mode == 'new'">
{_audios}
»
{_audio_new}
</div>
<div n:if="$mode == 'popular'">
{_audios}
»
{_audio_popular}
</div>
<div n:if="$mode == 'playlists'">
{_audios}
»
{if $isMy}{_my_playlists}{else}{_playlists}{/if}
</div>
{/block}
{block content}
{* ref: https://archive.li/P32em *}
{include "bigplayer.xml"}
<input n:if="$mode == 'list'" type="hidden" name="bigplayer_context" data-type="entity_audios" data-entity="{$ownerId}" data-page="{$page}">
<input n:if="$mode == 'new'" type="hidden" name="bigplayer_context" data-type="new_audios" data-entity="0" data-page="1">
<input n:if="$mode == 'popular'" type="hidden" name="bigplayer_context" data-type="popular_audios" data-entity="0" data-page="1">
<div class="bigPlayerDetector"></div>
<div style="width: 100%;display: flex;margin-bottom: -10px;" class="audiosDiv">
<div style="width: 74%;" class="audiosContainer" n:if="$mode != 'playlists'">
<div style="padding: 8px;">
<div n:if="$audiosCount <= 0">
{include "../components/error.xml", description => $ownerId > 0 ? ($ownerId == $thisUser->getId() ? tr("no_audios_thisuser") : tr("no_audios_user")) : tr("no_audios_club")}
</div>
<div n:if="$audiosCount > 0" class="infContainer">
<div class="infObj" n:foreach="$audios as $audio">
{include "player.xml", audio => $audio, club => $club}
</div>
</div>
<div n:if="$mode != 'new' && $mode != 'popular'">
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $audiosCount,
"amount" => sizeof($audios),
"perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE,
"atBottom" => true,
]}
</div>
</div>
</div>
<div style="width: 74%;" n:if="$mode == 'playlists'">
<div style="padding: 8px;">
<div n:if="$playlistsCount <= 0">
{include "../components/error.xml", description => $ownerId > 0 ? ($ownerId == $thisUser->getId() ? tr("no_playlists_thisuser") : tr("no_playlists_user")) : tr("no_playlists_club")}
</div>
<div class="infContainer playlistContainer" n:if="$playlistsCount > 0">
<div class="infObj" n:foreach="$playlists as $playlist">
<a href="/playlist{$playlist->getPrettyId()}">
<div class="playlistCover">
<img src="{$playlist->getCoverURL()}" alt="{_playlist_cover}">
</div>
</a>
<div class="playlistInfo">
<a href="/playlist{$playlist->getPrettyId()}">
<span style="font-size: 12px" class="playlistName">
{ovk_proc_strtr($playlist->getName(), 15)}
</span>
</a>
<a href="{$playlist->getOwner()->getURL()}">{ovk_proc_strtr($playlist->getOwner()->getCanonicalName(), 20)}</a>
</div>
</div>
</div>
<div>
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $playlistsCount,
"amount" => sizeof($playlists),
"perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE,
"atBottom" => true,
]}
</div>
</div>
</div>
{include "tabs.xml"}
</div>
{/block}

View file

@ -0,0 +1,109 @@
{extends "../@layout.xml"}
{block title}
{_new_playlist}
{/block}
{block header}
{if !$_GET["gid"]}
<a href="{$thisUser->getURL()}">{$thisUser->getCanonicalName()}</a>
»
<a href="/audios{$thisUser->getId()}">{_audios}</a>
{else}
<a href="{$club->getURL()}">{$club->getCanonicalName()}</a>
»
<a href="/audios-{$club->getId()}">{_audios}</a>
{/if}
»
{_new_playlist}
{/block}
{block content}
<style>
textarea[name='description'] {
padding: 4px;
}
.playlistInfo {
width: 76%;
margin-left: 8px;
}
</style>
<div style="display:flex;">
<div class="playlistCover" onclick="document.querySelector(`#newPlaylistForm input[name='cover']`).click()">
<a>
<img src="/assets/packages/static/openvk/img/song.jpg" alt="{_playlist_cover}">
</a>
</div>
<div style="padding-left: 17px;width: 75%;" class="plinfo">
<div>
<input type="text" name="title" placeholder="{_title}" maxlength="125" />
</div>
<div class="moreInfo" style="margin-top: 11px;">
<textarea placeholder="{_description}" name="description" maxlength="2045" />
</div>
</div>
</div>
<div style="margin-top: 19px;">
<input id="playlist_query" type="text" style="height: 26px;" placeholder="{_header_search}">
<div class="playlistAudiosContainer editContainer">
<div id="newPlaylistAudios" n:foreach="$audios as $audio">
<div style="width: 78%;float: left;">
{include "player.xml", audio => $audio, hideButtons => true}
</div>
<div class="attachAudio addToPlaylist" data-id="{$audio->getId()}">
<span>{_add_to_playlist}</span>
</div>
</div>
</div>
<div class="showMoreAudiosPlaylist" data-page="2" {if !is_null($_GET["gid"])}data-club="{abs($_GET['gid'])}"{/if} n:if="$pagesCount > 1">
{_show_more_audios}
</div>
</div>
<form method="post" id="newPlaylistForm" enctype="multipart/form-data">
<input type="hidden" name="title" maxlength="125" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<textarea style="display:none;" name="description" maxlength="2045" />
<input type="hidden" name="audios">
<input type="file" style="display:none;" name="cover" accept=".jpg,.png">
<div style="float: right;margin-top: 9px;">
<button class="button" type="submit">{_create}</button>
</div>
</form>
<script>
document.querySelector("input[name='audios']").value = ""
u("#newPlaylistForm").on("submit", (e) => {
document.querySelector("#newPlaylistForm input[name='title']").value = document.querySelector(".plinfo input[name='title']").value
document.querySelector("#newPlaylistForm textarea[name='description']").value = document.querySelector(".plinfo textarea[name='description']").value
})
u("#newPlaylistForm input[name='cover']").on("change", (e) => {
if(!e.currentTarget.files[0].type.startsWith("image/")) {
fastError(tr("not_a_photo"))
return
}
let image = URL.createObjectURL(e.currentTarget.files[0])
document.querySelector(".playlistCover img").src = image
document.querySelector(".playlistCover img").style.display = "block"
})
u(".playlistCover img").on("click", (e) => {
document.querySelector("#newPlaylistForm input[name='cover']").value = ""
e.currentTarget.href = ""
})
document.querySelector("#newPlaylistForm input[name='cover']").value = ""
</script>
{script "js/al_playlists.js"}
{/block}

View file

@ -0,0 +1,81 @@
{extends "../@layout.xml"}
{block title}{_playlist}{/block}
{block headIncludes}
<meta property="og:type" content="music.album">
<meta property="og:title" content="{$playlist->getName()}">
<meta property="og:url" content="{$playlist->getURL()}">
<meta property="og:description" content="{$playlist->getDescription()}">
<meta property="og:image" content="{$playlist->getCoverURL()}">
<script type="application/ld+json">
{
"@context": "http://schema.org/",
"type": "MusicAlbum",
"name": {$playlist->getName()},
"url": {$playlist->getURL()}
}
</script>
{/block}
{block header}
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
»
<a href="/audios{$ownerId}">{_audios}</a>
»
{_playlist}
{/block}
{block content}
{include "bigplayer.xml"}
{php $count = $playlist->size()}
<input type="hidden" name="bigplayer_context" data-type="playlist_context" data-entity="{$playlist->getId()}" data-page="{$page}">
<div class="playlistBlock">
<div class="playlistCover" style="float: left;">
<a href="{$playlist->getCoverURL()}" target="_blank">
<img src="{$playlist->getCoverURL('normal')}" alt="{_playlist_cover}">
</a>
<div class="profile_links" style="width: 139px;" n:if="isset($thisUser)">
<a class="profile_link" style="width: 98%;" href="/playlist{$playlist->getPrettyId()}/edit" n:if="$playlist->canBeModifiedBy($thisUser)">{_edit_playlist}</a>
<a class="profile_link" style="width: 98%;" id="bookmarkPlaylist" data-id="{$playlist->getId()}" n:if="!$isBookmarked">{_bookmark}</a>
<a class="profile_link" style="width: 98%;" id="unbookmarkPlaylist" data-id="{$playlist->getId()}" n:if="$isBookmarked">{_unbookmark}</a>
</div>
</div>
<div style="float: left;padding-left: 13px;width:75%">
<div class="playlistInfo">
<h4 style="border-bottom:unset;">{$playlist->getName()}</h4>
<div class="moreInfo">
{$playlist->getMetaDescription()|noescape}
<div style="margin-top: 11px;">
<span>{nl2br($playlist->getDescriptionHTML())|noescape}</span>
</div>
<hr style="color: #f7f7f7;">
</div>
</div>
<div class="audiosContainer infContainer" style="margin-top: 14px;">
{if $count < 1}
{_empty_playlist}
{else}
<div class="infObj" n:foreach="$audios as $audio">
{include "player.xml", audio => $audio}
</div>
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $count,
"amount" => sizeof($audios),
"perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE,
"atBottom" => true,
]}
{/if}
</div>
</div>
</div>
{/block}

View file

@ -0,0 +1,212 @@
{extends "../@layout.xml"}
{block title}
{_upload_audio}
{/block}
{block header}
{if !is_null($group)}
<a href="{$group->getURL()}">{$group->getCanonicalName()}</a>
»
<a href="/audios-{$group->getId()}">{_audios}</a>
{else}
<a href="{$thisUser->getURL()}">{$thisUser->getCanonicalName()}</a>
»
<a href="/audios{$thisUser->getId()}">{_audios}</a>
{/if}
»
{_upload_audio}
{/block}
{block content}
<div class="container_gray" style="border: 0;margin-top: -10px;">
<div id="upload_container">
<div id="firstStep">
<h4>{_select_audio}</h4><br/>
<b><a href="javascript:void(0)">{_limits}</a></b>
<ul>
<li>{tr("audio_requirements", 1, 30, 25)}</li>
<li>{tr("audio_requirements_2")}</li>
</ul>
<div id="audio_upload">
<form enctype="multipart/form-data" method="POST">
<input type="hidden" name="name" />
<input type="hidden" name="performer" />
<input type="hidden" name="lyrics" />
<input type="hidden" name="genre" />
<input type="hidden" name="explicit" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input id="audio_input" type="file" name="blob" accept="audio/*" style="display:none" />
<input value="{_upload_button}" class="button" type="button" onclick="document.querySelector('#audio_input').click()">
</form>
</div><br/>
<span>{_you_can_also_add_audio_using} <b><a href="/search?type=audios">{_search_audio_inst}</a></b>.<span>
</div>
<div id="lastStep" style="display:none;">
<table cellspacing="7" cellpadding="0" border="0" align="center">
<tbody>
<tr>
<td width="120" valign="top"><span class="nobold">{_audio_name}:</span></td>
<td><input type="text" name="name" autocomplete="off" maxlength="80" /></td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_performer}:</span></td>
<td><input name="performer" type="text" autocomplete="off" maxlength="80" /></td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_genre}:</span></td>
<td>
<select name="genre">
<option n:foreach='\openvk\Web\Models\Entities\Audio::genres as $genre' n:attr="selected: $genre == 'Other'" value="{$genre}">
{$genre}
</option>
</select>
</td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_lyrics}:</span></td>
<td><textarea name="lyrics" style="resize: vertical;max-height: 300px;"></textarea></td>
</tr>
<tr>
<td width="120" valign="top"></td>
<td>
<label><input type="checkbox" name="explicit">{_audios_explicit}</label>
</td>
</tr>
<tr>
<td width="120" valign="top"></td>
<td>
<input class="button" type="button" id="uploadMuziko" value="{_upload_button}">
<input class="button" type="button" id="backToUpload" value="{_select_another_file}">
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script type="module">
import * as id3 from "/assets/packages/static/openvk/js/node_modules/id3js/lib/id3.js";
u("#audio_input").on("change", async function(e) {
let files = e.currentTarget.files
if(files.length <= 0)
return;
document.querySelector("#firstStep").style.display = "none"
document.querySelector("#lastStep").style.display = "block"
let tags = await id3.fromFile(files[0]);
if(tags != null) {
console.log("ID" + tags.kind + " detected, setting values...");
if(tags.title != null)
document.querySelector("#lastStep input[name=name]").value = tags.title;
else
document.querySelector("#lastStep input[name=name]").value = files[0].name
if(tags.artist != null)
document.querySelector("#lastStep input[name=performer]").value = tags.artist;
else
document.querySelector("#lastStep input[name=performer]").value = tr("track_unknown");
if(tags.genre != null) {
if(document.querySelector("#lastStep select[name=genre] > option[value='" + tags.genre + "']") != null) {
document.querySelector("#lastStep select[name=genre]").value = tags.genre;
} else {
console.warn("Unknown genre: " + tags.genre);
document.querySelector("#lastStep select[name=genre]").value = "Other"
}
}
} else {
document.querySelector("#lastStep input[name=name]").value = files[0].name
document.querySelector("#lastStep select[name=genre]").value = "Other"
document.querySelector("#lastStep input[name=performer]").value = tr("track_unknown");
}
});
u("#backToUpload").on("click", (e) => {
document.querySelector("#firstStep").style.display = "block"
document.querySelector("#lastStep").style.display = "none"
document.querySelector("#lastStep input[name=name]").value = ""
document.querySelector("#lastStep input[name=performer]").value = ""
document.querySelector("#lastStep select[name=genre]").value = ""
document.querySelector("#lastStep textarea[name=lyrics]").value = ""
document.querySelector("#audio_input").value = ""
})
u("#uploadMuziko").on("click", (e) => {
var name_ = document.querySelector("#audio_upload input[name=name]");
var perf_ = document.querySelector("#audio_upload input[name=performer]");
var genre_ = document.querySelector("#audio_upload input[name=genre]");
var lyrics_ = document.querySelector("#audio_upload input[name=lyrics]");
var explicit_ = document.querySelector("#audio_upload input[name=explicit]");
name_.value = document.querySelector("#lastStep input[name=name]").value
perf_.value = document.querySelector("#lastStep input[name=performer]").value
genre_.value = document.querySelector("#lastStep select[name=genre]").value
lyrics_.value = document.querySelector("#lastStep textarea[name=lyrics]").value
explicit_.value = document.querySelector("#lastStep input[name=explicit]").checked ? "on" : "off"
$("#audio_upload > form").trigger("submit");
})
$(document).on("dragover drop", (e) => {
e.preventDefault()
return false;
})
$(".container_gray").on("drop", (e) => {
e.originalEvent.dataTransfer.dropEffect = 'move';
e.preventDefault()
let file = e.originalEvent.dataTransfer.files[0]
if(!file.type.startsWith('audio/')) {
MessageBox(tr("error"), tr("only_audios_accepted", escapeHtml(file.name)), [tr("ok")], [() => Function.noop])
return;
}
document.getElementById("audio_input").files = e.originalEvent.dataTransfer.files
u("#audio_input").trigger("change")
})
$("#audio_upload").on("submit", "form", (e) => {
e.preventDefault()
let fd = new FormData(e.currentTarget)
fd.append("ajax", 1)
$.ajax({
type: "POST",
url: location.href,
contentType: false,
processData: false,
data: fd,
beforeSend: function() {
document.querySelector("#lastStep").classList.add("lagged")
document.querySelector("#upload_container").classList.add("uploading")
},
success: (response) => {
document.querySelector("#lastStep").classList.remove("lagged")
document.querySelector("#upload_container").classList.remove("uploading")
if(response.success) {
u("#backToUpload").trigger("click")
NewNotification(tr("success"), tr("audio_successfully_uploaded"), null, () => {
window.location.assign(response.redirect_link)
})
} else {
fastError(response.flash.message)
}
}
})
})
</script>
{/block}

View file

@ -0,0 +1,56 @@
<div class="bigPlayer">
<audio class="audio" />
<div class="paddingLayer">
<div class="playButtons">
<div class="playButton musicIcon" title="{_play_tip} [Space]"></div>
<div class="arrowsButtons">
<div>
<div class="nextButton musicIcon"></div>
</div>
<div>
<div class="backButton musicIcon"></div>
</div>
</div>
</div>
<div class="trackPanel">
<div class="trackInfo">
<div class="trackName">
<b>{_track_unknown}</b>
<span>{_track_noname}</span>
</div>
<div class="timer" style="float:right">
<span class="time">00:00</span>
<span>/</span>
<span class="elapsedTime" style="cursor:pointer">-00:00</span>
</div>
</div>
<div class="track" style="margin-top: -2px;">
<div class="bigPlayerTip">00:00</div>
<div class="selectableTrack">
<div style="width: 95%;position: relative;">&nbsp;
<div class="slider"></div>
</div>
</div>
</div>
</div>
<div class="volumePanel">
<div class="selectableTrack">
<div style="position: relative;width:72%">&nbsp;
<div class="slider"></div>
</div>
</div>
</div>
<div class="additionalButtons">
<div class="repeatButton musicIcon" title="{_repeat_tip} [R]" ></div>
<div class="shuffleButton musicIcon" title="{_shuffle_tip}"></div>
<div class="deviceButton musicIcon" title="{_mute_tip} [M]"></div>
</div>
</div>
</div>

View file

@ -0,0 +1,69 @@
{php $id = $audio->getId() . rand(0, 1000)}
{php $isWithdrawn = $audio->isWithdrawn()}
{php $editable = isset($thisUser) && $audio->canBeModifiedBy($thisUser)}
<div id="audioEmbed-{$id}" data-realid="{$audio->getId()}" {if $hideButtons}data-prettyid="{$audio->getPrettyId()}" data-name="{$audio->getName()}"{/if} data-genre="{$audio->getGenre()}" class="audioEmbed {if !$audio->isAvailable()}processed{/if} {if $isWithdrawn}withdrawn{/if}" data-length="{$audio->getLength()}" data-keys="{json_encode($audio->getKeys())}" data-url="{$audio->getURL()}">
<audio class="audio" />
<div id="miniplayer" class="audioEntry" style="min-height: 39px;">
<div style="display: flex;">
<div class="playerButton">
<div class="playIcon"></div>
</div>
<div class="status" style="margin-top: 12px;">
<div class="mediaInfo noOverflow" style="margin-bottom: -8px; cursor: pointer;display:flex;width: 85%;">
<div class="info">
<strong class="performer">
<a href="/search?query=&type=audios&sort=id&only_performers=on&query={$audio->getPerformer()}">{$audio->getPerformer()}</a>
</strong>
<span class="title {if !empty($audio->getLyrics())}withLyrics{/if}">{$audio->getTitle()}</span>
</div>
<div class="explicitMark" n:if="$audio->isExplicit()"></div>
</div>
</div>
<div class="volume" style="display: flex; flex-direction: column;width:14%;">
<span class="nobold {if !$hideButtons}hideOnHover{/if}" data-unformatted="{$audio->getLength()}" style="text-align: center;margin-top: 12px;">{$audio->getFormattedLength()}</span>
<div class="buttons" style="margin-top: 8px;">
{php $hasAudio = isset($thisUser) && $audio->isInLibraryOf($thisUser)}
{if !$hideButtons}
<div class="remove-icon musicIcon" data-id="{$audio->getId()}" n:if="isset($thisUser) && $hasAudio" ></div>
<div class="add-icon musicIcon hovermeicon" data-id="{$audio->getId()}" n:if="isset($thisUser) && !$hasAudio && !$isWithdrawn" ></div>
<div class="remove-icon-group musicIcon" data-id="{$audio->getId()}" data-club="{$club->getId()}" n:if="isset($thisUser) && isset($club) && $club->canBeModifiedBy($thisUser)" ></div>
<div class="add-icon-group musicIcon" data-id="{$audio->getId()}" n:if="isset($thisUser) && !$isWithdrawn" ></div>
<div class="edit-icon musicIcon" data-lyrics="{$audio->getLyrics()}" data-title="{$audio->getTitle()}" data-performer="{$audio->getPerformer()}" data-explicit="{(int)$audio->isExplicit()}" data-searchable="{(int)!$audio->isUnlisted()}" n:if="isset($thisUser) && $editable && !$isWithdrawn" ></div>
<div class="report-icon musicIcon" data-id="{$audio->getId()}" n:if="isset($thisUser) && !$editable && !$isWithdrawn" ></div>
{/if}
</div>
</div>
</div>
<div class="subTracks">
<div style="width: 100%;">
<div class="track lengthTrack" style="margin-top: 3px;display:none">
<div class="selectableTrack" style="width: 100%;" n:attr="style => $isWithdrawn ? 'display: none;' : ''">
<div style="position: relative;width: calc(100% - 18px);">
<div class="slider"></div>
</div>
</div>
</div>
</div>
<div style="width: 81px;margin-left: 16px;">
<div class="track volumeTrack" style="margin-top: 3px;display:none">
<div class="selectableTrack" style="width: 100%;" n:attr="style => $isWithdrawn ? 'display: none;' : ''">
<div style="position: relative;width: calc(100% - 18px);">
<div class="slider"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="lyrics" n:if="!empty($audio->getLyrics())">
{nl2br($audio->getLyrics())|noescape}
</div>
</div>

View file

@ -0,0 +1,40 @@
<div class="searchOptions newer">
<div class="searchList" style="margin-top:10px">
<a n:attr="id => $mode === 'list' && $isMy ? 'used' : 'ki'" href="/audios{$thisUser->getId()}" n:if="isset($thisUser)">{_my_music}</a>
<a href="/player/upload{if $isMyClub}?gid={abs($ownerId)}{/if}" n:if="isset($thisUser)">{_upload_audio}</a>
<a n:attr="id => $mode === 'new' ? 'used' : 'ki'" href="/audios/new">{_audio_new}</a>
<a n:attr="id => $mode === 'popular' ? 'used' : 'ki'" href="/audios/popular">{_audio_popular}</a>
<a href="/search?type=audios" n:if="isset($thisUser)">{_audio_search}</a>
<hr n:if="isset($thisUser)">
<a n:attr="id => $mode === 'playlists' && $ownerId == $thisUser->getId() ? 'used' : 'ki'" href="/playlists{$thisUser->getId()}" n:if="isset($thisUser)">{_my_playlists}</a>
<a n:if="isset($thisUser)" href="/audios/newPlaylist">{_new_playlist}</a>
{if !$isMy && $mode !== 'popular' && $mode !== 'new'}
<hr>
<a n:if="!$isMy" n:attr="id => $mode === 'list' ? 'used' : 'ki'" href="/audios{$ownerId}">{if $ownerId > 0}{_music_user}{else}{_music_club}{/if}</a>
<a href="/player/upload?gid={abs($ownerId)}" n:if="isset($thisUser) && isset($club) && $club->canUploadAudio($thisUser)">{_upload_audio}</a>
<a n:attr="id => $mode === 'playlists' && $ownerId != $thisUser->getId() ? 'used' : 'ki'" href="/playlists{$ownerId}" n:if="isset($thisUser) && isset($ownerId) && !$isMy">{if $ownerId > 0}{_playlists_user}{else}{_playlists_club}{/if}</a>
<a href="/audios/newPlaylist{if $isMyClub}?gid={abs($ownerId)}{/if}" n:if="isset($thisUser) && $isMyClub">{_new_playlist}</a>
{/if}
{if $friendsAudios}
<div class="friendsAudiosList">
<a href="/audios{$fr->getRealId()}" style="width: 94%;padding-left: 10px;" n:foreach="$friendsAudios as $fr">
<div class="elem">
<img src="{$fr->getAvatarURL()}" />
<div class="additionalInfo">
{php $audioStatus = $fr->getCurrentAudioStatus()}
<span class="name">{$fr->getCanonicalName()}</span>
<span class="desc">{$audioStatus ? $audioStatus->getName() : tr("audios_count", $fr->getAudiosCollectionSize())}</span>
</div>
</div>
</a>
</div>
{/if}
</div>
</div>

View file

@ -102,6 +102,15 @@
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_audios}: </span>
</td>
<td>
<label><input type="checkbox" name="upload_audios" value="1" n:attr="checked => $club->isEveryoneCanUploadAudios()" /> {_everyone_can_upload_audios}</label>
</td>
</tr>
<tr>
<td>

View file

@ -64,7 +64,7 @@
<tr>
<td width="120" valign="top"><span class="nobold">{_role}: </span></td>
<td>
{$club->getOwner()->getId() == $user->getId() ? !$club->isOwnerHidden() || $club->canBeModifiedBy($thisUser) : !is_null($manager) ? tr("administrator") : tr("follower")}
{($club->getOwner()->getId() == $user->getId() ? !$club->isOwnerHidden() || $club->canBeModifiedBy($thisUser) : !is_null($manager)) ? tr("administrator") : tr("follower")}
</td>
</tr>
<tr n:if="$manager && !empty($manager->getComment()) || $club->getOwner()->getId() === $user->getId() && !empty($club->getOwnerComment()) && (!$club->isOwnerHidden() || $club->canBeModifiedBy($thisUser))">

View file

@ -90,6 +90,25 @@
</div>
</div>
</div>
<div>
<div class="content_title_expanded" onclick="hidePanel(this, {$audiosCount});">
{_audios}
</div>
<div>
<div class="content_subtitle">
{tr("audios_count", $audiosCount)}
<div style="float:right;">
<a href="/audios-{$club->getId()}">{_all_title}</a>
</div>
</div>
<div class="content_list long">
<div class="audio" n:foreach="$audios as $audio" style="width: 100%;">
{include "../Audio/player.xml", audio => $audio}
</div>
</div>
</div>
</div>
{presenter "openvk!Wall->wallEmbedded", -$club->getId()}
</div>

View file

@ -90,7 +90,7 @@
<div class="comments_count">
<a href="/note{$dat->getPrettyId()}">
{if sizeof($dat->getCommentsCount()) > 0}
{if $dat->getCommentsCount() > 0}
{_comments} ({$dat->getCommentsCount()})
{else}
{_no_comments}

View file

@ -35,7 +35,7 @@
<div class="limits" style="margin-top:17px">
<b style="color:#45688E">{_admin_limits}</b>
<ul class="blueList" style="margin-left: -25px;margin-top: 1px;">
<ul style="margin-top: 6px;">
<li>{_supported_formats}</li>
<li>{_max_load_photos}</li>
</ul>

View file

@ -46,6 +46,9 @@
<div n:attr="id => ($mode === 'user' ? 'activetabs' : 'ki')" class="tab" mode="user">
<a n:attr="id => ($mode === 'user' ? 'act_tab_a' : 'ki')">Пользователи</a>
</div>
<div n:attr="id => ($mode === 'audio' ? 'activetabs' : 'ki')" class="tab" mode="audio">
<a n:attr="id => ($mode === 'audio' ? 'act_tab_a' : 'ki')">{_audios}</a>
</div>
</center>
<script>

View file

@ -23,6 +23,8 @@
{if $appsSoftDeleting}
{include "./content/app.xml", app => $object}
{/if}
{elseif $type == "audio"}
{include "../Audio/player.xml", audio => $object}
{else}
{include "../components/error.xml", description => tr("version_incompatibility")}
{/if}

View file

@ -139,6 +139,8 @@
}
</style>
<div style="margin-top:-7px">
{var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $count,
@ -153,7 +155,6 @@
<div>
{include searchOptions}
{var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
<div class="container_gray borderup" style="float:left;width:73.3%;">
{if sizeof($data) > 0}
{if $type == "users" || $type == "groups" || $type == "apps"}
@ -204,12 +205,14 @@
</div>
{elseif $type == "videos"}
{foreach $data as $dat}
<div class="content">
{include "../components/video.xml", video => $dat}
</div>
<div class="content">
{include "../components/video.xml", video => $dat}
</div>
{/foreach}
{elseif $type == "audios"}
хуй
{foreach $data as $dat}
{include "../Audio/player.xml", audio => $dat}
{/foreach}
{/if}
{else}
{ifset customErrorMessage}
@ -230,12 +233,13 @@
{block searchOptions}
<div class="searchOptions">
<ul class="searchList">
<li {if $type === "users"} id="used"{/if}><a href="/search?type=users{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_people} {if $type === "users"} ({$count}){/if}</a></li>
<li {if $type === "groups"} id="used"{/if}><a href="/search?type=groups{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_groups} {if $type === "groups"} ({$count}){/if}</a></li>
<li {if $type === "comments"}id="used"{/if}><a href="/search?type=comments{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id">{_s_comments} {if $type === "comments"}({$count}){/if}</a></li>
<li {if $type === "posts"} id="used"{/if}><a href="/search?type=posts{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_posts} {if $type === "posts"} ({$count}){/if}</a></li>
<li {if $type === "videos"} id="used"{/if}><a href="/search?type=videos{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_videos} {if $type === "videos"} ({$count}){/if}</a></li>
<li {if $type === "apps"} id="used"{/if}><a href="/search?type=apps{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_apps} {if $type === "apps"} ({$count}){/if}</a></li>
<a {if $type === "users"} id="used"{/if} href="/search?type=users{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_people} {if $type === "users"} ({$count}){/if}</a>
<a {if $type === "groups"} id="used"{/if} href="/search?type=groups{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_groups} {if $type === "groups"} ({$count}){/if}</a>
<a {if $type === "comments"}id="used"{/if} href="/search?type=comments{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id">{_s_comments} {if $type === "comments"}({$count}){/if}</a>
<a {if $type === "posts"} id="used"{/if} href="/search?type=posts{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_posts} {if $type === "posts"} ({$count}){/if}</a>
<a {if $type === "videos"} id="used"{/if} href="/search?type=videos{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_videos} {if $type === "videos"} ({$count}){/if}</a>
<a {if $type === "apps"} id="used"{/if} href="/search?type=apps{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_apps} {if $type === "apps"} ({$count}){/if}</a>
<a {if $type === "audios"} id="used"{/if} href="/search?type=audios{if $_GET['query']}&query={urlencode($_GET['query'])}{/if}&sort=id"> {_s_audios} {if $type === "audios"} ({$count}){/if}</a>
</ul>
<div class="searchOption">
@ -249,6 +253,11 @@
<option value="rating" {if $_GET["sort"] == "rating"}selected{/if}>{_s_order_by_rating}</option>
{/if}
{/if}
{if $type == "audios"}
<option value="length" n:attr="selected => $_GET['sort'] == 'length'">{_s_order_by_length}</option>
<option value="listens" n:attr="selected => $_GET['sort'] == 'listens'">{_s_order_by_listens}</option>
{/if}
</select>
<div id="invertor">
<input type="checkbox" name="invert" value="1" form="searcher" {if !is_null($_GET['invert']) && $_GET['invert'] == "1"}checked{/if}>{_s_order_invert}
@ -351,9 +360,19 @@
<input type="text" value="{if !is_null($_GET['fav_quote'])}{$_GET['fav_quote']}{/if}" form="searcher" placeholder="{_favorite_quotes}" name="fav_quote">
</div>
</div>
<!--<input name="with_photo" type="checkbox" {if !is_null($_GET['with_photo']) && $_GET['with_photo'] == "on"}checked{/if} form="searcher">{_s_with_photo}-->
{/if}
<input class="button" type="button" id="dnt" value="{_reset}" onclick="resetSearch()">
{if $type == "audios"}
<div class="searchOption">
<div class="searchOptionName" id="n_main_audio" onclick="hideParams('main_audio')"><img src="/assets/packages/static/openvk/img/hide.png" class="searchHide">{_s_main}</div>
<div class="searchOptionBlock" id="s_main_audio">
<label><input type="checkbox" name="only_performers" n:attr="checked => !empty($_GET['only_performers'])" form="searcher">{_s_only_performers}</label><br>
<label><input type="checkbox" name="with_lyrics" n:attr="checked => !empty($_GET['with_lyrics'])" form="searcher">{_s_with_lyrics}</label>
</div>
</div>
{/if}
<input class="button" type="button" id="dnt" value="{_reset}" onclick="resetSearch()">
</div>
{/block}
{/block}

View file

@ -160,6 +160,13 @@
</select>
</td>
</tr>
<tr>
<td width="120" valign="top">
</td>
<td>
<label><input type="checkbox" name="broadcast_music" n:attr="checked => $user->isBroadcastEnabled()">{_broadcast_audio}</label>
</td>
</tr>
<tr>
<td>

View file

@ -323,6 +323,19 @@
</select>
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_privacy_setting_view_audio}</span>
</td>
<td>
<select name="audios.read" style="width: 164px;">
<option value="3" {if $user->getPrivacySetting('audios.read') == 3}selected{/if}>{_privacy_value_anybody_dative}</option>
<option value="2" {if $user->getPrivacySetting('audios.read') == 2}selected{/if}>{_privacy_value_users}</option>
<option value="1" {if $user->getPrivacySetting('audios.read') == 1}selected{/if}>{_privacy_value_friends_dative}</option>
<option value="0" {if $user->getPrivacySetting('audios.read') == 0}selected{/if}>{_privacy_value_only_me_dative}</option>
</select>
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_privacy_setting_see_notes}</span>
@ -609,6 +622,17 @@
<span class="nobold">{_my_videos}</span>
</td>
</tr>
<tr>
<td width="120" valign="top" align="right">
<input
n:attr="checked => $user->getLeftMenuItemStatus('audios')"
type="checkbox"
name="menu_muziko" />
</td>
<td>
<span class="nobold">{_my_audios}</span>
</td>
</tr>
<tr>
<td width="120" valign="top" align="right">
<input

View file

@ -417,6 +417,10 @@
<form name="status_popup_form" onsubmit="changeStatus(); return false;">
<div style="margin-bottom: 10px;">
<input type="text" name="status" size="50" value="{$user->getStatus()}" />
<label style="width: 316px;display: block;">
<input type="checkbox" name="broadcast" n:attr="checked => $user->isBroadcastEnabled()" />
{_broadcast_audio}
</label>
</div>
<input type="hidden" name="hash" value="{$csrfToken}" />
<button type="submit" name="submit" class="button" style="height: 22px;">{_send}</button>
@ -425,11 +429,20 @@
<div class="accountInfo clearFix">
<div class="profileName">
<h2>{$user->getFullName()}</h2>
{if !is_null($user->getStatus())}
<div n:class="page_status, $thatIsThisUser ? page_status_edit_button" n:attr="id => $thatIsThisUser ? page_status_text : NULL">{$user->getStatus()}</div>
{elseif $thatIsThisUser}
<div class="page_status">
<div n:class="edit_link, $thatIsThisUser ? page_status_edit_button" id="page_status_text">[ {_change_status} ]</div>
{if !$audioStatus}
{if !is_null($user->getStatus())}
<div n:class="page_status, $thatIsThisUser ? page_status_edit_button" n:attr="id => $thatIsThisUser ? page_status_text : NULL">{$user->getStatus()}</div>
{elseif $thatIsThisUser}
<div class="page_status">
<div n:class="edit_link, $thatIsThisUser ? page_status_edit_button" id="page_status_text">[ {_change_status} ]</div>
</div>
{/if}
{else}
<div class="page_status" style="display: flex;">
<div n:class="audioStatus, $thatIsThisUser ? page_status_edit_button" id="page_status_text">
{$audioStatus->getName()}
</div>
</div>
{/if}
</div>
@ -530,7 +543,7 @@
{var $musics = explode(", ", $user->getFavoriteMusic())}
{foreach $musics as $music}
<a href="/search?type=users&query=&fav_mus={urlencode($music)}">{$music}</a>{if $music != end($musics)},{/if}
<a href="/search?type=audios&query={urlencode($music)}">{$music}</a>{if $music != end($musics)},{/if}
{/foreach}
</td>
</tr>
@ -594,6 +607,25 @@
</div>
</div>
<div n:if="$audiosCount > 0 && $user->getPrivacyPermission('audios.read', $thisUser ?? NULL)">
<div class="content_title_expanded" onclick="hidePanel(this, {$audiosCount});">
{_audios}
</div>
<div>
<div class="content_subtitle">
{tr("audios_count", $audiosCount)}
<div style="float:right;">
<a href="/audios{$user->getId()}">{_all_title}</a>
</div>
</div>
<div class="content_list long">
<div class="audio" n:foreach="$audios as $audio" style="width: 100%;">
{include "../Audio/player.xml", audio => $audio}
</div>
</div>
</div>
</div>
<div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce'] && ($giftCount = $user->getGiftCount()) > 0">
<div class="content_title_expanded" onclick="hidePanel(this, {$giftCount});">
{_gifts}
@ -739,12 +771,14 @@
async function changeStatus() {
const status = document.status_popup_form.status.value;
const broadcast = document.status_popup_form.broadcast.checked;
document.status_popup_form.submit.innerHTML = "<div class=\"button-loading\"></div>";
document.status_popup_form.submit.disabled = true;
const formData = new FormData();
formData.append("status", status);
formData.append("broadcast", Number(broadcast));
formData.append("hash", document.status_popup_form.hash.value);
const response = await ky.post("/edit?act=status", {body: formData});

View file

@ -10,6 +10,7 @@
{css "css/dialog.css"}
{css "css/notifications.css"}
{css "css/avataredit.css"}
{css "css/audios.css"}
{if $isXmas}
{css "css/xmas.css"}
@ -27,6 +28,7 @@
{css "css/dialog.css"}
{css "css/notifications.css"}
{css "css/avataredit.css"}
{css "css/audios.css"}
{if $isXmas}
{css "css/xmas.css"}
@ -50,7 +52,7 @@
{css "css/dialog.css"}
{css "css/nsfw-posts.css"}
{css "css/notifications.css"}
{css "css/avataredit.css"}
{css "css/audios.css"}
{if $isXmas}
{css "css/xmas.css"}

View file

@ -50,6 +50,10 @@
{else}
{include "post.xml", post => $attachment, compact => true}
{/if}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Audio}
<div style="width:100%;">
{include "../Audio/player.xml", audio => $attachment}
</div>
{else}
<span style="color:red;">{_version_incompatibility}</span>
{/if}

View file

@ -22,6 +22,7 @@
</div>
<div class="post-has-videos"></div>
<div class="post-has-audios"></div>
<div n:if="$postOpts ?? true" class="post-opts">
{var $anonEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['wall']['anonymousPosting']['enable']}
@ -62,6 +63,7 @@
</div>
<input type="hidden" name="photos" value="" />
<input type="hidden" name="videos" value="" />
<input type="hidden" name="audios" value="" />
<input type="hidden" name="poll" value="none" />
<input type="hidden" id="note" name="note" value="none" />
<input type="hidden" name="type" value="1" />
@ -85,6 +87,10 @@
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-vnd.rn-realmedia.png" />
{_video}
</a>
<a id="_audioAttachment">
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/audio-ac3.png" />
{_audio}
</a>
<a n:if="$notes ?? false" href="javascript:attachNote({$textAreaId})">
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-srt.png" />
{_note}
@ -110,6 +116,7 @@
u("#post-buttons{$textAreaId} input[name='videos']")["nodes"].at(0).value = ""
u("#post-buttons{$textAreaId} input[name='photos']")["nodes"].at(0).value = ""
u("#post-buttons{$textAreaId} input[name='audios']")["nodes"].at(0).value = ""
</script>
{if $graffiti}

View file

@ -7,6 +7,7 @@ services:
- openvk\Web\Presenters\CommentPresenter
- openvk\Web\Presenters\PhotosPresenter
- openvk\Web\Presenters\VideosPresenter
- openvk\Web\Presenters\AudioPresenter
- openvk\Web\Presenters\BlobPresenter
- openvk\Web\Presenters\GroupPresenter
- openvk\Web\Presenters\SearchPresenter
@ -33,6 +34,7 @@ services:
- openvk\Web\Models\Repositories\Albums
- openvk\Web\Models\Repositories\Clubs
- openvk\Web\Models\Repositories\Videos
- openvk\Web\Models\Repositories\Audios
- openvk\Web\Models\Repositories\Notes
- openvk\Web\Models\Repositories\Tickets
- openvk\Web\Models\Repositories\Messages

View file

@ -187,6 +187,34 @@ routes:
handler: "Videos->edit"
- url: "/video{num}_{num}/remove"
handler: "Videos->remove"
- url: "/player/upload"
handler: "Audio->upload"
- url: "/audios{num}"
handler: "Audio->list"
- url: "/audios/popular"
handler: "Audio->popular"
- url: "/audios/new"
handler: "Audio->new"
- url: "/audio{num}_{num}/embed.xhtml"
handler: "Audio->embed"
- url: "/audio{num}/listen"
handler: "Audio->listen"
- url: "/audios/search"
handler: "Audio->search"
- url: "/audios/newPlaylist"
handler: "Audio->newPlaylist"
- url: "/audios/context"
handler: "Audio->apiGetContext"
- url: "/playlist{num}_{num}"
handler: "Audio->playlist"
- url: "/playlist{num}_{num}/edit"
handler: "Audio->editPlaylist"
- url: "/playlist{num}/action"
handler: "Audio->playlistAction"
- url: "/playlists{num}"
handler: "Audio->playlists"
- url: "/audio{num}/action"
handler: "Audio->action"
- url: "/{?!club}{num}"
handler: "Group->view"
placeholders:
@ -221,24 +249,6 @@ routes:
handler: "Topics->edit"
- url: "/topic{num}_{num}/delete"
handler: "Topics->delete"
- url: "/audios{num}"
handler: "Audios->app"
- url: "/audios{num}.json"
handler: "Audios->apiListSongs"
- url: "/audios/popular.json"
handler: "Audios->apiListPopSongs"
- url: "/audios/playlist{num}.json"
handler: "Audios->apiListPlaylists"
- url: "/audios/search.json"
handler: "Audios->apiSearch"
- url: "/audios/add.json"
handler: "Audios->apiAdd"
- url: "/audios/playlist.json"
handler: "Audios->apiAddPlaylist"
- url: "/audios/upload.json"
handler: "Audios->apiUpload"
- url: "/audios/beacon"
handler: "Audios->apiBeacon"
- url: "/im"
handler: "Messenger->index"
- url: "/im/sel{num}"
@ -341,6 +351,12 @@ routes:
handler: "Admin->bannedLink"
- url: "/admin/bannedLink/id{num}/unban"
handler: "Admin->unbanLink"
- url: "/admin/music"
handler: "Admin->music"
- url: "/admin/music/{num}/edit"
handler: "Admin->editMusic"
- url: "/admin/playlist/{num}/edit"
handler: "Admin->editPlaylist"
- url: "/admin/user{num}/bans"
handler: "Admin->bansHistory"
- url: "/upload/photo/{text}"

661
Web/static/css/audios.css Normal file
View file

@ -0,0 +1,661 @@
.noOverflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.overflowedName {
position: absolute;
z-index: 99;
}
.musicIcon {
background-image: url('/assets/packages/static/openvk/img/audios_controls.png');
background-repeat: no-repeat;
cursor: pointer;
}
.musicIcon:hover {
filter: brightness(99%);
}
.musicIcon.pressed {
filter: brightness(150%);
}
.bigPlayer {
background-color: rgb(240, 241, 242);
margin-left: -10px;
margin-top: -10px;
width: 102.8%;
height: 46px;
border-bottom: 1px solid #d8d8d8;
box-shadow: 1px 0px 8px 0px rgba(34, 60, 80, 0.2);
position: relative;
}
.bigPlayer.floating {
position: fixed;
z-index: 199;
width: 627px;
margin-top: -76px;
}
.bigPlayer .paddingLayer {
padding: 0px 0px 0px 14px;
}
.bigPlayer .paddingLayer .playButtons {
padding: 12px 0px;
}
.bigPlayer .paddingLayer .playButtons .playButton {
width: 22px;
height: 22px;
float: left;
background-position-x: -72px;
}
.bigPlayer .paddingLayer .playButtons .playButton.pause {
background-position-x: -168px;
}
.bigPlayer .paddingLayer .playButtons .nextButton {
width: 16px;
height: 16px;
background-position-y: -47px;
}
.bigPlayer .paddingLayer .playButtons .backButton {
width: 16px;
height: 16px;
background-position-y: -47px;
background-position-x: -16px;
margin-left: 6px;
}
.bigPlayer .paddingLayer .additionalButtons {
float: left;
margin-top: -6px;
width: 11%;
}
.bigPlayer .paddingLayer .additionalButtons .repeatButton {
width: 14px;
height: 16px;
background-position-y: -49px;
background-position-x: -31px;
margin-left: 7px;
float: left;
}
.broadcastButton {
width: 16px;
height: 12px;
background-position-y: -50px;
background-position-x: -64px;
margin-left: 6px;
float: left;
}
.broadcastButton.atProfile {
width: 13px;
height: 12px;
background-position-y: -50px;
background-position-x: -64px;
margin-left: 0px !important;
margin-right: 5px;
float: left;
}
.bigPlayer .paddingLayer .additionalButtons .shuffleButton {
width: 14px;
height: 16px;
background-position: -50px -50px;
margin-left: 7px;
float: left;
}
.bigPlayer .paddingLayer .additionalButtons .deviceButton {
width: 12px;
height: 16px;
background-position: -202px -50px;
margin-left: 7px;
float: left;
}
.bigPlayer .paddingLayer .playButtons .arrowsButtons {
float: left;
display: flex;
padding-left: 4px;
padding-top: 1.2px;
}
.bigPlayer .paddingLayer .trackPanel {
float: left;
margin-top: -13px;
margin-left: 13px;
width: 63%;
position: relative;
}
.bigPlayer .paddingLayer .bigPlayerTip {
display: none;
z-index: 999;
background: #cecece;
padding: 3px;
top: -3px;
position: absolute;
transition: all .1s ease-out;
user-select: none;
}
.bigPlayer .paddingLayer .volumePanel {
width: 73px;
float: left;
}
.bigPlayer .paddingLayer .slider, .audioEmbed .track .slider {
width: 18px;
height: 7px;
background: #606060;
position: absolute;
bottom: 0;
top: 0px;
pointer-events: none;
}
.bigPlayer .paddingLayer .trackInfo .timer {
float: right;
margin-right: 8px;
font-size: 10px;
}
.bigPlayer .paddingLayer .trackInfo .trackName {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 81%;
display: inline-block;
}
.bigPlayer .paddingLayer .trackInfo .timer span {
font-size: 10px;
}
.bigPlayer .paddingLayer .trackInfo b:hover {
text-decoration: underline;
cursor: pointer;
}
.audioEmbed .track > .selectableTrack, .bigPlayer .selectableTrack {
margin-top: 3px;
width: calc(100% - 8px);
border-top: #606060 1px solid;
height: 6px;
position: relative;
user-select: none;
}
#audioEmbed {
cursor: pointer;
user-select: none;
background: #eee;
height: 40px;
width: 486px;
position: absolute;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
overflow: hidden;
border: 1px solid #8B8B8B;
}
.audioEntry.nowPlaying {
background: #606060;
border: 1px solid #4f4f4f;
box-sizing: border-box;
}
.audioEntry.nowPlaying .playIcon {
background-position-y: -16px !important;
}
.audioEntry.nowPlaying:hover {
background: #4e4e4e !important;
}
.audioEntry.nowPlaying .performer a {
color: #f4f4f4 !important;
}
.audioEntry .performer a {
color: #4C4C4C;
}
.audioEntry.nowPlaying .title {
color: #fff;
}
.audioEntry.nowPlaying .status {
color: white;
}
.audioEntry.nowPlaying .volume .nobold {
color: white !important;
}
.audioEntry.nowPlaying .buttons .musicIcon, .audioEntry.nowPlaying .explicitMark {
filter: brightness(187%) opacity(72%);
}
.audioEntry {
height: 100%;
position: relative;
width: 100%;
}
.audioEntry .playerButton {
position: relative;
padding: 10px 9px 9px 9px;
width: 16px;
height: 16px;
}
.audioEntry .subTracks {
display: flex;
padding-bottom: 5px;
padding-left: 8px;
padding-right: 12px;
}
.audioEntry .playerButton .playIcon {
background-image: url('/assets/packages/static/openvk/img/play_buttons.gif');
cursor: pointer;
width: 16px;
height: 16px;
background-repeat: no-repeat;
}
.audioEntry .playerButton .playIcon.paused {
background-position-y: -16px;
}
.audioEntry .status {
overflow: hidden;
display: grid;
grid-template-columns: 1fr;
width: 85%;
height: 23px;
}
.audioEntry .status strong {
color: #4C4C4C;
}
.audioEmbed .track {
display: none;
padding: 4px 0;
}
.audioEmbed .track, .audioEmbed.playing .track {
display: unset;
}
.audioEntry:hover {
background: #EEF0F2;
border-radius: 2px;
}
.audioEntry:hover .buttons {
display: block;
}
.audioEntry:hover .volume .hideOnHover {
display: none;
}
.audioEntry .buttons {
display: none;
width: 62px;
height: 20px;
position: absolute;
right: 3%;
top: 2px;
/* чтоб избежать заедания во время ховера кнопки добавления */
clip-path: inset(0 0 0 0);
}
.audioEntry .buttons .edit-icon {
width: 11px;
height: 11px;
float: right;
margin-right: 4px;
margin-top: 3px;
background-position: -137px -51px;
}
.audioEntry .buttons .add-icon {
width: 11px;
height: 11px;
float: right;
background-position: -80px -52px;
margin-top: 3px;
margin-left: 2px;
}
.audioEntry .buttons .add-icon-group {
width: 14px;
height: 11px;
float: right;
background-position: -94px -52px;
margin-top: 3px;
transition: margin-right 0.1s ease-out, opacity 0.1s ease-out;
}
.audioEntry .buttons .report-icon {
width: 12px;
height: 11px;
float: right;
background-position: -67px -51px;
margin-top: 3px;
margin-right: 3px;
}
.add-icon-noaction {
background-image: url('/assets/packages/static/openvk/img/audios_controls.png');
width: 11px;
height: 11px;
float: right;
background-position: -94px -52px;
margin-top: 2px;
margin-right: 2px;
}
.audioEntry .buttons .remove-icon {
margin-top: 3px;
width: 11px;
height: 11px;
margin-left: 2px;
float: right;
background-position: -108px -52px;
}
.audioEntry .buttons .remove-icon-group {
margin-top: 3px;
width: 13px;
height: 11px;
float: right;
background-position: -122px -52px;
margin-left: 3px;
margin-right: 3px;
}
.audioEmbed .lyrics {
display: none;
padding: 6px 33px 10px 33px;
}
.audioEmbed .lyrics.showed {
display: block !important;
}
.audioEntry .withLyrics {
user-select: none;
color: #507597;
}
.audioEntry .withLyrics:hover {
text-decoration: underline;
}
.audioEmbed.withdrawn .status > *, .audioEmbed.processed .status > *, .audioEmbed.withdrawn .playerButton > *, .audioEmbed.processed .playerButton > * {
pointer-events: none;
}
.audioEmbed.withdrawn {
filter: opacity(0.8);
}
.playlistCover img {
max-width: 135px;
max-height: 135px;
}
.playlistBlock {
margin-top: 14px;
}
.playlistContainer {
display: grid;
grid-template-columns: repeat(3, 146px);
gap: 18px 10px;
}
.playlistContainer .playlistCover {
width: 111px;
height: 111px;
display: flex;
background: #c4c4c4;
}
.playlistContainer .playlistCover img {
max-width: 111px;
max-height: 111px;
margin: auto;
}
.ovk-diag-body .searchBox {
background: #e6e6e6;
padding-top: 10px;
height: 35px;
padding-left: 10px;
padding-right: 10px;
display: flex;
}
.ovk-diag-body .searchBox input {
height: 24px;
margin-right: -1px;
width: 77%;
}
.ovk-diag-body .searchBox select {
width: 29%;
padding-left: 8px;
height: 24px;
}
.ovk-diag-body .audiosInsert {
height: 82%;
padding: 9px 5px 9px 9px;
overflow-y: auto;
}
.attachAudio {
float: left;
width: 28%;
height: 26px;
padding-top: 11px;
text-align: center;
}
.attachAudio span {
user-select: none;
}
.attachAudio:hover {
background: rgb(236, 236, 236);
cursor: pointer;
}
.playlistCover img {
cursor: pointer;
}
.explicitMark {
margin-top: 2px;
margin-left: 3px;
width: 11px;
height: 11px;
background: url('/assets/packages/static/openvk/img/explicit.svg');
background-repeat: no-repeat;
}
.audioStatus span {
color: #2B587A;
}
.audioStatus span:hover {
text-decoration: underline;
cursor: pointer;
}
.audioStatus {
padding-top: 2px;
padding-bottom: 3px;
}
/* <center> 🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣*/
.audiosDiv center span {
color: #707070;
margin: 120px 0px !important;
display: block;
}
.audiosDiv center {
margin-left: -10px;
}
.playlistInfo {
display: flex;
flex-direction: column;
}
.playlistInfo .playlistName {
font-weight: 600;
}
.searchList.floating {
position: fixed;
z-index: 199;
width: 156px;
margin-top: -65px !important;
}
.audiosSearchBox input[type='search'] {
height: 25px;
width: 77%;
padding-left: 21px;
padding-top: 4px;
background: rgb(255, 255, 255) url("/assets/packages/static/openvk/img/search_icon.png") 5px 6px no-repeat;
}
.audiosSearchBox {
padding-bottom: 10px;
padding-top: 7px;
display: flex;
}
.audiosSearchBox select {
width: 30%;
padding-left: 7px;
margin-left: -2px;
}
.audioStatus {
color: #2B587A;
margin-top: -3px;
}
.audioStatus::before {
background-image: url('/assets/packages/static/openvk/img/audios_controls.png');
background-repeat: no-repeat;
width: 11px;
height: 11px;
background-position: -66px -51px;
margin-top: 1px;
display: inline-block;
vertical-align: bottom;
content: "";
padding-right: 2px;
}
.friendsAudiosList {
margin-left: -7px;
margin-top: 8px;
}
.friendsAudiosList .elem {
display: flex;
padding: 1px 1px;
width: 148px;
}
.friendsAudiosList .elem img {
width: 30px;
border-radius: 2px;
object-fit: cover;
height: 31px;
min-width: 30px;
}
.friendsAudiosList .elem .additionalInfo {
margin-left: 7px;
padding-top: 1px;
width: 100%;
display: flex;
flex-direction: column;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.friendsAudiosList .elem .additionalInfo .name {
color: #2B587A;
}
.friendsAudiosList #used .elem .additionalInfo .name {
color: #F4F4F4;
}
.friendsAudiosList .elem .additionalInfo .desc {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
color: #878A8F;
font-size: 11px;
}
.friendsAudiosList #used .elem .additionalInfo .desc {
color: #F4F4F4;
}
.friendsAudiosList .elem:hover {
background: #E8EBF0;
cursor: pointer;
}
.friendsAudiosList #used .elem:hover {
background: #787878;
cursor: pointer;
}
.editContainer {
display:table;
clear:both;
width:100%;
margin-top: 10px;
}
.editContainer .playerContainer {
width: 78%;
float: left;
max-width: 78%;
min-width: 68%;
}
.addToPlaylist {
width: 22%;
}

View file

@ -81,3 +81,10 @@ div.ovk-video > div > img
object-fit: cover;
border-radius: 100px;
}
.friendsAudiosList .elem img {
width: 30px;
border-radius: 100px;
height: 31px;
min-width: 30px;
}

View file

@ -405,6 +405,7 @@ h1 {
width: 200px;
text-align: left;
cursor: pointer;
font-family: tahoma, verdana, arial, sans-serif;
}
.profile_link_form {
@ -1242,64 +1243,6 @@ table.User {
border-bottom: 1px solid #c3cad2;
}
.music-app {
display: grid;
}
.music-app--player {
display: grid;
grid-template-columns: 32px 32px 32px 1fr;
padding: 8px;
border-bottom: 1px solid #c1c1c1;
border-bottom-style: solid;
border-bottom-style: dashed;
}
.music-app--player .play,
.music-app--player .perv,
.music-app--player .next {
-webkit-appearance: none;
-moz-appearance: none;
background-color: #507597;
color: #fff;
height: 20px;
margin: 5px;
border: none;
border-radius: 5px;
cursor: pointer;
padding: 0;
font-size: 10px;
}
.music-app--player .info {
margin-left: 10px;
width: 550px;
}
.music-app--player .info .song-name * {
color: black;
}
.music-app--player .info .song-name time {
float: right;
}
.music-app--player .info .track {
margin-top: 8px;
height: 5px;
width: 70%;
background-color: #fff;
border-top: 1px solid #507597;
float: left;
}
.music-app--player .info .track .inner-track {
background-color: #507597;
height: inherit;
width: 15px;
opacity: .7;
}
.settings_delete {
margin: -12px;
padding: 12px;
@ -1476,7 +1419,7 @@ body.scrolled .toTop:hover {
display: none;
}
.post-has-videos {
.post-has-videos, .post-has-audios {
margin-top: 11px;
margin-left: 3px;
color: #3c3c3c;
@ -1493,7 +1436,7 @@ body.scrolled .toTop:hover {
margin-left: 2px;
}
.post-has-video {
.post-has-video, .post-has-audio {
padding-bottom: 4px;
cursor: pointer;
}
@ -1502,6 +1445,10 @@ body.scrolled .toTop:hover {
text-decoration: underline;
}
.post-has-audio:hover span {
text-decoration: underline;
}
.post-has-video::before {
content: " ";
width: 14px;
@ -1515,6 +1462,19 @@ body.scrolled .toTop:hover {
margin-bottom: -1px;
}
.post-has-audio::before {
content: " ";
width: 14px;
height: 15px;
display: inline-block;
vertical-align: bottom;
background-image: url("/assets/packages/static/openvk/img/audio.png");
background-repeat: no-repeat;
margin: 3px;
margin-left: 2px;
margin-bottom: -1px;
}
.post-opts {
margin-top: 10px;
}
@ -2122,6 +2082,45 @@ table td[width="120"] {
margin: 0 auto;
}
#upload_container {
background: white;
padding: 30px 80px 20px;
margin: 10px 25px 30px;
border: 1px solid #d6d6d6;
}
#upload_container h4 {
border-bottom: solid 1px #daE1E8;
text-align: left;
padding: 0 0 4px 0;
margin: 0;
}
#audio_upload {
width: 350px;
margin: 20px auto;
margin-bottom: 10px;
padding: 15px 0;
border: 2px solid #ccc;
background-color: #EFEFEF;
text-align: center;
}
ul {
list-style: url(/assets/packages/static/openvk/img/bullet.gif) outside;
margin: 10px 0;
padding-left: 30px;
color: black;
}
li {
padding: 1px 0;
}
#upload_container ul {
padding-left: 15px;
}
#votesBalance {
margin-top: 10px;
padding: 7px;
@ -2472,8 +2471,7 @@ a.poll-retract-vote {
display: none;
}
.searchOptions
{
.searchOptions {
overflow: hidden;
width:25.5%;
border-top:1px solid #E5E7E6;
@ -2484,8 +2482,7 @@ a.poll-retract-vote {
margin-right: -7px;
}
.searchBtn
{
.searchBtn {
border: solid 1px #575757;
background-color: #696969;
color:white;
@ -2497,52 +2494,47 @@ a.poll-retract-vote {
margin-top: 1px;
}
.searchBtn:active
{
.searchBtn:active {
border: solid 1px #666666;
background-color: #696969;
color:white;
box-shadow: 0px -2px 0px 0px rgba(255, 255, 255, 0.18) inset;
}
.searchList
{
.searchList {
list-style: none;
user-select: none;
padding-left:0px;
}
.searchList #used
{
.searchList #used {
margin-left:0px;
color: white;
color: white !important;
padding:2px;
padding-top:5px;
padding-bottom:5px;
border: solid 0.125rem #696969;
background:linear-gradient(#888888,#858585);
border: solid 0.125rem #4F4F4F;
background: #606060;
margin-bottom:2px;
padding-left:9px;
width:87%;
}
.searchList #used a
{
.searchList #used a {
color: white;
}
.sr:focus
{
.sr:focus {
outline:none;
}
.searchHide
{
.searchHide {
padding-right: 5px;
}
.searchList li
.searchList li, .searchList a
{
display: block;
margin-left:0px;
color: #2B587A !important;
cursor:pointer;
@ -2553,26 +2545,27 @@ a.poll-retract-vote {
padding-left:9px;
}
.searchList li a
{
.searchList li a {
min-width:100%;
}
.searchList li:hover
{
margin-left:0px;
color: #2B587A !important;
background:#ebebeb;
padding:2px;
padding-top:5px;
padding-bottom:5px;
margin-bottom:2px;
padding-left:9px;
width:91%;
.searchList a {
min-width: 88%;
}
.whatFind
{
.searchList a:hover {
margin-left: 0px;
color: #2B587A !important;
background: #ebebeb;
padding: 2px;
padding-top: 5px;
padding-bottom: 5px;
margin-bottom: 2px;
padding-left: 9px;
width: 89.9%;
}
.whatFind {
color:rgb(128, 128, 128);
background:none;
border:none;
@ -2584,8 +2577,7 @@ a.poll-retract-vote {
margin-top: 0.5px;
}
.searchOptionName
{
.searchOptionName {
cursor:pointer;
background-color: #EAEAEA;
padding-left:5px;
@ -2597,8 +2589,7 @@ a.poll-retract-vote {
border-bottom: 2px solid #E4E4E4;
}
.searchOption
{
.searchOption {
user-select: none;
}
@ -2875,7 +2866,7 @@ body.article .floating_sidebar, body.article .page_content {
.lagged {
filter: opacity(0.5);
cursor: progress;
cursor: not-allowed;
user-select: none;
}
@ -2889,7 +2880,7 @@ body.article .floating_sidebar, body.article .page_content {
pointer-events: none;
}
.lagged * {
.lagged *, .lagged {
pointer-events: none;
}
@ -2974,3 +2965,40 @@ body.article .floating_sidebar, body.article .page_content {
background: #E9F0F1 !important;
}
.searchOptions.newer {
padding-left: 6px;
border-top: unset !important;
height: unset !important;
border-left: 1px solid #d8d8d8;
width: 26% !important;
}
hr {
background-color: #d8d8d8;
border: none;
height: 1px;
}
.searchList hr {
width: 153px;
margin-left: 0px;
margin-top: 6px;
}
.showMore, .showMoreAudiosPlaylist {
width: 100%;
text-align: center;
background: #d5d5d5;
height: 22px;
padding-top: 9px;
cursor: pointer;
}
#upload_container.uploading {
background: white url('/assets/packages/static/openvk/img/progressbar.gif') !important;
background-position-x: 0% !important;
background-position-y: 0% !important;
background-repeat: repeat !important;
background-repeat: no-repeat !important;
background-position: 50% !important;
}

BIN
Web/static/img/audio.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
Web/static/img/bullet.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="11" viewBox="0 0 11 11" width="11">
<path d="m1 2.506v5.988a1.5 1.5 0 0 0 1.491 1.506h6.019c.827 0 1.49-.674 1.49-1.506v-5.988a1.5 1.5 0 0 0 -1.491-1.506h-6.019c-.827 0-1.49.674-1.49 1.506zm4 2.494v-1h2v-1h-3v5h3v-1h-2v-1h2v-1zm-5-2.494a2.496 2.496 0 0 1 2.491-2.506h6.019a2.5 2.5 0 0 1 2.49 2.506v5.988a2.496 2.496 0 0 1 -2.491 2.506h-6.019a2.5 2.5 0 0 1 -2.49-2.506z"
fill="#828a99" fill-opacity=".7"/>
</svg>

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,018 B

BIN
Web/static/img/song.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

1434
Web/static/js/al_music.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,113 @@
let context_type = "entity_audios"
let context_id = 0
if(document.querySelector("#editPlaylistForm")) {
context_type = "playlist_context"
context_id = document.querySelector("#editPlaylistForm").dataset.id
}
if(document.querySelector(".showMoreAudiosPlaylist") && document.querySelector(".showMoreAudiosPlaylist").dataset.club != null) {
context_type = "entity_audios"
context_id = Number(document.querySelector(".showMoreAudiosPlaylist").dataset.club) * -1
}
let searcher = new playersSearcher(context_type, context_id)
searcher.successCallback = (response, thisc) => {
let domparser = new DOMParser()
let result = domparser.parseFromString(response, "text/html")
let pagesCount = Number(result.querySelector("input[name='pagesCount']").value)
let count = Number(result.querySelector("input[name='count']").value)
result.querySelectorAll(".audioEmbed").forEach(el => {
let id = Number(el.dataset.realid)
let isAttached = (document.querySelector("input[name='audios']").value.includes(`${id},`))
document.querySelector(".playlistAudiosContainer").insertAdjacentHTML("beforeend", `
<div id="newPlaylistAudios">
<div class="playerContainer">
${el.outerHTML}
</div>
<div class="attachAudio addToPlaylist" data-id="${id}">
<span>${isAttached ? tr("remove_from_playlist") : tr("add_to_playlist")}</span>
</div>
</div>
`)
})
if(count < 1)
document.querySelector(".playlistAudiosContainer").insertAdjacentHTML("beforeend", `
${tr("no_results")}
`)
if(Number(thisc.page) >= pagesCount)
u(".showMoreAudiosPlaylist").remove()
else {
if(document.querySelector(".showMoreAudiosPlaylist") != null) {
document.querySelector(".showMoreAudiosPlaylist").setAttribute("data-page", thisc.page + 1)
if(thisc.query != "") {
document.querySelector(".showMoreAudiosPlaylist").setAttribute("data-query", thisc.query)
}
document.querySelector(".showMoreAudiosPlaylist").style.display = "block"
} else {
document.querySelector(".playlistAudiosContainer").parentNode.insertAdjacentHTML("beforeend", `
<div class="showMoreAudiosPlaylist" data-page="2"
${thisc.query != "" ? `"data-query="${thisc.query}"` : ""}
${thisc.context_type == "entity_audios" ? `"data-playlist="${thisc.context_id}"` : ""}
${thisc.context_id < 0 ? `"data-club="${thisc.context_id}"` : ""}>
${tr("show_more_audios")}
</div>
`)
}
}
u("#loader").remove()
}
searcher.beforesendCallback = () => {
document.querySelector(".playlistAudiosContainer").parentNode.insertAdjacentHTML("beforeend", `<img id="loader" src="/assets/packages/static/openvk/img/loading_mini.gif">`)
if(document.querySelector(".showMoreAudiosPlaylist") != null)
document.querySelector(".showMoreAudiosPlaylist").style.display = "none"
}
searcher.errorCallback = () => {
fastError("Error when loading players")
}
searcher.clearContainer = () => {
document.querySelector(".playlistAudiosContainer").innerHTML = ""
}
$(document).on("click", ".showMoreAudiosPlaylist", (e) => {
searcher.movePage(Number(e.currentTarget.dataset.page))
})
$(document).on("change", "input#playlist_query", async (e) => {
e.preventDefault()
await new Promise(r => setTimeout(r, 500));
if(e.currentTarget.value === document.querySelector("input#playlist_query").value) {
searcher.clearContainer()
if(e.currentTarget.value == "") {
searcher.context_type = "entity_audios"
searcher.context_id = 0
searcher.query = ""
searcher.movePage(1)
return
}
searcher.context_type = "search_context"
searcher.context_id = 0
searcher.query = e.currentTarget.value
searcher.movePage(1)
return;
}
})

View file

@ -155,18 +155,6 @@ function setupWallPostInputHandlers(id) {
return;
}
});
u("#wall-post-input" + id).on("input", function(e) {
var boost = 5;
var textArea = e.target;
textArea.style.height = "5px";
var newHeight = textArea.scrollHeight;
textArea.style.height = newHeight + boost + "px";
return;
// revert to original size if it is larger (possibly changed by user)
// textArea.style.height = (newHeight > originalHeight ? (newHeight + boost) : originalHeight) + "px";
});
u("#wall-post-input" + id).on("dragover", function(e) {
e.preventDefault()
@ -182,6 +170,18 @@ function setupWallPostInputHandlers(id) {
});
}
u(document).on("input", "textarea", function(e) {
var boost = 5;
var textArea = e.target;
textArea.style.height = "5px";
var newHeight = textArea.scrollHeight;
textArea.style.height = newHeight + boost + "px";
return;
// revert to original size if it is larger (possibly changed by user)
// textArea.style.height = (newHeight > originalHeight ? (newHeight + boost) : originalHeight) + "px";
});
function OpenMiniature(e, photo, post, photo_id, type = "post") {
/*
костыли но смешные однако

View file

@ -2,6 +2,8 @@
"dependencies": {
"@atlassian/aui": "^9.6.0",
"create-react-class": "^15.7.0",
"dashjs": "^4.3.0",
"id3js": "^2.1.1",
"handlebars": "^4.7.7",
"jquery": "^3.0.0",
"knockout": "^3.5.1",

View file

@ -41,6 +41,11 @@ backbone@1.4.1:
dependencies:
underscore ">=1.8.3"
codem-isoboxer@0.3.6:
version "0.3.6"
resolved "https://registry.yarnpkg.com/codem-isoboxer/-/codem-isoboxer-0.3.6.tgz#867f670459b881d44f39168d5ff2a8f14c16151d"
integrity sha512-LuO8/7LW6XuR5ERn1yavXAfodGRhuY2yP60JTZIw5yNYMCE5lUVbk3NFUCJxjnphQH+Xemp5hOGb1LgUXm00Xw==
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@ -64,6 +69,18 @@ dompurify@2.4.5:
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.5.tgz#0e89a27601f0bad978f9a924e7a05d5d2cccdd87"
integrity sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA==
dashjs@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/dashjs/-/dashjs-4.3.0.tgz#cccda5a490cabf6c3b48aa887ec8c8ac0df1a233"
integrity sha512-cqpnJaPQpEY4DsEdF9prwD00+5dp5EGHCFc7yo9n2uuAH9k4zPkZJwXQ8dXmVRhPf3M89JfKSoAYIP3dbXmqcg==
dependencies:
codem-isoboxer "0.3.6"
es6-promise "^4.2.8"
fast-deep-equal "2.0.1"
html-entities "^1.2.1"
imsc "^1.0.2"
localforage "^1.7.1"
encoding@^0.1.11:
version "0.1.13"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
@ -71,6 +88,11 @@ encoding@^0.1.11:
dependencies:
iconv-lite "^0.6.2"
es6-promise@^4.2.8:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
event-lite@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.2.tgz#838a3e0fdddef8cc90f128006c8e55a4e4e4c11b"
@ -81,6 +103,11 @@ fancy-file-input@2.0.4:
resolved "https://registry.yarnpkg.com/fancy-file-input/-/fancy-file-input-2.0.4.tgz#698c216482e07649a827681c4db3054fddc9a32b"
integrity sha512-l+J0WwDl4nM/zMJ/C8qleYnXMUJKsLng7c5uWH/miAiHoTvPDtEoLW1tmVO6Cy2O8i/1VfA+2YOwg/Q3+kgO6w==
fast-deep-equal@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
fbjs@^0.8.0:
version "0.8.17"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
@ -94,6 +121,10 @@ fbjs@^0.8.0:
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
html-entities@^1.2.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc"
integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==
handlebars@^4.7.7:
version "4.7.7"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
@ -113,11 +144,28 @@ iconv-lite@^0.6.2:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
id3js@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/id3js/-/id3js-2.1.1.tgz#0c307d0d2f194bc5fa7a809bbed0b1a93577f16d"
integrity sha512-9Gi+sG0RHSa5qn8hkwi2KCl+2jV8YrtiZidXbOO3uLfRAxc2jilRg0fiQ3CbeoAmR7G7ap3RVs1kqUVhIyZaog==
ieee754@^1.1.8:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
imsc@^1.0.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/imsc/-/imsc-1.1.3.tgz#e96a60a50d4000dd7b44097272768b9fd6a4891d"
integrity sha512-IY0hMkVTNoqoYwKEp5UvNNKp/A5jeJUOrIO7judgOyhHT+xC6PA4VBOMAOhdtAYbMRHx9DTgI8p6Z6jhYQPFDA==
dependencies:
sax "1.2.1"
int64-buffer@^0.1.9:
version "0.1.10"
resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423"
@ -173,6 +221,13 @@ ky@^0.19.0:
resolved "https://registry.yarnpkg.com/ky/-/ky-0.19.0.tgz#d6ad117e89efe2d85a1c2e91462d48ca1cda1f7a"
integrity sha512-RkDgbg5ahMv1MjHfJI2WJA2+Qbxq0iNSLWhreYiCHeHry9Q12sedCnP5KYGPt7sydDvsyH+8UcG6Kanq5mpsyw==
lie@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
dependencies:
immediate "~3.0.5"
literallycanvas@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/literallycanvas/-/literallycanvas-0.5.2.tgz#7d4800a8d9c4b38a593e91695d52466689586abd"
@ -180,6 +235,13 @@ literallycanvas@^0.5.2:
dependencies:
react-addons-pure-render-mixin "^15.1"
localforage@^1.7.1:
version "1.10.0"
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4"
integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==
dependencies:
lie "3.1.1"
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@ -273,6 +335,11 @@ requirejs@^2.3.6:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sax@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"

View file

@ -64,6 +64,33 @@ function ovk_proc_strtr(string $string, int $length = 0): string
return $newString . ($string !== $newString ? "" : ""); #if cut hasn't happened, don't append "..."
}
function knuth_shuffle(iterable $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
{
return new Bitmask($input, $options["length"] ?? 1, $options["mappings"] ?? []);

View file

@ -10,12 +10,13 @@
"wapmorgan/binary-stream": "dev-master",
"al/emoji-detector": "dev-master",
"ezyang/htmlpurifier": "dev-master",
"scssphp/scssphp": "dev-master",
"scssphp/scssphp": "dev-main",
"lfkeitel/phptotp": "dev-master",
"chillerlan/php-qrcode": "dev-main",
"vearutop/php-obscene-censor-rus": "dev-master",
"erusev/parsedown": "dev-master",
"bhaktaraz/php-rss-generator": "dev-master",
"ext-openssl": "*",
"ext-simplexml": "*",
"symfony/console": "5.4.x-dev",
"wapmorgan/morphos": "dev-master",

View file

@ -54,27 +54,6 @@ CREATE TABLE `attachments` (
`index` bigint(20) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE `audios` (
`id` bigint(20) UNSIGNED NOT NULL,
`owner` bigint(20) UNSIGNED NOT NULL,
`virtual_id` bigint(20) UNSIGNED NOT NULL,
`created` bigint(20) UNSIGNED NOT NULL,
`edited` bigint(20) UNSIGNED DEFAULT NULL,
`hash` char(128) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`deleted` tinyint(4) DEFAULT 0,
`name` varchar(190) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '(no name)',
`performer` varchar(190) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'Unknown',
`genre` varchar(190) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'K-POP',
`lyrics` longtext COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
`explicit` tinyint(4) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE `audio_relations` (
`user` bigint(20) UNSIGNED NOT NULL,
`audio` bigint(20) UNSIGNED NOT NULL,
`index` bigint(20) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE `comments` (
`id` bigint(20) UNSIGNED NOT NULL,
`owner` bigint(20) NOT NULL,

View file

@ -0,0 +1,100 @@
-- Apply these two commands if you installed OpenVK before 12th November 2023 OR if it's just doesn't work out of box, then apply this file again
-- DROP TABLE `audios`;
-- DROP TABLE `audio_relations`;
CREATE TABLE IF NOT EXISTS `audios` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`owner` bigint unsigned NOT NULL,
`virtual_id` bigint unsigned NOT NULL,
`created` bigint unsigned NOT NULL,
`edited` bigint unsigned DEFAULT NULL,
`hash` char(128) NOT NULL,
`length` smallint unsigned NOT NULL,
`segment_size` decimal(20,6) NOT NULL DEFAULT '6.000000' COMMENT 'Size in seconds of each segment',
`kid` binary(16) NOT NULL,
`key` binary(16) NOT NULL,
`token` binary(28) NOT NULL COMMENT 'Key to access original file',
`listens` bigint unsigned NOT NULL DEFAULT '0',
`performer` varchar(256) NOT NULL,
`name` varchar(256) NOT NULL,
`lyrics` text,
`genre` enum('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') DEFAULT NULL,
`explicit` tinyint(1) NOT NULL DEFAULT '0',
`withdrawn` tinyint(1) NOT NULL DEFAULT '0',
`processed` tinyint unsigned NOT NULL DEFAULT '0',
`checked` bigint NOT NULL DEFAULT '0' COMMENT 'Last time the audio availability was checked',
`unlisted` tinyint(1) NOT NULL DEFAULT '0',
`deleted` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `owner_virtual_id` (`owner`,`virtual_id`),
KEY `genre` (`genre`),
KEY `unlisted` (`unlisted`),
KEY `listens` (`listens`),
KEY `deleted` (`deleted`),
KEY `length` (`length`),
KEY `listens_genre` (`listens`,`genre`),
FULLTEXT KEY `performer_name` (`performer`,`name`),
FULLTEXT KEY `lyrics` (`lyrics`),
FULLTEXT KEY `performer` (`performer`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `audio_listens` (
`entity` bigint NOT NULL,
`audio` bigint unsigned NOT NULL,
`time` bigint unsigned NOT NULL,
`index` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'Workaround for Nette DBE bug',
`playlist` bigint(20) UNSIGNED DEFAULT NULL,
PRIMARY KEY (`index`),
KEY `audio` (`audio`),
KEY `user` (`entity`) USING BTREE,
KEY `user_time` (`entity`,`time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `audio_relations` (
`entity` bigint NOT NULL,
`audio` bigint unsigned NOT NULL,
`index` bigint unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`index`),
KEY `user` (`entity`) USING BTREE,
KEY `entity_audio` (`entity`,`audio`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `playlists` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`owner` bigint NOT NULL,
`name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
`description` varchar(2048) DEFAULT NULL,
`cover_photo_id` bigint unsigned DEFAULT NULL,
`length` int unsigned NOT NULL DEFAULT '0',
`special_type` tinyint unsigned NOT NULL DEFAULT '0',
`created` bigint unsigned DEFAULT NULL,
`listens` bigint(20) unsigned NOT NULL DEFAULT 0,
`edited` bigint unsigned DEFAULT NULL,
`deleted` tinyint unsigned DEFAULT '0',
PRIMARY KEY (`id`),
KEY `owner_deleted` (`owner`,`deleted`),
FULLTEXT KEY `title_description` (`name`,`description`),
FULLTEXT KEY `title` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `playlist_imports` (
`entity` bigint NOT NULL,
`playlist` bigint unsigned NOT NULL,
`index` bigint unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`index`) USING BTREE,
KEY `user` (`entity`) USING BTREE,
KEY `entity_audio` (`entity`,`playlist`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `playlist_relations` (
`collection` bigint unsigned NOT NULL,
`media` bigint unsigned NOT NULL,
`index` bigint unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`index`) USING BTREE,
KEY `playlist` (`collection`) USING BTREE,
KEY `audio` (`media`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
ALTER TABLE `groups` ADD `everyone_can_upload_audios` TINYINT(1) NOT NULL DEFAULT '0' AFTER `backdrop_2`;
ALTER TABLE `profiles` ADD `last_played_track` BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER `client_name`, ADD `audio_broadcast_enabled` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `last_played_track`;
ALTER TABLE `groups` ADD `last_played_track` BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER `everyone_can_upload_audios`, ADD `audio_broadcast_enabled` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `last_played_track`;

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more