Music, finally! (#512)

* Add audio upload feature

* Add audio embed thing

* Move bullet.gif to ovk

* Draft some music API methods

* Add support for base64 ids to Audios.getById

* Disallow having more than 65k audios in playlist

* Add playlist model

* Draft some playlist-related API methods

* Fix behabiour of album-related methods

Generators f***** me in le a**

* Add IDv3 autofill

* Add sql dumps

i forgor to upload it xdddd

* Add playlists sql

* Fix audio upload not working on Windows 11 because Windows is the worst operating system which doesn't work properly under any circumstances

* Fix cocksex in audio.get

yes

* Интерфейсы

* Interface updade

* Update en.strings

* Add audio queue

* Make repeat button work

* Some improvements to audio queue

* Фгвшщ йгугу шьзкщмуьутеы

* Make shuffle and "наушники" buttons work, add f...

avicons when playing audio, save some values (like volume and last played track) to localstorage, add ability to toggle time type in player, fix uploading audios with cover (maybe) and add dragndrop to upload page

* Add funny tip with time when hover track div

* Add something

* Add audios picker & move track in smal player вниз

* Summary (required)

Description

* [WIP] Add calls, stories and clips.

Изменены фавиконки (поменьше стали)
У миниплеера ползунок теперь в стиле bsdn и большого плеера, добавлен ползунок громкости
Добавлена кнопка добавления аудио в группу (у миниплеера)
Если вы смотрите аудио группы, которой можете управлять, появляется кнопка "удалить аудио из группы"
Снизу плейлиста в списке теперь показывается автор.
При прикреплении аудиозаписей к посту теперь есть поиск "по композиции" и "по исполнителю"
Добавил explicit.svg, который я забыл добавить в предыдущем коммите.
Вкладочки немного переделаны
При наведении на кнопки "трек вперёд" или "трек назад" показывается название предыдущего или следующего трека соответственно

* 1 new commit to master: [WIP]: Add audios

- Теперь группа может разрешать загружать всем треки в неё
- Теперь треки загружаются на сервер ajax'ом, и так можно очень много аудио загружать
- Вёрстка списка плейлистов изменена, теперь она на гридах
- Немного изменено апи, теперь метод editAlbum сохраняет новую информацию ee объект плейлистов теперь возвращают реальное время
- Удалены лишние пути из routes.yml
- При переключении страниц теперь если на текущей странице есть играющий трек, он нормально подсвечивается
- Из init-db.sql удалены таблицы аудиозаписей
- В Groups.getSettings и groups.edit теперь есть информация о аудиозаписях

* (смешное название коммита)

- Теперь на странице пользователя/группы показываются три случайные песни, а не первые три как раньше
- Теперь пробел на странице аудио не перемещает вас в низ страницы
- Оптимизирован мини-плеер, теперь он инициализируется при любом нажатии на него, а не при наведении
- Теперь при завершении проигрывания трека в мини-плеере он ищет другой трек рядом, и если находит то воспроизводит. Будет удобно для постов с подборками треков
- Поиск теперь показывает 14 результатов
- Теперь при возникновении ошибки загрузки аудио она нормально отображается
- Вместе с плеером на странице с аудиозаписями теперь двигаются и вкладки
- Добавление аудио в группу по идее должно нормально работать

* Implement playlists listens

- У плейлистов теперь есть прослушивания в общем.
- Прослушивания у большого плеера теперь засчитываются, если трек был дослушан до конца
- В объекте плейлистов теперь возвращается listens и cover_url
- Получение плееров через /audios/context переписано, повторяющийся код удалён, правда сильно количество строк сократить не получилось
- Теперь цвета плеера темнее, а иконка проигрывания изменена
- Теперь, если очередь из треков кончилась, то плеер перенаправляет вас в начало очереди.

* php 8.2 fixxxxxxxxxxxxxxxxxxxxxxx

* Implement audiostatuses

Добавлены аудиостатусы (у пользователей), блок с друзьями, слушающих музыку на странице аудиозаписей, объект status_audio в users.get, улучшены настройки приватности и ещё что-то

* ?

- Переделан метод в классе user для получения друзей с проигрываемыми песнями. Теперь среди них могут появляться и группы (хз стоит ли оставлять это или нет). Так же больше не показываются удалённые пользователи
- Трек у плеера теперь двигается немного плавнее. Ещё теперь нету смешных багов с подсказкой времени, когда можно было увести её за экран или  промотать дальше трека. Переключить повторение трека теперь можно нажатием кнопки R.
- Длинное название трека больше не сносит время
- Наверное, теперь аудиозаписи нормально отображаются в темах midnight и modern
- Аудиозаписи больше не крашаются, если пользователь неавторизован.
- Немного переделан миниплеер.
- В миниплеере теперь громкость берётся из локалсторейджа.
- Улучшено редактирование аудиозаписей. Теперь данные в дата атрибуты нормально сохраняются, а так же слова песни и метка "explicit" меняются
- Удалён css, оставшийся ещё от public technical preview 1, а так же путь /audios{num}
- При наведении на трек теперь пропадает время, и на его месте появляются кнопки
- Стандартная аватарка в midnight теперь инвертируется
- В админке в редактировании аудио теперь показывается дата редактирования, дата создания, длина и оригинальный файл аудио. Так же на странице редактирования больше нет вылетов, если вы задали несуществующий аккаунт

* !

- Добавлены строки для мобильной темы
- Добавлено предупреждение перед полным удалением плейлиста
- Нажатие кнопки M = нажатие кнопки наушников
- В классе апи Audio поставлены willExecuteWriteAction, ещё теперь нельзя получить число аудиозаписей у пользователей, которые их закрыли. Ещё теперь нельзя получать uploaded_only аудиозаписи у тех ну вы поняли короче.
- При наведении на длинное название песни оно теперь показывается полностью
- Надо ещё что-то сюда написать, так что: При редактировании аудиозаписи название окна теперь не "Редактировать", а "Редактировать аудиозапись", а вместо кнопки OK кнопка "Сохранить"

* .

- Добавлен тур по аудиозаписям, но пока без скриншотов.
- "Мои Аудиозаписи" в меню теперь располагаются под Моими Видеозаписями для канона
- В настройках приватности "кто может видеть мои аудиозаписи" теперь располагаются под "кто может видеть мои видеозаписи"
- В настройках внешнего вида мои аудиозаписи тоже под видео
- Изменён <title> на странице аудиозаписей. Теперь показывается "Аудиозаписи" + имя пользователя в родительном падеже. А если это группа, то "Аудиозаписи группы". То же самое с плейлистами
- Исправлены ссылка в ссылке на странице с плейлистами
- При наведении на название песни больше не сносится иконка explicit
- Добавлена максимальная длина названия и описания плейлиста при редактировании.

* М

- Долокализована админка (точно помню, что уже делал это, но ладно)
- Удалён лишний пункт "audios" в getLeftMenuItemStatus (реально)
- Если. У плеера есть параметр "hideButtons", то при наведении на него не пропадает время.
- На странице редактирования/создания плейлиста если у песни длинное название, то оно да похуй короче. Ну в общем лучше стало
- Там где нужно, добавлена строка в конце файла
- Возвращена строка "photo" в английской локали (я её случайно удалил 👍 )

* у

- У изъятых аудиозаписей больше не показывается кнопка "добавить в группу". Так же при нажатии на кнопку удаления из коллекции окно не всплывает.
- "Удаление аудио из группы" тоже лучше работать стало с изъятыми аудио.

* з

- В пикере аудиозаписей "more..." заменено на "показать больше аудиозаписей"
- Если включен режим показа оставшегося времени, то при окончании песни больше не показывается "--1:--1"
- В пикере аудиозаписей, если у вас нет аудиозаписей и вы ничего не искали, показывается "Вы ещё не добавляли аудиозаписей"
- <hr>'ы стали серыми
- Добавлены title'ы у кнопок в большом плеере
- Проставлены alt'ы у плейлистов

* Musique: linux saport)

