mirror of
https://github.com/openvk/openvk
synced 2024-12-22 08:31:18 +03:00
feat(music): syncronized lyrics by lrc file format
!!!DRAFT!!! Co-authored-by: Mikhail Serebryakov <126566943+synzr@users.noreply.github.com>
This commit is contained in:
parent
6007a81546
commit
da82c84730
11 changed files with 214 additions and 8 deletions
|
@ -167,6 +167,11 @@ class Audio extends Media
|
|||
return !is_null($this->getRecord()->lyrics) ? htmlspecialchars($this->getRecord()->lyrics, ENT_DISALLOWED | ENT_XHTML) : NULL;
|
||||
}
|
||||
|
||||
function getLRC(): ?string
|
||||
{
|
||||
return !is_null($this->getRecord()->lrc) ? htmlspecialchars($this->getRecord()->lrc, ENT_DISALLOWED | ENT_XHTML) : NULL;
|
||||
}
|
||||
|
||||
function getLength(): int
|
||||
{
|
||||
return $this->getRecord()->length;
|
||||
|
|
|
@ -802,4 +802,15 @@ final class AudioPresenter extends OpenVKPresenter
|
|||
|
||||
$this->returnJson($resultArr);
|
||||
}
|
||||
|
||||
function renderLRC(int $id) {
|
||||
$audio = (new Audios)->get($id);
|
||||
header("Content-Type: application/octet-stream");
|
||||
|
||||
if ($audio->isAvailable()) {
|
||||
exit($audio->getLRC());
|
||||
} else {
|
||||
exit("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,9 +48,11 @@
|
|||
</div>
|
||||
|
||||
<div class="additionalButtons">
|
||||
<div class="lyricsButton musicIcon" title="{_lyrics_tip}"></div>
|
||||
<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 class="lyrics"></div>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{php $isWithdrawn = $audio->isWithdrawn()}
|
||||
{php $isAvailable = $audio->isAvailable()}
|
||||
{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 !$isAvailable}processed{/if} {if $isWithdrawn}withdrawn{/if}" data-length="{$audio->getLength()}" data-keys="{json_encode($audio->getKeys())}" data-url="{$audio->getURL()}">
|
||||
<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 !$isAvailable}processed{/if} {if $isWithdrawn}withdrawn{/if}" data-length="{$audio->getLength()}" data-keys="{json_encode($audio->getKeys())}" data-url="{$audio->getURL()}" {if !empty($audio->getLRC()) }data-lrc="/audio{$audio->getId()}/lrc"{/if}>
|
||||
<audio class="audio" />
|
||||
|
||||
<div id="miniplayer" class="audioEntry">
|
||||
|
|
|
@ -225,6 +225,8 @@ routes:
|
|||
handler: "Audio->playlists"
|
||||
- url: "/audio{num}/action"
|
||||
handler: "Audio->action"
|
||||
- url: "/audio{num}/lrc"
|
||||
handler: "Audio->lrc"
|
||||
- url: "/{?!club}{num}"
|
||||
handler: "Group->view"
|
||||
placeholders:
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
.bigPlayer .paddingLayer .additionalButtons {
|
||||
float: left;
|
||||
margin-top: -6px;
|
||||
width: 11%;
|
||||
width: 14%;
|
||||
}
|
||||
|
||||
.bigPlayer .paddingLayer .additionalButtons .repeatButton {
|
||||
|
@ -92,6 +92,14 @@
|
|||
float: left;
|
||||
}
|
||||
|
||||
.bigPlayer .paddingLayer .additionalButtons .lyricsButton {
|
||||
width: 12px;
|
||||
height: 16px;
|
||||
background-position: -79px -65px;
|
||||
margin-left: 7px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.broadcastButton {
|
||||
width: 16px;
|
||||
height: 12px;
|
||||
|
@ -138,7 +146,7 @@
|
|||
float: left;
|
||||
margin-top: -13px;
|
||||
margin-left: 13px;
|
||||
width: 63%;
|
||||
width: 61%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
@ -212,6 +220,47 @@
|
|||
width:72%
|
||||
}
|
||||
|
||||
.bigPlayer .lyrics {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background-color: #ffffff;
|
||||
margin-top: 23px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
box-shadow: 0px 2px 3px -2px #000a;
|
||||
transition: 250ms ease-in;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bigPlayer .lyrics.shown {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bigPlayer .lyrics .lyrics__line {
|
||||
font-family: Tahoma;
|
||||
font-size: 15pt;
|
||||
padding: 7px 20px;
|
||||
color: gray;
|
||||
transition: 250ms ease-out;
|
||||
}
|
||||
|
||||
.bigPlayer .lyrics .lyrics__line__active {
|
||||
color:black;
|
||||
font-size: 17pt;
|
||||
}
|
||||
|
||||
.bigPlayer .lyrics .lyrics__message {
|
||||
font-family: Tahoma;
|
||||
font-size: 9pt;
|
||||
text-align: center;
|
||||
padding: 3px;
|
||||
padding-top: 50px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
.audioEmbed .track > .selectableTrack, .bigPlayer .selectableTrack {
|
||||
margin-top: 3px;
|
||||
width: calc(100% - 8px);
|
||||
|
|
BIN
Web/static/img/audios_controls.png
Normal file → Executable file
BIN
Web/static/img/audios_controls.png
Normal file → Executable file
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.4 KiB |
|
@ -78,10 +78,117 @@ class bigPlayer {
|
|||
|
||||
timeType = 0
|
||||
|
||||
lyricIndex = 0
|
||||
lrcInterval = null;
|
||||
|
||||
findTrack(id) {
|
||||
return this.tracks["tracks"].find(item => item.id == id)
|
||||
}
|
||||
|
||||
|
||||
// LRC | Synced lyrics functionality
|
||||
_parseTimestamp = (timestamp) => {
|
||||
if (timestamp[2] !== ":" || timestamp[5] !== ".") return;
|
||||
|
||||
const minutes = +timestamp.substring(0, 2);
|
||||
const seconds = +timestamp.substring(3, 5);
|
||||
const hundredths = +timestamp.substring(6, 8);
|
||||
|
||||
return minutes * 60 + seconds + hundredths / 100;
|
||||
};
|
||||
|
||||
_parseLrcLine = (line) => {
|
||||
if (!line.startsWith("[") && line.indexOf("]") !== 9) return;
|
||||
|
||||
const timestamp = this._parseTimestamp(line.substring(1, 9));
|
||||
if (!timestamp) return;
|
||||
|
||||
return { timestamp, textContent: line.substring(10).trim() };
|
||||
};
|
||||
|
||||
_parseLrc = (textContent) => {
|
||||
return textContent.split("\n").map(this._parseLrcLine).filter(Boolean);
|
||||
};
|
||||
|
||||
_importLrc = async (path) => {
|
||||
return await fetch(path)
|
||||
.then((response) => response.text())
|
||||
.then(this._parseLrc);
|
||||
};
|
||||
|
||||
// NOTE: LYRICS DOM FUNCTIONS
|
||||
_createLineElement = (timestamp, textContent) => {
|
||||
const element = document.createElement("div");
|
||||
|
||||
element.classList.add("lyrics__line");
|
||||
element.setAttribute("data-timestamp", timestamp);
|
||||
|
||||
element.textContent = textContent;
|
||||
document.querySelectorAll(`.lyrics`)[0].appendChild(element);
|
||||
};
|
||||
|
||||
_loadLyrics = (lyrics) => {
|
||||
document.querySelectorAll(`.lyrics`)[0].innerHTML = "";
|
||||
document.querySelectorAll(`.lyrics`)[0].scrollTop = 0;
|
||||
|
||||
lyrics.forEach(({ timestamp, textContent }) =>
|
||||
this._createLineElement(timestamp, textContent)
|
||||
);
|
||||
|
||||
if (lyrics.length == 0)
|
||||
{
|
||||
document.querySelectorAll(`.lyrics`)[0].innerHTML = `<div class="lyrics__message">${tr("sync_lyrics_not_available")}</div>`
|
||||
}
|
||||
};
|
||||
|
||||
handleLyricsSync = () => {
|
||||
let currentLyric = document.querySelectorAll(`.lyrics`)[0].children[this.lyricIndex];
|
||||
|
||||
if (currentLyric == undefined) return;
|
||||
|
||||
let timestamp = currentLyric.getAttribute("data-timestamp");
|
||||
let isActive = currentLyric.classList.contains("lyrics__line__active");
|
||||
|
||||
if (!isActive && this.player().currentTime >= timestamp) {
|
||||
document.querySelectorAll(`.lyrics`)[0].scrollTop = currentLyric.offsetTop - 55
|
||||
return currentLyric.classList.add("lyrics__line__active");
|
||||
}
|
||||
|
||||
if (document.querySelectorAll(`.lyrics`)[0].children.length === this.lyricIndex + 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextLyric = document.querySelectorAll(`.lyrics`)[0].children[this.lyricIndex + 1];
|
||||
let nextTimestamp = +nextLyric.getAttribute("data-timestamp");
|
||||
|
||||
if (isActive && this.player().currentTime >= nextTimestamp) {
|
||||
currentLyric.classList.remove("lyrics__line__active");
|
||||
return this.lyricIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
updateLyricIndex = () => {
|
||||
if (this.lyricIndex < 0) {
|
||||
this.lyricIndex = 0;
|
||||
}
|
||||
|
||||
document.querySelectorAll(`.lyrics`)[0].children[this.lyricIndex].classList.remove(
|
||||
"lyrics__line__active"
|
||||
);
|
||||
|
||||
this.lyricIndex = [...document.querySelectorAll(`.lyrics`)[0].children].findIndex(
|
||||
(lyric) => {
|
||||
const timestamp = +lyric.getAttribute("data-timestamp");
|
||||
return timestamp >= this.player().currentTime;
|
||||
}
|
||||
);
|
||||
|
||||
let currentLyric = document.querySelectorAll(`.lyrics`)[0].children[this.lyricIndex];
|
||||
|
||||
document.querySelectorAll(`.lyrics`)[0].scrollTop = currentLyric.offsetTop - 55
|
||||
currentLyric.classList.add("lyrics__line__active");
|
||||
}
|
||||
|
||||
constructor(context, context_id, page = 1) {
|
||||
this.context["context_type"] = context
|
||||
this.context["context_id"] = context_id
|
||||
|
@ -151,7 +258,6 @@ class bigPlayer {
|
|||
|
||||
if (ps <= 100)
|
||||
this.nodes["thisPlayer"].querySelector(".selectableTrack .slider").style.left = `${ ps}%`;
|
||||
|
||||
})
|
||||
|
||||
u(this.player()).on("volumechange", (e) => {
|
||||
|
@ -173,6 +279,7 @@ class bigPlayer {
|
|||
const width = e.clientX - rect.left;
|
||||
const time = Math.ceil((width * this.tracks["currentTrack"].length) / (rect.right - rect.left));
|
||||
|
||||
this.updateLyricIndex();
|
||||
this.player().currentTime = time;
|
||||
})
|
||||
|
||||
|
@ -275,6 +382,22 @@ class bigPlayer {
|
|||
else
|
||||
this.player().loop = false
|
||||
})
|
||||
|
||||
u(".bigPlayer .additionalButtons .lyricsButton").on("click", (e) => {
|
||||
if(this.tracks["currentTrack"] == null)
|
||||
return
|
||||
|
||||
e.currentTarget.classList.toggle("pressed")
|
||||
|
||||
if (document.querySelectorAll(`.lyrics`)[0].classList.contains("shown")) {
|
||||
document.querySelectorAll(`.lyrics`)[0].style.display = "block";
|
||||
document.querySelectorAll(`.lyrics`)[0].classList.remove("shown");
|
||||
setTimeout(() => {document.querySelectorAll(`.lyrics`)[0].style.display = ""}, 250);
|
||||
} else {
|
||||
document.querySelectorAll(`.lyrics`)[0].style.display = "block";
|
||||
setTimeout(() => {document.querySelectorAll(`.lyrics`)[0].classList.add("shown")}, 50);
|
||||
}
|
||||
})
|
||||
|
||||
u(".bigPlayer .additionalButtons .shuffleButton").on("click", (e) => {
|
||||
if(this.tracks["currentTrack"] == null)
|
||||
|
@ -327,9 +450,11 @@ class bigPlayer {
|
|||
break
|
||||
case "ArrowLeft":
|
||||
this.player().currentTime = this.player().currentTime - 3
|
||||
this.updateLyricIndex();
|
||||
break
|
||||
case "ArrowRight":
|
||||
this.player().currentTime = this.player().currentTime + 3
|
||||
this.updateLyricIndex();
|
||||
break
|
||||
// буквально
|
||||
case " ":
|
||||
|
@ -400,7 +525,7 @@ class bigPlayer {
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
this.lrcInterval = clearInterval(this.lrcInterval);
|
||||
this.showNextTrack()
|
||||
})
|
||||
|
||||
|
@ -429,6 +554,9 @@ class bigPlayer {
|
|||
}
|
||||
})
|
||||
}, 2000)
|
||||
|
||||
this.lyricIndex = 0;
|
||||
this._importLrc(`/audio${this.tracks.currentTrack.id}/lrc`).then(this._loadLyrics)
|
||||
})
|
||||
|
||||
if(localStorage.volume != null && localStorage.volume < 1 && localStorage.volume > 0)
|
||||
|
@ -447,6 +575,7 @@ class bigPlayer {
|
|||
navigator.mediaSession.setActionHandler('nexttrack', () => { this.showNextTrack() });
|
||||
navigator.mediaSession.setActionHandler("seekto", (details) => {
|
||||
this.player().currentTime = details.seekTime;
|
||||
this.updateLyricIndex;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -459,7 +588,8 @@ class bigPlayer {
|
|||
this.player().play()
|
||||
this.nodes["playButtons"].querySelector(".playButton").classList.add("pause")
|
||||
document.querySelector('link[rel="icon"], link[rel="shortcut icon"]').setAttribute("href", "/assets/packages/static/openvk/img/favicons/favicon24_paused.png")
|
||||
|
||||
this.lrcInterval = setInterval(this.handleLyricsSync);
|
||||
|
||||
navigator.mediaSession.playbackState = "playing"
|
||||
}
|
||||
|
||||
|
@ -471,7 +601,8 @@ class bigPlayer {
|
|||
this.player().pause()
|
||||
this.nodes["playButtons"].querySelector(".playButton").classList.remove("pause")
|
||||
document.querySelector('link[rel="icon"], link[rel="shortcut icon"]').setAttribute("href", "/assets/packages/static/openvk/img/favicons/favicon24_playing.png")
|
||||
|
||||
this.lrcInterval = clearInterval(this.lrcInterval);
|
||||
|
||||
navigator.mediaSession.playbackState = "paused"
|
||||
}
|
||||
|
||||
|
@ -788,7 +919,7 @@ function initPlayer(id, keys, url, length) {
|
|||
$(`#audioEmbed-${ id} .track`).removeClass('shown')
|
||||
$(`#audioEmbed-${ id}`).removeClass("havePlayed")
|
||||
}
|
||||
|
||||
|
||||
u(audio).on("play", playButtonImageUpdate);
|
||||
u(audio).on(["pause", "suspended"], playButtonImageUpdate);
|
||||
u(audio).on("ended", (e) => {
|
||||
|
|
|
@ -912,6 +912,8 @@
|
|||
"audio_popular" = "Popular";
|
||||
"audio_search" = "Search";
|
||||
|
||||
"sync_lyrics_not_available" = "Syncronized lyrics are not available.";
|
||||
|
||||
"my_audios_small" = "My audios";
|
||||
"my_playlists" = "My playlists";
|
||||
"playlists" = "Playlists";
|
||||
|
|
|
@ -867,6 +867,8 @@
|
|||
"audio_popular" = "Популярное";
|
||||
"audio_search" = "Поиск";
|
||||
|
||||
"sync_lyrics_not_available" = "Синхронизированный текст песни недоступен.";
|
||||
|
||||
"my_audios_small" = "Мои аудиозаписи";
|
||||
"my_playlists" = "Мои плейлисты";
|
||||
"playlists" = "Плейлисты";
|
||||
|
|
|
@ -828,6 +828,8 @@
|
|||
"audio_popular" = "Популярні";
|
||||
"audio_search" = "Пошук";
|
||||
|
||||
"sync_lyrics_not_available" = "Синхронізований текст пісні недоступний.";
|
||||
|
||||
"my_audios_small" = "Мої аудіозаписи";
|
||||
"my_playlists" = "Мій список відтворення";
|
||||
"playlists" = "Списки відтворення";
|
||||
|
|
Loading…
Reference in a new issue