diff --git a/knip.config.ts b/knip.config.ts index e32756a97..1d22ab1ee 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -38,6 +38,7 @@ const config: KnipConfig = { 'i18next-parser', 'luxon', // Used in tests 'playwright', // Used in test configs + 'sharp', // Requirement for @vite-pwa/assets-generator 'tsconfig-paths', // Used for e2e test setup 'virtual:pwa-register', // Service Worker code is injected at build time 'virtual:polyfills', // Polyfills are conditionally injected diff --git a/packages/common/src/utils/configSchema.ts b/packages/common/src/utils/configSchema.ts index 43c2d60af..37c3dd62d 100644 --- a/packages/common/src/utils/configSchema.ts +++ b/packages/common/src/utils/configSchema.ts @@ -14,7 +14,7 @@ const menuSchema: SchemaOf = object().shape({ label: string().defined(), contentId: string().defined(), filterTags: string().notRequired(), - type: mixed().oneOf(['playlist', 'content_list']).notRequired(), + type: mixed().oneOf(['playlist', 'content_list', 'media']).notRequired(), }); const featuresSchema: SchemaOf = object({ diff --git a/packages/common/src/utils/structuredData.ts b/packages/common/src/utils/structuredData.ts index 6470ae526..292076a2b 100644 --- a/packages/common/src/utils/structuredData.ts +++ b/packages/common/src/utils/structuredData.ts @@ -5,7 +5,8 @@ import { mediaURL } from './urlFormatting'; import { secondsToISO8601 } from './datetime'; export const generateMovieJSONLD = (item: PlaylistItem, origin: string) => { - const movieCanonical = `${origin}${mediaURL({ media: item })}`; + const { mediaid: id, title } = item; + const movieCanonical = `${origin}${mediaURL({ id, title })}`; return JSON.stringify({ '@context': 'http://schema.org/', diff --git a/packages/common/src/utils/urlFormatting.test.ts b/packages/common/src/utils/urlFormatting.test.ts index 4fc93fc1f..c0c616f4a 100644 --- a/packages/common/src/utils/urlFormatting.test.ts +++ b/packages/common/src/utils/urlFormatting.test.ts @@ -35,7 +35,9 @@ describe('createPath, mediaURL, playlistURL and liveChannelsURL', () => { test('valid media path', () => { const playlist = playlistFixture as Playlist; const media = playlist.playlist[0] as PlaylistItem; - const url = mediaURL({ media, playlistId: playlist.feedid, play: true }); + + const { mediaid: id, title } = media; + const url = mediaURL({ id, title, playlistId: playlist.feedid, play: true }); expect(url).toEqual('/m/uB8aRnu6/agent-327?r=dGSUzs9o&play=1'); }); diff --git a/packages/common/src/utils/urlFormatting.ts b/packages/common/src/utils/urlFormatting.ts index cc968c82f..de2f8ada5 100644 --- a/packages/common/src/utils/urlFormatting.ts +++ b/packages/common/src/utils/urlFormatting.ts @@ -99,19 +99,21 @@ export const slugify = (text: string, whitespaceChar: string = '-') => .replace(/-/g, whitespaceChar); export const mediaURL = ({ - media, + id, + title, playlistId, play = false, episodeId, }: { - media: PlaylistItem; + id: string; + title?: string; playlistId?: string | null; play?: boolean; episodeId?: string; }) => { return createPath( PATH_MEDIA, - { id: media.mediaid, title: slugify(media.title) }, + { id, title: title ? slugify(title) : undefined }, { r: playlistId, play: play ? '1' : null, @@ -120,10 +122,6 @@ export const mediaURL = ({ ); }; -export const singleMediaURL = (id: string, title?: string) => { - return createPath(PATH_MEDIA, { id, title: title ? slugify(title) : undefined }); -}; - export const playlistURL = (id: string, title?: string) => { return createPath(PATH_PLAYLIST, { id, title: title ? slugify(title) : undefined }); }; @@ -132,14 +130,14 @@ export const contentListURL = (id: string, title?: string) => { return createPath(PATH_CONTENT_LIST, { id, title: title ? slugify(title) : undefined }); }; -export const determinePath = ({ type, contentId }: { type: AppMenuType | undefined; contentId: string }) => { +export const determinePath = ({ type, contentId, label }: { type: AppMenuType | undefined; contentId: string; label?: string }) => { switch (type) { case APP_CONFIG_ITEM_TYPE.content_list: - return contentListURL(contentId); + return contentListURL(contentId, label); case APP_CONFIG_ITEM_TYPE.media: - return singleMediaURL(contentId); + return mediaURL({ id: contentId, title: label }); case APP_CONFIG_ITEM_TYPE.playlist: - return playlistURL(contentId); + return playlistURL(contentId, label); default: return ''; } diff --git a/packages/ui-react/src/components/Favorites/Favorites.tsx b/packages/ui-react/src/components/Favorites/Favorites.tsx index 4d991ba59..237417b08 100644 --- a/packages/ui-react/src/components/Favorites/Favorites.tsx +++ b/packages/ui-react/src/components/Favorites/Favorites.tsx @@ -28,7 +28,7 @@ const cols: Breakpoints = { const Favorites = ({ playlist, accessModel, hasSubscription, onCardHover, onClearFavoritesClick }: Props): JSX.Element => { const { t } = useTranslation('user'); - const getURL = (playlistItem: PlaylistItem) => mediaURL({ media: playlistItem, playlistId: playlistItem.feedid }); + const getURL = (playlistItem: PlaylistItem) => mediaURL({ id: playlistItem.mediaid, title: playlistItem.title, playlistId: playlistItem.feedid }); return (
diff --git a/packages/ui-react/src/components/Shelf/Shelf.tsx b/packages/ui-react/src/components/Shelf/Shelf.tsx index 92c9eb2d6..c24457903 100644 --- a/packages/ui-react/src/components/Shelf/Shelf.tsx +++ b/packages/ui-react/src/components/Shelf/Shelf.tsx @@ -76,7 +76,8 @@ const Shelf = ({ const renderTile = useCallback( ({ item, isVisible }: { item: PlaylistItem; isVisible: boolean }) => { - const url = mediaURL({ media: item, playlistId: playlist.feedid, play: type === PersonalShelf.ContinueWatching }); + const { mediaid: id, title } = item; + const url = mediaURL({ id, title, playlistId: playlist.feedid, play: type === PersonalShelf.ContinueWatching }); return ( { { label: t('home'), to: '/' }, ...menu.map(({ label, contentId, type }) => ({ label, - to: determinePath({ type, contentId }), + to: determinePath({ type, contentId, label }), })), ]; diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEpisode/MediaEpisode.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEpisode/MediaEpisode.tsx index d0edbb727..b4c61c440 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEpisode/MediaEpisode.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEpisode/MediaEpisode.tsx @@ -30,8 +30,21 @@ const MediaEpisode: ScreenComponent = ({ data: media, isLoading: i return ; } + const { mediaid: id, title } = seriesMedia as PlaylistItem; + // Use media episode item for legacy series flow - return ; + return ( + + ); }; export default MediaEpisode; diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEvent/MediaEvent.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEvent/MediaEvent.tsx index d6da6c8f2..8ecccd493 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEvent/MediaEvent.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEvent/MediaEvent.tsx @@ -65,8 +65,8 @@ const MediaEvent: ScreenComponent = ({ data: media, isLoading }) = const hasMediaOffers = !!mediaOffers.length; // Handlers - const goBack = () => media && navigate(mediaURL({ media, playlistId, play: false })); - const getUrl = (item: PlaylistItem) => mediaURL({ media: item, playlistId }); + const goBack = () => media && navigate(mediaURL({ id: media.mediaid, title: media.title, playlistId, play: false })); + const getUrl = (item: PlaylistItem) => mediaURL({ id: item.mediaid, title: item.title, playlistId }); const handleComplete = useCallback(() => { if (!id || !playlist) return; @@ -78,7 +78,7 @@ const MediaEvent: ScreenComponent = ({ data: media, isLoading }) = return; } - return nextItem && navigate(mediaURL({ media: nextItem, playlistId, play: true })); + return nextItem && navigate(mediaURL({ id: nextItem.mediaid, title: nextItem.title, playlistId, play: true })); }, [id, playlist, navigate, playlistId]); // Effects @@ -88,8 +88,9 @@ const MediaEvent: ScreenComponent = ({ data: media, isLoading }) = }, [id]); // UI - const pageTitle = `${media.title} - ${siteName}`; - const canonicalUrl = media ? `${window.location.origin}${mediaURL({ media: media })}` : window.location.href; + const { title, mediaid } = media; + const pageTitle = `${title} - ${siteName}`; + const canonicalUrl = media ? `${window.location.origin}${mediaURL({ id: mediaid, title })}` : window.location.href; const primaryMetadata = ( <> @@ -103,7 +104,7 @@ const MediaEvent: ScreenComponent = ({ data: media, isLoading }) = ); diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaMovie/MediaMovie.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaMovie/MediaMovie.tsx index dd3d5df03..feccb3110 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaMovie/MediaMovie.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaMovie/MediaMovie.tsx @@ -59,8 +59,8 @@ const MediaMovie: ScreenComponent = ({ data, isLoading }) => { const hasMediaOffers = !!mediaOffers.length; // Handlers - const goBack = () => data && navigate(mediaURL({ media: data, playlistId: feedId, play: false })); - const getUrl = (item: PlaylistItem) => mediaURL({ media: item, playlistId: features?.recommendationsPlaylist }); + const goBack = () => data && navigate(mediaURL({ id: data.mediaid, title: data.title, playlistId: feedId, play: false })); + const getUrl = (item: PlaylistItem) => mediaURL({ id: item.mediaid, title: item.title, playlistId: features?.recommendationsPlaylist }); const handleComplete = useCallback(() => { if (!id || !playlist) return; @@ -68,7 +68,7 @@ const MediaMovie: ScreenComponent = ({ data, isLoading }) => { const index = playlist.playlist.findIndex(({ mediaid }) => mediaid === id); const nextItem = playlist.playlist[index + 1]; - return nextItem && navigate(mediaURL({ media: nextItem, playlistId: features?.recommendationsPlaylist, play: true })); + return nextItem && navigate(mediaURL({ id: nextItem.mediaid, title: nextItem.title, playlistId: features?.recommendationsPlaylist, play: true })); }, [id, playlist, navigate, features?.recommendationsPlaylist]); useEffect(() => { @@ -78,7 +78,7 @@ const MediaMovie: ScreenComponent = ({ data, isLoading }) => { // UI const pageTitle = `${data.title} - ${siteName}`; - const canonicalUrl = data ? `${window.location.origin}${mediaURL({ media: data })}` : window.location.href; + const canonicalUrl = data ? `${window.location.origin}${mediaURL({ id: data.mediaid, title: data.title })}` : window.location.href; const primaryMetadata = ; const shareButton = ; @@ -86,7 +86,7 @@ const MediaMovie: ScreenComponent = ({ data, isLoading }) => { ); diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx index 37c617bc8..ea2cd831f 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx @@ -193,7 +193,7 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { if (!seriesMedia || !series || !playEpisode) return ; const pageTitle = `${selectedItem.title} - ${siteName}`; - const canonicalUrl = `${window.location.origin}${mediaURL({ media: seriesMedia, episodeId: episode?.mediaid })}`; + const canonicalUrl = `${window.location.origin}${mediaURL({ id: seriesMedia.mediaid, title: seriesMedia.title, episodeId: episode?.mediaid })}`; const primaryMetadata = ; const secondaryMetadata = episodeMetadata && episode && ( diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaStaticPage/MediaStaticPage.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaStaticPage/MediaStaticPage.tsx index ae66dbded..508933b8b 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaStaticPage/MediaStaticPage.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaStaticPage/MediaStaticPage.tsx @@ -14,7 +14,7 @@ const MediaStaticPage: ScreenComponent = ({ data }) => { const { config } = useConfigStore(({ config }) => ({ config }), shallow); const { siteName } = config; const pageTitle = `${data.title} - ${siteName}`; - const canonicalUrl = data ? `${window.location.origin}${mediaURL({ media: data })}` : window.location.href; + const canonicalUrl = data ? `${window.location.origin}${mediaURL({ id: data.mediaid, title: data.title })}` : window.location.href; useEffect(() => { (document.scrollingElement || document.body).scroll({ top: 0 }); diff --git a/packages/ui-react/src/pages/ScreenRouting/playlistScreens/PlaylistGrid/PlaylistGrid.tsx b/packages/ui-react/src/pages/ScreenRouting/playlistScreens/PlaylistGrid/PlaylistGrid.tsx index 1797c7267..810925f4d 100644 --- a/packages/ui-react/src/pages/ScreenRouting/playlistScreens/PlaylistGrid/PlaylistGrid.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/playlistScreens/PlaylistGrid/PlaylistGrid.tsx @@ -32,7 +32,7 @@ const PlaylistGrid: ScreenComponent = ({ data, isLoading }) => { const pageTitle = `${data.title} - ${config.siteName}`; - const getUrl = (playlistItem: PlaylistItem) => mediaURL({ media: playlistItem, playlistId: playlistItem.feedid }); + const getUrl = (playlistItem: PlaylistItem) => mediaURL({ id: playlistItem.mediaid, title: playlistItem.title, playlistId: playlistItem.feedid }); return (
diff --git a/packages/ui-react/src/pages/Search/Search.tsx b/packages/ui-react/src/pages/Search/Search.tsx index 30004fac1..19b699850 100644 --- a/packages/ui-react/src/pages/Search/Search.tsx +++ b/packages/ui-react/src/pages/Search/Search.tsx @@ -36,7 +36,8 @@ const Search = () => { const getURL = (playlistItem: PlaylistItem) => mediaURL({ - media: playlistItem, + id: playlistItem.mediaid, + title: playlistItem.title, playlistId: features?.searchPlaylist, }); diff --git a/platforms/access-bridge/test/unit/logger.test.ts b/platforms/access-bridge/test/unit/logger.test.ts index a12c386a9..ae20d0d74 100644 --- a/platforms/access-bridge/test/unit/logger.test.ts +++ b/platforms/access-bridge/test/unit/logger.test.ts @@ -4,9 +4,26 @@ import * as Sentry from '@sentry/node'; import logger from '../../src/pipeline/logger.js'; describe('Logger Tests', () => { + // Preserve the original console methods + const originalConsole = { ...console }; + beforeEach(() => { // Reset all mocks to ensure a clean slate for each test vi.resetAllMocks(); + + // Mock console methods to suppress log outputs during tests + // Suppressing info output to avoid clutter + global.console = { + log: () => {}, + error: () => {}, + warn: () => {}, + info: () => {}, + } as unknown as Console; + }); + + afterEach(() => { + // Restore the original console methods after each test + global.console = originalConsole; }); describe('when Sentry is configured', () => { diff --git a/platforms/access-bridge/vite.config.ts b/platforms/access-bridge/vite.config.ts index c671b75ca..a755490e7 100644 --- a/platforms/access-bridge/vite.config.ts +++ b/platforms/access-bridge/vite.config.ts @@ -48,7 +48,7 @@ export default ({ mode, command }: ConfigEnv): UserConfigExport => { }, test: { globals: true, - include: ['**/*.test.ts'], + environment: 'node', setupFiles: 'test/vitest.setup.ts', chaiConfig: { truncateThreshold: 1000,