назар хуйню релизнул кста, плейерс клаб два не слушайте не рекомендую

* Update and rename gamma-00000-disco.sql to 00041-music.sql

* Update 00041-music.sql

---------

Co-authored-by: Ilya Prokopenko <dsrev@protonmail.com>
Co-authored-by: n1rwana <aydashkin@vk.com>
Co-authored-by: lalka2018 <99399973+lalka2016@users.noreply.github.com>
Co-authored-by: veselcraft <veselcraft@icloud.com>
Co-authored-by: DeathPleiad <43928323+Parad1seF0x@users.noreply.github.com>
This commit is contained in:
celestora 2023-11-11 23:41:07 +02:00 committed by GitHub
parent c9665ac77d
commit 9d7a465d0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 7333 additions and 274 deletions

View file

@ -1,22 +1,788 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\Audio as AEntity;
use openvk\Web\Models\Entities\Playlist;
use openvk\Web\Models\Repositories\Audios;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Repositories\Util\EntityStream;
final class Audio extends VKAPIRequestHandler
{
function get(): object
{
$serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"];
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() . ")");
return (object) [
"count" => 1,
"items" => [(object) [
"id" => 1,
"owner_id" => 1,
"artist" => "В ОВК ПОКА НЕТ МУЗЫКИ",
"title" => "ЖДИТЕ :)))",
"duration" => 22,
"url" => $serverUrl . "/assets/packages/static/openvk/audio/nomusic.mp3"
]]
];
# рофлан ебало
$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
{
$this->requireUser();
$shuffleSeed = NULL;
$shuffleSeedStr = NULL;
if($shuffle == 1) {
if(!$shuffle_seed) {
if($need_seed == 1) {
$shuffleSeed = openssl_random_pseudo_bytes(6);
$shuffleSeedStr = base64_encode($shuffleSeed);
$shuffleSeed = hexdec(bin2hex($shuffleSeed));
} else {
$hOffset = ((int) date("i") * 60) + (int) date("s");
$thisHour = time() - $hOffset;
$shuffleSeed = $thisHour + $this->getUser()->getId();
$shuffleSeedStr = base64_encode(hex2bin(dechex($shuffleSeed)));
}
} else {
$shuffleSeed = hexdec(bin2hex(base64_decode($shuffle_seed)));
$shuffleSeedStr = $shuffle_seed;
}
}
if($album_id != 0) {
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "album_id invalid");
else if(!$album->canBeViewedBy($this->getUser()))
$this->fail(600, "Can't open this album for reading");
$songs = [];
$list = $album->getAudios($offset, $count, $shuffleSeed);
foreach($list as $song)
$songs[] = $this->toSafeAudioStruct($song, $hash, $need_user == 1);
$response = (object) [
"count" => sizeof($songs),
"items" => $songs,
];
if(!is_null($shuffleSeed))
$response->shuffle_seed = $shuffleSeedStr;
return $response;
}
if(!empty($audio_ids)) {
$audio_ids = explode(",", $audio_ids);
if(!$audio_ids)
$this->fail(10, "Audio::get@L0d186:explode(string): Unknown error");
else if(sizeof($audio_ids) < 1)
$this->fail(8, "Invalid audio_ids syntax");
if(!is_null($shuffleSeed))
$audio_ids = knuth_shuffle($audio_ids, $shuffleSeed);
$obj = $this->getById(implode(",", $audio_ids), $hash, $need_user);
if(!is_null($shuffleSeed))
$obj->shuffle_seed = $shuffleSeedStr;
return $obj;
}
$dbCtx = DatabaseConnection::i()->getContext();
if($uploaded_only == 1) {
if($owner_id <= 0)
$this->fail(8, "uploaded_only can only be used with owner_id > 0");
$user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id);
if(!$user)
$this->fail(0602, "Invalid user");
if(!$user->getPrivacyPermission("audios.read", $this->getUser()))
$this->fail(15, "Access denied: this user chose to hide his audios");
if(!is_null($shuffleSeed)) {
$audio_ids = [];
$query = $dbCtx->table("audios")->select("virtual_id")->where([
"owner" => $owner_id,
"deleted" => 0,
]);
foreach($query as $res)
$audio_ids[] = $res->virtual_id;
$audio_ids = knuth_shuffle($audio_ids, $shuffleSeed);
$audio_ids = array_slice($audio_ids, $offset, $count);
$audio_q = ""; # audio.getById query
foreach($audio_ids as $aid)
$audio_q .= ",$owner_id" . "_$aid";
$obj = $this->getById(substr($audio_q, 1), $hash, $need_user);
$obj->shuffle_seed = $shuffleSeedStr;
return $obj;
}
$res = (new Audios)->getByUploader((new \openvk\Web\Models\Repositories\Users)->get($owner_id));
return $this->streamToResponse($res, $offset, $count, $hash, $need_user);
}
$query = $dbCtx->table("audio_relations")->select("audio")->where("entity", $owner_id);
if(!is_null($shuffleSeed)) {
$audio_ids = [];
foreach($query as $aid)
$audio_ids[] = $aid->audio;
$audio_ids = knuth_shuffle($audio_ids, $shuffleSeed);
$audio_ids = array_slice($audio_ids, $offset, $count);
$audio_q = "";
foreach($audio_ids as $aid)
$audio_q .= ",$aid";
$obj = $this->getById(substr($audio_q, 1), $hash, $need_user);
$obj->shuffle_seed = $shuffleSeedStr;
return $obj;
}
$items = [];
if($owner_id > 0) {
$user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id);
if(!$user)
$this->fail(50, "Invalid user");
if(!$user->getPrivacyPermission("audios.read", $this->getUser()))
$this->fail(15, "Access denied: this user chose to hide his audios");
}
$audios = (new Audios)->getByEntityID($owner_id, $offset, $count);
foreach($audios as $audio)
$items[] = $this->toSafeAudioStruct($audio, $hash, $need_user == 1);
return (object) [
"count" => sizeof($items),
"items" => $items,
];
}
function getLyrics(int $lyrics_id): object
{
$this->requireUser();
$audio = (new Audios)->get($lyrics_id);
if(!$audio || !$audio->getLyrics())
$this->fail(0404, "Not found");
if(!$audio->canBeViewedBy($this->getUser()))
$this->fail(201, "Access denied to lyrics");
return (object) [
"lyrics_id" => $lyrics_id,
"text" => preg_replace("%\r\n?%", "\n", $audio->getLyrics()),
];
}
function beacon(int $aid, ?int $gid = NULL): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$audio = (new Audios)->get($aid);
if(!$audio)
$this->fail(0404, "Not Found");
else if(!$audio->canBeViewedBy($this->getUser()))
$this->fail(201, "Insufficient permissions to listen this audio");
$group = NULL;
if(!is_null($gid)) {
$group = (new Clubs)->get($gid);
if(!$group)
$this->fail(0404, "Not Found");
else if(!$group->canBeModifiedBy($this->getUser()))
$this->fail(203, "Insufficient rights to this group");
}
return (int) $audio->listen($group ?? $this->getUser());
}
function setBroadcast(string $audio, string $target_ids): array
{
$this->requireUser();
[$owner, $aid] = explode("_", $audio);
$song = (new Audios)->getByOwnerAndVID((int) $owner, (int) $aid);
$ids = [];
foreach(explode(",", $target_ids) as $id) {
$id = (int) $id;
if($id > 0) {
if ($id != $this->getUser()->getId()) {
$this->fail(600, "Can't listen on behalf of $id");
} else {
$ids[] = $id;
$this->beacon($song->getId());
continue;
}
}
$group = (new Clubs)->get($id * -1);
if(!$group)
$this->fail(0404, "Not Found");
else if(!$group->canBeModifiedBy($this->getUser()))
$this->fail(203,"Insufficient rights to this group");
$ids[] = $id;
$this->beacon($song ? $song->getId() : 0, $id * -1);
}
return $ids;
}
function getBroadcastList(string $filter = "all", int $active = 0, ?string $hash = NULL): object
{
$this->requireUser();
if(!in_array($filter, ["all", "friends", "groups"]))
$this->fail(8, "Invalid filter $filter");
$broadcastList = $this->getUser()->getBroadcastList($filter);
$items = [];
foreach($broadcastList as $res) {
$struct = $res->toVkApiStruct();
$status = $res->getCurrentAudioStatus();
$struct->status_audio = $status ? $this->toSafeAudioStruct($status) : NULL;
$items[] = $struct;
}
return (object) [
"count" => sizeof($items),
"items" => $items,
];
}
function edit(int $owner_id, int $audio_id, ?string $artist = NULL, ?string $title = NULL, ?string $text = NULL, ?int $genre_id = NULL, ?string $genre_str = NULL, int $no_search = 0): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id);
if(!$audio)
$this->fail(0404, "Not Found");
else if(!$audio->canBeModifiedBy($this->getUser()))
$this->fail(201, "Insufficient permissions to edit this audio");
if(!is_null($genre_id)) {
$genre = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL;
if(!$genre)
$this->fail(8, "Invalid genre ID $genre_id");
$audio->setGenre($genre);
} else if(!is_null($genre_str)) {
if(!in_array($genre_str, AEntity::genres))
$this->fail(8, "Invalid genre ID $genre_str");
$audio->setGenre($genre_str);
}
$lyrics = 0;
if(!is_null($text)) {
$audio->setLyrics($text);
$lyrics = $audio->getId();
}
if(!is_null($artist))
$audio->setPerformer($artist);
if(!is_null($title))
$audio->setName($title);
$audio->setSearchability(!((bool) $no_search));
$audio->setEdited(time());
$audio->save();
return $lyrics;
}
function add(int $audio_id, int $owner_id, ?int $group_id = NULL, ?int $album_id = NULL): string
{
$this->requireUser();
$this->willExecuteWriteAction();
if(!is_null($album_id))
$this->fail(10, "album_id not implemented");
// TODO get rid of dups
$to = $this->getUser();
if(!is_null($group_id)) {
$group = (new Clubs)->get($group_id);
if(!$group)
$this->fail(0404, "Invalid group_id");
else if(!$group->canBeModifiedBy($this->getUser()))
$this->fail(203, "Insufficient rights to this group");
$to = $group;
}
$audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id);
if(!$audio)
$this->fail(0404, "Not found");
else if(!$audio->canBeViewedBy($this->getUser()))
$this->fail(201, "Access denied to audio(owner=$owner_id, vid=$audio_id)");
try {
$audio->add($to);
} catch(\OverflowException $ex) {
$this->fail(300, "Album is full");
}
return $audio->getPrettyId();
}
function delete(int $audio_id, int $owner_id, ?int $group_id = NULL): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$from = $this->getUser();
if(!is_null($group_id)) {
$group = (new Clubs)->get($group_id);
if(!$group)
$this->fail(0404, "Invalid group_id");
else if(!$group->canBeModifiedBy($this->getUser()))
$this->fail(203, "Insufficient rights to this group");
$from = $group;
}
$audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id);
if(!$audio)
$this->fail(0404, "Not found");
$audio->remove($from);
return 1;
}
function restore(int $audio_id, int $owner_id, ?int $group_id = NULL, ?string $hash = NULL): object
{
$this->requireUser();
$vid = $this->add($audio_id, $owner_id, $group_id);
return $this->getById($vid, $hash)->items[0];
}
function getAlbums(int $owner_id = 0, int $offset = 0, int $count = 50, int $drop_private = 1): object
{
$this->requireUser();
$owner_id = $owner_id == 0 ? $this->getUser()->getId() : $owner_id;
$playlists = [];
if($owner_id > 0 && $owner_id != $this->getUser()->getId()) {
$user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id);
if(!$user->getPrivacyPermission("audios.read", $this->getUser()))
$this->fail(50, "Access to playlists denied");
}
foreach((new Audios)->getPlaylistsByEntityId($owner_id, $offset, $count) as $playlist) {
if(!$playlist->canBeViewedBy($this->getUser())) {
if($drop_private == 1)
continue;
$playlists[] = NULL;
continue;
}
$playlists[] = $playlist->toVkApiStruct($this->getUser());
}
return (object) [
"count" => sizeof($playlists),
"items" => $playlists,
];
}
function searchAlbums(string $query, int $offset = 0, int $limit = 25, int $drop_private = 0): object
{
$this->requireUser();
$playlists = [];
$search = (new Audios)->searchPlaylists($query)->offsetLimit($offset, $limit);
foreach($search as $playlist) {
if(!$playlist->canBeViewedBy($this->getUser())) {
if($drop_private == 0)
$playlists[] = NULL;
continue;
}
$playlists[] = $playlist->toVkApiStruct($this->getUser());
}
return (object) [
"count" => sizeof($playlists),
"items" => $playlists,
];
}
function addAlbum(string $title, ?string $description = NULL, int $group_id = 0): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$group = NULL;
if($group_id != 0) {
$group = (new Clubs)->get($group_id);
if(!$group)
$this->fail(0404, "Invalid group_id");
else if(!$group->canBeModifiedBy($this->getUser()))
$this->fail(600, "Insufficient rights to this group");
}
$album = new Playlist;
$album->setName($title);
if(!is_null($group))
$album->setOwner($group_id * -1);
else
$album->setOwner($this->getUser()->getId());
if(!is_null($description))
$album->setDescription($description);
$album->save();
if(!is_null($group))
$album->bookmark($group);
else
$album->bookmark($this->getUser());
return $album->getId();
}
function editAlbum(int $album_id, ?string $title = NULL, ?string $description = NULL): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "Album not found");
else if(!$album->canBeModifiedBy($this->getUser()))
$this->fail(600, "Insufficient rights to this album");
if(!is_null($title))
$album->setName($title);
if(!is_null($description))
$album->setDescription($description);
$album->setEdited(time());
$album->save();
return (int) !(!$title && !$description);
}
function deleteAlbum(int $album_id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "Album not found");
else if(!$album->canBeModifiedBy($this->getUser()))
$this->fail(600, "Insufficient rights to this album");
$album->delete();
return 1;
}
function moveToAlbum(int $album_id, string $audio_ids): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "Album not found");
else if(!$album->canBeModifiedBy($this->getUser()))
$this->fail(600, "Insufficient rights to this album");
$audios = [];
$audio_ids = array_unique(explode(",", $audio_ids));
if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000)
$this->fail(8, "audio_ids must contain at least 1 audio and at most 1000");
foreach($audio_ids as $audio_id) {
$audio = $this->audioFromAnyId($audio_id);
if(!$audio)
continue;
else if(!$audio->canBeViewedBy($this->getUser()))
continue;
$audios[] = $audio;
}
if(sizeof($audios) < 1)
return 0;
$res = 1;
try {
foreach ($audios as $audio)
$res = min($res, (int) $album->add($audio));
} catch(\OutOfBoundsException $ex) {
return 0;
}
return $res;
}
function removeFromAlbum(int $album_id, string $audio_ids): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($album_id);
if(!$album)
$this->fail(0404, "Album not found");
else if(!$album->canBeModifiedBy($this->getUser()))
$this->fail(600, "Insufficient rights to this album");
$audios = [];
$audio_ids = array_unique(explode(",", $audio_ids));
if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000)
$this->fail(8, "audio_ids must contain at least 1 audio and at most 1000");
foreach($audio_ids as $audio_id) {
$audio = $this->audioFromAnyId($audio_id);
if(!$audio)
continue;
else if($audio->canBeViewedBy($this->getUser()))
continue;
$audios[] = $audio;
}
if(sizeof($audios) < 1)
return 0;
foreach($audios as $audio)
$album->remove($audio);
return 1;
}
function copyToAlbum(int $album_id, string $audio_ids): int
{
return $this->moveToAlbum($album_id, $audio_ids);
}
function bookmarkAlbum(int $id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($id);
if(!$album)
$this->fail(0404, "Not found");
if(!$album->canBeViewedBy($this->getUser()))
$this->fail(600, "Access error");
return (int) $album->bookmark($this->getUser());
}
function unBookmarkAlbum(int $id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$album = (new Audios)->getPlaylist($id);
if(!$album)
$this->fail(0404, "Not found");
if(!$album->canBeViewedBy($this->getUser()))
$this->fail(600, "Access error");
return (int) $album->unbookmark($this->getUser());
}
}

