From ef26b1044a2766e526304e7544d43986bbfe38d3 Mon Sep 17 00:00:00 2001
From: lalka2018 <99399973+lalka2016@users.noreply.github.com>
Date: Wed, 1 Nov 2023 15:09:05 +0300
Subject: [PATCH] Implement audiostatuses
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Добавлены аудиостатусы (у пользователей), блок с друзьями, слушающих музыку на странице аудиозаписей, объект status_audio в users.get, улучшены настройки приватности и ещё что-то
---
 VKAPI/Handlers/Audio.php                      |  8 ++
 VKAPI/Handlers/Status.php                     | 24 ++++--
 VKAPI/Handlers/Users.php                      |  6 ++
 Web/Models/Entities/Audio.php                 |  7 +-
 Web/Models/Entities/Club.php                  |  1 +
 Web/Models/Entities/Traits/TAudioStatuses.php | 37 +++++++++
 Web/Models/Entities/User.php                  | 27 +++++-
 Web/Presenters/AudioPresenter.php             |  3 +
 Web/Presenters/UserPresenter.php              |  5 +-
 Web/Presenters/templates/Audio/tabs.xml       | 15 ++++
 Web/Presenters/templates/User/Edit.xml        |  7 ++
 Web/Presenters/templates/User/Settings.xml    |  7 +-
 Web/Presenters/templates/User/View.xml        | 27 ++++--
 Web/static/css/audios.css                     | 82 +++++++++++++++++++
 Web/static/js/al_music.js                     | 77 +++++++++++------
 Web/static/js/al_playlists.js                 |  2 +-
 install/sqls/gamma-00000-disco.sql            |  3 +-
 locales/en.strings                            |  4 +-
 locales/ru.strings                            |  4 +-
 19 files changed, 298 insertions(+), 48 deletions(-)
 create mode 100644 Web/Models/Entities/Traits/TAudioStatuses.php

diff --git a/VKAPI/Handlers/Audio.php b/VKAPI/Handlers/Audio.php
index 58bce510..9670a7ff 100644
--- a/VKAPI/Handlers/Audio.php
+++ b/VKAPI/Handlers/Audio.php
@@ -566,6 +566,14 @@ final class Audio extends VKAPIRequestHandler
 
         $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)
diff --git a/VKAPI/Handlers/Status.php b/VKAPI/Handlers/Status.php
index 843f42bd..a1b104a2 100644
--- a/VKAPI/Handlers/Status.php
+++ b/VKAPI/Handlers/Status.php
@@ -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();
         }
     }
 
diff --git a/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php
index a4298787..68bf828f 100644
--- a/VKAPI/Handlers/Users.php
+++ b/VKAPI/Handlers/Users.php
@@ -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)
diff --git a/Web/Models/Entities/Audio.php b/Web/Models/Entities/Audio.php
index 4fab06be..0fe110e3 100644
--- a/Web/Models/Entities/Audio.php
+++ b/Web/Models/Entities/Audio.php
@@ -313,8 +313,8 @@ class Audio extends Media
         $lastListen   = $listensTable->where([
             "entity" => $entityId,
             "audio"  => $this->getId(),
-        ])->fetch();
-
+        ])->order("index DESC")->fetch();
+        
         if(!$lastListen || (time() - $lastListen->time >= $this->getLength())) {
             $listensTable->insert([
                 "entity" => $entityId,
@@ -331,6 +331,9 @@ class Audio extends Media
                     $playlist->incrementListens();
                     $playlist->save();
                 }
+
+                $entity->setLast_played_track($this->getId());
+                $entity->save();
             }
 
             return true;
diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php
index c89f0b47..126c5127 100644
--- a/Web/Models/Entities/Club.php
+++ b/Web/Models/Entities/Club.php
@@ -421,4 +421,5 @@ class Club extends RowModel
 
     use Traits\TBackDrops;
     use Traits\TSubscribable;
+    use Traits\TAudioStatuses;
 }
diff --git a/Web/Models/Entities/Traits/TAudioStatuses.php b/Web/Models/Entities/Traits/TAudioStatuses.php
new file mode 100644
index 00000000..d49c4b29
--- /dev/null
+++ b/Web/Models/Entities/Traits/TAudioStatuses.php
@@ -0,0 +1,37 @@
+<?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
+    {
+        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;
+    }
+}
\ No newline at end of file
diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php
index 2114b5ab..ac903404 100644
--- a/Web/Models/Entities/User.php
+++ b/Web/Models/Entities/User.php
@@ -1249,7 +1249,32 @@ class User extends RowModel
 
         return $res;
     }
