Implement playlists listens

- У плейлистов теперь есть прослушивания в общем.
- Прослушивания у большого плеера теперь засчитываются, если трек был дослушан до конца
- В объекте плейлистов теперь возвращается listens и cover_url
- Получение плееров через /audios/context переписано, повторяющийся код удалён, правда сильно количество строк сократить не получилось
- Теперь цвета плеера темнее, а иконка проигрывания изменена
- Теперь, если очередь из треков кончилась, то плеер перенаправляет вас в начало очереди.
This commit is contained in:
lalka2018 2023-10-30 14:29:25 +03:00
parent edde634782
commit 9095caae73
12 changed files with 329 additions and 192 deletions

View file

@ -303,7 +303,7 @@ class Audio extends Media
return true;
}
function listen($entity): bool
function listen($entity, Playlist $playlist = NULL): bool
{
$entityId = $entity->getId();
if($entity instanceof Club)
@ -320,11 +320,17 @@ class Audio extends Media
"entity" => $entityId,
"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();
}
}
return true;

View file

@ -137,6 +137,11 @@ class Playlist extends MediaCollection
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();
@ -155,6 +160,8 @@ class Playlist extends MediaCollection
"accessible" => $this->canBeViewedBy($user),
"editable" => $this->canBeModifiedBy($user),
"bookmarked" => $this->isBookmarkedBy($user),
"listens" => $this->getListens(),
"cover_url" => $this->getCoverURL(),
];
}
@ -227,4 +234,23 @@ class Playlist extends MediaCollection
{
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

@ -249,11 +249,26 @@ final class AudioPresenter extends OpenVKPresenter
$audio = $this->audios->get($id);
if ($audio && !$audio->isDeleted() && !$audio->isWithdrawn()) {
$listen = $audio->listen($this->user->identity);
$this->returnJson(["success" => $listen]);
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("/");
}
}
@ -338,7 +353,7 @@ final class AudioPresenter extends OpenVKPresenter
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit(",");
$this->redirect("/");
}
$playlist = $this->audios->getPlaylist($id);
@ -471,7 +486,7 @@ final class AudioPresenter extends OpenVKPresenter
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit(":)");
$this->redirect("/");
}
$audio = $this->audios->get($audio_id);
@ -582,7 +597,7 @@ final class AudioPresenter extends OpenVKPresenter
{
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
exit("<select><select><select><select>");
$this->redirect("/");
}
$ctx_type = $this->postParam("context");

View file

@ -53,7 +53,7 @@
</div>
</div>
<form method="post" id="editPlaylistForm" enctype="multipart/form-data">
<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" />
@ -90,4 +90,6 @@
document.querySelector("#editPlaylistForm input[name='new_cover']").value = ""
</script>
{script "js/al_playlists.js"}
{/block}

View file

@ -60,7 +60,7 @@
</div>
</div>
<div class="showMoreAudiosPlaylist" data-page="2" {if !is_null($_GET["owner"])}data-club="{abs($_GET['owner'])}"{/if} n:if="$pagesCount > 1">
<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>
@ -104,4 +104,6 @@
document.querySelector("#newPlaylistForm input[name='cover']").value = ""
</script>
{script "js/al_playlists.js"}
{/block}

View file

@ -51,12 +51,7 @@
<h4 style="border-bottom:unset;">{$playlist->getName()}</h4>
<div class="moreInfo">
{php $length = $playlist->getLengthInMinutes()}
<span>{tr("audios_count", $count)}</span>
<span>{_created_playlist}</span> {$playlist->getPublicationTime()}
{if $length > 0} •
<span>{tr("minutes_count", $length)}</span>
{/if}
{$playlist->getMetaDescription()|noescape}
<div style="margin-top: 11px;">
<span>{nl2br($playlist->getDescriptionHTML())|noescape}</span>

View file