View file

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

View file

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

View file

@ -95,6 +95,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)

View file

@ -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,8 @@ 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[] = $attachment->toVkApiStruct($this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = [];
@ -233,6 +236,8 @@ 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[] = $attachment->toVkApiStruct($this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = [];
@ -450,6 +455,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 +490,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 +575,8 @@ 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[] = $attachment->toVkApiStruct($this->getUser());
}
}
@ -628,6 +643,8 @@ 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[] = $attachment->toVkApiStruct($this->getUser());
}
}
@ -719,6 +736,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 +763,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);
}
}

View file

@ -0,0 +1,469 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Util\Shell\Exceptions\UnknownCommandException;
use openvk\Web\Util\Shell\Shell;
/**
* @method setName(string)
* @method setPerformer(string)
* @method setLyrics(string)
* @method setExplicit(bool)
*/
class Audio extends Media
{
protected $tableName = "audios";
protected $fileExtension = "mpd";
# Taken from winamp :D
const genres = [
'Blues','Big Band','Classic Rock','Chorus','Country','Easy Listening','Dance','Acoustic','Disco','Humour','Funk','Speech','Grunge','Chanson','Hip-Hop','Opera','Jazz','Chamber Music','Metal','Sonata','New Age','Symphony','Oldies','Booty Bass','Other','Primus','Pop','Porn Groove','R&B','Satire','Rap','Slow Jam','Reggae','Club','Rock','Tango','Techno','Samba','Industrial','Folklore','Alternative','Ballad','Ska','Power Ballad','Death Metal','Rhythmic Soul','Pranks','Freestyle','Soundtrack','Duet','Euro-Techno','Punk Rock','Ambient','Drum Solo','Trip-Hop','A Cappella','Vocal','Euro-House','Jazz+Funk','Dance Hall','Fusion','Goa','Trance','Drum & Bass','Classical','Club-House','Instrumental','Hardcore','Acid','Terror','House','Indie','Game','BritPop','Sound Clip','Negerpunk','Gospel','Polsk Punk','Noise','Beat','AlternRock','Christian Gangsta Rap','Bass','Heavy Metal','Soul','Black Metal','Punk','Crossover','Space','Contemporary Christian','Meditative','Christian Rock','Instrumental Pop','Merengue','Instrumental Rock','Salsa','Ethnic','Thrash Metal','Gothic','Anime','Darkwave','JPop','Techno-Industrial','Synthpop','Electronic','Abstract','Pop-Folk','Art Rock','Eurodance','Baroque','Dream','Bhangra','Southern Rock','Big Beat','Comedy','Breakbeat','Cult','Chillout','Gangsta Rap','Downtempo','Top 40','Dub','Christian Rap','EBM','Pop / Funk','Eclectic','Jungle','Electro','Native American','Electroclash','Cabaret','Emo','New Wave','Experimental','Psychedelic','Garage','Rave','Global','Showtunes','IDM','Trailer','Illbient','Lo-Fi','Industro-Goth','Tribal','Jam Band','Acid Punk','Krautrock','Acid Jazz','Leftfield','Polka','Lounge','Retro','Math Rock','Musical','New Romantic','Rock & Roll','Nu-Breakz','Hard Rock','Post-Punk','Folk','Post-Rock','Folk-Rock','Psytrance','National Folk','Shoegaze','Swing','Space Rock','Fast Fusion','Trop Rock','Bebob','World Music','Latin','Neoclassical','Revival','Audiobook','Celtic','Audio Theatre','Bluegrass','Neue Deutsche Welle','Avantgarde','Podcast','Gothic Rock','Indie Rock','Progressive Rock','G-Funk','Psychedelic Rock','Dubstep','Symphonic Rock','Garage Rock','Slow Rock','Psybient','Psychobilly','Touhou'
];
# Taken from: https://web.archive.org/web/20220322153107/https://dev.vk.com/reference/objects/audio-genres
const vkGenres = [
"Rock" => 1,
"Pop" => 2,
"Rap" => 3,
"Hip-Hop" => 3, # VK API lists №3 as Rap & Hip-Hop, but these genres are distinct in OpenVK
"Easy Listening" => 4,
"House" => 5,
"Dance" => 5,
"Instrumental" => 6,
"Metal" => 7,
"Alternative" => 21,
"Dubstep" => 8,
"Jazz" => 1001,
"Blues" => 1001,
"Drum & Bass" => 10,
"Trance" => 11,
"Chanson" => 12,
"Ethnic" => 13,
"Acoustic" => 14,
"Vocal" => 14,
"Reggae" => 15,
"Classical" => 16,
"Indie Pop" => 17,
"Speech" => 19,
"Disco" => 22,
"Other" => 18,
];
private function fileLength(string $filename): int
{
if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe"))
throw new \Exception();
$error = NULL;
$streams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams a", "-loglevel error")->execute($error);
if($error !== 0)
throw new \DomainException("$filename is not recognized as media container");
else if(empty($streams) || ctype_space($streams))
throw new \DomainException("$filename does not contain any audio streams");
$vstreams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams v", "-loglevel error")->execute($error);
# check if audio has cover (attached_pic)
preg_match("%attached_pic=([0-1])%", $vstreams, $hasCover);
if(!empty($vstreams) && !ctype_space($vstreams) && ((int)($hasCover[1]) !== 1))
throw new \DomainException("$filename is a video");
$durations = [];
preg_match_all('%duration=([0-9\.]++)%', $streams, $durations);
if(sizeof($durations[1]) === 0)
throw new \DomainException("$filename does not contain any meaningful audio streams");
$length = 0;
foreach($durations[1] as $duration) {
$duration = floatval($duration);
if($duration < 1.0 || $duration > 65536.0)
throw new \DomainException("$filename does not contain any meaningful audio streams");
else
$length = max($length, $duration);
}
return (int) round($length, 0, PHP_ROUND_HALF_EVEN);
}
/**
* @throws \Exception
*/
protected function saveFile(string $filename, string $hash): bool
{
$duration = $this->fileLength($filename);
$kid = openssl_random_pseudo_bytes(16);
$key = openssl_random_pseudo_bytes(16);
$tok = openssl_random_pseudo_bytes(28);
$ss = ceil($duration / 15);
$this->stateChanges("kid", $kid);
$this->stateChanges("key", $key);
$this->stateChanges("token", $tok);
$this->stateChanges("segment_size", $ss);
$this->stateChanges("length", $duration);
try {
$args = [
str_replace("enabled", "available", OPENVK_ROOT),
str_replace("enabled", "available", $this->getBaseDir()),
$hash,
$filename,
bin2hex($kid),
bin2hex($key),
bin2hex($tok),
$ss,
];
if(Shell::isPowershell()) {
Shell::powershell("-executionpolicy bypass", "-File", __DIR__ . "/../shell/processAudio.ps1", ...$args)
->start();
} else {
Shell::bash(__DIR__ . "/../shell/processAudio.sh", ...$args) // Pls workkkkk
->start(); // idk, not tested :")
}
# Wait until processAudio will consume the file
$start = time();
while(file_exists($filename))
if(time() - $start > 5)
throw new \RuntimeException("Timed out waiting FFMPEG");
} catch(UnknownCommandException $ucex) {
exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash/pwsh is not installed" : VIDEOS_FRIENDLY_ERROR);
}
return true;
}
function getTitle(): string
{
return $this->getRecord()->name;
}
function getPerformer(): string
{
return $this->getRecord()->performer;
}
function getName(): string
{
return $this->getPerformer() . "" . $this->getTitle();
}
function getGenre(): ?string
{
return $this->getRecord()->genre;
}
function getLyrics(): ?string
{
return !is_null($this->getRecord()->lyrics) ? htmlspecialchars($this->getRecord()->lyrics, ENT_DISALLOWED | ENT_XHTML) : NULL;
}
function getLength(): int
{
return $this->getRecord()->length;
}
function getFormattedLength(): string
{
$len = $this->getLength();
$mins = floor($len / 60);
$secs = $len - ($mins * 60);
return (
str_pad((string) $mins, 2, "0", STR_PAD_LEFT)
. ":" .
str_pad((string) $secs, 2, "0", STR_PAD_LEFT)
);
}
function getSegmentSize(): float
{
return $this->getRecord()->segment_size;
}
function getListens(): int
{
return $this->getRecord()->listens;
}
function getOriginalURL(bool $force = false): string
{
$disallowed = !OPENVK_ROOT_CONF["openvk"]["preferences"]["music"]["exposeOriginalURLs"] && !$force;
if(!$this->isAvailable() || $disallowed)
return ovk_scheme(true)
. $_SERVER["HTTP_HOST"] . ":"
. $_SERVER["HTTP_PORT"]
. "/assets/packages/static/openvk/audio/nomusic.mp3";
$key = bin2hex($this->getRecord()->token);
return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3";
}
function getURL(?bool $force = false): string
{
if ($this->isWithdrawn()) return "";
return parent::getURL();
}
function getKeys(): array
{
$keys[bin2hex($this->getRecord()->kid)] = bin2hex($this->getRecord()->key);
return $keys;
}
function isAnonymous(): bool
{
return false;
}
function isExplicit(): bool
{
return (bool) $this->getRecord()->explicit;
}
function isWithdrawn(): bool
{
return (bool) $this->getRecord()->withdrawn;
}
function isUnlisted(): bool
{
return (bool) $this->getRecord()->unlisted;
}
# NOTICE may flush model to DB if it was just processed
function isAvailable(): bool
{
if($this->getRecord()->processed)
return true;
# throttle requests to isAvailable to prevent DoS attack if filesystem is actually an S3 storage
if(time() - $this->getRecord()->checked < 5)
return false;
try {
$fragments = str_replace(".mpd", "_fragments", $this->getFileName());
$original = "original_" . bin2hex($this->getRecord()->token) . ".mp3";
if(file_exists("$fragments/$original")) {
# Original gets uploaded after fragments
$this->stateChanges("processed", 0x01);
return true;
}
} finally {
$this->stateChanges("checked", time());
$this->save();
}
return false;
}
function isInLibraryOf($entity): bool
{
return sizeof(DatabaseConnection::i()->getContext()->table("audio_relations")->where([
"entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1),
"audio" => $this->getId(),
])) != 0;
}
function add($entity): bool
{
if($this->isInLibraryOf($entity))
return false;
$entityId = $entity->getId() * ($entity instanceof Club ? -1 : 1);
$audioRels = DatabaseConnection::i()->getContext()->table("audio_relations");
if(sizeof($audioRels->where("entity", $entityId)) > 65536)
throw new \OverflowException("Can't have more than 65536 audios in a playlist");
$audioRels->insert([
"entity" => $entityId,
"audio" => $this->getId(),
]);
return true;
}
function remove($entity): bool
{
if(!$this->isInLibraryOf($entity))
return false;
DatabaseConnection::i()->getContext()->table("audio_relations")->where([
"entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1),
"audio" => $this->getId(),
])->delete();
return true;
}
function listen($entity, Playlist $playlist = NULL): bool
{
$listensTable = DatabaseConnection::i()->getContext()->table("audio_listens");
$lastListen = $listensTable->where([
"entity" => $entity->getRealId(),
"audio" => $this->getId(),
])->order("index DESC")->fetch();
if(!$lastListen || (time() - $lastListen->time >= $this->getLength())) {
$listensTable->insert([
"entity" => $entity->getRealId(),
"audio" => $this->getId(),
"time" => time(),
"playlist" => $playlist ? $playlist->getId() : NULL,
]);
if($entity instanceof User) {
$this->stateChanges("listens", ($this->getListens() + 1));
$this->save();
if($playlist) {
$playlist->incrementListens();
$playlist->save();
}
}
$entity->setLast_played_track($this->getId());
$entity->save();
return true;
}
$lastListen->update([
"time" => time(),
]);
return false;
}
/**
* Returns compatible with VK API 4.x, 5.x structure.
*
* Always sets album(_id) to NULL at this time.
* If genre is not present in VK genre list, fallbacks to "Other".
* The url and manifest properties will be set to false if the audio can't be played (processing, removed).
*
* Aside from standard VK properties, this method will also return some OVK extended props:
* 1. added - Is in the library of $user?
* 2. editable - Can be edited by $user?
* 3. withdrawn - Removed due to copyright request?
* 4. ready - Can be played at this time?
* 5. genre_str - Full name of genre, NULL if it's undefined
* 6. manifest - URL to MPEG-DASH manifest
* 7. keys - ClearKey DRM keys
* 8. explicit - Marked as NSFW?
* 9. searchable - Can be found via search?
* 10. unique_id - Unique ID of audio
*
* @notice that in case if exposeOriginalURLs is set to false in config, "url" will always contain link to nomusic.mp3,
* unless $forceURLExposure is set to true.
*
* @notice may trigger db flush if the audio is not processed yet, use with caution on unsaved models.
*
* @param ?User $user user, relative to whom "added", "editable" will be set
* @param bool $forceURLExposure force set "url" regardless of config
*/
function toVkApiStruct(?User $user = NULL, bool $forceURLExposure = false): object
{
$obj = (object) [];
$obj->unique_id = base64_encode((string) $this->getId());
$obj->id = $obj->aid = $this->getVirtualId();
$obj->artist = $this->getPerformer();
$obj->title = $this->getTitle();
$obj->duration = $this->getLength();
$obj->album_id = $obj->album = NULL; # i forgor to implement
$obj->url = false;
$obj->manifest = false;
$obj->keys = false;
$obj->genre_id = $obj->genre = self::vkGenres[$this->getGenre() ?? ""] ?? 18; # return Other if no match
$obj->genre_str = $this->getGenre();
$obj->owner_id = $this->getOwner()->getId();
if($this->getOwner() instanceof Club)
$obj->owner_id *= -1;
$obj->lyrics = NULL;
if(!is_null($this->getLyrics()))
$obj->lyrics = $this->getId();
$obj->added = $user && $this->isInLibraryOf($user);
$obj->editable = $user && $this->canBeModifiedBy($user);
$obj->searchable = !$this->isUnlisted();
$obj->explicit = $this->isExplicit();
$obj->withdrawn = $this->isWithdrawn();
$obj->ready = $this->isAvailable() && !$obj->withdrawn;
if($obj->ready) {
$obj->url = $this->getOriginalURL($forceURLExposure);
$obj->manifest = $this->getURL();
$obj->keys = $this->getKeys();
}
return $obj;
}
function setOwner(int $oid): void
{
# WARNING: API implementation won't be able to handle groups like that, don't remove
if($oid <= 0)
throw new \OutOfRangeException("Only users can be owners of audio!");
$this->stateChanges("owner", $oid);
}
function setGenre(string $genre): void
{
if(!in_array($genre, Audio::genres)) {
$this->stateChanges("genre", NULL);
return;
}
$this->stateChanges("genre", $genre);
}
function setCopyrightStatus(bool $withdrawn = true): void {
$this->stateChanges("withdrawn", $withdrawn);
}
function setSearchability(bool $searchable = true): void {
$this->stateChanges("unlisted", !$searchable);
}
function setToken(string $tok): void {
throw new \LogicException("Changing keys is not supported.");
}
function setKid(string $kid): void {
throw new \LogicException("Changing keys is not supported.");
}
function setKey(string $key): void {
throw new \LogicException("Changing keys is not supported.");
}
function setLength(int $len): void {
throw new \LogicException("Changing length is not supported.");
}
function setSegment_Size(int $len): void {
throw new \LogicException("Changing length is not supported.");
}
function delete(bool $softly = true): void
{
$ctx = DatabaseConnection::i()->getContext();
$ctx->table("audio_relations")->where("audio", $this->getId())
->delete();
$ctx->table("audio_listens")->where("audio", $this->getId())
->delete();
$ctx->table("playlist_relations")->where("media", $this->getId())
->delete();
parent::delete($softly);
}
}

