diff --git a/src/apps/stable/features/playback/utils/itemText.test.ts b/src/apps/stable/features/playback/utils/itemText.test.ts new file mode 100644 index 0000000000..53c8d707ed --- /dev/null +++ b/src/apps/stable/features/playback/utils/itemText.test.ts @@ -0,0 +1,102 @@ +import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type'; +import { describe, expect, it } from 'vitest'; + +import type { ItemDto } from 'types/base/models/item-dto'; + +import { getItemTextLines } from './itemText'; + +describe('getItemTextLines', () => { + it('Should return undefined if item is invalid', () => { + let lines = getItemTextLines({}); + expect(lines).toBeUndefined(); + lines = getItemTextLines(null); + expect(lines).toBeUndefined(); + lines = getItemTextLines(undefined); + expect(lines).toBeUndefined(); + }); + + it('Should return the name and index number', () => { + const item: ItemDto = { + Name: 'Item Name' + }; + let lines = getItemTextLines(item); + expect(lines).toBeDefined(); + expect(lines).toHaveLength(1); + expect(lines?.[0]).toBe(item.Name); + + item.MediaType = MediaType.Video; + item.IndexNumber = 5; + lines = getItemTextLines(item); + expect(lines).toBeDefined(); + expect(lines).toHaveLength(1); + expect(lines?.[0]).toBe(`${item.IndexNumber} - ${item.Name}`); + + item.ParentIndexNumber = 2; + lines = getItemTextLines(item); + expect(lines).toBeDefined(); + expect(lines).toHaveLength(1); + expect(lines?.[0]).toBe(`${item.ParentIndexNumber}.${item.IndexNumber} - ${item.Name}`); + }); + + it('Should add artist names', () => { + let item: ItemDto = { + Name: 'Item Name', + ArtistItems: [ + { Name: 'Artist 1' }, + { Name: 'Artist 2' } + ] + }; + let lines = getItemTextLines(item); + expect(lines).toBeDefined(); + expect(lines).toHaveLength(2); + expect(lines?.[0]).toBe(item.Name); + expect(lines?.[1]).toBe('Artist 1, Artist 2'); + + item = { + Name: 'Item Name', + Artists: [ + 'Artist 1', + 'Artist 2' + ] + }; + lines = getItemTextLines(item); + expect(lines).toBeDefined(); + expect(lines).toHaveLength(2); + expect(lines?.[0]).toBe(item.Name); + expect(lines?.[1]).toBe('Artist 1, Artist 2'); + }); + + it('Should add album or series name', () => { + let item: ItemDto = { + Name: 'Item Name', + SeriesName: 'Series' + }; + let lines = getItemTextLines(item); + expect(lines).toBeDefined(); + expect(lines).toHaveLength(2); + expect(lines?.[0]).toBe(item.SeriesName); + expect(lines?.[1]).toBe(item.Name); + + item = { + Name: 'Item Name', + Album: 'Album' + }; + lines = getItemTextLines(item); + expect(lines).toBeDefined(); + expect(lines).toHaveLength(2); + expect(lines?.[0]).toBe(item.Album); + expect(lines?.[1]).toBe(item.Name); + }); + + it('Should add production year', () => { + const item = { + Name: 'Item Name', + ProductionYear: 2025 + }; + const lines = getItemTextLines(item); + expect(lines).toBeDefined(); + expect(lines).toHaveLength(2); + expect(lines?.[0]).toBe(item.Name); + expect(lines?.[1]).toBe(String(item.ProductionYear)); + }); +}); diff --git a/src/apps/stable/features/playback/utils/itemText.ts b/src/apps/stable/features/playback/utils/itemText.ts new file mode 100644 index 0000000000..ae8dc837f7 --- /dev/null +++ b/src/apps/stable/features/playback/utils/itemText.ts @@ -0,0 +1,44 @@ +import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type'; + +import type { ItemDto } from 'types/base/models/item-dto'; + +/** + * Gets lines of text used to describe an item for display. + * @param nowPlayingItem The item to describe + * @param isYearIncluded Should the production year be included + * @returns The list of strings describing the item for display + */ +export function getItemTextLines( + nowPlayingItem: ItemDto | null | undefined, + isYearIncluded = true +) { + let line1 = nowPlayingItem?.Name; + if (nowPlayingItem?.MediaType === MediaType.Video) { + if (nowPlayingItem.IndexNumber != null) { + line1 = nowPlayingItem.IndexNumber + ' - ' + line1; + } + if (nowPlayingItem.ParentIndexNumber != null) { + line1 = nowPlayingItem.ParentIndexNumber + '.' + line1; + } + } + + let line2: string | null | undefined; + if (nowPlayingItem?.ArtistItems?.length) { + line2 = nowPlayingItem.ArtistItems.map(a => a.Name).join(', '); + } else if (nowPlayingItem?.Artists?.length) { + line2 = nowPlayingItem.Artists.join(', '); + } else if (nowPlayingItem?.SeriesName || nowPlayingItem?.Album) { + line2 = line1; + line1 = nowPlayingItem.SeriesName || nowPlayingItem.Album; + } else if (nowPlayingItem?.ProductionYear && isYearIncluded) { + line2 = String(nowPlayingItem.ProductionYear); + } + + if (!line1) return; + + const lines = [ line1 ]; + + if (line2) lines.push(line2); + + return lines; +} diff --git a/src/apps/stable/features/playback/utils/mediaSessionSubscriber.ts b/src/apps/stable/features/playback/utils/mediaSessionSubscriber.ts index d5287e065c..911635bbf4 100644 --- a/src/apps/stable/features/playback/utils/mediaSessionSubscriber.ts +++ b/src/apps/stable/features/playback/utils/mediaSessionSubscriber.ts @@ -1,8 +1,8 @@ import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type'; import { getImageUrl } from 'apps/stable/features/playback/utils/image'; +import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText'; import { PlaybackSubscriber } from 'apps/stable/features/playback/utils/playbackSubscriber'; -import { getNowPlayingNames } from 'components/playback/nowplayinghelper'; import type { PlaybackManager } from 'components/playback/playbackmanager'; import { MILLISECONDS_PER_SECOND, TICKS_PER_MILLISECOND } from 'constants/time'; import browser from 'scripts/browser'; @@ -110,11 +110,11 @@ class MediaSessionSubscriber extends PlaybackSubscriber { } const album = item.Album || undefined; - const [ line1, line2 ] = getNowPlayingNames(item, false) || []; + const [ line1, line2 ] = getItemTextLines(item, false) || []; // The artist will be the second line if present or the first line otherwise - const artist = (line2 || line1)?.text; + const artist = line2 || line1; // The title will be the first line if there are two lines - const title = (line2 && line1)?.text; + const title = line2 && line1; if (hasNavigatorSession) { if ( diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index 3e205651b2..c16b167d0d 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -1,11 +1,13 @@ +import { getImageUrl } from 'apps/stable/features/playback/utils/image'; +import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText'; import { appRouter, isLyricsPage } from 'components/router/appRouter'; + import datetime from '../../scripts/datetime'; import Events from '../../utils/events.ts'; import browser from '../../scripts/browser'; import imageLoader from '../images/imageLoader'; import layoutManager from '../layoutManager'; import { playbackManager } from '../playback/playbackmanager'; -import nowPlayingHelper from '../playback/nowplayinghelper'; import { appHost } from '../apphost'; import dom from '../../scripts/dom'; import globalize from 'lib/globalize'; @@ -17,7 +19,6 @@ import appFooter from '../appFooter/appFooter'; import itemShortcuts from '../shortcuts'; import './nowPlayingBar.scss'; import '../../elements/emby-slider/emby-slider'; -import { getImageUrl } from 'apps/stable/features/playback/utils/image'; let currentPlayer; let currentPlayerSupportedCommands = []; @@ -474,24 +475,21 @@ function setLyricButtonActiveStatus() { function updateNowPlayingInfo(state) { const nowPlayingItem = state.NowPlayingItem; - const textLines = nowPlayingItem ? nowPlayingHelper.getNowPlayingNames(nowPlayingItem) : []; + const textLines = nowPlayingItem ? getItemTextLines(nowPlayingItem) : undefined; nowPlayingTextElement.innerHTML = ''; if (textLines) { const itemText = document.createElement('div'); const secondaryText = document.createElement('div'); secondaryText.classList.add('nowPlayingBarSecondaryText'); - if (textLines.length > 1) { - textLines[1].secondary = true; - if (textLines[1].text) { - const text = document.createElement('a'); - text.innerText = textLines[1].text; - secondaryText.appendChild(text); - } + if (textLines.length > 1 && textLines[1]) { + const text = document.createElement('a'); + text.innerText = textLines[1]; + secondaryText.appendChild(text); } - if (textLines[0].text) { + if (textLines[0]) { const text = document.createElement('a'); - text.innerText = textLines[0].text; + text.innerText = textLines[0]; itemText.appendChild(text); } nowPlayingTextElement.appendChild(itemText); diff --git a/src/components/playback/nowplayinghelper.js b/src/components/playback/nowplayinghelper.js deleted file mode 100644 index 7c5dccaf35..0000000000 --- a/src/components/playback/nowplayinghelper.js +++ /dev/null @@ -1,78 +0,0 @@ -export function getNowPlayingNames(nowPlayingItem, includeNonNameInfo) { - let topItem = nowPlayingItem; - let bottomItem = null; - let topText = nowPlayingItem.Name; - - if (nowPlayingItem.AlbumId && nowPlayingItem.MediaType === 'Audio') { - topItem = { - Id: nowPlayingItem.AlbumId, - Name: nowPlayingItem.Album, - Type: 'MusicAlbum', - IsFolder: true - }; - } - - if (nowPlayingItem.MediaType === 'Video') { - if (nowPlayingItem.IndexNumber != null) { - topText = nowPlayingItem.IndexNumber + ' - ' + topText; - } - if (nowPlayingItem.ParentIndexNumber != null) { - topText = nowPlayingItem.ParentIndexNumber + '.' + topText; - } - } - - let bottomText = ''; - - if (nowPlayingItem.ArtistItems?.length) { - bottomItem = { - Id: nowPlayingItem.ArtistItems[0].Id, - Name: nowPlayingItem.ArtistItems[0].Name, - Type: 'MusicArtist', - IsFolder: true - }; - - bottomText = nowPlayingItem.ArtistItems.map(function (a) { - return a.Name; - }).join(', '); - } else if (nowPlayingItem.Artists?.length) { - bottomText = nowPlayingItem.Artists.join(', '); - } else if (nowPlayingItem.SeriesName || nowPlayingItem.Album) { - bottomText = topText; - topText = nowPlayingItem.SeriesName || nowPlayingItem.Album; - - bottomItem = topItem; - - if (nowPlayingItem.SeriesId) { - topItem = { - Id: nowPlayingItem.SeriesId, - Name: nowPlayingItem.SeriesName, - Type: 'Series', - IsFolder: true - }; - } else { - topItem = null; - } - } else if (nowPlayingItem.ProductionYear && includeNonNameInfo !== false) { - bottomText = nowPlayingItem.ProductionYear; - } - - const list = []; - - list.push({ - text: topText, - item: topItem - }); - - if (bottomText) { - list.push({ - text: bottomText, - item: bottomItem - }); - } - - return list; -} - -export default { - getNowPlayingNames: getNowPlayingNames -}; diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 8702da65a6..b4344b5f1f 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -1,10 +1,13 @@ import escapeHtml from 'escape-html'; + +import { getImageUrl } from 'apps/stable/features/playback/utils/image'; +import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText'; + import datetime from '../../scripts/datetime'; import { clearBackdrop, setBackdrops } from '../backdrop/backdrop'; import listView from '../listview/listview'; import imageLoader from '../images/imageLoader'; import { playbackManager } from '../playback/playbackmanager'; -import nowPlayingHelper from '../playback/nowplayinghelper'; import Events from '../../utils/events.ts'; import { appHost } from '../apphost'; import globalize from '../../lib/globalize'; @@ -22,7 +25,6 @@ import ServerConnections from '../ServerConnections'; import toast from '../toast/toast'; import { appRouter } from '../router/appRouter'; import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils'; -import { getImageUrl } from 'apps/stable/features/playback/utils/image'; let showMuteButton = true; let showVolumeSlider = true; @@ -86,15 +88,11 @@ function showSubtitleMenu(context, player, button) { }); } -function getNowPlayingNameHtml(nowPlayingItem, includeNonNameInfo) { - return nowPlayingHelper.getNowPlayingNames(nowPlayingItem, includeNonNameInfo).map(function (i) { - return escapeHtml(i.text); - }).join('
'); -} - function updateNowPlayingInfo(context, state, serverId) { const item = state.NowPlayingItem; - const displayName = item ? getNowPlayingNameHtml(item).replace('
', ' - ') : ''; + const displayName = item ? + getItemTextLines(item).map(escapeHtml).join(' - ') : + ''; if (item) { const nowPlayingServerId = (item.ServerId || serverId); if (item.Type == 'AudioBook' || item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') {