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') {