-    
+
+    function getFriendsAudios()
+    {
+        $friendsCount = $this->getFriendsCount();
+        $friends = $this->getFriends(max(rand(1, (int)ceil($friendsCount / 6)), 1), 6);
+
+        $shuffleSeed    = openssl_random_pseudo_bytes(6);
+        $shuffleSeed    = hexdec(bin2hex($shuffleSeed));
+
+        $friends = knuth_shuffle($friends, $shuffleSeed);
+        $returnArr = [];
+        
+        foreach($friends as $friend) {
+            $returnArr[] = [
+                "id"   => $friend->getRealId(),
+                "name" => $friend->getCanonicalName(),
+                "avatar" => $friend->getAvatarURL("miniscule"),
+                "tracksCount" => (new \openvk\Web\Models\Repositories\Audios)->getUserCollectionSize($friend),
+                "nowListening" => $friend->getCurrentAudioStatus(),
+            ];
+        }
+
+        return $returnArr;
+    }
+
     use Traits\TBackDrops;
     use Traits\TSubscribable;
+    use Traits\TAudioStatuses;
 }
diff --git a/Web/Presenters/AudioPresenter.php b/Web/Presenters/AudioPresenter.php
index 2b5786c1..a673e7d1 100644
--- a/Web/Presenters/AudioPresenter.php
+++ b/Web/Presenters/AudioPresenter.php
@@ -109,6 +109,9 @@ final class AudioPresenter extends OpenVKPresenter
 
         $this->template->mode = $mode;
         $this->template->page = $page;
+
+        if(in_array($mode, ["list", "new", "popular"]))
+            $this->template->friendsAudios = $this->user->identity->getFriendsAudios();
     }
 
     function renderEmbed(int $owner, int $id): void
diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php
index 8dc65840..56d5685b 100644
--- a/Web/Presenters/UserPresenter.php
+++ b/Web/Presenters/UserPresenter.php
@@ -47,7 +47,8 @@ final class UserPresenter extends OpenVKPresenter
             $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;
         }
     }
@@ -171,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"])
@@ -243,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([
diff --git a/Web/Presenters/templates/Audio/tabs.xml b/Web/Presenters/templates/Audio/tabs.xml
index 96db234e..3f58c94f 100644
--- a/Web/Presenters/templates/Audio/tabs.xml
+++ b/Web/Presenters/templates/Audio/tabs.xml
@@ -20,5 +20,20 @@
             <a n:attr="id => $mode === 'playlists' && $ownerId != $thisUser->getId() ? 'used' : 'ki'" href="/playlists{$ownerId}"  n:if="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['id']}" style="width: 94%;padding-left: 10px;" n:foreach="$friendsAudios as $fr">
+                    <div class="elem">
+                        <img src="{$fr['avatar']}" />
+
+                        <div class="additionalInfo">
+                            <span class="name">{$fr["name"]}</span>
+                            <span class="desc">{$fr["nowListening"] ? $fr["nowListening"]->getName() : tr("audios_count", $fr["tracksCount"])}</span>
+                        </div>
+                    </div>
+                </a>
+            </div>
+        {/if}
     </div>
 </div>
\ No newline at end of file
diff --git a/Web/Presenters/templates/User/Edit.xml b/Web/Presenters/templates/User/Edit.xml
index b8e8398c..0a28b879 100644
--- a/Web/Presenters/templates/User/Edit.xml
+++ b/Web/Presenters/templates/User/Edit.xml
@@ -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>
 
diff --git a/Web/Presenters/templates/User/Settings.xml b/Web/Presenters/templates/User/Settings.xml
index 8da520ce..3ddf4561 100644
--- a/Web/Presenters/templates/User/Settings.xml
+++ b/Web/Presenters/templates/User/Settings.xml
@@ -390,9 +390,10 @@
                     </td>
                     <td>
                         <select name="audios.read" style="width: 164px;">
-                            <option value="2" {if $user->getPrivacySetting('audios.read') == 2}selected{/if}>{_privacy_value_anybody}</option>
-                            <option value="1" {if $user->getPrivacySetting('audios.read') == 1}selected{/if}>{_privacy_value_friends}</option>
-                            <option value="0" {if $user->getPrivacySetting('audios.read') == 0}selected{/if}>{_privacy_value_nobody}</option>
+                            <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>
diff --git a/Web/Presenters/templates/User/View.xml b/Web/Presenters/templates/User/View.xml
index 58a9c33e..f5cb6176 100644
--- a/Web/Presenters/templates/User/View.xml
+++ b/Web/Presenters/templates/User/View.xml
@@ -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>
@@ -594,7 +607,7 @@
             </div>
         </div>
 
-        <div n:if="$audiosCount > 0">
+        <div n:if="$audiosCount > 0 && $user->getPrivacyPermission('audios.read', $thisUser ?? NULL)">
             <div class="content_title_expanded" onclick="hidePanel(this, {$audiosCount});">
                 {_audios}
             </div>
@@ -758,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});
 
