From 6c2443fa2c21bffbce09c1d9b916e538ece53664 Mon Sep 17 00:00:00 2001 From: Garret Patti <42485635+garretpatti@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:29:17 -0400 Subject: [PATCH] Filter non-browser-playable formats from Doom Scroll Formats like .mkv, .avi, .wmv, .ts, .m2ts and .tiff are not natively supported by browsers and would stall silently in Doom Scroll mode. Add src/lib/browser-media.ts with BROWSER_VIDEO_EXTENSIONS (.mp4, .webm, .mov, .m4v), BROWSER_IMAGE_EXTENSIONS (.jpg, .jpeg, .png, .gif, .webp, .bmp), and an isBrowserPlayable() helper that extracts the extension without importing Node's path module. Filter doomScrollItems in MixedView, MoviesView, and TvView using this helper so only natively renderable files are passed to DoomScrollView. Co-Authored-By: Claude Sonnet 4.6 --- src/components/mixed/MixedView.tsx | 3 ++- src/components/movies/MoviesView.tsx | 3 ++- src/components/tv/TvView.tsx | 5 +++-- src/lib/browser-media.ts | 19 +++++++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 src/lib/browser-media.ts diff --git a/src/components/mixed/MixedView.tsx b/src/components/mixed/MixedView.tsx index 783bfe4..7f6e9b6 100644 --- a/src/components/mixed/MixedView.tsx +++ b/src/components/mixed/MixedView.tsx @@ -7,6 +7,7 @@ import ImageLightbox from './ImageLightbox' import TagSelector from '@/components/tags/TagSelector' import FilterPanel from '@/components/FilterPanel' import DoomScrollView, { type DoomScrollItem } from '@/components/DoomScrollView' +import { isBrowserPlayable } from '@/lib/browser-media' interface Props { libraryId: string @@ -200,7 +201,7 @@ export default function MixedView({ libraryId, initialPath }: Props) { // When filters are active, doom scroll uses filteredEntries (already filtered by search/tags). // When no filters, doom scroll uses the full recursiveEntries. const doomScrollItems: DoomScrollItem[] = (filtersActive ? filteredEntries : recursiveEntries) - .filter((e) => e.type === 'file' && (e.mediaType === 'video' || e.mediaType === 'image') && e.url) + .filter((e) => e.type === 'file' && (e.mediaType === 'video' || e.mediaType === 'image') && e.url && isBrowserPlayable(e.name)) .map((e) => ({ url: e.url!, name: e.name, mediaType: e.mediaType as 'video' | 'image' })) return ( diff --git a/src/components/movies/MoviesView.tsx b/src/components/movies/MoviesView.tsx index bb28d68..cc446d1 100644 --- a/src/components/movies/MoviesView.tsx +++ b/src/components/movies/MoviesView.tsx @@ -5,6 +5,7 @@ import type { Movie } from '@/types' import MovieDetailModal from './MovieDetailModal' import FilterPanel from '@/components/FilterPanel' import DoomScrollView, { type DoomScrollItem } from '@/components/DoomScrollView' +import { isBrowserPlayable } from '@/lib/browser-media' interface Props { libraryId: string @@ -74,7 +75,7 @@ export default function MoviesView({ libraryId }: Props) { const handleDoomScroll = () => { // Use filtered movies — respects any active search/tag filters automatically - const items: DoomScrollItem[] = filtered.map((m) => ({ + const items: DoomScrollItem[] = filtered.filter((m) => isBrowserPlayable(m.videoPath)).map((m) => ({ url: `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(m.videoPath)}`, name: m.title, mediaType: 'video' as const, diff --git a/src/components/tv/TvView.tsx b/src/components/tv/TvView.tsx index 87cfafe..9a95816 100644 --- a/src/components/tv/TvView.tsx +++ b/src/components/tv/TvView.tsx @@ -8,6 +8,7 @@ import VideoPlayerModal from '@/components/mixed/VideoPlayerModal' import TagSelector from '@/components/tags/TagSelector' import EpisodeCard from './EpisodeCard' import DoomScrollView, { type DoomScrollItem } from '@/components/DoomScrollView' +import { isBrowserPlayable } from '@/lib/browser-media' interface Props { libraryId: string @@ -184,7 +185,7 @@ export default function TvView({ libraryId }: Props) { return seasonEps.flat() }) ) - items = episodeLists.flat().map((ep) => ({ + items = episodeLists.flat().filter((ep) => isBrowserPlayable(ep.videoPath)).map((ep) => ({ url: `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(ep.videoPath)}`, name: ep.title, mediaType: 'video' as const, @@ -209,7 +210,7 @@ export default function TvView({ libraryId }: Props) { return seasonEps.flat() }) ) - items = episodeLists.flat().map((ep) => ({ + items = episodeLists.flat().filter((ep) => isBrowserPlayable(ep.videoPath)).map((ep) => ({ url: `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(ep.videoPath)}`, name: ep.title, mediaType: 'video' as const, diff --git a/src/lib/browser-media.ts b/src/lib/browser-media.ts new file mode 100644 index 0000000..99d87cb --- /dev/null +++ b/src/lib/browser-media.ts @@ -0,0 +1,19 @@ +/** + * Browser-native media formats safe for use in