View file

@ -372,35 +372,59 @@ 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;
}

View file

@ -17,7 +17,17 @@ abstract class MediaCollection extends RowModel
protected $specialNames = [];
private $relations;
protected $relations;
/**
* Maximum amount of items Collection can have
*/
const MAX_ITEMS = INF;
/**
* Maximum amount of Collections with same "owner" allowed
*/
const MAX_COUNT = INF;
function __construct(?ActiveRow $ar = NULL)
{
@ -71,9 +81,12 @@ 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)
@ -83,6 +96,14 @@ abstract class MediaCollection extends RowModel
}
}
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
{
return sizeof($this->getRecord()->related("$this->relTableName.collection"));
@ -119,6 +140,10 @@ abstract class MediaCollection extends RowModel
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(),
"media" => $entity->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
@ -149,5 +174,32 @@ abstract class MediaCollection extends RowModel
return !is_null($rel);
}
function save(?bool $log = false): void
{
$thisTable = DatabaseConnection::i()->getContext()->table($this->tableName);
if(self::MAX_COUNT != INF)
if(isset($this->changes["owner"]))
if(sizeof($thisTable->where("owner", $this->changes["owner"])) > self::MAX_COUNT)
throw new \OutOfBoundsException("Maximum amount of collections");
if(is_null($this->getRecord()))
if(!isset($this->changes["created"]))
$this->stateChanges("created", time());
else
$this->stateChanges("edited", time());
parent::save($log);
}
function delete(bool $softly = true): void
{
if(!$softly) {
$this->relations->where("collection", $this->getId())
->delete();
}
parent::delete($softly);
}
use Traits\TOwnable;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ use morphos\Gender;
use openvk\Web\Themes\{Themepack, Themepacks};
use openvk\Web\Util\DateTime;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift};
use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift, Audio};
use openvk\Web\Models\Repositories\{Applications, Bans, Comments, Notes, Posts, Users, Clubs, Albums, Gifts, Notifications, Videos, Photos};
use openvk\Web\Models\Exceptions\InvalidUserNameException;
use Nette\Database\Table\ActiveRow;
@ -190,7 +190,7 @@ class User extends RowModel
function getMorphedName(string $case = "genitive", bool $fullName = true): string
{
$name = $fullName ? ($this->getLastName() . " " . $this->getFirstName()) : $this->getFirstName();
if(!preg_match("%^[А-яё\-]+$%", $name))
if(!preg_match("%[А-яё\-]+$%", $name))
return $name; # name is probably not russian
$inflected = inflectName($name, $case, $this->isFemale() ? Gender::FEMALE : Gender::MALE);
@ -455,6 +455,7 @@ class User extends RowModel
"length" => 1,
"mappings" => [
"photos",
"audios",
"videos",
"messages",
"notes",
@ -462,7 +463,7 @@ class User extends RowModel
"news",
"links",
"poster",
"apps"
"apps",
],
])->get($id);
}
@ -482,6 +483,7 @@ class User extends RowModel
"friends.add",
"wall.write",
"messages.write",
"audios.read",
],
])->get($id);
}
@ -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) [];
@ -1240,6 +1249,44 @@ 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);
}
$returnArr = [];
foreach($entityIds as $id) {
$entit = $id > 0 ? (new Users)->get($id) : (new Clubs)->get(abs($id));
if($id > 0 && $entit->isDeleted()) return;
$returnArr[] = $entit;
}
return $returnArr;
}
use Traits\TBackDrops;
use Traits\TSubscribable;
use Traits\TAudioStatuses;
}

