clean-up #15
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
19
src/lib/browser-media.ts
Normal file
19
src/lib/browser-media.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Browser-native media formats safe for use in <video> and <img> elements.
|
||||
* Kept separate from the broader scanner extension sets (media-utils.ts, files.ts)
|
||||
* which include server-side-only formats like .mkv, .avi, .tiff, etc.
|
||||
*/
|
||||
|
||||
export const BROWSER_VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov', '.m4v'])
|
||||
export const BROWSER_IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'])
|
||||
|
||||
/**
|
||||
* Returns true if the file at `filename` (or path) has a browser-playable extension.
|
||||
* Uses lastIndexOf to avoid importing the Node `path` module in client components.
|
||||
*/
|
||||
export function isBrowserPlayable(filename: string): boolean {
|
||||
const dot = filename.lastIndexOf('.')
|
||||
if (dot === -1) return false
|
||||
const ext = filename.slice(dot).toLowerCase()
|
||||
return BROWSER_VIDEO_EXTENSIONS.has(ext) || BROWSER_IMAGE_EXTENSIONS.has(ext)
|
||||
}
|
||||
Reference in New Issue
Block a user