1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge pull request #4733 from robert-hamilton36/LyricsSupport

Add Lyric support
This commit is contained in:
Bill Thornton 2024-04-21 14:29:28 -04:00 committed by GitHub
commit 3f967f70f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 456 additions and 7 deletions

View file

@ -187,6 +187,11 @@
</div>
</div>
<div id="lyricsSection" class="verticalSection-extrabottompadding detailVerticalSection lyricsContainer hide">
<h2 class="sectionTitle sectionTitle-cards padded-right">${Lyrics}</h2>
<div is="emby-itemscontainer" class="vertical-list itemsContainer"></div>
</div>
<div class="verticalSection detailVerticalSection moreFromArtistSection hide">
<h2 class="sectionTitle sectionTitle-cards padded-right"></h2>
<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">

View file

@ -1,7 +1,7 @@
import { intervalToDuration } from 'date-fns';
import DOMPurify from 'dompurify';
import markdownIt from 'markdown-it';
import escapeHtml from 'escape-html';
import markdownIt from 'markdown-it';
import isEqual from 'lodash-es/isEqual';
import { appHost } from 'components/apphost';
@ -1055,6 +1055,7 @@ function renderDetails(page, item, apiClient, context) {
renderOverview(page, item);
renderMiscInfo(page, item);
reloadUserDataButtons(page, item);
renderLyricsContainer(page, item, apiClient);
// Don't allow redirection to other websites from the TV layout
if (!layoutManager.tv && appHost.supports('externallinks')) {
@ -1069,6 +1070,38 @@ function enableScrollX() {
return browser.mobile && window.screen.availWidth <= 1000;
}
function renderLyricsContainer(view, item, apiClient) {
const lyricContainer = view.querySelector('.lyricsContainer');
if (lyricContainer && item.HasLyrics) {
if (item.Type !== 'Audio') {
lyricContainer.classList.add('hide');
return;
}
//get lyrics
apiClient.ajax({
url: apiClient.getUrl('Audio/' + item.Id + '/Lyrics'),
type: 'GET',
dataType: 'json'
}).then((response) => {
if (!response.Lyrics) {
lyricContainer.classList.add('hide');
return;
}
lyricContainer.classList.remove('hide');
const itemsContainer = lyricContainer.querySelector('.itemsContainer');
if (itemsContainer) {
const html = response.Lyrics.reduce((htmlAccumulator, lyric) => {
htmlAccumulator += escapeHtml(lyric.Text) + '<br/>';
return htmlAccumulator;
}, '');
itemsContainer.innerHTML = html;
}
}).catch(() => {
lyricContainer.classList.add('hide');
});
}
}
function renderMoreFromSeason(view, item, apiClient) {
const section = view.querySelector('.moreFromSeasonSection');
@ -1119,7 +1152,7 @@ function renderMoreFromArtist(view, item, apiClient) {
const section = view.querySelector('.moreFromArtistSection');
if (section) {
if (item.Type !== 'MusicArtist' && (item.Type !== 'MusicAlbum' || !item.AlbumArtists || !item.AlbumArtists.length)) {
if (item.Type !== 'MusicArtist' && item.Type !== 'Audio' && (item.Type !== 'MusicAlbum' || !item.AlbumArtists || !item.AlbumArtists.length)) {
section.classList.add('hide');
return;
}
@ -1174,7 +1207,7 @@ function renderSimilarItems(page, item, context) {
const similarCollapsible = page.querySelector('#similarCollapsible');
if (similarCollapsible) {
if (item.Type != 'Movie' && item.Type != 'Trailer' && item.Type != 'Series' && item.Type != 'Program' && item.Type != 'Recording' && item.Type != 'MusicAlbum' && item.Type != 'MusicArtist' && item.Type != 'Playlist') {
if (item.Type != 'Movie' && item.Type != 'Trailer' && item.Type != 'Series' && item.Type != 'Program' && item.Type != 'Recording' && item.Type != 'MusicAlbum' && item.Type != 'MusicArtist' && item.Type != 'Playlist' && item.Type != 'Audio') {
similarCollapsible.classList.add('hide');
return;
}

View file

@ -0,0 +1,6 @@
<div id="lyricPage" data-role="page" class="page lyricPage" data-backbutton="true">
<div>
<div class="dynamicLyricsContainer padded-bottom-page">
</div>
</div>
</div>

250
src/controllers/lyrics.js Normal file
View file

@ -0,0 +1,250 @@
import escapeHtml from 'escape-html';
import autoFocuser from 'components/autoFocuser';
import { appRouter } from '../components/router/appRouter';
import layoutManager from 'components/layoutManager';
import { playbackManager } from '../components/playback/playbackmanager';
import ServerConnections from '../components/ServerConnections';
import globalize from '../scripts/globalize';
import LibraryMenu from '../scripts/libraryMenu';
import Events from '../utils/events.ts';
import '../styles/lyrics.scss';
let currentPlayer;
let currentItem;
let savedLyrics;
let isDynamicLyric = false;
function dynamicLyricHtmlReducer(htmlAccumulator, lyric, index) {
if (layoutManager.tv) {
htmlAccumulator += `<button class="lyricsLine dynamicLyric listItem show-focus" id="lyricPosition${index}" data-lyrictime="${lyric.Start}">${escapeHtml(lyric.Text)}</button>`;
} else {
htmlAccumulator += `<div class="lyricsLine dynamicLyric" id="lyricPosition${index}" data-lyrictime="${lyric.Start}">${escapeHtml(lyric.Text)}</div>`;
}
return htmlAccumulator;
}
function staticLyricHtmlReducer(htmlAccumulator, lyric, index) {
if (layoutManager.tv) {
htmlAccumulator += `<button class="lyricsLine listItem show-focus" id="lyricPosition${index}">${escapeHtml(lyric.Text)}</button>`;
} else {
htmlAccumulator += `<div class="lyricsLine" id="lyricPosition${index}">${escapeHtml(lyric.Text)}</div>`;
}
return htmlAccumulator;
}
function getLyricIndex(time, lyrics) {
return lyrics.findLastIndex(lyric => lyric.Start <= time);
}
function getCurrentPlayTime() {
let currentTime = playbackManager.currentTime();
if (currentTime === undefined) currentTime = 0;
//convert to ticks
return currentTime * 10000;
}
export default function (view) {
function setPastLyricClassOnLine(line) {
const lyric = view.querySelector(`#lyricPosition${line}`);
if (lyric) {
lyric.classList.remove('futureLyric');
lyric.classList.add('pastLyric');
}
}
function setFutureLyricClassOnLine(line) {
const lyric = view.querySelector(`#lyricPosition${line}`);
if (lyric) {
lyric.classList.remove('pastLyric');
lyric.classList.add('futureLyric');
}
}
function setCurrentLyricClassOnLine(line) {
const lyric = view.querySelector(`#lyricPosition${line}`);
if (lyric) {
lyric.classList.remove('pastLyric');
lyric.classList.remove('futureLyric');
}
}
function updateAllLyricLines(currentLine, lyrics) {
for (let lyricIndex = 0; lyricIndex <= lyrics.length; lyricIndex++) {
if (lyricIndex < currentLine) {
setPastLyricClassOnLine(lyricIndex);
} else if (lyricIndex === currentLine) {
setCurrentLyricClassOnLine(lyricIndex);
} else if (lyricIndex > currentLine) {
setFutureLyricClassOnLine(lyricIndex);
}
}
}
function renderNoLyricMessage() {
const itemsContainer = view.querySelector('.dynamicLyricsContainer');
if (itemsContainer) {
const html = `<h1> ${globalize.translate('HeaderNoLyrics')} </h1>`;
itemsContainer.innerHTML = html;
}
autoFocuser.autoFocus();
}
function renderDynamicLyrics(lyrics) {
const itemsContainer = view.querySelector('.dynamicLyricsContainer');
if (itemsContainer) {
const html = lyrics.reduce(dynamicLyricHtmlReducer, '');
itemsContainer.innerHTML = html;
}
const lyricLineArray = itemsContainer.querySelectorAll('.lyricsLine');
// attaches click event listener to change playtime to lyric start
lyricLineArray.forEach(element => {
element.addEventListener('click', () => onLyricClick(element.getAttribute('data-lyrictime')));
});
const currentIndex = getLyricIndex(getCurrentPlayTime(), lyrics);
updateAllLyricLines(currentIndex, savedLyrics);
}
function renderStaticLyrics(lyrics) {
const itemsContainer = view.querySelector('.dynamicLyricsContainer');
if (itemsContainer) {
const html = lyrics.reduce(staticLyricHtmlReducer, '');
itemsContainer.innerHTML = html;
}
}
function updateLyrics(lyrics) {
savedLyrics = lyrics;
isDynamicLyric = Object.prototype.hasOwnProperty.call(lyrics[0], 'Start');
if (isDynamicLyric) {
renderDynamicLyrics(savedLyrics);
} else {
renderStaticLyrics(savedLyrics);
}
autoFocuser.autoFocus(view);
}
function getLyrics(serverId, itemId) {
const apiClient = ServerConnections.getApiClient(serverId);
return apiClient.ajax({
url: apiClient.getUrl('Audio/' + itemId + '/Lyrics'),
type: 'GET',
dataType: 'json'
}).then((response) => {
if (!response.Lyrics) {
throw new Error();
}
return response.Lyrics;
});
}
function bindToPlayer(player) {
if (player === currentPlayer) {
return;
}
releaseCurrentPlayer();
currentPlayer = player;
if (!player) {
return;
}
Events.on(player, 'timeupdate', onTimeUpdate);
Events.on(player, 'playbackstart', onPlaybackStart);
Events.on(player, 'playbackstop', onPlaybackStop);
}
function releaseCurrentPlayer() {
const player = currentPlayer;
if (player) {
Events.off(player, 'timeupdate', onTimeUpdate);
Events.off(player, 'playbackstart', onPlaybackStart);
Events.off(player, 'playbackstop', onPlaybackStop);
currentPlayer = null;
}
}
function onLyricClick(lyricTime) {
playbackManager.seek(lyricTime);
if (playbackManager.paused()) {
playbackManager.playPause(currentPlayer);
}
}
function onTimeUpdate() {
if (isDynamicLyric) {
const currentIndex = getLyricIndex(getCurrentPlayTime(), savedLyrics);
updateAllLyricLines(currentIndex, savedLyrics);
}
}
function onPlaybackStart(event, state) {
if (currentItem.Id !== state.NowPlayingItem.Id) {
onLoad();
}
}
function onPlaybackStop(_, state) {
// TODO: switch to appRouter.back(), with fix to navigation to /#/queue. Which is broken when it has nothing playing
if (!state.NextMediaType) {
appRouter.goHome();
}
}
function onPlayerChange() {
const player = playbackManager.getCurrentPlayer();
bindToPlayer(player);
}
function onLoad() {
savedLyrics = null;
currentItem = null;
isDynamicLyric = false;
LibraryMenu.setTitle(globalize.translate('Lyrics'));
const player = playbackManager.getCurrentPlayer();
if (player) {
bindToPlayer(player);
const state = playbackManager.getPlayerState(player);
currentItem = state.NowPlayingItem;
const serverId = state.NowPlayingItem.ServerId;
const itemId = state.NowPlayingItem.Id;
getLyrics(serverId, itemId).then(updateLyrics).catch(renderNoLyricMessage);
} else {
// if nothing is currently playing, no lyrics to display redirect to home
appRouter.goHome();
}
}
view.addEventListener('viewshow', function () {
Events.on(playbackManager, 'playerchange', onPlayerChange);
try {
onLoad();
} catch (e) {
appRouter.goHome();
}
});
view.addEventListener('viewbeforehide', function () {
Events.off(playbackManager, 'playerchange', onPlayerChange);
releaseCurrentPlayer();
});
}

View file

@ -81,6 +81,10 @@
<span class="material-icons fullscreen" aria-hidden="true"></span>
</button>
<button is="paper-icon-button-light" class="btnLyrics autoSize hide" title="${Lyrics}">
<span class="material-icons lyrics" style="top:0.05em" aria-hidden="true"></span>
</button>
<button is="paper-icon-button-light" class="btnShuffleQueue autoSize" title="${Shuffle}">
<span class="material-icons shuffle" aria-hidden="true"></span>
</button>