@@ -148,6 +148,7 @@ const Details = ({itemId, initialItem, onPlay, onSelectItem, onSelectPerson, bac
148148 const [ artistAlbums , setArtistAlbums ] = useState ( [ ] ) ;
149149 const [ playlistItems , setPlaylistItems ] = useState ( [ ] ) ;
150150 const [ isLoading , setIsLoading ] = useState ( true ) ;
151+ const [ selectedVersionIndex , setSelectedVersionIndex ] = useState ( 0 ) ;
151152 const [ selectedAudioIndex , setSelectedAudioIndex ] = useState ( 0 ) ;
152153 const [ selectedSubtitleIndex , setSelectedSubtitleIndex ] = useState ( - 1 ) ;
153154 const [ showMediaInfo , setShowMediaInfo ] = useState ( false ) ;
@@ -178,6 +179,7 @@ const Details = ({itemId, initialItem, onPlay, onSelectItem, onSelectPerson, bac
178179 const data = await effectiveApi . getItem ( itemId ) ;
179180 setItem ( tagWithServerInfo ( data ) ) ;
180181
182+ setSelectedVersionIndex ( 0 ) ;
181183 const ms = data . MediaSources ?. [ 0 ] ;
182184 if ( ms ) {
183185 const audioStreams = ms . MediaStreams ?. filter ( s => s . Type === 'Audio' ) || [ ] ;
@@ -316,12 +318,13 @@ const Details = ({itemId, initialItem, onPlay, onSelectItem, onSelectPerson, bac
316318
317319 let playbackOptions = { } ;
318320 if ( supportsSelection ) {
319- const playMediaSource = item . MediaSources [ 0 ] ;
321+ const playMediaSource = item . MediaSources [ selectedVersionIndex ] || item . MediaSources [ 0 ] ;
320322 const audioStreamsList = playMediaSource ?. MediaStreams ?. filter ( s => s . Type === 'Audio' ) || [ ] ;
321323 const subtitleStreamsList = playMediaSource ?. MediaStreams ?. filter ( s => s . Type === 'Subtitle' ) || [ ] ;
322324 const selectedAudio = audioStreamsList [ selectedAudioIndex ] ;
323325 const subtitleStream = selectedSubtitleIndex >= 0 ? subtitleStreamsList [ selectedSubtitleIndex ] : null ;
324326 playbackOptions = {
327+ mediaSourceId : playMediaSource . Id ,
325328 audioStreamIndex : selectedAudio ?. Index ?? selectedAudioIndex ,
326329 subtitleStreamIndex : subtitleStream ?. Index ?? selectedSubtitleIndex
327330 } ;
@@ -354,7 +357,7 @@ const Details = ({itemId, initialItem, onPlay, onSelectItem, onSelectPerson, bac
354357 } else {
355358 onPlay ?. ( item , false , playbackOptions ) ;
356359 }
357- } , [ item , episodes , nextUp , seasons , albumTracks , playlistItems , onPlay , onSelectItem , selectedAudioIndex , selectedSubtitleIndex ] ) ;
360+ } , [ item , episodes , nextUp , seasons , albumTracks , playlistItems , onPlay , onSelectItem , selectedAudioIndex , selectedSubtitleIndex , selectedVersionIndex ] ) ;
358361
359362 const handleResume = useCallback ( ( ) => {
360363 if ( ! item ) return ;
@@ -365,19 +368,20 @@ const Details = ({itemId, initialItem, onPlay, onSelectItem, onSelectPerson, bac
365368
366369 let playbackOptions = { } ;
367370 if ( supportsSelection ) {
368- const resumeMediaSource = item . MediaSources [ 0 ] ;
371+ const resumeMediaSource = item . MediaSources [ selectedVersionIndex ] || item . MediaSources [ 0 ] ;
369372 const audioStreamsList = resumeMediaSource ?. MediaStreams ?. filter ( s => s . Type === 'Audio' ) || [ ] ;
370373 const subtitleStreamsList = resumeMediaSource ?. MediaStreams ?. filter ( s => s . Type === 'Subtitle' ) || [ ] ;
371374 const selectedAudio = audioStreamsList [ selectedAudioIndex ] ;
372375 const subtitleStream = selectedSubtitleIndex >= 0 ? subtitleStreamsList [ selectedSubtitleIndex ] : null ;
373376 playbackOptions = {
377+ mediaSourceId : resumeMediaSource . Id ,
374378 audioStreamIndex : selectedAudio ?. Index ?? selectedAudioIndex ,
375379 subtitleStreamIndex : subtitleStream ?. Index ?? selectedSubtitleIndex
376380 } ;
377381 }
378382
379383 onPlay ?. ( item , true , playbackOptions ) ;
380- } , [ item , onPlay , selectedAudioIndex , selectedSubtitleIndex ] ) ;
384+ } , [ item , onPlay , selectedAudioIndex , selectedSubtitleIndex , selectedVersionIndex ] ) ;
381385
382386 const handleShuffle = useCallback ( ( ) => {
383387 if ( item ) {
@@ -505,6 +509,7 @@ const Details = ({itemId, initialItem, onPlay, onSelectItem, onSelectPerson, bac
505509
506510 const handleOpenAudioModal = useCallback ( ( ) => openModal ( 'audio' ) , [ openModal ] ) ;
507511 const handleOpenSubtitleModal = useCallback ( ( ) => openModal ( 'subtitle' ) , [ openModal ] ) ;
512+ const handleOpenVersionModal = useCallback ( ( ) => openModal ( 'version' ) , [ openModal ] ) ;
508513
509514 const closeModal = useCallback ( ( ) => {
510515 setActiveModal ( null ) ;
@@ -524,6 +529,28 @@ const Details = ({itemId, initialItem, onPlay, onSelectItem, onSelectPerson, bac
524529 closeModal ( ) ;
525530 } , [ closeModal ] ) ;
526531
532+ const handleSelectVersion = useCallback ( ( e ) => {
533+ const index = parseInt ( e . currentTarget . dataset . index , 10 ) ;
534+ if ( isNaN ( index ) || ! item ?. MediaSources ?. [ index ] ) return ;
535+ setSelectedVersionIndex ( index ) ;
536+ const ms = item . MediaSources [ index ] ;
537+ const audioStreams = ms . MediaStreams ?. filter ( s => s . Type === 'Audio' ) || [ ] ;
538+ const subtitleStreams = ms . MediaStreams ?. filter ( s => s . Type === 'Subtitle' ) || [ ] ;
539+ if ( ms . DefaultAudioStreamIndex != null ) {
540+ const idx = audioStreams . findIndex ( s => s . Index === ms . DefaultAudioStreamIndex ) ;
541+ setSelectedAudioIndex ( idx >= 0 ? idx : 0 ) ;
542+ } else {
543+ setSelectedAudioIndex ( 0 ) ;
544+ }
545+ if ( ms . DefaultSubtitleStreamIndex != null ) {
546+ const idx = subtitleStreams . findIndex ( s => s . Index === ms . DefaultSubtitleStreamIndex ) ;
547+ setSelectedSubtitleIndex ( idx >= 0 ? idx : - 1 ) ;
548+ } else {
549+ setSelectedSubtitleIndex ( - 1 ) ;
550+ }
551+ closeModal ( ) ;
552+ } , [ item , closeModal ] ) ;
553+
527554 const handleSeasonSelect = useCallback ( ( ev ) => {
528555 const seasonId = ev . currentTarget . dataset . seasonId ;
529556 const season = seasons . find ( s => s . Id === seasonId ) ;
@@ -877,12 +904,13 @@ const handleSectionKeyDown = useCallback((ev) => {
877904 const seasonCount = item . ChildCount || seasons . length || 0 ;
878905
879906 // Media source info
880- const mediaSource = item . MediaSources ?. [ 0 ] ;
907+ const mediaSource = item . MediaSources ?. [ selectedVersionIndex ] || item . MediaSources ?. [ 0 ] ;
881908 const audioStreams = mediaSource ?. MediaStreams ?. filter ( s => s . Type === 'Audio' ) || [ ] ;
882909 const subtitleStreams = mediaSource ?. MediaStreams ?. filter ( s => s . Type === 'Subtitle' ) || [ ] ;
883910 const supportsMediaSourceSelection = item . MediaType === 'Video' &&
884911 item . MediaSources ?. length > 0 &&
885912 item . MediaSources [ 0 ] . Type !== 'Placeholder' ;
913+ const hasMultipleVersions = supportsMediaSourceSelection && ( item . MediaSources ?. length || 0 ) > 1 ;
886914 const hasMultipleAudio = supportsMediaSourceSelection && audioStreams . length > 1 ;
887915 const hasSubtitles = supportsMediaSourceSelection && subtitleStreams . length > 0 ;
888916 const currentAudioStream = audioStreams [ selectedAudioIndex ] ;
@@ -1016,6 +1044,17 @@ const handleSectionKeyDown = useCallback((ev) => {
10161044 < span className = { css . btnLabel } > Shuffle</ span >
10171045 </ SpottableDiv >
10181046 ) }
1047+ { hasMultipleVersions && (
1048+ < SpottableDiv className = { css . btnWrapper } onClick = { handleOpenVersionModal } >
1049+ < div className = { css . btnAction } >
1050+ < svg className = { css . btnIcon } viewBox = "0 -960 960 960" fill = "currentColor" >
1051+ < path d = "M320-240h320v-80H320v80Zm0-160h320v-80H320v80ZM240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v480q0 33-23.5 56.5T740-80H240Zm280-520v-200H240v640h500v-440H520ZM240-800v200-200 640-640Z" />
1052+ </ svg >
1053+ </ div >
1054+ < span className = { css . btnLabel } > Version</ span >
1055+ < span className = { css . btnDetail } > { mediaSource ?. Name || `Version ${ selectedVersionIndex + 1 } ` } </ span >
1056+ </ SpottableDiv >
1057+ ) }
10191058 { hasMultipleAudio && (
10201059 < SpottableDiv className = { css . btnWrapper } onClick = { handleOpenAudioModal } >
10211060 < div className = { css . btnAction } >
@@ -1944,7 +1983,33 @@ const handleSectionKeyDown = useCallback((ev) => {
19441983 </ div >
19451984 </ Scroller >
19461985
1947- { /* Audio/Subtitle Track Modals */ }
1986+ { /* Version / Audio / Subtitle Track Modals */ }
1987+ { activeModal === 'version' && (
1988+ < div className = { css . trackModal } onClick = { closeModal } >
1989+ < ModalContainer className = { css . trackModalPanel } onClick = { handleStopPropagation } data-modal = "version" spotlightId = "version-modal" >
1990+ < h2 className = { css . trackModalTitle } > Select Version</ h2 >
1991+ < div className = { css . trackList } >
1992+ { item . MediaSources . map ( ( source , i ) => {
1993+ const video = source . MediaStreams ?. find ( s => s . Type === 'Video' ) ;
1994+ const resLabel = video ?. Width >= 3800 ? '4K' : video ?. Width >= 1900 ? '1080p' : video ?. Width >= 1260 ? '720p' : video ?. Width ? `${ video . Width } p` : '' ;
1995+ return (
1996+ < SpottableButton
1997+ key = { source . Id }
1998+ className = { `${ css . trackItem } ${ i === selectedVersionIndex ? css . selected : '' } ` }
1999+ data-index = { i }
2000+ data-selected = { i === selectedVersionIndex ? 'true' : undefined }
2001+ onClick = { handleSelectVersion }
2002+ >
2003+ < span className = { css . trackName } > { source . Name || `Version ${ i + 1 } ` } </ span >
2004+ { resLabel && < span className = { css . trackInfo } > { resLabel } </ span > }
2005+ </ SpottableButton >
2006+ ) ;
2007+ } ) }
2008+ </ div >
2009+ < p className = { css . trackModalFooter } > Press BACK to close</ p >
2010+ </ ModalContainer >
2011+ </ div >
2012+ ) }
19482013 { activeModal === 'audio' && (
19492014 < div className = { css . trackModal } onClick = { closeModal } >
19502015 < ModalContainer className = { css . trackModalPanel } onClick = { handleStopPropagation } data-modal = "audio" spotlightId = "audio-modal" >
0 commit comments