From 648e8ff2a690cc0621dedbe59f96e71e7aeb3f72 Mon Sep 17 00:00:00 2001 From: LJQ Date: Mon, 22 Apr 2024 20:09:27 +0800 Subject: [PATCH 1/8] Preliminary Lyrics Editor --- src/components/itemContextMenu.js | 30 +- src/components/itemHelper.js | 11 + src/components/lyricseditor/lyricseditor.js | 383 ++++++++++++++++++ src/components/lyricseditor/lyricseditor.scss | 11 + .../lyricseditor/lyricseditor.template.html | 27 ++ .../lyricseditor/lyricspreview.template.html | 10 + .../lyricsuploader/lyricsuploader.js | 171 ++++++++ .../lyricsuploader/lyricsuploader.scss | 15 + .../lyricsuploader.template.html | 36 ++ src/strings/en-us.json | 9 + src/utils/file.ts | 15 + 11 files changed, 701 insertions(+), 17 deletions(-) create mode 100644 src/components/lyricseditor/lyricseditor.js create mode 100644 src/components/lyricseditor/lyricseditor.scss create mode 100644 src/components/lyricseditor/lyricseditor.template.html create mode 100644 src/components/lyricseditor/lyricspreview.template.html create mode 100644 src/components/lyricsuploader/lyricsuploader.js create mode 100644 src/components/lyricsuploader/lyricsuploader.scss create mode 100644 src/components/lyricsuploader/lyricsuploader.template.html diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js index f265eafbfe..26c7770386 100644 --- a/src/components/itemContextMenu.js +++ b/src/components/itemContextMenu.js @@ -201,14 +201,6 @@ export function getCommands(options) { id: 'delete', icon: 'delete' }); - - if (item.Type === 'Audio' && item.HasLyrics && window.location.href.includes(item.Id)) { - commands.push({ - name: globalize.translate('DeleteLyrics'), - id: 'deleteLyrics', - icon: 'delete_sweep' - }); - } } if (commands.length) { @@ -243,6 +235,14 @@ export function getCommands(options) { }); } + if (itemHelper.canEditLyrics(user, item)) { + commands.push({ + name: globalize.translate('EditLyrics'), + id: 'editlyrics', + icon: 'lyrics' + }); + } + if (options.identify !== false && itemHelper.canIdentify(user, item)) { commands.push({ name: globalize.translate('Identify'), @@ -441,6 +441,11 @@ function executeCommand(item, id, options) { subtitleEditor.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id)); }); break; + case 'editlyrics': + import('./lyricseditor/lyricseditor').then(({ default: lyricseditor }) => { + lyricseditor.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id)); + }); + break; case 'edit': editItem(apiClient, item).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id)); break; @@ -514,9 +519,6 @@ function executeCommand(item, id, options) { case 'delete': deleteItem(apiClient, item).then(getResolveFunction(resolve, id, true, true), getResolveFunction(resolve, id)); break; - case 'deleteLyrics': - deleteLyrics(apiClient, item).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id)); - break; case 'share': navigator.share({ title: item.Name, @@ -667,12 +669,6 @@ function deleteItem(apiClient, item) { }); } -function deleteLyrics(apiClient, item) { - return import('../scripts/deleteHelper').then((deleteHelper) => { - return deleteHelper.deleteLyrics(item); - }); -} - function refresh(apiClient, item) { import('./refreshdialog/refreshdialog').then(({ default: RefreshDialog }) => { new RefreshDialog({ diff --git a/src/components/itemHelper.js b/src/components/itemHelper.js index df31167860..260c80b32c 100644 --- a/src/components/itemHelper.js +++ b/src/components/itemHelper.js @@ -186,6 +186,16 @@ export function canEditSubtitles (user, item) { || user.Policy.IsAdministrator; } +export function canEditLyrics (user, item) { + if (item.MediaType !== MediaType.Audio) { + return false; + } + if (isLocalItem(item)) { + return false; + } + return user.Policy.IsAdministrator; +} + export function canShare (item, user) { if (item.Type === 'Program') { return false; @@ -332,6 +342,7 @@ export default { canEdit: canEdit, canEditImages: canEditImages, canEditSubtitles, + canEditLyrics, canShare: canShare, enableDateAddedDisplay: enableDateAddedDisplay, canMarkPlayed: canMarkPlayed, diff --git a/src/components/lyricseditor/lyricseditor.js b/src/components/lyricseditor/lyricseditor.js new file mode 100644 index 0000000000..7775db4540 --- /dev/null +++ b/src/components/lyricseditor/lyricseditor.js @@ -0,0 +1,383 @@ +import escapeHtml from 'escape-html'; + +import { LyricsApi } from '@jellyfin/sdk/lib/generated-client/api/lyrics-api'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; +import dialogHelper from '../dialogHelper/dialogHelper'; +import layoutManager from '../layoutManager'; +import globalize from '../../scripts/globalize'; +import loading from '../loading/loading'; +import focusManager from '../focusManager'; +import dom from '../../scripts/dom'; +import '../../elements/emby-select/emby-select'; +import '../listview/listview.scss'; +import '../../elements/emby-button/paper-icon-button-light'; +import '../formdialog.scss'; +import 'material-design-icons-iconfont'; +import './lyricseditor.scss'; +import '../../elements/emby-button/emby-button'; +import '../../styles/flexstyles.scss'; +import ServerConnections from '../ServerConnections'; +import toast from '../toast/toast'; +import template from './lyricseditor.template.html'; +import templatePreview from './lyricspreview.template.html'; +import { deleteLyrics } from '../../scripts/deleteHelper'; + +let currentItem; +let hasChanges; + +function downloadRemoteLyrics(context, id) { + const api = toApi(ServerConnections.getApiClient(currentItem.ServerId)); + const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance); + lyricsApi.downloadRemoteLyrics({ + itemId: currentItem.Id, lyricId: id + }).then(function () { + hasChanges = true; + + toast(globalize.translate('MessageDownloadQueued')); + + focusManager.autoFocus(context); + }); +} + +function renderSearchResults(context, results) { + let lastProvider = ''; + let html = ''; + + if (!results.length) { + context.querySelector('.noSearchResults').classList.remove('hide'); + context.querySelector('.lyricsResults').innerHTML = ''; + loading.hide(); + return; + } + + context.querySelector('.noSearchResults').classList.add('hide'); + + for (let i = 0, length = results.length; i < length; i++) { + const result = results[i]; + + const provider = result.ProviderName; + const metadata = result.Lyrics.Metadata; + const lyrics = result.Lyrics.Lyrics.reduce((htmlAccumulator, lyric) => { + htmlAccumulator += escapeHtml(lyric.Text) + '
'; + return htmlAccumulator; + }, ''); + if (provider !== lastProvider) { + if (i > 0) { + html += ''; + } + html += '

' + provider + '

'; + html += '
'; + lastProvider = provider; + } + + const tagName = layoutManager.tv ? 'button' : 'div'; + let className = layoutManager.tv ? 'listItem listItem-border btnOptions' : 'listItem listItem-border'; + if (layoutManager.tv) { + className += ' listItem-focusscale listItem-button'; + } + + html += '<' + tagName + ' class="' + className + '" data-lyricsid="' + result.Id + '">'; + + html += ''; + + html += '
'; + + html += '
' + escapeHtml(metadata.Artist + ' - ' + metadata.Album + ' - ' + metadata.Title) + '
'; + + const minutes = Math.floor(metadata.Length / 600000000); + const seconds = Math.floor((metadata.Length % 600000000) / 10000000); + + html += '
' + globalize.translate('LabelDuration') + ': ' + minutes + ':' + String(seconds).padStart(2, '0') + '
'; + + html += '
' + globalize.translate('LabelIsSynced') + ': ' + escapeHtml(metadata.IsSynced ? 'True' : 'False') + '
'; + + html += '
'; + + if (!layoutManager.tv) { + html += ''; + html += ''; + } + html += '
'; + html += '

' + globalize.translate('Lyrics') + '

'; + html += '
' + lyrics + '
'; + html += '
'; + html += ''; + } + + if (results.length) { + html += '
'; + } + + const elem = context.querySelector('.lyricsResults'); + elem.innerHTML = html; + + loading.hide(); +} + +function searchForLyrics(context) { + loading.show(); + + const api = toApi(ServerConnections.getApiClient(currentItem.ServerId)); + const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance); + lyricsApi.searchRemoteLyrics({ + itemId: currentItem.Id + }).then(function (results) { + renderSearchResults(context, results.data); + }); +} + +function reload(context, apiClient, itemId) { + context.querySelector('.noSearchResults').classList.add('hide'); + + function onGetItem(item) { + currentItem = item; + + fillCurrentLyrics(context, apiClient, item); + let file = item.Path || ''; + const index = Math.max(file.lastIndexOf('/'), file.lastIndexOf('\\')); + if (index > -1) { + file = file.substring(index + 1); + } + + if (file) { + context.querySelector('.pathValue').innerText = file; + context.querySelector('.originalFile').classList.remove('hide'); + } else { + context.querySelector('.pathValue').innerHTML = ''; + context.querySelector('.originalFile').classList.add('hide'); + } + + loading.hide(); + } + + if (typeof itemId === 'string') { + apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(onGetItem); + } else { + onGetItem(itemId); + } +} + +function onSearchSubmit(e) { + const form = this; + + searchForLyrics(dom.parentWithClass(form, 'formDialogContent')); + + e.preventDefault(); + return false; +} + +function onLyricsResultsClick(e) { + let lyricsId; + let context; + let lyrics; + + const btnOptions = dom.parentWithClass(e.target, 'btnOptions'); + if (btnOptions) { + lyricsId = btnOptions.getAttribute('data-lyricsid'); + lyrics = btnOptions.querySelector('.hiddenLyrics'); + context = dom.parentWithClass(btnOptions, 'lyricsEditorDialog'); + showOptions(btnOptions, context, lyricsId, lyrics.innerHTML); + } + + const btnPreview = dom.parentWithClass(e.target, 'btnPreview'); + if (btnPreview) { + lyrics = btnPreview.parentNode.querySelector('.hiddenLyrics'); + showLyricsPreview(lyrics.innerHTML); + } + + const btnDownload = dom.parentWithClass(e.target, 'btnDownload'); + if (btnDownload) { + lyricsId = btnDownload.getAttribute('data-lyricsid'); + context = dom.parentWithClass(btnDownload, 'lyricsEditorDialog'); + downloadRemoteLyrics(context, lyricsId); + } +} + +function showLyricsPreview(lyrics) { + const dialogOptions = { + removeOnClose: true, + scrollY: false + }; + + if (layoutManager.tv) { + dialogOptions.size = 'fullscreen'; + } else { + dialogOptions.size = 'small'; + } + + const dlg = dialogHelper.createDialog(dialogOptions); + + dlg.classList.add('formDialog'); + dlg.classList.add('lyricsEditorDialog'); + + dlg.innerHTML = globalize.translateHtml(templatePreview, 'core'); + + dlg.querySelector('.lyricsPreview').innerHTML = lyrics; + + dlg.querySelector('.btnCancel').addEventListener('click', function () { + dialogHelper.close(dlg); + }); + + dialogHelper.open(dlg); +} + +function showOptions(button, context, lyricsId, lyrics) { + const items = []; + + items.push({ + name: globalize.translate('LyricsPreview'), + id: 'preview' + } + , { + name: globalize.translate('Download'), + id: 'download' + }); + + import('../actionSheet/actionSheet').then((actionsheet) => { + actionsheet.show({ + items: items, + positionTo: button + + }).then(function (id) { + if (id === 'download') { + downloadRemoteLyrics(context, lyricsId); + } + if (id === 'preview') { + showLyricsPreview(lyrics); + } + }); + }); +} + +function centerFocus(elem, horiz, on) { + import('../../scripts/scrollHelper').then(({ default: scrollHelper }) => { + const fn = on ? 'on' : 'off'; + scrollHelper.centerFocus[fn](elem, horiz); + }); +} + +function onOpenUploadMenu(e) { + const dialog = dom.parentWithClass(e.target, 'lyricsEditorDialog'); + const apiClient = ServerConnections.getApiClient(currentItem.ServerId); + + import('../lyricsuploader/lyricsuploader').then(({ default: lyricsUploader }) => { + lyricsUploader.show({ + itemId: currentItem.Id, + serverId: currentItem.ServerId + }).then(function (hasChanged) { + if (hasChanged) { + hasChanges = true; + reload(dialog, apiClient, currentItem.Id); + } + }); + }); +} + +function onDeleteLyrics(e) { + deleteLyrics(currentItem).then(() => { + hasChanges = true; + const context = dom.parentWithClass(e.target, 'formDialogContent'); + const apiClient = ServerConnections.getApiClient(currentItem.ServerId); + reload(context, apiClient, currentItem.Id); + }); +} + +function fillCurrentLyrics(context, apiClient, item) { + const api = toApi(apiClient); + const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance); + lyricsApi.getLyrics({ + itemId: item.Id + }).then((response) => { + if (!response.data.Lyrics) { + context.querySelector('.currentLyrics').innerHTML = ''; + } else { + let html = ''; + html += '

' + globalize.translate('Lyrics') + '

'; + html += '
'; + html += response.data.Lyrics.reduce((htmlAccumulator, lyric) => { + htmlAccumulator += escapeHtml(lyric.Text) + '
'; + return htmlAccumulator; + }, ''); + html += '
'; + context.querySelector('.currentLyrics').innerHTML = html; + } + }).catch(() =>{ + context.querySelector('.currentLyrics').innerHTML = ''; + }); +} + +function showEditorInternal(itemId, serverId) { + hasChanges = false; + const apiClient = ServerConnections.getApiClient(serverId); + return apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(function (item) { + const dialogOptions = { + removeOnClose: true, + scrollY: false + }; + + if (layoutManager.tv) { + dialogOptions.size = 'fullscreen'; + } else { + dialogOptions.size = 'small'; + } + + const dlg = dialogHelper.createDialog(dialogOptions); + + dlg.classList.add('formDialog'); + dlg.classList.add('lyricsEditorDialog'); + + dlg.innerHTML = globalize.translateHtml(template, 'core'); + + dlg.querySelector('.originalLyricsFileLabel').innerHTML = globalize.translate('File'); + + dlg.querySelector('.lyricsSearchForm').addEventListener('submit', onSearchSubmit); + + dlg.querySelector('.btnOpenUploadMenu').addEventListener('click', onOpenUploadMenu); + + dlg.querySelector('.btnDeleteLyrics').addEventListener('click', onDeleteLyrics); + + const btnSubmit = dlg.querySelector('.btnSubmit'); + + if (layoutManager.tv) { + centerFocus(dlg.querySelector('.formDialogContent'), false, true); + dlg.querySelector('.btnSearchLyrics').classList.add('hide'); + } else { + btnSubmit.classList.add('hide'); + } + const editorContent = dlg.querySelector('.formDialogContent'); + + dlg.querySelector('.lyricsResults').addEventListener('click', onLyricsResultsClick); + + dlg.querySelector('.btnCancel').addEventListener('click', function () { + dialogHelper.close(dlg); + }); + + return new Promise(function (resolve, reject) { + dlg.addEventListener('close', function () { + if (layoutManager.tv) { + centerFocus(dlg.querySelector('.formDialogContent'), false, false); + } + + if (hasChanges) { + resolve(); + } else { + reject(); + } + }); + + dialogHelper.open(dlg); + + reload(editorContent, apiClient, item); + }); + }); +} + +function showEditor(itemId, serverId) { + loading.show(); + + return showEditorInternal(itemId, serverId); +} + +export default { + show: showEditor +}; diff --git a/src/components/lyricseditor/lyricseditor.scss b/src/components/lyricseditor/lyricseditor.scss new file mode 100644 index 0000000000..e7baf4cee1 --- /dev/null +++ b/src/components/lyricseditor/lyricseditor.scss @@ -0,0 +1,11 @@ +.originalLyricsFileLabel { + margin-right: 1em; +} + +.lyricsFeaturePillow { + background: #00a4dc; + color: #000; + padding: 0.3em 1em; + border-radius: 1000em; + margin-right: 0.25em; +} diff --git a/src/components/lyricseditor/lyricseditor.template.html b/src/components/lyricseditor/lyricseditor.template.html new file mode 100644 index 0000000000..15eae3bf90 --- /dev/null +++ b/src/components/lyricseditor/lyricseditor.template.html @@ -0,0 +1,27 @@ +
+ +

${Lyrics}

+ +
+
+
+
+

${SearchForLyrics}

+ +

+ +
+
+ + + +
+ +
+
+
+
+ ${NoLyricsSearchResultsFound} +
+
+
\ No newline at end of file diff --git a/src/components/lyricseditor/lyricspreview.template.html b/src/components/lyricseditor/lyricspreview.template.html new file mode 100644 index 0000000000..fa807d7293 --- /dev/null +++ b/src/components/lyricseditor/lyricspreview.template.html @@ -0,0 +1,10 @@ +
+ +

${LyricsPreview}

+ +
+
+
+
+
+
\ No newline at end of file diff --git a/src/components/lyricsuploader/lyricsuploader.js b/src/components/lyricsuploader/lyricsuploader.js new file mode 100644 index 0000000000..9e21980348 --- /dev/null +++ b/src/components/lyricsuploader/lyricsuploader.js @@ -0,0 +1,171 @@ +import escapeHtml from 'escape-html'; + +import { LyricsApi } from '@jellyfin/sdk/lib/generated-client/api/lyrics-api'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; +import dialogHelper from '../../components/dialogHelper/dialogHelper'; +import ServerConnections from '../ServerConnections'; +import dom from '../../scripts/dom'; +import loading from '../../components/loading/loading'; +import scrollHelper from '../../scripts/scrollHelper'; +import layoutManager from '../layoutManager'; +import globalize from '../../scripts/globalize'; +import template from './lyricsuploader.template.html'; +import toast from '../toast/toast'; +import '../../elements/emby-button/emby-button'; +import '../../elements/emby-select/emby-select'; +import '../formdialog.scss'; +import './lyricsuploader.scss'; +import { readFileAsText } from 'utils/file'; + +let currentItemId; +let currentServerId; +let currentFile; +let hasChanges = false; + +function onFileReaderError(evt) { + loading.hide(); + + const error = evt.target.error; + if (error.code !== error.ABORT_ERR) { + toast(globalize.translate('MessageFileReadError')); + } +} + +function isValidLyricsFile(file) { + return file && ['.lrc', '.txt'] + .some(function(ext) { + return file.name.endsWith(ext); + }); +} + +function setFiles(page, files) { + const file = files[0]; + + if (!isValidLyricsFile(file)) { + page.querySelector('#lyricsOutput').innerHTML = ''; + page.querySelector('#fldUpload').classList.add('hide'); + page.querySelector('#labelDropLyrics').classList.remove('hide'); + currentFile = null; + return; + } + + currentFile = file; + + const reader = new FileReader(); + + reader.onerror = onFileReaderError; + reader.onloadstart = function () { + page.querySelector('#fldUpload').classList.add('hide'); + }; + reader.onabort = function () { + loading.hide(); + console.debug('File read cancelled'); + }; + + // Closure to capture the file information. + reader.onload = (function (theFile) { + return function () { + // Render file. + const html = `
${escapeHtml(theFile.name)}
`; + + page.querySelector('#lyricsOutput').innerHTML = html; + page.querySelector('#fldUpload').classList.remove('hide'); + page.querySelector('#labelDropLyrics').classList.add('hide'); + }; + })(file); + + // Read in the lyrics file as a data URL. + reader.readAsDataURL(file); +} + +async function onSubmit(e) { + e.preventDefault(); + const file = currentFile; + + if (!isValidLyricsFile(file)) { + toast(globalize.translate('MessageLyricsFileTypeAllowed')); + return; + } + + loading.show(); + const dlg = dom.parentWithClass(this, 'dialog'); + + const api = toApi(ServerConnections.getApiClient(currentServerId)); + const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance); + const data = await readFileAsText(file); + + lyricsApi.uploadLyrics({ + itemId: currentItemId, fileName: file.name, body: data + }).then(function () { + dlg.querySelector('#uploadLyrics').value = ''; + loading.hide(); + hasChanges = true; + dialogHelper.close(dlg); + }); +} + +function initEditor(page) { + page.querySelector('.uploadLyricsForm').addEventListener('submit', onSubmit); + page.querySelector('#uploadLyrics').addEventListener('change', function () { + setFiles(page, this.files); + }); + page.querySelector('.btnBrowse').addEventListener('click', function () { + page.querySelector('#uploadLyrics').click(); + }); +} + +function showEditor(options, resolve) { + options = options || {}; + currentItemId = options.itemId; + currentServerId = options.serverId; + + const dialogOptions = { + removeOnClose: true, + scrollY: false + }; + + if (layoutManager.tv) { + dialogOptions.size = 'fullscreen'; + } else { + dialogOptions.size = 'small'; + } + + const dlg = dialogHelper.createDialog(dialogOptions); + + dlg.classList.add('formDialog'); + dlg.classList.add('lyricsUploaderDialog'); + + dlg.innerHTML = globalize.translateHtml(template, 'core'); + + if (layoutManager.tv) { + scrollHelper.centerFocus.on(dlg, false); + } + + // Has to be assigned a z-index after the call to .open() + dlg.addEventListener('close', function () { + if (layoutManager.tv) { + scrollHelper.centerFocus.off(dlg, false); + } + loading.hide(); + resolve(hasChanges); + }); + + dialogHelper.open(dlg); + + initEditor(dlg); + + dlg.querySelector('.btnCancel').addEventListener('click', function () { + dialogHelper.close(dlg); + }); +} + +export function show(options) { + return new Promise(function (resolve) { + hasChanges = false; + showEditor(options, resolve); + }); +} + +export default { + show: show +}; diff --git a/src/components/lyricsuploader/lyricsuploader.scss b/src/components/lyricsuploader/lyricsuploader.scss new file mode 100644 index 0000000000..2b778dcdbe --- /dev/null +++ b/src/components/lyricsuploader/lyricsuploader.scss @@ -0,0 +1,15 @@ +.lyricsEditor-dropZone { + border: 0.2em dashed currentcolor; + border-radius: 0.25em; + + text-align: center; + position: relative; + height: 12em; + display: flex; + align-items: center; + justify-content: center; +} + +.raised.raised-mini.btnBrowse { + margin-left: 1.5em; +} diff --git a/src/components/lyricsuploader/lyricsuploader.template.html b/src/components/lyricsuploader/lyricsuploader.template.html new file mode 100644 index 0000000000..3d7e951fd9 --- /dev/null +++ b/src/components/lyricsuploader/lyricsuploader.template.html @@ -0,0 +1,36 @@ +
+ +

+ ${HeaderUploadLyrics} +

+
+ +
+
+ +
+ +
+

${HeaderAddLyrics}

+ + +
+
+
+
${LabelDropLyricsHere}
+ + +
+
+
+ +
+
+
+
+
diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 56b6c4f673..0f611666ad 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -241,6 +241,7 @@ "Edit": "Edit", "Editor": "Editor", "EditImages": "Edit images", + "EditLyrics": "Edit lyrics", "EditMetadata": "Edit metadata", "EditSubtitles": "Edit subtitles", "EnableAutoCast": "Set as default", @@ -344,6 +345,7 @@ "HeaderActiveRecordings": "Active Recordings", "HeaderActivity": "Activity", "HeaderAdditionalParts": "Additional Parts", + "HeaderAddLyrics": "Add Lyrics", "HeaderAddToCollection": "Add to Collection", "HeaderAddToPlaylist": "Add to Playlist", "HeaderAddUpdateImage": "Add/Update Image", @@ -509,6 +511,7 @@ "HeaderUninstallPlugin": "Uninstall Plugin", "HeaderUpcomingOnTV": "Upcoming On TV", "HeaderUploadImage": "Upload Image", + "HeaderUploadLyrics": "Upload Lyrics", "HeaderUploadSubtitle": "Upload Subtitle", "HeaderUser": "User", "HeaderUsers": "Users", @@ -639,7 +642,9 @@ "LabelDownMixAudioScale": "Audio boost when downmixing", "LabelDownMixAudioScaleHelp": "Boost audio when downmixing. A value of one will preserve the original volume.", "LabelStereoDownmixAlgorithm": "Stereo Downmix Algorithm", + "LabelDuration" : "Duration", "LabelDropImageHere": "Drop image here, or click to browse.", + "LabelDropLyricsHere": "Drop lyrics here, or click to browse.", "LabelDroppedFrames": "Dropped frames", "LabelDropShadow": "Drop shadow", "LabelDropSubtitleHere": "Drop subtitle here, or click to browse.", @@ -698,6 +703,7 @@ "LabelInstalled": "Installed", "LabelInternetQuality": "Internet quality", "LabelIsForced": "Forced", + "LabelIsSynced": "Is Synced", "LabelKeepUpTo": "Keep up to", "LabelKidsCategories": "Children's categories", "LabelKnownProxies": "Known proxies", @@ -990,6 +996,7 @@ "Lyric": "Lyric", "Lyricist": "Lyricist", "Lyrics": "Lyrics", + "LyricsPreview": "Lyrics Preview", "ManageLibrary": "Manage library", "ManageRecording": "Manage recording", "MapChannels": "Map Channels", @@ -1145,6 +1152,7 @@ "NextUp": "Next Up", "No": "No", "NoCreatedLibraries": "Seems like you haven't created any libraries yet. {0}Would you like to create one now?{1}", + "NoLyricsSearchResultsFound": "No lyrics found.", "None": "None", "NoNewDevicesFound": "No new devices found. To add a new tuner, close this dialog and enter the device information manually.", "Normal": "Normal", @@ -1390,6 +1398,7 @@ "ScreenResolution": "Screen Resolution", "Search": "Search", "SearchForCollectionInternetMetadata": "Search the internet for artwork and metadata", + "SearchForLyrics" : "Search for Lyrics", "SearchForMissingMetadata": "Search for missing metadata", "SearchForSubtitles": "Search for Subtitles", "SearchResults": "Search Results", diff --git a/src/utils/file.ts b/src/utils/file.ts index b85178e2f2..0019a5068f 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -13,3 +13,18 @@ export function readFileAsBase64(file: File): Promise { reader.readAsDataURL(file); }); } + +/** + * Reads and returns the file in text format + */ +export function readFileAsText(file: File): Promise { + return new Promise(function (resolve, reject) { + const reader = new FileReader(); + reader.onload = (e) => { + const data = e.target?.result as string; + resolve(data); + }; + reader.onerror = reject; + reader.readAsText(file); + }); +} From 8a61ff890f28c6f1b5d8f74630d5cb07e5b97743 Mon Sep 17 00:00:00 2001 From: LJQ Date: Sun, 5 May 2024 00:01:52 +0800 Subject: [PATCH 2/8] If possible display lyrics in lrc format --- src/components/lyricseditor/lyricseditor.js | 25 ++++++++++++------- .../lyricseditor/lyricspreview.template.html | 2 +- src/strings/en-us.json | 3 ++- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/lyricseditor/lyricseditor.js b/src/components/lyricseditor/lyricseditor.js index 7775db4540..197d257d49 100644 --- a/src/components/lyricseditor/lyricseditor.js +++ b/src/components/lyricseditor/lyricseditor.js @@ -39,6 +39,19 @@ function downloadRemoteLyrics(context, id) { }); } +function getLyricsText(lyricsObject) { + return lyricsObject.reduce((htmlAccumulator, lyric) => { + if (lyric.Start) { + const minutes = Math.floor(lyric.Start / 600000000); + const seconds = Math.floor((lyric.Start % 600000000) / 10000000); + const hundredths = Math.floor((lyric.Start % 10000000) / 100000); + htmlAccumulator += '[' + String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0') + '.' + String(hundredths).padStart(2, '0') + '] '; + } + htmlAccumulator += escapeHtml(lyric.Text) + '
'; + return htmlAccumulator; + }, ''); +} + function renderSearchResults(context, results) { let lastProvider = ''; let html = ''; @@ -57,10 +70,7 @@ function renderSearchResults(context, results) { const provider = result.ProviderName; const metadata = result.Lyrics.Metadata; - const lyrics = result.Lyrics.Lyrics.reduce((htmlAccumulator, lyric) => { - htmlAccumulator += escapeHtml(lyric.Text) + '
'; - return htmlAccumulator; - }, ''); + const lyrics = getLyricsText(result.Lyrics.Lyrics); if (provider !== lastProvider) { if (i > 0) { html += ''; @@ -225,7 +235,7 @@ function showOptions(button, context, lyricsId, lyrics) { const items = []; items.push({ - name: globalize.translate('LyricsPreview'), + name: globalize.translate('PreviewLyrics'), id: 'preview' } , { @@ -294,10 +304,7 @@ function fillCurrentLyrics(context, apiClient, item) { let html = ''; html += '

' + globalize.translate('Lyrics') + '

'; html += '
'; - html += response.data.Lyrics.reduce((htmlAccumulator, lyric) => { - htmlAccumulator += escapeHtml(lyric.Text) + '
'; - return htmlAccumulator; - }, ''); + html += getLyricsText(response.data.Lyrics); html += '
'; context.querySelector('.currentLyrics').innerHTML = html; } diff --git a/src/components/lyricseditor/lyricspreview.template.html b/src/components/lyricseditor/lyricspreview.template.html index fa807d7293..93bdd9ce6e 100644 --- a/src/components/lyricseditor/lyricspreview.template.html +++ b/src/components/lyricseditor/lyricspreview.template.html @@ -1,6 +1,6 @@
-

${LyricsPreview}

+

${HeaderPreviewLyrics}

diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 0f611666ad..26d2c6e93c 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -462,6 +462,7 @@ "HeaderPleaseSignIn": "Please sign in", "HeaderPortRanges": "Firewall and Proxy Settings", "HeaderPreferredMetadataLanguage": "Preferred Metadata Language", + "HeaderPreviewLyrics": "Preview Lyrics", "HeaderRecentlyPlayed": "Recently Played", "HeaderRecordingMetadataSaving": "Recording Metadata", "HeaderRecordingOptions": "Recording Options", @@ -996,7 +997,6 @@ "Lyric": "Lyric", "Lyricist": "Lyricist", "Lyrics": "Lyrics", - "LyricsPreview": "Lyrics Preview", "ManageLibrary": "Manage library", "ManageRecording": "Manage recording", "MapChannels": "Map Channels", @@ -1319,6 +1319,7 @@ "Premiere": "Premiere", "Premieres": "Premieres", "Preview": "Preview", + "PreviewLyrics": "Preview Lyrics", "Previous": "Previous", "PreviousChapter": "Previous chapter", "PreviousTrack": "Skip to previous", From f6b9104cc26227ad75073f48e989b0c23c122cd3 Mon Sep 17 00:00:00 2001 From: LJQ Date: Tue, 7 May 2024 02:26:19 +0800 Subject: [PATCH 3/8] fix for when lyric.Start is 0 --- src/components/lyricseditor/lyricseditor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/lyricseditor/lyricseditor.js b/src/components/lyricseditor/lyricseditor.js index 197d257d49..3b69bc03d2 100644 --- a/src/components/lyricseditor/lyricseditor.js +++ b/src/components/lyricseditor/lyricseditor.js @@ -41,7 +41,7 @@ function downloadRemoteLyrics(context, id) { function getLyricsText(lyricsObject) { return lyricsObject.reduce((htmlAccumulator, lyric) => { - if (lyric.Start) { + if (lyric.Start || lyric.Start === 0) { const minutes = Math.floor(lyric.Start / 600000000); const seconds = Math.floor((lyric.Start % 600000000) / 10000000); const hundredths = Math.floor((lyric.Start % 10000000) / 100000); From ab0df4dcf057cc1648a35f519b49d890ad6dc98d Mon Sep 17 00:00:00 2001 From: LJQ Date: Tue, 16 Jul 2024 13:19:41 +0800 Subject: [PATCH 4/8] Update to use getLyricsApi --- src/components/lyricseditor/lyricseditor.js | 8 ++++---- src/components/lyricsuploader/lyricsuploader.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/lyricseditor/lyricseditor.js b/src/components/lyricseditor/lyricseditor.js index 3b69bc03d2..135f87980d 100644 --- a/src/components/lyricseditor/lyricseditor.js +++ b/src/components/lyricseditor/lyricseditor.js @@ -1,6 +1,6 @@ import escapeHtml from 'escape-html'; -import { LyricsApi } from '@jellyfin/sdk/lib/generated-client/api/lyrics-api'; +import { getLyricsApi } from '@jellyfin/sdk/lib/utils/api/lyrics-api'; import { toApi } from 'utils/jellyfin-apiclient/compat'; import dialogHelper from '../dialogHelper/dialogHelper'; import layoutManager from '../layoutManager'; @@ -27,7 +27,7 @@ let hasChanges; function downloadRemoteLyrics(context, id) { const api = toApi(ServerConnections.getApiClient(currentItem.ServerId)); - const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance); + const lyricsApi = getLyricsApi(api); lyricsApi.downloadRemoteLyrics({ itemId: currentItem.Id, lyricId: id }).then(function () { @@ -128,7 +128,7 @@ function searchForLyrics(context) { loading.show(); const api = toApi(ServerConnections.getApiClient(currentItem.ServerId)); - const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance); + const lyricsApi = getLyricsApi(api); lyricsApi.searchRemoteLyrics({ itemId: currentItem.Id }).then(function (results) { @@ -294,7 +294,7 @@ function onDeleteLyrics(e) { function fillCurrentLyrics(context, apiClient, item) { const api = toApi(apiClient); - const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance); + const lyricsApi = getLyricsApi(api); lyricsApi.getLyrics({ itemId: item.Id }).then((response) => { diff --git a/src/components/lyricsuploader/lyricsuploader.js b/src/components/lyricsuploader/lyricsuploader.js index 9e21980348..817ccdda75 100644 --- a/src/components/lyricsuploader/lyricsuploader.js +++ b/src/components/lyricsuploader/lyricsuploader.js @@ -1,6 +1,6 @@ import escapeHtml from 'escape-html'; -import { LyricsApi } from '@jellyfin/sdk/lib/generated-client/api/lyrics-api'; +import { getLyricsApi } from '@jellyfin/sdk/lib/utils/api/lyrics-api'; import { toApi } from 'utils/jellyfin-apiclient/compat'; import dialogHelper from '../../components/dialogHelper/dialogHelper'; import ServerConnections from '../ServerConnections'; @@ -91,7 +91,7 @@ async function onSubmit(e) { const dlg = dom.parentWithClass(this, 'dialog'); const api = toApi(ServerConnections.getApiClient(currentServerId)); - const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance); + const lyricsApi = getLyricsApi(api); const data = await readFileAsText(file); lyricsApi.uploadLyrics({ From 5d8d6fdb28fc126b34efda19a288d6a902fc188f Mon Sep 17 00:00:00 2001 From: LJQ Date: Sun, 18 Aug 2024 13:42:07 +0800 Subject: [PATCH 5/8] Add suggested changes --- src/components/lyricseditor/lyricseditor.js | 5 ++++- src/components/lyricseditor/lyricseditor.template.html | 2 +- src/components/lyricseditor/lyricspreview.template.html | 2 +- src/components/lyricsuploader/lyricsuploader.js | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/lyricseditor/lyricseditor.js b/src/components/lyricseditor/lyricseditor.js index 135f87980d..486a426cc5 100644 --- a/src/components/lyricseditor/lyricseditor.js +++ b/src/components/lyricseditor/lyricseditor.js @@ -29,7 +29,8 @@ function downloadRemoteLyrics(context, id) { const api = toApi(ServerConnections.getApiClient(currentItem.ServerId)); const lyricsApi = getLyricsApi(api); lyricsApi.downloadRemoteLyrics({ - itemId: currentItem.Id, lyricId: id + itemId: currentItem.Id, + lyricId: id }).then(function () { hasChanges = true; @@ -289,6 +290,8 @@ function onDeleteLyrics(e) { const context = dom.parentWithClass(e.target, 'formDialogContent'); const apiClient = ServerConnections.getApiClient(currentItem.ServerId); reload(context, apiClient, currentItem.Id); + }).catch(() => { + console.warn('Failed to delete lyrics for', currentItem.Name); }); } diff --git a/src/components/lyricseditor/lyricseditor.template.html b/src/components/lyricseditor/lyricseditor.template.html index 15eae3bf90..84d3a0bec0 100644 --- a/src/components/lyricseditor/lyricseditor.template.html +++ b/src/components/lyricseditor/lyricseditor.template.html @@ -24,4 +24,4 @@ ${NoLyricsSearchResultsFound}
- \ No newline at end of file + diff --git a/src/components/lyricseditor/lyricspreview.template.html b/src/components/lyricseditor/lyricspreview.template.html index 93bdd9ce6e..21f706f3fb 100644 --- a/src/components/lyricseditor/lyricspreview.template.html +++ b/src/components/lyricseditor/lyricspreview.template.html @@ -7,4 +7,4 @@
- \ No newline at end of file + diff --git a/src/components/lyricsuploader/lyricsuploader.js b/src/components/lyricsuploader/lyricsuploader.js index 817ccdda75..b55561c51c 100644 --- a/src/components/lyricsuploader/lyricsuploader.js +++ b/src/components/lyricsuploader/lyricsuploader.js @@ -167,5 +167,5 @@ export function show(options) { } export default { - show: show + show }; From 0a80cbc89150ce8d3f7b79c619ceabf4fa1c0822 Mon Sep 17 00:00:00 2001 From: JQ <81431263+scampower3@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:25:50 +0800 Subject: [PATCH 6/8] Update src/components/lyricseditor/lyricseditor.js Co-authored-by: Bill Thornton --- src/components/lyricseditor/lyricseditor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/lyricseditor/lyricseditor.js b/src/components/lyricseditor/lyricseditor.js index 486a426cc5..745d50b51d 100644 --- a/src/components/lyricseditor/lyricseditor.js +++ b/src/components/lyricseditor/lyricseditor.js @@ -291,7 +291,7 @@ function onDeleteLyrics(e) { const apiClient = ServerConnections.getApiClient(currentItem.ServerId); reload(context, apiClient, currentItem.Id); }).catch(() => { - console.warn('Failed to delete lyrics for', currentItem.Name); + // delete dialog closed }); } From 64c59e2f2abaa190fac2a6cf2480367eb40fe31c Mon Sep 17 00:00:00 2001 From: JQ <81431263+scampower3@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:33:02 +0800 Subject: [PATCH 7/8] Update src/components/lyricseditor/lyricseditor.js Co-authored-by: Bill Thornton --- src/components/lyricseditor/lyricseditor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/lyricseditor/lyricseditor.js b/src/components/lyricseditor/lyricseditor.js index 745d50b51d..f2e37175f5 100644 --- a/src/components/lyricseditor/lyricseditor.js +++ b/src/components/lyricseditor/lyricseditor.js @@ -4,7 +4,7 @@ import { getLyricsApi } from '@jellyfin/sdk/lib/utils/api/lyrics-api'; import { toApi } from 'utils/jellyfin-apiclient/compat'; import dialogHelper from '../dialogHelper/dialogHelper'; import layoutManager from '../layoutManager'; -import globalize from '../../scripts/globalize'; +import globalize from 'lib/globalize'; import loading from '../loading/loading'; import focusManager from '../focusManager'; import dom from '../../scripts/dom'; From 8d728ca9d47c43ad98e33023b890af2b3e87f97c Mon Sep 17 00:00:00 2001 From: JQ <81431263+scampower3@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:33:09 +0800 Subject: [PATCH 8/8] Update src/components/lyricsuploader/lyricsuploader.js Co-authored-by: Bill Thornton --- src/components/lyricsuploader/lyricsuploader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/lyricsuploader/lyricsuploader.js b/src/components/lyricsuploader/lyricsuploader.js index b55561c51c..6d6030a7b5 100644 --- a/src/components/lyricsuploader/lyricsuploader.js +++ b/src/components/lyricsuploader/lyricsuploader.js @@ -8,7 +8,7 @@ import dom from '../../scripts/dom'; import loading from '../../components/loading/loading'; import scrollHelper from '../../scripts/scrollHelper'; import layoutManager from '../layoutManager'; -import globalize from '../../scripts/globalize'; +import globalize from 'lib/globalize'; import template from './lyricsuploader.template.html'; import toast from '../toast/toast'; import '../../elements/emby-button/emby-button';