@ -42,10 +42,11 @@
width: 22px;
height: 22px;
float: left;
background-position-x: -72px;
}
.bigPlayer .paddingLayer .playButtons .playButton.pause {
background-position-x: -144px;
background-position-x: -168px;
}
.bigPlayer .paddingLayer .playButtons .nextButton {
@ -146,7 +147,7 @@
.bigPlayer .paddingLayer .slider, .audioEmbed .track .slider {
width: 18px;
height: 7px;
background: #707070;
background: #606060;
position: absolute;
bottom: 0;
top: 0px;
@ -229,7 +230,7 @@
.audioEmbed .track > .selectableTrack, .bigPlayer .selectableTrack {
margin-top: 3px;
width: calc(100% - 8px);
border-top: #707070 1px solid;
border-top: #606060 1px solid;
height: 6px;
position: relative;
user-select: none;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 560 B

View file

@ -16,6 +16,50 @@ function getElapsedTime(fullTime, time) {
window.savedAudiosPages = {}
class playersSearcher {
constructor(context_type, context_id) {
this.context_type = context_type
this.context_id = context_id
this.searchType = "by_name"
this.query = ""
this.page = 1
this.successCallback = () => {}
this.errorCallback = () => {}
this.beforesendCallback = () => {}
this.clearContainer = () => {}
}
execute() {
$.ajax({
type: "POST",
url: "/audios/context",
data: {
context: this.context_type,
hash: u("meta[name=csrf]").attr("value"),
page: this.page,
query: this.query,
context_entity: this.context_id,
type: this.searchType,
returnPlayers: 1,
},
beforeSend: () => {
this.beforesendCallback()
},
error: () => {
this.errorCallback()
},
success: (response) => {
this.successCallback(response, this)
}
})
}
movePage(page) {
this.page = page
this.execute()
}
}
class bigPlayer {
tracks = {
currentTrack: null,
@ -60,6 +104,7 @@ class bigPlayer {
let formdata = new FormData()
formdata.append("context", context)
formdata.append("context_entity", context_id)
formdata.append("query", context_id)
formdata.append("hash", u("meta[name=csrf]").attr("value"))
formdata.append("page", page)
@ -251,10 +296,7 @@ class bigPlayer {
e.currentTarget.classList.toggle("pressed")
if(e.currentTarget.classList.contains("pressed"))
this.player().muted = true
else
this.player().muted = false
this.player().muted = e.currentTarget.classList.contains("pressed")
})
u(".bigPlayer .arrowsButtons .nextButton").on("click", (e) => {
@ -324,6 +366,31 @@ 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)
return
}
this.showNextTrack()
})
@ -513,27 +580,6 @@ class bigPlayer {
if(this.timeType == 1)
this.nodes["thisPlayer"].querySelector(".elapsedTime").innerHTML = fmtTime(this.tracks["currentTrack"].length)
let tempThisTrack = this.tracks["currentTrack"]
// если трек слушали больше 10 сек.
setTimeout(() => {
if(tempThisTrack.id != this.tracks["currentTrack"].id)
return;
$.ajax({
type: "POST",
url: `/audio${id}/listen`,
data: {
hash: u("meta[name=csrf]").attr("value"),
},
success: (response) => {
if(response.success)
console.info("Listen is counted.")
else
console.info("Listen is not counted.")
}
})
}, "10000")
let album = document.querySelector(".playlistBlock")
navigator.mediaSession.metadata = new MediaMetadata({
@ -1058,79 +1104,92 @@ $(document).on("click", "#_audioAttachment", (e) => {
document.querySelector(".ovk-diag-cont").style.width = "580px"
document.querySelector(".ovk-diag-body").style.height = "335px"
async function insertAudios(page, query = "", type = "by_name") {
document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", `<img id="loader" src="/assets/packages/static/openvk/img/loading_mini.gif">`)
let searcher = new playersSearcher("entity_audios", 0)
searcher.successCallback = (response, page) => {
let domparser = new DOMParser()
let result = domparser.parseFromString(response, "text/html")
$.ajax({
type: "POST",
url: "/audios/context",
data: {
context: query == "" ? "entity_audios" : "search_context",
hash: u("meta[name=csrf]").attr("value"),
page: page,
query: query == "" ? null : query,
context_entity: 0,
type: type,
returnPlayers: 1,
},
success: (response) => {
let domparser = new DOMParser()
let result = domparser.parseFromString(response, "text/html")
let pagesCount = result.querySelector("input[name='pagesCount']").value
let count = Number(result.querySelector("input[name='count']").value)
let pagesCount = result.querySelector("input[name='pagesCount']").value
let count = Number(result.querySelector("input[name='count']").value)
if(count < 1) {
document.querySelector(".audiosInsert").innerHTML = tr("no_results")
return
}
if(count < 1) {
document.querySelector(".audiosInsert").innerHTML = tr("no_results")
return
}
result.querySelectorAll(".audioEmbed").forEach(el => {
let id = el.dataset.prettyid
let name = el.dataset.name
let isAttached = (form.querySelector("input[name='audios']").value.includes(`${id},`))
document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", `
<div style="display: table;width: 100%;clear: both;">
<div style="width: 72%;float: left;">${el.outerHTML}</div>
<div class="attachAudio" data-attachmentdata="${id}" data-name="${name}">
<span>${isAttached ? tr("detach_audio") : tr("attach_audio")}</span>
</div>
</div>
`)
})
u("#loader").remove()
if(page < pagesCount) {
document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", `
<div id="showMoreAudios" data-pagesCount="${pagesCount}" data-page="${page + 1}" style="width: 100%;text-align: center;background: #d5d5d5;height: 22px;padding-top: 9px;cursor:pointer;">
<span>more...</span>
</div>`)
}
}
result.querySelectorAll(".audioEmbed").forEach(el => {
let id = el.dataset.prettyid
let name = el.dataset.name
let isAttached = (form.querySelector("input[name='audios']").value.includes(`${id},`))
document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", `
<div style="display: table;width: 100%;clear: both;">
<div style="width: 72%;float: left;">${el.outerHTML}</div>
<div class="attachAudio" data-attachmentdata="${id}" data-name="${name}">
<span>${isAttached ? tr("detach_audio") : tr("attach_audio")}</span>
</div>
</div>
`)
})
u("#loader").remove()
if(this.page < pagesCount) {
document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", `
<div id="showMoreAudios" data-pagesCount="${pagesCount}" data-page="${this.page + 1}" style="width: 100%;text-align: center;background: #d5d5d5;height: 22px;padding-top: 9px;cursor:pointer;">
<span>more...</span>
</div>`)
}
}
insertAudios(1)
searcher.errorCallback = () => {
fastError("Error when loading players.")
}
searcher.beforesendCallback = () => {
document.querySelector(".audiosInsert").insertAdjacentHTML("beforeend", `<img id="loader" src="/assets/packages/static/openvk/img/loading_mini.gif">`)
}
searcher.clearContainer = () => {
document.querySelector(".audiosInsert").innerHTML = ""
}
searcher.movePage(1)
$(".audiosInsert").on("click", "#showMoreAudios", (e) => {
u(e.currentTarget).remove()
insertAudios(Number(e.currentTarget.dataset.page))
searcher.movePage(Number(e.currentTarget.dataset.page))
})
$(".searchBox input").on("change", async (e) => {
await new Promise(r => setTimeout(r, 1000));
await new Promise(r => setTimeout(r, 500));
if(e.currentTarget.value === document.querySelector(".searchBox input").value) {
document.querySelector(".audiosInsert").innerHTML = ""
insertAudios(1, e.currentTarget.value, document.querySelector(".searchBox select").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;
}
})
$(".searchBox select").on("change", async (e) => {
document.querySelector(".audiosInsert").innerHTML = ""
insertAudios(1, document.querySelector(".searchBox input").value, e.currentTarget.value)
searcher.clearContainer()
searcher.searchType = e.currentTarget.value
$(".searchBox input").trigger("change")
return;
})
@ -1296,99 +1355,3 @@ $(document).on("click", "#bookmarkPlaylist, #unbookmarkPlaylist", (e) => {
}
})
})
function getPlayers(page = 1, query = "", playlist = 0, club = 0) {
$.ajax({
type: "POST",
url: "/audios/context",
data: {
context: query == "" ? (playlist == 0 ? "entity_audios" : "playlist_context") : "search_context",
hash: u("meta[name=csrf]").attr("value"),
page: page,
context_entity: playlist == 0 ? club * -1 : playlist,
query: query,
returnPlayers: 1,
},
beforeSend: () => {
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"
},
error: () => {
fastError("Error when loading players")
},
success: (response) => {
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 style="width: 78%;float: left;">
${el.outerHTML}
</div>
<div class="attachAudio addToPlaylist" data-id="${id}" style="width: 22%;">
<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(page) >= pagesCount)
u(".showMoreAudiosPlaylist").remove()
else {
if(document.querySelector(".showMoreAudiosPlaylist") != null) {
document.querySelector(".showMoreAudiosPlaylist").setAttribute("data-page", page + 1)
if(query != "") {
document.querySelector(".showMoreAudiosPlaylist").setAttribute("data-query", query)
}
document.querySelector(".showMoreAudiosPlaylist").style.display = "block"
} else {
document.querySelector(".playlistAudiosContainer").parentNode.insertAdjacentHTML("beforeend", `
<div class="showMoreAudiosPlaylist" data-page="2"
${query != "" ? `"data-query="${query}"` : ""}
${playlist != 0 ? `"data-playlist="${playlist}"` : ""}
${club != 0 ? `"data-club="${club}"` : ""}>
${tr("show_more_audios")}
</div>
`)
}
}
u("#loader").remove()
}
})
}
$(document).on("click", ".showMoreAudiosPlaylist", (e) => {
getPlayers(Number(e.currentTarget.dataset.page),
e.currentTarget.dataset.query != null ? e.currentTarget.dataset.query : "",
e.currentTarget.dataset.playlist != null ? Number(e.currentTarget.dataset.playlist) : 0,
e.currentTarget.dataset.club != null ? Number(e.currentTarget.dataset.club) : 0,
)
})
$(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) {
document.querySelector(".playlistAudiosContainer").innerHTML = ""
getPlayers(1, e.currentTarget.value)
return
} else
return
})

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").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 style="width: 78%;float: left;">
${el.outerHTML}
</div>
<div class="attachAudio addToPlaylist" data-id="${id}" style="width: 22%;">
<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

@ -786,6 +786,7 @@
"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.";
@ -813,6 +814,12 @@
"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.";

View file

@ -742,6 +742,7 @@
"new_playlist" = "Новый плейлист";
"created_playlist" = "создан";
"updated_playlist" = "обновлён";
"bookmark" = "Добавить в коллекцию";
"unbookmark" = "Убрать из коллекции";
"empty_playlist" = "В этом плейлисте нет аудиозаписей.";
@ -768,6 +769,12 @@
"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" = "У группы уже есть эта песня.";