Merge branch 'master' into patch-1
|
@ -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.
|
||||
|
||||
|
|
|
@ -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-совместимую базу данных.
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
|
@ -14,6 +14,10 @@ final class Friends extends VKAPIRequestHandler
|
|||
|
||||
$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");
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,9 @@ final class Users extends VKAPIRequestHandler
|
|||
$users = new UsersRepo;
|
||||
if($user_ids == "0")
|
||||
$user_ids = (string) $authuser->getId();
|
||||
|
||||
if($user_ids == "")
|
||||
return array();
|
||||
|
||||
$usrs = explode(',', $user_ids);
|
||||
$response = array();
|
||||
|
@ -95,6 +98,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)
|
||||
|
@ -159,6 +168,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;
|
||||
|
|
|
@ -15,6 +15,7 @@ use openvk\Web\Models\Entities\Video;
|
|||
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\Audios as AudiosRepo;
|
||||
|
||||
final class Wall extends VKAPIRequestHandler
|
||||
{
|
||||
|
@ -58,6 +59,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 = [];
|
||||
|
||||
|
@ -233,6 +239,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 = [];
|
||||
|
||||
|
@ -450,6 +461,8 @@ final class Wall extends VKAPIRequestHandler
|
|||
$attachmentType = "video";
|
||||
elseif(str_contains($attac, "note"))
|
||||
$attachmentType = "note";
|
||||
elseif(str_contains($attac, "audio"))
|
||||
$attachmentType = "audio";
|
||||
else
|
||||
$this->fail(205, "Unknown attachment type");
|
||||
|
||||
|
@ -483,6 +496,12 @@ final class Wall extends VKAPIRequestHandler
|
|||
if(!$attacc->getOwner()->getPrivacyPermission('notes.read', $this->getUser()))
|
||||
$this->fail(11, "Access to note denied");
|
||||
|
||||
$post->attach($attacc);
|
||||
} elseif($attachmentType == "audio") {
|
||||
$attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId);
|
||||
if(!$attacc || $attacc->isDeleted())
|
||||
$this->fail(100, "Audio does not exist");
|
||||
|
||||
$post->attach($attacc);
|
||||
}
|
||||
}
|
||||
|
@ -562,6 +581,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()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -621,6 +645,9 @@ final class Wall extends VKAPIRequestHandler
|
|||
|
||||
$comment = (new CommentsRepo)->get($comment_id); # один хуй айди всех комментов общий
|
||||
|
||||
if(!$comment || $comment->isDeleted())
|
||||
$this->fail(100, "Invalid comment");
|
||||
|
||||
$profiles = [];
|
||||
|
||||
$attachments = [];
|
||||
|
@ -628,6 +655,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()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -719,6 +751,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");
|
||||
|
||||
|
@ -744,6 +778,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);
|
||||
}
|
||||
}
|
||||
|
@ -777,7 +817,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(),
|
||||
|
|
469
Web/Models/Entities/Audio.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -371,36 +371,60 @@ 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
|
||||
{
|
||||
$res = [];
|
||||
$res = (object)[];
|
||||
|
||||
$res->id = $this->getId();
|
||||
$res->name = $this->getName();
|
||||
$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 (object) $res;
|
||||
return $res;
|
||||
}
|
||||
|
||||
use Traits\TBackDrops;
|
||||
use Traits\TSubscribable;
|
||||
use Traits\TAudioStatuses;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
256
Web/Models/Entities/Playlist.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
38
Web/Models/Entities/Traits/TAudioStatuses.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -130,7 +130,7 @@ class Albums
|
|||
"owner" => $owner,
|
||||
"id" => $id
|
||||
])->fetch();
|
||||
|
||||
return new Album($album);
|
||||
|
||||
return $album ? new Album($album) : NULL;
|
||||
}
|
||||
}
|
||||
|
|
296
Web/Models/Repositories/Audios.php
Normal 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);
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
39
Web/Models/shell/processAudio.ps1
Normal 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
|
35
Web/Models/shell/processAudio.sh
Normal 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"
|
|
@ -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 = [];
|
||||
|
|
696
Web/Presenters/AudioPresenter.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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") ?? "";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,6 +435,7 @@ final class UserPresenter extends OpenVKPresenter
|
|||
"friends.add",
|
||||
"wall.write",
|
||||
"messages.write",
|
||||
"audios.read",
|
||||
];
|
||||
foreach($settings as $setting) {
|
||||
$input = $this->postParam(str_replace(".", "_", $setting));
|
||||
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
@ -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();
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
81
Web/Presenters/templates/Admin/EditMusic.xml
Normal 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}
|
54
Web/Presenters/templates/Admin/EditPlaylist.xml
Normal 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}
|
135
Web/Presenters/templates/Admin/Music.xml
Normal 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}">«</a>
|
||||
<a n:if="$isLast" class="aui-button" href="/admin/music?act={($_GET['act'] ?? 'audios')}&p={($_GET['p'] ?? 1) + 1}">»</a>
|
||||
</div>
|
||||
{/block}
|
7
Web/Presenters/templates/Audio/ApiGetContext.xml
Normal 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}
|
95
Web/Presenters/templates/Audio/EditPlaylist.xml
Normal 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}
|
20
Web/Presenters/templates/Audio/Embed.xml
Normal 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>
|
126
Web/Presenters/templates/Audio/List.xml
Normal 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}
|
109
Web/Presenters/templates/Audio/NewPlaylist.xml
Normal 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}
|
81
Web/Presenters/templates/Audio/Playlist.xml
Normal 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}
|
212
Web/Presenters/templates/Audio/Upload.xml
Normal 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}
|
56
Web/Presenters/templates/Audio/bigplayer.xml
Normal 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;">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="volumePanel">
|
||||
<div class="selectableTrack">
|
||||
<div style="position: relative;width:72%">
|
||||
<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>
|
69
Web/Presenters/templates/Audio/player.xml
Normal 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>
|
40
Web/Presenters/templates/Audio/tabs.xml
Normal 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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -205,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}
|
||||
|
@ -231,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">
|
||||
|
@ -250,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}
|
||||
|
@ -352,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}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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});
|
||||
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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%;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1243,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;
|
||||
|
@ -1477,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;
|
||||
|
@ -1494,7 +1436,7 @@ body.scrolled .toTop:hover {
|
|||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.post-has-video {
|
||||
.post-has-video, .post-has-audio {
|
||||
padding-bottom: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -1503,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;
|
||||
|
@ -1516,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;
|
||||
}
|
||||
|
@ -2123,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;
|
||||
|
@ -2473,8 +2471,7 @@ a.poll-retract-vote {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.searchOptions
|
||||
{
|
||||
.searchOptions {
|
||||
overflow: hidden;
|
||||
width:25.5%;
|
||||
border-top:1px solid #E5E7E6;
|
||||
|
@ -2485,8 +2482,7 @@ a.poll-retract-vote {
|
|||
margin-right: -7px;
|
||||
}
|
||||
|
||||
.searchBtn
|
||||
{
|
||||
.searchBtn {
|
||||
border: solid 1px #575757;
|
||||
background-color: #696969;
|
||||
color:white;
|
||||
|
@ -2498,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;
|
||||
|
@ -2554,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;
|
||||
|
@ -2585,8 +2577,7 @@ a.poll-retract-vote {
|
|||
margin-top: 0.5px;
|
||||
}
|
||||
|
||||
.searchOptionName
|
||||
{
|
||||
.searchOptionName {
|
||||
cursor:pointer;
|
||||
background-color: #EAEAEA;
|
||||
padding-left:5px;
|
||||
|
@ -2598,8 +2589,7 @@ a.poll-retract-vote {
|
|||
border-bottom: 2px solid #E4E4E4;
|
||||
}
|
||||
|
||||
.searchOption
|
||||
{
|
||||
.searchOption {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
@ -2876,7 +2866,7 @@ body.article .floating_sidebar, body.article .page_content {
|
|||
|
||||
.lagged {
|
||||
filter: opacity(0.5);
|
||||
cursor: progress;
|
||||
cursor: not-allowed;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
@ -2890,7 +2880,7 @@ body.article .floating_sidebar, body.article .page_content {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lagged * {
|
||||
.lagged *, .lagged {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
@ -2975,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
After Width: | Height: | Size: 560 B |
BIN
Web/static/img/audios_controls.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
Web/static/img/bullet.gif
Normal file
After Width: | Height: | Size: 53 B |
4
Web/static/img/explicit.svg
Normal 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 |
BIN
Web/static/img/favicons/favicon24_paused.png
Normal file
After Width: | Height: | Size: 932 B |
BIN
Web/static/img/favicons/favicon24_playing.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Web/static/img/play_buttons.gif
Normal file
After Width: | Height: | Size: 103 B |
BIN
Web/static/img/progressbar.gif
Normal file
After Width: | Height: | Size: 1,018 B |
BIN
Web/static/img/song.jpg
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
Web/static/img/tour/audios.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
Web/static/img/tour/audios_playlists.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
Web/static/img/tour/audios_search.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
Web/static/img/tour/audios_upload.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
1434
Web/static/js/al_music.js
Normal file
113
Web/static/js/al_playlists.js
Normal 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;
|
||||
}
|
||||
})
|
|
@ -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") {
|
||||
/*
|
||||
костыли но смешные однако
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"] ?? []);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
100
install/sqls/00041-music.sql
Normal 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`;
|
|
@ -72,7 +72,7 @@
|
|||
"change_status" = "change status";
|
||||
"name" = "Name";
|
||||
"surname" = "Surname";
|
||||
"gender" = "Gender";
|
||||
"gender" = "Sex";
|
||||
"male" = "male";
|
||||
"female" = "female";
|
||||
"description" = "Description";
|
||||
|
@ -479,6 +479,7 @@
|
|||
"my_videos" = "My Videos";
|
||||
"my_messages" = "My Messages";
|
||||
"my_notes" = "My Notes";
|
||||
"my_audios" = "My Audios";
|
||||
"my_groups" = "My Groups";
|
||||
"my_feed" = "My Feed";
|
||||
"my_feedback" = "My Feedback";
|
||||
|
@ -568,6 +569,7 @@
|
|||
"privacy_setting_add_to_friends" = "Who can add me to friends";
|
||||
"privacy_setting_write_wall" = "Who can publish posts on my wall";
|
||||
"privacy_setting_write_messages" = "Who can write messages to me";
|
||||
"privacy_setting_view_audio" = "Who can see my audios";
|
||||
"privacy_value_anybody" = "Anybody";
|
||||
"privacy_value_anybody_dative" = "Anybody";
|
||||
"privacy_value_users" = "OpenVK users";
|
||||
|
@ -713,12 +715,133 @@
|
|||
"upload_new_video" = "Upload new video";
|
||||
"max_attached_videos" = "Max is 10 videos";
|
||||
"max_attached_photos" = "Max is 10 photos";
|
||||
"max_attached_audios" = "Max is 10 audios";
|
||||
"no_videos" = "You don't have uploaded videos.";
|
||||
"no_videos_results" = "No results.";
|
||||
|
||||
"change_video" = "Change video";
|
||||
"unknown_video" = "This video is not supported in your version of OpenVK.";
|
||||
|
||||
|
||||
/* Audios */
|
||||
|
||||
"audios" = "Audios";
|
||||
"audio" = "Audio";
|
||||
"playlist" = "Playlist";
|
||||
"upload_audio" = "Upload audio";
|
||||
"upload_audio_to_group" = "Upload audio to group";
|
||||
|
||||
"performer" = "Performer";
|
||||
"audio_name" = "Name";
|
||||
"genre" = "Genre";
|
||||
"lyrics" = "Lyrics";
|
||||
|
||||
"select_another_file" = "Select another file";
|
||||
|
||||
"limits" = "Limits";
|
||||
"select_audio" = "Select audio from your computer";
|
||||
"audio_requirements" = "Audio must be between $1s to $2 minutes, weights to $3MB and contain audio stream.";
|
||||
"audio_requirements_2" = "Audio must not infringe copyright and related rights";
|
||||
"you_can_also_add_audio_using" = "You can also add audio from among the files you have already downloaded using";
|
||||
"search_audio_inst" = "audios search";
|
||||
|
||||
"audio_embed_not_found" = "Audio not found";
|
||||
"audio_embed_deleted" = "Audio was deleted";
|
||||
"audio_embed_withdrawn" = "The audio was withdrawn at the request of the copyright holder";
|
||||
"audio_embed_forbidden" = "The user's privacy settings do not allow this embed this audio";
|
||||
"audio_embed_processing" = "Audio is still being processed, or has not been processed correctly.";
|
||||
|
||||
"audios_count_zero" = "No audios";
|
||||
"audios_count_one" = "One audio";
|
||||
"audios_count_few" = "$1 audios";
|
||||
"audios_count_many" = "$1 audios";
|
||||
"audios_count_other" = "$1 audios";
|
||||
|
||||
"track_unknown" = "Unknown";
|
||||
"track_noname" = "Without name";
|
||||
|
||||
"my_music" = "My music";
|
||||
"music_user" = "User's music";
|
||||
"music_club" = "Club's music";
|
||||
"audio_new" = "New";
|
||||
"audio_popular" = "Popular";
|
||||
"audio_search" = "Search";
|
||||
|
||||
"my_audios_small" = "My audios";
|
||||
"my_playlists" = "My playlists";
|
||||
"playlists" = "Playlists";
|
||||
"audios_explicit" = "Contains obscene language";
|
||||
"withdrawn" = "Withdrawn";
|
||||
"deleted" = "Deleted";
|
||||
"owner" = "Owner";
|
||||
"searchable" = "Searchable";
|
||||
|
||||
"select_audio" = "Select audios";
|
||||
"no_playlists_thisuser" = "You haven't added any playlists yet.";
|
||||
"no_playlists_user" = "This user has not added any playlists yet.";
|
||||
"no_playlists_club" = "This group hasn't added playlists yet.";
|
||||
|
||||
"no_audios_thisuser" = "You haven't added any audios yet.";
|
||||
"no_audios_user" = "This user has not added any audios yet.";
|
||||
"no_audios_club" = "This group has not added any audios yet.";
|
||||
|
||||
"new_playlist" = "New playlist";
|
||||
"created_playlist" = "created";
|
||||
"updated_playlist" = "updated";
|
||||
"bookmark" = "Add to collection";
|
||||
"unbookmark" = "Remove from collection";
|
||||
"empty_playlist" = "There are no audios in this playlist.";
|
||||
"edit_playlist" = "Edit playlist";
|
||||
"unable_to_load_queue" = "Error when loading queue.";
|
||||
|
||||
"fully_delete_audio" = "Fully delete audio";
|
||||
"attach_audio" = "Attach audio";
|
||||
"detach_audio" = "Detach audio";
|
||||
|
||||
"show_more_audios" = "Show more audios";
|
||||
"add_to_playlist" = "Add to playlist";
|
||||
"remove_from_playlist" = "Remove from playlist";
|
||||
"delete_playlist" = "Delete playlist";
|
||||
"playlist_cover" = "Playlist cover";
|
||||
|
||||
"playlists_user" = "Users playlists";
|
||||
"playlists_club" = "Groups playlists";
|
||||
"change_cover" = "Change cover";
|
||||
"playlist_cover" = "Playlist's cover";
|
||||
|
||||
"minutes_count_zero" = "lasts no minutes";
|
||||
"minutes_count_one" = "lasts one minute";
|
||||
"minutes_count_few" = "lasts $1 minutes";
|
||||
"minutes_count_many" = "lasts $1 minutes";
|
||||
"minutes_count_other" = "lasts $1 minutes";
|
||||
|
||||
"listens_count_zero" = "no listens";
|
||||
"listens_count_one" = "one listen";
|
||||
"listens_count_few" = "$1 listens";
|
||||
"listens_count_many" = "$1 listens";
|
||||
"listens_count_other" = "$1 listens";
|
||||
|
||||
"add_audio_to_club" = "Add audio to group";
|
||||
"what_club_add" = "Which group do you want to add the song to?";
|
||||
"group_has_audio" = "This group already has this song.";
|
||||
"group_hasnt_audio" = "This group doesn't have this song.";
|
||||
|
||||
"by_name" = "by name";
|
||||
"by_performer" = "by performer";
|
||||
"no_access_clubs" = "There are no groups where you are an administrator.";
|
||||
"audio_successfully_uploaded" = "Audio has been successfully uploaded and is currently being processed.";
|
||||
|
||||
"broadcast_audio" = "Broadcast audio to status";
|
||||
"sure_delete_playlist" = "Do you sure want to delete this playlist?";
|
||||
"edit_audio" = "Edit audio";
|
||||
"audios_group" = "Audios from group";
|
||||
"playlists_group" = "Playlists from group";
|
||||
|
||||
"play_tip" = "Play/pause";
|
||||
"repeat_tip" = "Repeat";
|
||||
"shuffle_tip" = "Shuffle";
|
||||
"mute_tip" = "Mute";
|
||||
|
||||
/* Notifications */
|
||||
|
||||
"feedback" = "Feedback";
|
||||
|
@ -983,6 +1106,7 @@
|
|||
"going_to_report_photo" = "You are about to report this photo.";
|
||||
"going_to_report_user" = "You are about to report this user.";
|
||||
"going_to_report_video" = "You are about to report this video.";
|
||||
"going_to_report_audio" = "You are about to report this audio.";
|
||||
"going_to_report_post" = "You are about to report this post.";
|
||||
"going_to_report_comment" = "You are about to report this comment.";
|
||||
|
||||
|
@ -1108,6 +1232,7 @@
|
|||
"created" = "Created";
|
||||
|
||||
"everyone_can_create_topics" = "Everyone can create topics";
|
||||
"everyone_can_upload_audios" = "Everyone can upload audios";
|
||||
"display_list_of_topics_above_wall" = "Display a list of topics above the wall";
|
||||
|
||||
"topic_changes_saved_comment" = "The updated title and settings will appear on the topic page.";
|
||||
|
@ -1284,6 +1409,22 @@
|
|||
|
||||
"description_too_long" = "Description is too long.";
|
||||
|
||||
"invalid_audio" = "Invalid audio.";
|
||||
"do_not_have_audio" = "You don't have this audio.";
|
||||
"do_have_audio" = "You already have this audio.";
|
||||
|
||||
"set_playlist_name" = "Enter the playlist name.";
|
||||
"playlist_already_bookmarked" = "This playlist is already in your collection.";
|
||||
"playlist_not_bookmarked" = "This playlist is not in your collection.";
|
||||
"invalid_cover_photo" = "Error when loading cover photo.";
|
||||
"not_a_photo" = "Uploaded file doesn't look like a photo.";
|
||||
"file_too_big" = "File is too big.";
|
||||
"file_loaded_partially" = "The file has been uploaded partially.";
|
||||
"file_not_uploaded" = "Failed to upload the file.";
|
||||
"error_code" = "Error code: $1.";
|
||||
"ffmpeg_timeout" = "Timed out waiting ffmpeg. Try to upload file again.";
|
||||
"ffmpeg_not_installed" = "Failed to proccess the file. It looks like ffmpeg is not installed on this server.";
|
||||
|
||||
/* Admin actions */
|
||||
|
||||
"login_as" = "Login as $1";
|
||||
|
@ -1313,7 +1454,7 @@
|
|||
"admin_verification" = "Verification";
|
||||
"admin_banreason" = "Ban reason";
|
||||
"admin_banned" = "banned";
|
||||
"admin_gender" = "Gender";
|
||||
"admin_gender" = "Sex";
|
||||
"admin_registrationdate" = "Registration date";
|
||||
"admin_actions" = "Actions";
|
||||
"admin_image" = "Image";
|
||||
|
@ -1390,6 +1531,10 @@
|
|||
|
||||
"admin_gift_moved_successfully" = "Gift moved successfully";
|
||||
"admin_gift_moved_to_recycle" = "This gift was moved to <b>Recycle Bin</b>.";
|
||||
"admin_original_file" = "Original file";
|
||||
"admin_audio_length" = "Length";
|
||||
"admin_cover_id" = "Cover (photo ID)";
|
||||
"admin_music" = "Music";
|
||||
|
||||
"logs" = "Logs";
|
||||
"logs_anything" = "Anything";
|
||||
|
@ -1617,8 +1762,22 @@
|
|||
"tour_section_5_text_3" = "In addition to direct uploads, the site also supports embedding videos from YouTube.";
|
||||
|
||||
|
||||
"tour_section_6_title_1" = "Audios section, which doesn't exist yet xdddd";
|
||||
"tour_section_6_text_1" = "I would love to do a tutorial on this section, but our sunshine Vriska didn't implement this feature yet :c";
|
||||
"tour_section_6_title_1" = "Listen to music";
|
||||
"tour_section_6_text_1" = "You can listen to music in \"My Audios\"";
|
||||
"tour_section_6_text_2" = "This section is also controlled by the privacy settings.";
|
||||
"tour_section_6_text_3" = "The most listened songs are in \"Popular\", and recently uploaded songs are in \"New\"";
|
||||
"tour_section_6_text_4" = "To add a song to your collection, hover over it and click on the \"plus\". You can search for the song you want.";
|
||||
"tour_section_6_text_5" = "If you can't find the song you want, you can upload it yourself";
|
||||
"tour_section_6_bottom_text_1" = "<b>Important:</b> the song must not infringe copyright";
|
||||
"tour_section_6_title_2" = "Create playlists";
|
||||
"tour_section_6_text_6" = "You can create playlists in the \"My Playlists\" tab";
|
||||
"tour_section_6_text_7" = "You can also add another's playlists to your collection";
|
||||
|
||||
|
||||
"tour_section_7_title_1" = "Follow what your friends write";
|
||||
"tour_section_7_text_1" = "The "My News" section is divided into two types: local feed and global feed";
|
||||
"tour_section_7_text_2" = "The local feed will only show news from your friends and groups";
|
||||
"tour_section_7_bottom_text_1" = "No recommendation system. <b>It's up to you to create your own news feed</b>";
|
||||
|
||||
|
||||
"tour_section_7_title_1" = "Stay Updated with Your Friends' Updates";
|
||||
|
@ -1727,6 +1886,8 @@
|
|||
"s_order_by_name" = "By name";
|
||||
"s_order_by_random" = "By random";
|
||||
"s_order_by_rating" = "By rating";
|
||||
"s_order_by_length" = "By length";
|
||||
"s_order_by_listens" = "By listens count";
|
||||
"s_order_invert" = "Invert";
|
||||
|
||||
"s_by_date" = "By date";
|
||||
|
@ -1748,6 +1909,8 @@
|
|||
"deleted_target_comment" = "This comment belongs to deleted post";
|
||||
|
||||
"no_results" = "No results";
|
||||
"s_only_performers" = "Performers only";
|
||||
"s_with_lyrics" = "With lyrics";
|
||||
|
||||
/* BadBrowser */
|
||||
|
||||
|
@ -1787,6 +1950,7 @@
|
|||
/* Mobile */
|
||||
"mobile_friends" = "Friends";
|
||||
"mobile_photos" = "Photos";
|
||||
"mobile_audios" = "Audios";
|
||||
"mobile_videos" = "Videos";
|
||||
"mobile_messages" = "Messages";
|
||||
"mobile_notes" = "Notes";
|
||||
|
@ -1800,6 +1964,9 @@
|
|||
"mobile_user_info_hide" = "Hide";
|
||||
"mobile_user_info_show_details" = "Show details";
|
||||
|
||||
"my" = "My";
|
||||
"enter_a_name_or_artist" = "Enter a name or artist...";
|
||||
|
||||
/* Moderation */
|
||||
|
||||
"section" = "Section";
|
||||
|
|
|
@ -461,6 +461,7 @@
|
|||
"my_videos" = "Мои Видеозаписи";
|
||||
"my_messages" = "Мои Сообщения";
|
||||
"my_notes" = "Мои Заметки";
|
||||
"my_audios" = "Мои Аудиозаписи";
|
||||
"my_groups" = "Мои Группы";
|
||||
"my_feed" = "Мои Новости";
|
||||
"my_feedback" = "Мои Ответы";
|
||||
|
@ -540,6 +541,7 @@
|
|||
"privacy_setting_add_to_friends" = "Кто может называть меня другом";
|
||||
"privacy_setting_write_wall" = "Кто может писать у меня на стене";
|
||||
"privacy_setting_write_messages" = "Кто может писать мне сообщения";
|
||||
"privacy_setting_view_audio" = "Кому видно мои аудиозаписи";
|
||||
"privacy_value_anybody" = "Все желающие";
|
||||
"privacy_value_anybody_dative" = "Всем желающим";
|
||||
"privacy_value_users" = "Пользователям OpenVK";
|
||||
|
@ -672,9 +674,128 @@
|
|||
"upload_new_video" = "Загрузить новое видео";
|
||||
"max_attached_videos" = "Максимум 10 видеозаписей";
|
||||
"max_attached_photos" = "Максимум 10 фотографий";
|
||||
"max_attached_audios" = "Максимум 10 аудиозаписей";
|
||||
"no_videos" = "У вас нет видео.";
|
||||
"no_videos_results" = "Нет результатов.";
|
||||
|
||||
/* Audios */
|
||||
|
||||
"audios" = "Аудиозаписи";
|
||||
"audio" = "Аудиозапись";
|
||||
"playlist" = "Плейлист";
|
||||
"upload_audio" = "Загрузить аудио";
|
||||
"upload_audio_to_group" = "Загрузить аудио в группу";
|
||||
|
||||
"performer" = "Исполнитель";
|
||||
"audio_name" = "Название";
|
||||
"genre" = "Жанр";
|
||||
"lyrics" = "Текст";
|
||||
|
||||
"select_another_file" = "Выбрать другой файл";
|
||||
|
||||
"limits" = "Ограничения";
|
||||
"select_audio" = "Выберите аудиозапись на Вашем компьютере";
|
||||
"audio_requirements" = "Аудиозапись должна быть длинной от $1c до $2 минут, весить до $3мб и содержать аудиопоток.";
|
||||
"audio_requirements_2" = "Аудиозапись не должна нарушать авторские и смежные права.";
|
||||
"you_can_also_add_audio_using" = "Вы также можете добавить аудиозапись из числа уже загруженных файлов, воспользовавшись";
|
||||
"search_audio_inst" = "поиском по аудио";
|
||||
|
||||
"audio_embed_not_found" = "Аудиозапись не найдена";
|
||||
"audio_embed_deleted" = "Аудиозапись была удалена";
|
||||
"audio_embed_withdrawn" = "Аудиозапись была изъята по обращению правообладателя.";
|
||||
"audio_embed_forbidden" = "Настройки приватности пользователя не позволяют встраивать эту композицию";
|
||||
"audio_embed_processing" = "Аудио ещё обрабатывается, либо обработалось неправильно.";
|
||||
|
||||
"audios_count_zero" = "Нет аудиозаписей";
|
||||
"audios_count_one" = "Одна аудиозапись"; /* сингл */
|
||||
"audios_count_few" = "$1 аудиозаписи";
|
||||
"audios_count_many" = "$1 аудиозаписей";
|
||||
"audios_count_other" = "$1 аудиозаписей";
|
||||
|
||||
"track_unknown" = "Неизвестен";
|
||||
"track_noname" = "Без названия";
|
||||
|
||||
"my_music" = "Моя музыка";
|
||||
"music_user" = "Музыка пользователя";
|
||||
"music_club" = "Музыка группы";
|
||||
"audio_new" = "Новое";
|
||||
"audio_popular" = "Популярное";
|
||||
"audio_search" = "Поиск";
|
||||
|
||||
"my_audios_small" = "Мои аудиозаписи";
|
||||
"my_playlists" = "Мои плейлисты";
|
||||
"playlists" = "Плейлисты";
|
||||
"audios_explicit" = "Содержит нецензурную лексику";
|
||||
"withdrawn" = "Изъято";
|
||||
"deleted" = "Удалено";
|
||||
"owner" = "Владелец";
|
||||
"searchable" = "Доступно в поиске";
|
||||
|
||||
"select_audio" = "Выбрать аудиозаписи";
|
||||
"no_playlists_thisuser" = "Вы ещё не добавляли плейлистов.";
|
||||
"no_playlists_user" = "Этот пользователь ещё не добавлял плейлистов.";
|
||||
"no_playlists_club" = "Эта группа ещё не добавляла плейлистов.";
|
||||
|
||||
"no_audios_thisuser" = "Вы ещё не добавляли аудиозаписей.";
|
||||
"no_audios_user" = "Этот пользователь ещё не добавлял аудиозаписей.";
|
||||
"no_audios_club" = "Эта группа ещё не добавляла аудиозаписей.";
|
||||
|
||||
"new_playlist" = "Новый плейлист";
|
||||
"created_playlist" = "создан";
|
||||
"updated_playlist" = "обновлён";
|
||||
"bookmark" = "Добавить в коллекцию";
|
||||
"unbookmark" = "Убрать из коллекции";
|
||||
"empty_playlist" = "В этом плейлисте нет аудиозаписей.";
|
||||
"edit_playlist" = "Редактировать плейлист";
|
||||
"unable_to_load_queue" = "Не удалось загрузить очередь.";
|
||||
|
||||
"fully_delete_audio" = "Полностью удалить аудиозапись";
|
||||
"attach_audio" = "Прикрепить аудиозапись";
|
||||
"detach_audio" = "Открепить аудиозапись";
|
||||
|
||||
"show_more_audios" = "Показать больше аудиозаписей";
|
||||
"add_to_playlist" = "Добавить в плейлист";
|
||||
"remove_from_playlist" = "Удалить из плейлиста";
|
||||
"delete_playlist" = "Удалить плейлист";
|
||||
"playlist_cover" = "Обложка плейлиста";
|
||||
"playlists_user" = "Плейлисты польз.";
|
||||
"playlists_club" = "Плейлисты группы";
|
||||
"change_cover" = "Сменить обложку";
|
||||
"playlist_cover" = "Обложка плейлиста";
|
||||
|
||||
"minutes_count_zero" = "длится ноль минут";
|
||||
"minutes_count_one" = "длится одну минуту";
|
||||
"minutes_count_few" = "длится $1 минуты";
|
||||
"minutes_count_many" = "длится $1 минут";
|
||||
"minutes_count_other" = "длится $1 минут";
|
||||
|
||||
"listens_count_zero" = "нет прослушиваний";
|
||||
"listens_count_one" = "одно прослушивание";
|
||||
"listens_count_few" = "$1 прослушивания";
|
||||
"listens_count_many" = "$1 прослушиваний";
|
||||
"listens_count_other" = "$1 прослушиваний";
|
||||
|
||||
"add_audio_to_club" = "Добавить аудио в группу";
|
||||
"what_club_add" = "В какую группу вы хотите добавить песню?";
|
||||
"group_has_audio" = "У группы уже есть эта песня.";
|
||||
"group_hasnt_audio" = "У группы нет этой песни.";
|
||||
|
||||
"by_name" = "по композициям";
|
||||
"by_performer" = "по исполнителю";
|
||||
"no_access_clubs" = "Нет групп, где вы являетесь администратором.";
|
||||
"audio_successfully_uploaded" = "Аудио успешно загружено и на данный момент обрабатывается.";
|
||||
|
||||
"broadcast_audio" = "Транслировать аудио в статус";
|
||||
"sure_delete_playlist" = "Вы действительно хотите удалить этот плейлист?";
|
||||
"edit_audio" = "Редактировать аудиозапись";
|
||||
"audios_group" = "Аудиозаписи группы";
|
||||
"playlists_group" = "Плейлисты группы";
|
||||
|
||||
"play_tip" = "Проигрывание/пауза";
|
||||
"repeat_tip" = "Повторение";
|
||||
"shuffle_tip" = "Перемешать";
|
||||
"mute_tip" = "Заглушить";
|
||||
|
||||
/* Notifications */
|
||||
|
||||
"feedback" = "Ответы";
|
||||
|
@ -915,6 +1036,7 @@
|
|||
"going_to_report_photo" = "Вы собираетесь пожаловаться на данную фотографию.";
|
||||
"going_to_report_user" = "Вы собираетесь пожаловаться на данного пользователя.";
|
||||
"going_to_report_video" = "Вы собираетесь пожаловаться на данную видеозапись.";
|
||||
"going_to_report_audio" = "Вы собираетесь пожаловаться на данную аудиозапись.";
|
||||
"going_to_report_post" = "Вы собираетесь пожаловаться на данную запись.";
|
||||
"going_to_report_comment" = "Вы собираетесь пожаловаться на данный комментарий.";
|
||||
|
||||
|
@ -1030,6 +1152,7 @@
|
|||
"topics_other" = "$1 тем";
|
||||
"created" = "Создано";
|
||||
"everyone_can_create_topics" = "Все могут создавать темы";
|
||||
"everyone_can_upload_audios" = "Все могут загружать аудиозаписи";
|
||||
"display_list_of_topics_above_wall" = "Отображать список тем над стеной";
|
||||
"topic_changes_saved_comment" = "Обновлённый заголовок и настройки появятся на странице с темой.";
|
||||
"failed_to_create_topic" = "Не удалось создать тему";
|
||||
|
@ -1047,6 +1170,7 @@
|
|||
"no_data" = "Нет данных";
|
||||
"no_data_description" = "Тут ничего нет... Пока...";
|
||||
"error" = "Ошибка";
|
||||
"error_generic" = "Произошла ошибка общего характера: ";
|
||||
"error_shorturl" = "Данный короткий адрес уже занят.";
|
||||
"error_segmentation" = "Ошибка сегментации";
|
||||
"error_upload_failed" = "Не удалось загрузить фото";
|
||||
|
@ -1055,6 +1179,8 @@
|
|||
"error_weak_password" = "Ненадёжный пароль. Пароль должен содержать не менее 8 символов, цифры, прописные и строчные буквы";
|
||||
"error_shorturl_incorrect" = "Короткий адрес имеет некорректный формат.";
|
||||
"error_repost_fail" = "Не удалось поделиться записью";
|
||||
|
||||
"error_insufficient_info" = "Вы не указали необходимую информацию.";
|
||||
"error_data_too_big" = "Аттрибут '$1' не может быть длиннее $2 $3";
|
||||
"forbidden" = "Ошибка доступа";
|
||||
"unknown_error" = "Неизвестная ошибка";
|
||||
|
@ -1181,6 +1307,21 @@
|
|||
"group_owner_is_banned" = "Создатель сообщества успешно забанен.";
|
||||
"group_is_banned" = "Сообщество успешно забанено";
|
||||
"description_too_long" = "Описание слишком длинное.";
|
||||
"invalid_audio" = "Такой аудиозаписи не существует.";
|
||||
"do_not_have_audio" = "У вас нет этой аудиозаписи.";
|
||||
"do_have_audio" = "У вас уже есть эта аудиозапись.";
|
||||
|
||||
"set_playlist_name" = "Укажите название плейлиста.";
|
||||
"playlist_already_bookmarked" = "Плейлист уже есть в вашей коллекции.";
|
||||
"playlist_not_bookmarked" = "Плейлиста нет в вашей коллекции.";
|
||||
"invalid_cover_photo" = "Не удалось сохранить обложку плейлиста.";
|
||||
"not_a_photo" = "Загруженный файл не похож на фотографию.";
|
||||
"file_too_big" = "Файл слишком большой.";
|
||||
"file_loaded_partially" = "Файл загрузился частично.";
|
||||
"file_not_uploaded" = "Не удалось загрузить файл.";
|
||||
"error_code" = "Код ошибки: $1.";
|
||||
"ffmpeg_timeout" = "Превышено время ожидания обработки ffmpeg. Попробуйте загрузить файл снова.";
|
||||
"ffmpeg_not_installed" = "Не удалось обработать файл. Похоже, на сервере не установлен ffmpeg.";
|
||||
|
||||
/* Admin actions */
|
||||
|
||||
|
@ -1277,6 +1418,10 @@
|
|||
|
||||
"admin_gift_moved_successfully" = "Подарок успешно перемещён";
|
||||
"admin_gift_moved_to_recycle" = "Теперь подарок находится в <b>корзине</b>.";
|
||||
"admin_original_file" = "Оригинальный файл";
|
||||
"admin_audio_length" = "Длина";
|
||||
"admin_cover_id" = "Обложка (ID фото)";
|
||||
"admin_music" = "Музыка";
|
||||
|
||||
"logs" = "Логи";
|
||||
"logs_anything" = "Любое";
|
||||
|
@ -1508,8 +1653,16 @@
|
|||
"tour_section_5_text_3" = "Кроме загрузки видео напрямую, сайт поддерживает и встраивание видео из YouTube";
|
||||
|
||||
|
||||
"tour_section_6_title_1" = "Аудиозаписи, которых пока что нет XD";
|
||||
"tour_section_6_text_1" = "Я был бы очень рад сделать туториал по этому разделу, но солнышко Вриска не сделала музыку";
|
||||
"tour_section_6_title_1" = "Слушайте аудиозаписи";
|
||||
"tour_section_6_text_1" = "Вы можете слушать аудиозаписи в разделе \"Мои Аудиозаписи\"";
|
||||
"tour_section_6_text_2" = "Этот раздел также регулируется настройками приватности";
|
||||
"tour_section_6_text_3" = "Самые прослушиваемые песни находятся во вкладке \"Популярное\", а недавно загруженные — во вкладке \"Новое\"";
|
||||
"tour_section_6_text_4" = "Чтобы добавить песню в свою коллекцию, наведите на неё и нажмите на плюс. Найти нужную песню можно в поиске";
|
||||
"tour_section_6_text_5" = "Если вы не можете найти нужную песню, вы можете загрузить её самостоятельно";
|
||||
"tour_section_6_bottom_text_1" = "<b>Важно:</b> песня не должна нарушать авторские права";
|
||||
"tour_section_6_title_2" = "Создавайте плейлисты";
|
||||
"tour_section_6_text_6" = "Вы можете создавать сборники треков во вкладке \"Мои плейлисты\"";
|
||||
"tour_section_6_text_7" = "Можно также добавлять чужие плейлисты в свою коллекцию";
|
||||
|
||||
|
||||
"tour_section_7_title_1" = "Следите за тем, что пишут ваши друзья";
|
||||
|
@ -1620,6 +1773,8 @@
|
|||
"s_order_by_name" = "По имени";
|
||||
"s_order_by_random" = "По случайности";
|
||||
"s_order_by_rating" = "По рейтингу";
|
||||
"s_order_by_length" = "По длине";
|
||||
"s_order_by_listens" = "По числу прослушиваний";
|
||||
"s_order_invert" = "Инвертировать";
|
||||
|
||||
"s_by_date" = "По дате";
|
||||
|
@ -1641,6 +1796,8 @@
|
|||
"deleted_target_comment" = "Этот комментарий принадлежит к удалённой записи";
|
||||
|
||||
"no_results" = "Результатов нет";
|
||||
"s_only_performers" = "Только исполнители";
|
||||
"s_with_lyrics" = "С текстом";
|
||||
|
||||
/* BadBrowser */
|
||||
|
||||
|
@ -1680,6 +1837,7 @@
|
|||
/* Mobile */
|
||||
"mobile_friends" = "Друзья";
|
||||
"mobile_photos" = "Фотографии";
|
||||
"mobile_audios" = "Аудиозаписи";
|
||||
"mobile_videos" = "Видеозаписи";
|
||||
"mobile_messages" = "Сообщения";
|
||||
"mobile_notes" = "Заметки";
|
||||
|
@ -1693,6 +1851,9 @@
|
|||
"mobile_user_info_hide" = "Скрыть";
|
||||
"mobile_user_info_show_details" = "Показать подробнее";
|
||||
|
||||
"my" = "Мои";
|
||||
"enter_a_name_or_artist" = "Введите название или автора...";
|
||||
|
||||
/* Moderation */
|
||||
|
||||
"section" = "Раздел";
|
||||
|
|
|
@ -151,7 +151,7 @@
|
|||
"post_deact_silent_m" = "molča udalil svoju stranicu.";
|
||||
"post_deact_silent_f" = "molča udalila svoju stranicu.";
|
||||
"post_on_your_wall" = "na vašej stene";
|
||||
"post_on_group_wall" = "v $1";
|
||||
"post_on_group_wall" = "в $1";
|
||||
"post_on_user_wall" = "na stene $1";
|
||||
"wall" = "Stena";
|
||||
"post" = "Zapisí";
|
||||
|
@ -464,6 +464,7 @@
|
|||
"my_videos" = "Moi Videozapisi";
|
||||
"my_messages" = "Moi Soobsčenija";
|
||||
"my_notes" = "Moi Zametki";
|
||||
"my_audios" = "Moi Audiozapisi";
|
||||
"my_groups" = "Moi Gruppy";
|
||||
"my_feed" = "Moi Novosti";
|
||||
"my_feedback" = "Moi Otvety";
|
||||
|
@ -543,6 +544,7 @@
|
|||
"privacy_setting_add_to_friends" = "Kto možet nazyvatí menja drugom";
|
||||
"privacy_setting_write_wall" = "Kto možet pisatí u menja na stene";
|
||||
"privacy_setting_write_messages" = "Kto možet pisatí mne soobsčenija";
|
||||
"privacy_setting_view_audio" = "Komu vidno moi audiozapisi";
|
||||
"privacy_value_anybody" = "Vse želajusčie";
|
||||
"privacy_value_anybody_dative" = "Vsem želajusčim";
|
||||
"privacy_value_users" = "Polízovateljam OpenVK";
|
||||
|
@ -675,9 +677,128 @@
|
|||
"upload_new_video" = "Zagruzití novoe video";
|
||||
"max_attached_videos" = "Maksimum 10 videozapisej";
|
||||
"max_attached_photos" = "Maksimum 10 fotografij";
|
||||
"max_attached_audios" = "Maksimum 10 audiozapisej";
|
||||
"no_videos" = "U vas net video.";
|
||||
"no_videos_results" = "Net rezulítatov.";
|
||||
|
||||
/* Audios */
|
||||
|
||||
"audios" = "Audiozapisi";
|
||||
"audio" = "Audiozapisí";
|
||||
"playlist" = "Plejlist";
|
||||
"upload_audio" = "Zagruzití audio";
|
||||
"upload_audio_to_group" = "Zagruzití audio v gruppu";
|
||||
|
||||
"performer" = "Ispolnitelí";
|
||||
"audio_name" = "Nazvanie";
|
||||
"genre" = "Žanr";
|
||||
"lyrics" = "Tekst";
|
||||
|
||||
"select_another_file" = "Vybratí drugoj fajl";
|
||||
|
||||
"limits" = "Ograničenija";
|
||||
"select_audio" = "Vyberite audiozapisí na Vašem kompíjutere";
|
||||
"audio_requirements" = "Audiozapisí dolžna bytí dlinnoj ot $1c do $2 minut, vesití do $3mb i soderžatí audiopotok.";
|
||||
"audio_requirements_2" = "Audiozapisí ne dolžna narušatí avtorskie i smežnye prava.";
|
||||
"you_can_also_add_audio_using" = "Vy takže možete dobavití audiozapisí iz čisla uže zagružennyh fajlov, vospolízovavšisí";
|
||||
"search_audio_inst" = "poiskom po audio";
|
||||
|
||||
"audio_embed_not_found" = "Audiozapisí ne najdena";
|
||||
"audio_embed_deleted" = "Audiozapisí byla udalena";
|
||||
"audio_embed_withdrawn" = "Audiozapisí byla izjjata po obrasčeniju pravoobladatelja.";
|
||||
"audio_embed_forbidden" = "Nastrojki privatnosti polízovatelja ne pozvoljajut vstraivatí etu kompoziciju";
|
||||
"audio_embed_processing" = "Audio esčjo obrabatyvaetsja, libo obrabotalosí nepravilíno.";
|
||||
|
||||
"audios_count_zero" = "Net audiozapisej";
|
||||
"audios_count_one" = "Odna audiozapisí";
|
||||
"audios_count_few" = "$1 audiozapisi";
|
||||
"audios_count_many" = "$1 audiozapisej";
|
||||
"audios_count_other" = "$1 audiozapisej";
|
||||
|
||||
"track_unknown" = "Neizvesten";
|
||||
"track_noname" = "Bez nazvanija";
|
||||
|
||||
"my_music" = "Moja muzyka";
|
||||
"music_user" = "Muzyka polízovatelja";
|
||||
"music_club" = "Muzyka gruppy";
|
||||
"audio_new" = "Novoe";
|
||||
"audio_popular" = "Populjarnoe";
|
||||
"audio_search" = "Poisk";
|
||||
|
||||
"my_audios_small" = "Moi audiozapisi";
|
||||
"my_playlists" = "Moi plejlisty";
|
||||
"playlists" = "Plejlisty";
|
||||
"audios_explicit" = "Soderžit necenzurnuju leksiku";
|
||||
"withdrawn" = "Izjjato";
|
||||
"deleted" = "Udaleno";
|
||||
"owner" = "Vladelec";
|
||||
"searchable" = "Dostupno v poiske";
|
||||
|
||||
"select_audio" = "Vybratí audiozapisi";
|
||||
"no_playlists_thisuser" = "Vy esčjo ne dobavljali plejlistov.";
|
||||
"no_playlists_user" = "Etot polízovatelí esčjo ne dobavljal plejlistov.";
|
||||
"no_playlists_club" = "Eta gruppa esčjo ne dobavljala plejlistov.";
|
||||
|
||||
"no_audios_thisuser" = "Vy esčjo ne dobavljali audiozapisej.";
|
||||
"no_audios_user" = "Etot polízovatelí esčjo ne dobavljal audiozapisej.";
|
||||
"no_audios_club" = "Eta gruppa esčjo ne dobavljala audiozapisej.";
|
||||
|
||||
"new_playlist" = "Novyj plejlist";
|
||||
"created_playlist" = "sozdan";
|
||||
"updated_playlist" = "obnovljon";
|
||||
"bookmark" = "Dobavití v kollekciju";
|
||||
"unbookmark" = "Ubratí iz kollekcii";
|
||||
"empty_playlist" = "V etom plejliste net audiozapisej.";
|
||||
"edit_playlist" = "Redaktirovatí plejlist";
|
||||
"unable_to_load_queue" = "Ne udalosí zagruzití očeredí.";
|
||||
|
||||
"fully_delete_audio" = "Polnostíju udalití audiozapisí";
|
||||
"attach_audio" = "Prikrepití audiozapisí";
|
||||
"detach_audio" = "Otkrepití audiozapisí";
|
||||
|
||||
"show_more_audios" = "Pokazatí bolíše audiozapisej";
|
||||
"add_to_playlist" = "Dobavití v plejlist";
|
||||
"remove_from_playlist" = "Udalití iz plejlista";
|
||||
"delete_playlist" = "Udalití plejlist";
|
||||
"playlist_cover" = "Obložka plejlista";
|
||||
"playlists_user" = "Plejlisty políz.";
|
||||
"playlists_club" = "Plejlisty gruppy";
|
||||
"change_cover" = "Smenití obložku";
|
||||
"playlist_cover" = "Obložka plejlista";
|
||||
|
||||
"minutes_count_zero" = "dlitsja nolí minut";
|
||||
"minutes_count_one" = "dlitsja odnu minutu";
|
||||
"minutes_count_few" = "dlitsja $1 minuty";
|
||||
"minutes_count_many" = "dlitsja $1 minut";
|
||||
"minutes_count_other" = "dlitsja $1 minut";
|
||||
|
||||
"listens_count_zero" = "net proslušivanij";
|
||||
"listens_count_one" = "odno proslušivanie";
|
||||
"listens_count_few" = "$1 proslušivanija";
|
||||
"listens_count_many" = "$1 proslušivanij";
|
||||
"listens_count_other" = "$1 proslušivanij";
|
||||
|
||||
"add_audio_to_club" = "Dobavití audio v gruppu";
|
||||
"what_club_add" = "V kakuju gruppu vy hotite dobavití pesnju?";
|
||||
"group_has_audio" = "U gruppy uže estí eta pesnja.";
|
||||
"group_hasnt_audio" = "U gruppy net etoj pesni.";
|
||||
|
||||
"by_name" = "po kompozicijam";
|
||||
"by_performer" = "po ispolnitelju";
|
||||
"no_access_clubs" = "Net grupp, gde vy javljaetesí administratorom.";
|
||||
"audio_successfully_uploaded" = "Audio uspešno zagruženo i na dannyj moment obrabatyvaetsja.";
|
||||
|
||||
"broadcast_audio" = "Translirovatí audio v status";
|
||||
"sure_delete_playlist" = "Vy dejstvitelíno hotite udalití etot plejlist?";
|
||||
"edit_audio" = "Redaktirovatí audiozapisí";
|
||||
"audios_group" = "Audiozapisi gruppy";
|
||||
"playlists_group" = "Plejlisty gruppy";
|
||||
|
||||
"play_tip" = "Proigryvanie/pauza";
|
||||
"repeat_tip" = "Povtorenie";
|
||||
"shuffle_tip" = "Peremešatí";
|
||||
"mute_tip" = "Zaglušití";
|
||||
|
||||
/* Notifications */
|
||||
|
||||
"feedback" = "Otvety";
|
||||
|
|
|
@ -50,6 +50,8 @@ openvk:
|
|||
- "Good luck filling! If you are a regular support agent, inform the administrator that he forgot to fill the config"
|
||||
messages:
|
||||
strict: false
|
||||
music:
|
||||
exposeOriginalURLs: true
|
||||
wall:
|
||||
christian: false
|
||||
anonymousPosting:
|
||||
|
|
|
@ -235,10 +235,133 @@ input[type="radio"] {
|
|||
}
|
||||
|
||||
.searchList #used {
|
||||
background: linear-gradient(#453e5e,#473f61);
|
||||
background: #463f60 !important;
|
||||
}
|
||||
|
||||
#backdropEditor {
|
||||
background-image: url("/themepack/midnight/0.0.2.8/resource/backdrop-editor.gif") !important;
|
||||
border-color: #473e66 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bigPlayer {
|
||||
background-color: rgb(30, 26, 43) !important;
|
||||
}
|
||||
|
||||
.bigPlayer .selectableTrack, .audioEmbed .track > .selectableTrack {
|
||||
border-top: #b9b9b9 1px solid !important;
|
||||
}
|
||||
|
||||
.bigPlayer .paddingLayer .slider, .audioEmbed .track .slider {
|
||||
background: #b9b9b9 !important;
|
||||
}
|
||||
|
||||
.musicIcon {
|
||||
filter: invert(81%) !important;
|
||||
}
|
||||
|
||||
.audioEntry.nowPlaying {
|
||||
background: #463f60 !important;
|
||||
border: 1px solid #645a86 !important;
|
||||
}
|
||||
|
||||
.preformer {
|
||||
color: #b7b7b7 !important;
|
||||
}
|
||||
|
||||
.bigPlayer .paddingLayer .trackPanel .track .timeTip {
|
||||
background: #b9b9b9 !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.audioEntry.nowPlaying:hover {
|
||||
background: #50486f !important;
|
||||
}
|
||||
|
||||
.audioEntry:hover {
|
||||
background: #19142D !important;
|
||||
}
|
||||
|
||||
.audioEntry .performer a {
|
||||
color: #a2a1a1 !important;
|
||||
}
|
||||
|
||||
.musicIcon.lagged {
|
||||
opacity: 49%;
|
||||
}
|
||||
|
||||
.bigPlayer .paddingLayer .bigPlayerTip {
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.searchList a {
|
||||
color: #bbb !important;
|
||||
}
|
||||
|
||||
.searchList a:hover {
|
||||
color: #eeeeee !important;
|
||||
background: #332d46 !important;
|
||||
}
|
||||
|
||||
.friendsAudiosList .elem:hover {
|
||||
background: #332d46 !important;
|
||||
}
|
||||
|
||||
.audioEntry .playerButton .playIcon {
|
||||
filter: invert(81%);
|
||||
}
|
||||
|
||||
img[src$='/assets/packages/static/openvk/img/camera_200.png'], img[src$='/assets/packages/static/openvk/img/song.jpg'] {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
.audioStatus {
|
||||
color: #8E8E8E !important;
|
||||
}
|
||||
|
||||
.audioEntry .withLyrics {
|
||||
color: #6f6497 !important;
|
||||
}
|
||||
|
||||
#listensCount {
|
||||
color: unset !important;
|
||||
}
|
||||
|
||||
#upload_container, .whiteBox {
|
||||
background: #1d1928 !important;
|
||||
border: 1px solid #383052 !important;
|
||||
}
|
||||
|
||||
ul {
|
||||
color: #8b9ab5 !important;
|
||||
}
|
||||
|
||||
#audio_upload {
|
||||
border: 2px solid #383052 !important;
|
||||
background-color: #262133 !important;
|
||||
}
|
||||
|
||||
/* вот бы css в овк был бы написан на var()'ах( */
|
||||
#upload_container.uploading {
|
||||
background: #121017 url('/assets/packages/static/openvk/img/progressbar.gif') !important;
|
||||
}
|
||||
|
||||
.musicIcon.pressed {
|
||||
opacity: 41% !important;
|
||||
}
|
||||
|
||||
.ovk-diag-body .searchBox {
|
||||
background: #1e1a2b !important;
|
||||
}
|
||||
|
||||
.audioEntry.nowPlaying .title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.attachAudio:hover {
|
||||
background: #19142D !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.showMore, .showMoreAudiosPlaylist {
|
||||
background: #181826 !important;
|
||||
}
|
||||
|
|
|
@ -279,18 +279,9 @@ input[type=checkbox] {
|
|||
box-shadow: none;
|
||||
}
|
||||
|
||||
.searchList #used
|
||||
{
|
||||
margin-left:0px;
|
||||
color: white;
|
||||
padding: 2px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
border: none;
|
||||
background: #a4a4a4;
|
||||
margin-bottom: 2px;
|
||||
padding-left: 5px;
|
||||
width: 90%;
|
||||
.searchList #used {
|
||||
background: #3c3c3c !important;
|
||||
border: unset !important;
|
||||
}
|
||||
|
||||
.searchList #used a
|
||||
|
@ -337,3 +328,35 @@ input[type=checkbox] {
|
|||
{
|
||||
border-top: 1px solid #2f2f2f;
|
||||
}
|
||||
|
||||
.musicIcon {
|
||||
filter: contrast(202%) !important;
|
||||
}
|
||||
|
||||
.audioEntry .playerButton .playIcon {
|
||||
filter: contrast(7) !important;
|
||||
}
|
||||
|
||||
.audioEmbed .track > .selectableTrack, .bigPlayer .selectableTrack {
|
||||
border-top: #404040 1px solid !important;
|
||||
}
|
||||
|
||||
.bigPlayer .paddingLayer .slider, .audioEmbed .track .slider {
|
||||
background: #3c3c3c !important;
|
||||
}
|
||||
|
||||
.audioEntry.nowPlaying {
|
||||
background: #4b4b4b !important;
|
||||
}
|
||||
|
||||
.audioEntry.nowPlaying:hover {
|
||||
background: #373737 !important;
|
||||
}
|
||||
|
||||
.musicIcon.pressed {
|
||||
filter: brightness(150%) !important;
|
||||
}
|
||||
|
||||
.musicIcon.lagged {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
|