mirror of
https://github.com/openvk/openvk
synced 2025-04-23 16:43:02 +03:00
Add audio upload feature
This commit is contained in:
parent
edccb34c53
commit
ee56859acd
14 changed files with 879 additions and 3 deletions
301
Web/Models/Entities/Audio.php
Normal file
301
Web/Models/Entities/Audio.php
Normal file
|
@ -0,0 +1,301 @@
|
|||
<?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'
|
||||
];
|
||||
|
||||
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);
|
||||
if(!empty($vstreams) && !ctype_space($vstreams))
|
||||
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", $ss);
|
||||
|
||||
try {
|
||||
$args = [
|
||||
OPENVK_ROOT,
|
||||
$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)->start();
|
||||
|
||||
# Wait until processAudio will consume the file
|
||||
$start = time();
|
||||
while(file_exists($filename))
|
||||
if(time() - $start > 5)
|
||||
exit("Timed out waiting for 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->getTitle() . " - " . $this->getPerformer();
|
||||
}
|
||||
|
||||
function getLyrics(): ?string
|
||||
{
|
||||
return $this->getRecord()->lyrics ?? NULL;
|
||||
}
|
||||
|
||||
function getLength(): int
|
||||
{
|
||||
return $this->getRecord()->length;
|
||||
}
|
||||
|
||||
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);
|
||||
$garbage = sha1((string) time());
|
||||
|
||||
return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3?tk=$garbage";
|
||||
}
|
||||
|
||||
function getKeys(): array
|
||||
{
|
||||
$keys[bin2hex($this->getRecord()->kid)] = bin2hex($this->getRecord()->key);
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
function isExplicit(): bool
|
||||
{
|
||||
return $this->getRecord()->explicit;
|
||||
}
|
||||
|
||||
function isWithdrawn(): bool
|
||||
{
|
||||
return $this->getRecord()->withdrawn;
|
||||
}
|
||||
|
||||
function isUnlisted(): bool
|
||||
{
|
||||
return $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;
|
||||
|
||||
DatabaseConnection::i()->getContext()->table("audio_relations")->insert([
|
||||
"entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1),
|
||||
"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(User $user): bool
|
||||
{
|
||||
$listensTable = DatabaseConnection::i()->getContext()->table("audio_listens");
|
||||
$lastListen = $listensTable->where([
|
||||
"user" => $user->getId(),
|
||||
"audio" => $this->getId(),
|
||||
])->fetch();
|
||||
|
||||
if(!$lastListen || (time() - $lastListen->time >= 900)) {
|
||||
$listensTable->insert([
|
||||
"user" => $user->getId(),
|
||||
"audio" => $this->getId(),
|
||||
"time" => time(),
|
||||
]);
|
||||
$this->stateChanges("listens", $this->getListens() + 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$lastListen->update([
|
||||
"time" => time(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function setGenre(string $genre): void
|
||||
{
|
||||
if(!in_array($genre, Audio::genres)) {
|
||||
$this->stateChanges("genre", NULL);
|
||||
}
|
||||
|
||||
$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.");
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
110
Web/Models/Repositories/Audios.php
Normal file
110
Web/Models/Repositories/Audios.php
Normal file
|
@ -0,0 +1,110 @@
|
|||
<?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\User;
|
||||
use openvk\Web\Models\Repositories\Util\EntityStream;
|
||||
|
||||
class Audios
|
||||
{
|
||||
private $context;
|
||||
private $audios;
|
||||
private $rels;
|
||||
|
||||
const ORDER_NEW = 0;
|
||||
const ORDER_POPULAR = 1;
|
||||
|
||||
function __construct()
|
||||
{
|
||||
$this->context = DatabaseConnection::i()->getContext();
|
||||
$this->audios = $this->context->table("audios");
|
||||
$this->rels = $this->context->table("audio_relations");
|
||||
}
|
||||
|
||||
function get(int $id): ?Audio
|
||||
{
|
||||
$audio = $this->audios->get($id);
|
||||
if(!$audio)
|
||||
return NULL;
|
||||
|
||||
return new Audio($audio);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private function getByEntityID(int $entity, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
|
||||
{
|
||||
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
|
||||
$iter = $this->rels->where("entity", $entity)->page($page, $perPage);
|
||||
foreach($iter as $rel) {
|
||||
$audio = $this->get($rel->audio);
|
||||
if(!$audio || $audio->isDeleted()) {
|
||||
$deleted++;
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $audio;
|
||||
}
|
||||
}
|
||||
|
||||
function getByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
|
||||
{
|
||||
return $this->getByEntityID($user->getId(), $page, $perPage, $deleted);
|
||||
}
|
||||
|
||||
function getByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable
|
||||
{
|
||||
return $this->getByEntityID($club->getId() * -1, $page, $perPage, $deleted);
|
||||
}
|
||||
|
||||
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 getByUploader(User $user): EntityStream
|
||||
{
|
||||
$search = $this->audios->where([
|
||||
"owner" => $user->getId(),
|
||||
"deleted" => 0,
|
||||
]);
|
||||
|
||||
return new EntityStream("Audio", $search);
|
||||
}
|
||||
|
||||
function getGlobal(int $order): EntityStream
|
||||
{
|
||||
$search = $this->audios->where([
|
||||
"deleted" => 0,
|
||||
"unlisted" => 0,
|
||||
"withdrawn" => 0,
|
||||
])->order($order == Audios::ORDER_NEW ? "created DESC" : "listens DESC");
|
||||
|
||||
return new EntityStream("Audio", $search);
|
||||
}
|
||||
|
||||
function search(string $query): EntityStream
|
||||
{
|
||||
$search = $this->audios->where([
|
||||
"unlisted" => 0,
|
||||
"deleted" => 0,
|
||||
])->where("MATCH (performer, name) AGAINST (? WITH QUERY EXPANSION)", $query);
|
||||
|
||||
return new EntityStream("Audio", $search);
|
||||
}
|
||||
}
|
39
Web/Models/shell/processAudio.ps1
Normal file
39
Web/Models/shell/processAudio.ps1
Normal file
|
@ -0,0 +1,39 @@
|
|||
$ovkRoot = $args[0]
|
||||
$storageDir = $args[1]
|
||||
$fileHash = $args[2]
|
||||
$hashPart = $fileHash.substring(0, 2)
|
||||
$filename = $args[3]
|
||||
$audioFile = [System.IO.Path]::GetTempFileName()
|
||||
$temp = [System.IO.Path]::GetTempFileName()
|
||||
|
||||
$keyID = $args[4]
|
||||
$key = $args[5]
|
||||
$token = $args[6]
|
||||
$seg = $args[7]
|
||||
|
||||
$shell = Get-WmiObject Win32_process -filter "ProcessId = $PID"
|
||||
$shell.SetPriority(16384) # because there's no "nice" program in Windows we just set a lower priority for entire tree
|
||||
|
||||
Remove-Item $temp
|
||||
Remove-Item $audioFile
|
||||
New-Item -ItemType "directory" $temp
|
||||
New-Item -ItemType "directory" ("$temp/$fileHash" + '_fragments')
|
||||
New-Item -ItemType "directory" ("$storageDir/$hashPart/$fileHash" + '_fragments')
|
||||
Set-Location -Path $temp
|
||||
|
||||
Move-Item $filename $audioFile
|
||||
ffmpeg -i $audioFile -f dash -encryption_scheme cenc-aes-ctr -encryption_key $key `
|
||||
-encryption_kid $keyID -map 0 -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')
|
||||
|
||||
Move-Item -Path ($fileHash + '_fragments') -Destination "$storageDir/$hashPart"
|
||||
Move-Item -Path ("$fileHash.mpd") -Destination "$storageDir/$hashPart"
|
||||
|
||||
cd ..
|
||||
Remove-Item -Recurse $temp
|
||||
Remove-Item $audioFile
|
149
Web/Presenters/AudioPresenter.php
Normal file
149
Web/Presenters/AudioPresenter.php
Normal file
|
@ -0,0 +1,149 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Presenters;
|
||||
use openvk\Web\Models\Entities\Audio;
|
||||
use openvk\Web\Models\Entities\Club;
|
||||
use openvk\Web\Models\Entities\User;
|
||||
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;
|
||||
|
||||
const MAX_AUDIO_SIZE = 25000000;
|
||||
|
||||
function __construct(Audios $audios)
|
||||
{
|
||||
$this->audios = $audios;
|
||||
}
|
||||
|
||||
private function renderApp(string $playlistHandle): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
|
||||
$this->template->_template = "Audio/Player";
|
||||
$this->template->handle = $playlistHandle;
|
||||
}
|
||||
|
||||
function renderPopular(): void
|
||||
{
|
||||
$this->renderApp("_popular");
|
||||
}
|
||||
|
||||
function renderNew(): void
|
||||
{
|
||||
$this->renderApp("_new");
|
||||
}
|
||||
|
||||
function renderList(int $owner): void
|
||||
{
|
||||
$entity = NULL;
|
||||
if($owner < 0)
|
||||
$entity = (new Clubs)->get($owner);
|
||||
else
|
||||
$entity = (new Users)->get($owner);
|
||||
|
||||
if(!$entity)
|
||||
$this->notFound();
|
||||
|
||||
$this->renderApp("owner=$owner");
|
||||
}
|
||||
|
||||
function renderView(int $owner, int $id): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
|
||||
$audio = $this->audios->getByOwnerAndVID($owner, $id);
|
||||
if(!$audio || $audio->isDeleted())
|
||||
$this->notFound();
|
||||
|
||||
if(!$audio->canBeViewedBy($this->user->identity))
|
||||
$this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
|
||||
|
||||
$this->renderApp("id=" . $audio->getId());
|
||||
}
|
||||
|
||||
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;
|
||||
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"));
|
||||
|
||||
// TODO check if group allows uploads to anyone
|
||||
if(!$group->canBeModifiedBy($this->user->identity))
|
||||
$this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"));
|
||||
}
|
||||
|
||||
$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"));
|
||||
} else {
|
||||
$err = !isset($upload) ? 65536 : $upload["error"];
|
||||
$err = str_pad(dechex($err), 9, "0", STR_PAD_LEFT);
|
||||
$this->flashFail("err", tr("error"), tr("error_generic") . "Upload error: 0x$err");
|
||||
}
|
||||
|
||||
$performer = $this->postParam("performer");
|
||||
$name = $this->postParam("name");
|
||||
$lyrics = $this->postParam("lyrics");
|
||||
$genre = empty($this->postParam("genre")) ? "undefined" : $this->postParam("genre");
|
||||
$nsfw = ($this->postParam("nsfw") ?? "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"));
|
||||
|
||||
$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.");
|
||||
}
|
||||
|
||||
$audio->save();
|
||||
$audio->add($group ?? $this->user->identity);
|
||||
|
||||
$this->redirect(is_null($group) ? "/audios" . $this->user->id : "/audios-" . $group->getId());
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@ final class BlobPresenter extends OpenVKPresenter
|
|||
|
||||
function renderFile(/*string*/ $dir, string $name, string $format)
|
||||
{
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
|
||||
$dir = $this->getDirName($dir);
|
||||
$name = preg_replace("%[^a-zA-Z0-9_\-]++%", "", $name);
|
||||
$path = OPENVK_ROOT . "/storage/$dir/$name.$format";
|
||||
|
|
104
Web/Presenters/templates/Audio/Upload.xml
Normal file
104
Web/Presenters/templates/Audio/Upload.xml
Normal file
|
@ -0,0 +1,104 @@
|
|||
{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="background: white; border: 0;">
|
||||
<div id="upload_container">
|
||||
<h4>{_select_audio}</h4><br/>
|
||||
<b><a href="javascript:false">{_limits}</a></b>
|
||||
<ul>
|
||||
<li>{tr("audio_requirements", 1, 30, 25)}</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="hash" value="{$csrfToken}" />
|
||||
<input id="audio_input" type="file" name="blob" accept="audio/*" />
|
||||
</form>
|
||||
</div><br/>
|
||||
|
||||
<span>{_you_can_also_add_audio_using} <b><a href="/player">{_search_audio_inst}</a></b>.<span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dialogBoxHtml" style="display: none;">
|
||||
<table cellspacing="7" cellpadding="0" border="0" align="center">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="120" valign="top"><span class="nobold">Имя:</span></td>
|
||||
<td><input type="text" name="name" autocomplete="off" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top"><span class="nobold">Исполнитель:</span></td>
|
||||
<td><input name="performer" autocomplete="off" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="120" valign="top"><span class="nobold">Жанр:</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">Текст:</span></td>
|
||||
<td><textarea name="lyrics"></textarea></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
u("#audio_input").on("change", function(e) {
|
||||
if(e.currentTarget.files.length <= 0)
|
||||
return;
|
||||
|
||||
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]");
|
||||
|
||||
MessageBox({_upload_audio}, document.querySelector("#dialogBoxHtml").innerHTML, [{_ok}, {_cancel}], [
|
||||
function() {
|
||||
var name = u("input[name=name]", this.$dialog().nodes[0]).nodes[0].value;
|
||||
var perf = u("input[name=performer]", this.$dialog().nodes[0]).nodes[0].value;
|
||||
var genre = u("select[name=genre]", this.$dialog().nodes[0]).nodes[0].value;
|
||||
var lyrics = u("textarea[name=lyrics]", this.$dialog().nodes[0]).nodes[0].value;
|
||||
|
||||
name_.value = name;
|
||||
perf_.value = perf;
|
||||
genre_.value = genre;
|
||||
lyrics_.value = lyrics;
|
||||
|
||||
document.querySelector("#audio_upload > form").submit();
|
||||
},
|
||||
|
||||
Function.noop
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
{/block}
|
|
@ -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
|
||||
|
@ -28,6 +29,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
|
||||
|
|
|
@ -163,6 +163,20 @@ routes:
|
|||
handler: "Videos->edit"
|
||||
- url: "/video{num}_{num}/remove"
|
||||
handler: "Videos->remove"
|
||||
- url: "/player"
|
||||
handler: "Audio->app"
|
||||
- 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}"
|
||||
handler: "Audio->view"
|
||||
- url: "/audio{num}_{num}/embed.xhtml"
|
||||
handler: "Audio->embed"
|
||||
- url: "/{?!club}{num}"
|
||||
handler: "Group->view"
|
||||
placeholders:
|
||||
|
|
|
@ -1977,3 +1977,124 @@ table td[width="120"] {
|
|||
.center {
|
||||
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: 0px 0px 4px 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#audio_upload {
|
||||
width: 350px;
|
||||
margin: 20px auto;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 10px;
|
||||
padding: 15px 0px;
|
||||
border: 2px solid #ccc;
|
||||
background-color: #EFEFEF;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: url(http://vkontakte.ru/images/bullet.gif) outside;
|
||||
margin: 10px 0px;
|
||||
padding-left: 30px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 1px 0px;
|
||||
}
|
||||
|
||||
#upload_container ul {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.audio_list {
|
||||
margin: 5px 20px;
|
||||
}
|
||||
|
||||
.audio_row {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.audio_row .play_button {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
background: url(http://web.archive.org/web/20101203010619im_/http://vkontakte.ru/images/play.gif);
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.audio_row .info {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 1px;
|
||||
width: 514px;
|
||||
}
|
||||
|
||||
.audio_row .duration {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
color: #777;
|
||||
font-size: 10px;
|
||||
margin-left: 5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.audio_row .lines {
|
||||
height: 5px;
|
||||
margin-top: 15px;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.audio_row .lines .dotted {
|
||||
border-top: dashed 1px #d8dfea;
|
||||
}
|
||||
|
||||
.audio_row .active.volume_line {
|
||||
width: 50px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.audio_row .volume_line .dot {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.audio_row .active {
|
||||
border-top: 1px solid #5f7d9d;
|
||||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.audio_row .active.duraton_line .dot {
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.audio_row .dot {
|
||||
height: 5px;
|
||||
background: #5f7d9d;
|
||||
}
|
||||
|
||||
.audio_row .active.duraton_line {
|
||||
width: calc(100% - 70px);
|
||||
}
|
||||
|
||||
.audio_row .add_button {
|
||||
background: url(https://vkontakte.ru/images/plus.gif) no-repeat center;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: inline-block;
|
||||
margin-left: -16px;
|
||||
}
|
|
@ -14,7 +14,8 @@
|
|||
"chillerlan/php-qrcode": "dev-main",
|
||||
"vearutop/php-obscene-censor-rus": "dev-master",
|
||||
"erusev/parsedown": "dev-master",
|
||||
"bhaktaraz/php-rss-generator": "dev-master"
|
||||
"bhaktaraz/php-rss-generator": "dev-master",
|
||||
"ext-openssl": "*"
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
|
|
|
@ -545,6 +545,29 @@
|
|||
|
||||
"view_video" = "Просмотр";
|
||||
|
||||
/* Audios */
|
||||
|
||||
"audios" = "Аудиозаписи";
|
||||
"audio" = "Аудиозапись";
|
||||
"upload_audio" = "Загрузить аудио";
|
||||
|
||||
"performer" = "Исполнитель";
|
||||
"audio_name" = "Имя композиции";
|
||||
"genre" = "Жанр";
|
||||
"lyrics" = "Текст";
|
||||
|
||||
"limits" = "Ограничения";
|
||||
"select_audio" = "Выберите аудиозапись на Вашем компьютере";
|
||||
"audio_requirements" = "Аудиозапись должна быть длинной от $1c до $2 минут, весить до $3мб, содержать аудиопоток и не нарушать авторские права.";
|
||||
"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" = "Композиция ещё обрабатывается";
|
||||
|
||||
/* Notifications */
|
||||
|
||||
"feedback" = "Ответы";
|
||||
|
@ -807,6 +830,7 @@
|
|||
"no_data_description" = "Тут ничего нет... Пока...";
|
||||
|
||||
"error" = "Ошибка";
|
||||
"error_generic" = "Произошла ошибка общего характера: ";
|
||||
"error_shorturl" = "Данный короткий адрес уже занят.";
|
||||
"error_segmentation" = "Ошибка сегментации";
|
||||
"error_upload_failed" = "Не удалось загрузить фото";
|
||||
|
@ -814,6 +838,7 @@
|
|||
"error_new_password" = "Новые пароли не совпадает";
|
||||
"error_shorturl_incorrect" = "Короткий адрес имеет некорректный формат.";
|
||||
"error_repost_fail" = "Не удалось поделиться записью";
|
||||
"error_insufficient_info" = "Вы не указали необходимую информацию.";
|
||||
|
||||
"forbidden" = "Ошибка доступа";
|
||||
"forbidden_comment" = "Настройки приватности этого пользователя не разрешают вам смотреть на его страницу.";
|
||||
|
|
|
@ -37,6 +37,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:
|
||||
|
|
Loading…
Reference in a new issue