diff --git a/Web/static/css/audios.css b/Web/static/css/audios.css
index 7db5a3fa..6d08d861 100644
--- a/Web/static/css/audios.css
+++ b/Web/static/css/audios.css
@@ -566,4 +566,86 @@
     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: 100%;
+}
+
+.friendsAudiosList .elem img {
+    width: 30px;
+    border-radius: 2px;
+}
+
+.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 .elem .additionalInfo .desc {
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+    color: #878A8F;
+    font-size: 11px;
+}
+
+.friendsAudiosList .elem:hover {
+    background: #E8EBF0;
+    cursor: pointer;
 }
\ No newline at end of file
diff --git a/Web/static/js/al_music.js b/Web/static/js/al_music.js
index d0bd604d..3b4bc170 100644
--- a/Web/static/js/al_music.js
+++ b/Web/static/js/al_music.js
@@ -96,8 +96,9 @@ class bigPlayer {
 
         this.nodes["thisPlayer"] = document.querySelector(".bigPlayer")
         this.nodes["thisPlayer"].classList.add("lagged")
+        this.nodes["audioPlayer"] = document.createElement("audio")
 
-        this.player = () => { return this.nodes["thisPlayer"].querySelector("audio.audio") }
+        this.player = () => { return this.nodes["audioPlayer"] }
         this.nodes["playButtons"] = this.nodes["thisPlayer"].querySelector(".playButtons")
         this.nodes["dashPlayer"] = dashjs.MediaPlayer().create()
 
@@ -365,34 +366,62 @@ class bigPlayer {
 
         u(this.player()).on("ended", (e) => {
             e.preventDefault()
-
-            let playlist = this.context.context_type == "playlist_context" ? this.context.context_id : null
-    
-            $.ajax({
-                type: "POST",
-                url: `/audio${this.tracks["currentTrack"].id}/listen`,
-                data: {
-                    hash: u("meta[name=csrf]").attr("value"),
-                    playlist: playlist
-                },
-                success: (response) => {
-                    if(response.success) {
-                        console.info("Listen is counted.")
-    
-                        if(response.new_playlists_listens)
-                            document.querySelector("#listensCount").innerHTML = tr("listens_count", response.new_playlists_listens)
-                    } else
-                        console.info("Listen is not counted.")
-                }
-            })
             
+            // в начало очереди
             if(!this.tracks.nextTrack) {
-                this.setTrack(this.tracks.tracks[0].id)
+                if(!this.context["playedPages"].includes("1")) {
+                    $.ajax({
+                        type: "POST",
+                        url: "/audios/context",
+                        data: {
+                            context: this["context"].context_type,
+                            context_entity: this["context"].context_id,
+                            hash: u("meta[name=csrf]").attr("value"),
+                            page: 1
+                        },
+                        success: (response_2) => {
+                            this.tracks["tracks"] = response_2["items"].concat(this.tracks["tracks"])
+                            this.context["playedPages"].push(String(1))
+
+                            this.setTrack(this.tracks["tracks"][0].id)
+                        }
+                    })
+                } else {
+                    this.setTrack(this.tracks.tracks[0].id)
+                }
+                
                 return
             }
 
             this.showNextTrack()
         })
+        
+        u(this.player()).on("loadstart", (e) => {
+            let playlist = this.context.context_type == "playlist_context" ? this.context.context_id : null
+
+            let tempThisId = this.tracks.currentTrack.id
+            setTimeout(() => {
+                if(tempThisId != this.tracks.currentTrack.id) return
+
+                $.ajax({
+                    type: "POST",
+                    url: `/audio${this.tracks["currentTrack"].id}/listen`,
+                    data: {
+                        hash: u("meta[name=csrf]").attr("value"),
+                        playlist: playlist
+                    },
+                    success: (response) => {
+                        if(response.success) {
+                            console.info("Listen is counted.")
+        
+                            if(response.new_playlists_listens)
+                                document.querySelector("#listensCount").innerHTML = tr("listens_count", response.new_playlists_listens)
+                        } else
+                            console.info("Listen is not counted.")
+                    }
+                })
+            }, 2000)
+        })
 
         if(localStorage.volume != null && localStorage.volume < 1 && localStorage.volume > 0)
             this.player().volume = localStorage.volume
@@ -537,7 +566,7 @@ class bigPlayer {
                             else
                                 this.tracks["tracks"] = this.tracks["tracks"].concat(newArr["items"])
 
-                            this.context["playedPages"].push(Number(newArr["page"]))
+                            this.context["playedPages"].push(String(newArr["page"]))
 
                             if(lesser)
                                 this.tracks["previousTrack"] = this.tracks["tracks"].at(this.tracks["tracks"].indexOf(obj) - 1).id
@@ -1310,7 +1339,7 @@ $(document).on("click", ".audiosContainer .paginator a", (e) => {
                     },
                     success: (response_2) => {
                         window.player.tracks["tracks"] = window.player.tracks["tracks"].concat(response_2["items"])
-                        window.player.context["playedPages"].push(page)
+                        window.player.context["playedPages"].push(String(page))
                         console.info("Page is switched")
                     }
                 })
diff --git a/Web/static/js/al_playlists.js b/Web/static/js/al_playlists.js
index 1316e96b..443e6f30 100644
--- a/Web/static/js/al_playlists.js
+++ b/Web/static/js/al_playlists.js
@@ -6,7 +6,7 @@ if(document.querySelector("#editPlaylistForm")) {
     context_id = document.querySelector("#editPlaylistForm").dataset.id
 }
 
-if(document.querySelector(".showMoreAudiosPlaylist").dataset.club != null) {
+if(document.querySelector(".showMoreAudiosPlaylist") && document.querySelector(".showMoreAudiosPlaylist").dataset.club != null) {
     context_type = "entity_audios"
     context_id = Number(document.querySelector(".showMoreAudiosPlaylist").dataset.club) * -1
 }
diff --git a/install/sqls/gamma-00000-disco.sql b/install/sqls/gamma-00000-disco.sql
index df7a2054..3783c05f 100644
--- a/install/sqls/gamma-00000-disco.sql
+++ b/install/sqls/gamma-00000-disco.sql
@@ -91,4 +91,5 @@ CREATE TABLE IF NOT EXISTS `playlist_relations` (
   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`; 
\ No newline at end of file
+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`; 
\ No newline at end of file
diff --git a/locales/en.strings b/locales/en.strings
index 2e35498f..c1077a36 100644
--- a/locales/en.strings
+++ b/locales/en.strings
@@ -568,7 +568,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 view my audios";
+"privacy_setting_view_audio" = "Who can see my audios";
 "privacy_value_anybody" = "Anybody";
 "privacy_value_anybody_dative" = "Anybody";
 "privacy_value_users" = "OpenVK users";
@@ -830,6 +830,8 @@
 "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";
+
 /* Notifications */
 
 "feedback" = "Feedback";
diff --git a/locales/ru.strings b/locales/ru.strings
index ab9d9a02..e0e66b84 100644
--- a/locales/ru.strings
+++ b/locales/ru.strings
@@ -541,7 +541,7 @@
 "privacy_setting_add_to_friends" = "Кто может называть меня другом";
 "privacy_setting_write_wall" = "Кто может писать у меня на стене";
 "privacy_setting_write_messages" = "Кто может писать мне сообщения";
-"privacy_setting_view_audio" = "Кто может видеть мои аудиозаписи";
+"privacy_setting_view_audio" = "Кому видно мои аудиозаписи";
 "privacy_value_anybody" = "Все желающие";
 "privacy_value_anybody_dative" = "Всем желающим";
 "privacy_value_users" = "Пользователям OpenVK";
@@ -785,6 +785,8 @@
 "no_access_clubs" = "Нет групп, где вы являетесь администратором.";
 "audio_successfully_uploaded" = "Аудио успешно загружено и на данный момент обрабатывается.";
 
+"broadcast_audio" = "Транслировать аудио в статус";
+
 /* Notifications */
 
 "feedback" = "Ответы";