View file

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

View file

@ -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);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, Photo, Video, User, Topic, Post};
use openvk\Web\Models\Entities\Notifications\CommentNotification;
use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos};
use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos, Audios};
final class CommentPresenter extends OpenVKPresenter
{
@ -104,7 +104,26 @@ final class CommentPresenter extends OpenVKPresenter
}
}
if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1)
$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 && sizeof($audios) < 1)
$this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty"));
try {
@ -127,6 +146,9 @@ final class CommentPresenter extends OpenVKPresenter
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)
(new CommentNotification($owner, $comment, $entity, $this->user->identity))->emit();

View file

@ -3,7 +3,7 @@ namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Club, Photo, Post};
use Nette\InvalidStateException;
use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification;
use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics};
use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios};
use Chandler\Security\Authenticator;
final class GroupPresenter extends OpenVKPresenter
@ -31,6 +31,8 @@ final class GroupPresenter extends OpenVKPresenter
$this->template->albumsCount = (new Albums)->getClubAlbumsCount($club);
$this->template->topics = (new Topics)->getLastTopics($club, 3);
$this->template->topicsCount = (new Topics)->getClubTopicsCount($club);
$this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($club->getRealId());
$this->template->audiosCount = (new Audios)->getClubCollectionSize($club);
}
$this->template->club = $club;
@ -218,6 +220,7 @@ final class GroupPresenter extends OpenVKPresenter
$club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display"));
$club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1);
$club->setDisplay_Topics_Above_Wall(empty($this->postParam("display_topics_above_wall")) ? 0 : 1);
$club->setEveryone_can_upload_audios(empty($this->postParam("upload_audios")) ? 0 : 1);
$club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed")) ? 0 : 1);
$website = $this->postParam("website") ?? "";

View file

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

View file

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

View file

@ -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,6 +45,9 @@ 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",

View file

@ -3,7 +3,7 @@ namespace openvk\Web\Presenters;
use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos, Audios};
use Chandler\Database\DatabaseConnection;
use Nette\InvalidStateException as ISE;
use Bhaktaraz\RSSGenerator\Item;
@ -312,7 +312,26 @@ final class WallPresenter extends OpenVKPresenter
}
}
if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && !$poll && !$note)
$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 && sizeof($audios) < 1 && !$poll && !$note)
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
try {
@ -342,6 +361,9 @@ final class WallPresenter extends OpenVKPresenter
if(!is_null($note))
$post->attach($note);
foreach($audios as $audio)
$post->attach($audio);
if($wall > 0 && $wall !== $this->user->identity->getId())
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -91,6 +91,25 @@
</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>
<div class="right_small_block">

View file

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

View file

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

View file

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

View file

@ -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}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 B

View file

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

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -64,6 +64,33 @@ function ovk_proc_strtr(string $string, int $length = 0): string
return $newString . ($string !== $newString ? "" : ""); #if cut hasn't happened, don't append "..."
}
function knuth_shuffle(iterable $arr, int $seed): array
{
$data = is_array($arr) ? $arr : iterator_to_array($arr);
$retVal = [];
$ind = [];
$count = sizeof($data);
srand($seed, MT_RAND_PHP);
for($i = 0; $i < $count; ++$i)
$ind[$i] = 0;
for($i = 0; $i < $count; ++$i) {
do {
$index = rand() % $count;
} while($ind[$index] != 0);
$ind[$index] = 1;
$retVal[$i] = $data[$index];
}
# Reseed
srand(hexdec(bin2hex(openssl_random_pseudo_bytes(4))));
return $retVal;
}
function bmask(int $input, array $options = []): Bitmask
{
return new Bitmask($input, $options["length"] ?? 1, $options["mappings"] ?? []);

View file

@ -16,6 +16,7 @@
"vearutop/php-obscene-censor-rus": "dev-master",
"erusev/parsedown": "dev-master",
"bhaktaraz/php-rss-generator": "dev-master",
"ext-openssl": "*",
"ext-simplexml": "*",
"symfony/console": "5.4.x-dev",
"wapmorgan/morphos": "dev-master",

View file

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

View file

@ -0,0 +1,100 @@
-- Apply these two commands if you installed OpenVK before 12th November 2023 OR if it's just doesn't work out of box, then apply this file again
-- DROP TABLE `audios`;
-- DROP TABLE `audio_relations`;
CREATE TABLE IF NOT EXISTS `audios` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`owner` bigint unsigned NOT NULL,
`virtual_id` bigint unsigned NOT NULL,
`created` bigint unsigned NOT NULL,
`edited` bigint unsigned DEFAULT NULL,
`hash` char(128) NOT NULL,
`length` smallint unsigned NOT NULL,
`segment_size` decimal(20,6) NOT NULL DEFAULT '6.000000' COMMENT 'Size in seconds of each segment',
`kid` binary(16) NOT NULL,
`key` binary(16) NOT NULL,
`token` binary(28) NOT NULL COMMENT 'Key to access original file',
`listens` bigint unsigned NOT NULL DEFAULT '0',
`performer` varchar(256) NOT NULL,
`name` varchar(256) NOT NULL,
`lyrics` text,
`genre` enum('Blues','Big Band','Classic Rock','Chorus','Country','Easy Listening','Dance','Acoustic','Disco','Humour','Funk','Speech','Grunge','Chanson','Hip-Hop','Opera','Jazz','Chamber Music','Metal','Sonata','New Age','Symphony','Oldies','Booty Bass','Other','Primus','Pop','Porn Groove','R&B','Satire','Rap','Slow Jam','Reggae','Club','Rock','Tango','Techno','Samba','Industrial','Folklore','Alternative','Ballad','Ska','Power Ballad','Death Metal','Rhythmic Soul','Pranks','Freestyle','Soundtrack','Duet','Euro-Techno','Punk Rock','Ambient','Drum Solo','Trip-Hop','A Cappella','Vocal','Euro-House','Jazz+Funk','Dance Hall','Fusion','Goa','Trance','Drum & Bass','Classical','Club-House','Instrumental','Hardcore','Acid','Terror','House','Indie','Game','BritPop','Sound Clip','Negerpunk','Gospel','Polsk Punk','Noise','Beat','AlternRock','Christian Gangsta Rap','Bass','Heavy Metal','Soul','Black Metal','Punk','Crossover','Space','Contemporary Christian','Meditative','Christian Rock','Instrumental Pop','Merengue','Instrumental Rock','Salsa','Ethnic','Thrash Metal','Gothic','Anime','Darkwave','JPop','Techno-Industrial','Synthpop','Electronic','Abstract','Pop-Folk','Art Rock','Eurodance','Baroque','Dream','Bhangra','Southern Rock','Big Beat','Comedy','Breakbeat','Cult','Chillout','Gangsta Rap','Downtempo','Top 40','Dub','Christian Rap','EBM','Pop / Funk','Eclectic','Jungle','Electro','Native American','Electroclash','Cabaret','Emo','New Wave','Experimental','Psychedelic','Garage','Rave','Global','Showtunes','IDM','Trailer','Illbient','Lo-Fi','Industro-Goth','Tribal','Jam Band','Acid Punk','Krautrock','Acid Jazz','Leftfield','Polka','Lounge','Retro','Math Rock','Musical','New Romantic','Rock & Roll','Nu-Breakz','Hard Rock','Post-Punk','Folk','Post-Rock','Folk-Rock','Psytrance','National Folk','Shoegaze','Swing','Space Rock','Fast Fusion','Trop Rock','Bebob','World Music','Latin','Neoclassical','Revival','Audiobook','Celtic','Audio Theatre','Bluegrass','Neue Deutsche Welle','Avantgarde','Podcast','Gothic Rock','Indie Rock','Progressive Rock','G-Funk','Psychedelic Rock','Dubstep','Symphonic Rock','Garage Rock','Slow Rock','Psybient','Psychobilly','Touhou') DEFAULT NULL,
`explicit` tinyint(1) NOT NULL DEFAULT '0',
`withdrawn` tinyint(1) NOT NULL DEFAULT '0',
`processed` tinyint unsigned NOT NULL DEFAULT '0',
`checked` bigint NOT NULL DEFAULT '0' COMMENT 'Last time the audio availability was checked',
`unlisted` tinyint(1) NOT NULL DEFAULT '0',
`deleted` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `owner_virtual_id` (`owner`,`virtual_id`),
KEY `genre` (`genre`),
KEY `unlisted` (`unlisted`),
KEY `listens` (`listens`),
KEY `deleted` (`deleted`),
KEY `length` (`length`),
KEY `listens_genre` (`listens`,`genre`),
FULLTEXT KEY `performer_name` (`performer`,`name`),
FULLTEXT KEY `lyrics` (`lyrics`),
FULLTEXT KEY `performer` (`performer`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `audio_listens` (
`entity` bigint NOT NULL,
`audio` bigint unsigned NOT NULL,
`time` bigint unsigned NOT NULL,
`index` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'Workaround for Nette DBE bug',
`playlist` bigint(20) UNSIGNED DEFAULT NULL,
PRIMARY KEY (`index`),
KEY `audio` (`audio`),
KEY `user` (`entity`) USING BTREE,
KEY `user_time` (`entity`,`time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `audio_relations` (
`entity` bigint NOT NULL,
`audio` bigint unsigned NOT NULL,
`index` bigint unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`index`),
KEY `user` (`entity`) USING BTREE,
KEY `entity_audio` (`entity`,`audio`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `playlists` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`owner` bigint NOT NULL,
`name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
`description` varchar(2048) DEFAULT NULL,
`cover_photo_id` bigint unsigned DEFAULT NULL,
`length` int unsigned NOT NULL DEFAULT '0',
`special_type` tinyint unsigned NOT NULL DEFAULT '0',
`created` bigint unsigned DEFAULT NULL,
`listens` bigint(20) unsigned NOT NULL DEFAULT 0,
`edited` bigint unsigned DEFAULT NULL,
`deleted` tinyint unsigned DEFAULT '0',
PRIMARY KEY (`id`),
KEY `owner_deleted` (`owner`,`deleted`),
FULLTEXT KEY `title_description` (`name`,`description`),
FULLTEXT KEY `title` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `playlist_imports` (
`entity` bigint NOT NULL,
`playlist` bigint unsigned NOT NULL,
`index` bigint unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`index`) USING BTREE,
KEY `user` (`entity`) USING BTREE,
KEY `entity_audio` (`entity`,`playlist`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `playlist_relations` (
`collection` bigint unsigned NOT NULL,
`media` bigint unsigned NOT NULL,
`index` bigint unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`index`) USING BTREE,
KEY `playlist` (`collection`) USING BTREE,
KEY `audio` (`media`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
ALTER TABLE `groups` ADD `everyone_can_upload_audios` TINYINT(1) NOT NULL DEFAULT '0' AFTER `backdrop_2`;
ALTER TABLE `profiles` ADD `last_played_track` BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER `client_name`, ADD `audio_broadcast_enabled` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `last_played_track`;
ALTER TABLE `groups` ADD `last_played_track` BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER `everyone_can_upload_audios`, ADD `audio_broadcast_enabled` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `last_played_track`;

View file

@ -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 post 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.";
@ -1111,6 +1235,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.";
@ -1287,6 +1412,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";
@ -1393,6 +1534,10 @@
"admin_gift_moved_successfully" = "Gift moved successfully";
"admin_gift_moved_to_recycle" = "This gift will now be in <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";
@ -1620,8 +1765,16 @@
"tour_section_5_text_3" = "In addition to uploading videos directly, 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 sunshine Vriska didn't make the music :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";
@ -1732,6 +1885,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";
@ -1753,6 +1908,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 */
@ -1792,6 +1949,7 @@
/* Mobile */
"mobile_friends" = "Friends";
"mobile_photos" = "Photos";
"mobile_audios" = "Audios";
"mobile_videos" = "Videos";
"mobile_messages" = "Messages";
"mobile_notes" = "Notes";
@ -1805,6 +1963,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";

View file

@ -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" = "Раздел";

View file

@ -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:

View file

@ -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;
}

View file

@ -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%;
}