mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-31 02:27:11 -04:00 
			
		
		
		
	wip: Add subtitles handling
This commit is contained in:
		
							parent
							
								
									dfdeca35f3
								
							
						
					
					
						commit
						5ca1ae938f
					
				| @ -1265,7 +1265,7 @@ | |||||||
| 
 | 
 | ||||||
|     "react-native-svg-transformer": ["react-native-svg-transformer@1.5.1", "", { "dependencies": { "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "path-dirname": "^1.0.2" }, "peerDependencies": { "react-native": ">=0.59.0", "react-native-svg": ">=12.0.0" } }, "sha512-dFvBNR8A9VPum9KCfh+LE49YiJEF8zUSnEFciKQroR/bEOhlPoZA0SuQ0qNk7m2iZl2w59FYjdRe0pMHWMDl0Q=="], |     "react-native-svg-transformer": ["react-native-svg-transformer@1.5.1", "", { "dependencies": { "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "path-dirname": "^1.0.2" }, "peerDependencies": { "react-native": ">=0.59.0", "react-native-svg": ">=12.0.0" } }, "sha512-dFvBNR8A9VPum9KCfh+LE49YiJEF8zUSnEFciKQroR/bEOhlPoZA0SuQ0qNk7m2iZl2w59FYjdRe0pMHWMDl0Q=="], | ||||||
| 
 | 
 | ||||||
|     "react-native-video": ["react-native-video@github:zoriya/react-native-video#8287a84", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": ">=0.27.2" } }, "zoriya-react-native-video-8287a84"], |     "react-native-video": ["react-native-video@github:zoriya/react-native-video#ad69842", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": ">=0.27.2" } }, "zoriya-react-native-video-ad69842"], | ||||||
| 
 | 
 | ||||||
|     "react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="], |     "react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="], | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -163,7 +163,7 @@ const ControlButtons = ({ | |||||||
| 				<ProgressText player={player} {...spacing} /> | 				<ProgressText player={player} {...spacing} /> | ||||||
| 			</View> | 			</View> | ||||||
| 			<View {...css({ flexDirection: "row" })}> | 			<View {...css({ flexDirection: "row" })}> | ||||||
| 				<SubtitleMenu {...menuProps} /> | 				<SubtitleMenu player={player} {...menuProps} /> | ||||||
| 				<AudioMenu player={player} {...menuProps} /> | 				<AudioMenu player={player} {...menuProps} /> | ||||||
| 				<VideoMenu player={player} {...menuProps} /> | 				<VideoMenu player={player} {...menuProps} /> | ||||||
| 				<QualityMenu player={player} {...menuProps} /> | 				<QualityMenu player={player} {...menuProps} /> | ||||||
|  | |||||||
| @ -3,19 +3,50 @@ import MusicNote from "@material-symbols/svg-400/rounded/music_note-fill.svg"; | |||||||
| import SettingsIcon from "@material-symbols/svg-400/rounded/settings-fill.svg"; | import SettingsIcon from "@material-symbols/svg-400/rounded/settings-fill.svg"; | ||||||
| import VideoSettings from "@material-symbols/svg-400/rounded/video_settings-fill.svg"; | import VideoSettings from "@material-symbols/svg-400/rounded/video_settings-fill.svg"; | ||||||
| import { type ComponentProps, createContext, useContext } from "react"; | import { type ComponentProps, createContext, useContext } from "react"; | ||||||
| import { useEvent, type VideoPlayer } from "react-native-video"; |  | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import { IconButton, Menu, tooltip } from "~/primitives"; | import { useEvent, type VideoPlayer } from "react-native-video"; | ||||||
| import { useDisplayName } from "~/track-utils"; |  | ||||||
| import { useForceRerender } from "yoshiki"; | import { useForceRerender } from "yoshiki"; | ||||||
|  | import type { Subtitle } from "~/models"; | ||||||
|  | import { IconButton, Menu, tooltip } from "~/primitives"; | ||||||
|  | import { useFetch } from "~/query"; | ||||||
|  | import { useDisplayName, useSubtitleName } from "~/track-utils"; | ||||||
|  | import { useQueryState } from "~/utils"; | ||||||
|  | import { Player } from ".."; | ||||||
| 
 | 
 | ||||||
| type MenuProps = ComponentProps<typeof Menu<ComponentProps<typeof IconButton>>>; | type MenuProps = ComponentProps<typeof Menu<ComponentProps<typeof IconButton>>>; | ||||||
| 
 | 
 | ||||||
| export const SubtitleMenu = (props: Partial<MenuProps>) => { | export const SubtitleMenu = ({ | ||||||
|  | 	player, | ||||||
|  | 	...props | ||||||
|  | }: { | ||||||
|  | 	player: VideoPlayer; | ||||||
|  | } & Partial<MenuProps>) => { | ||||||
| 	const { t } = useTranslation(); | 	const { t } = useTranslation(); | ||||||
|  | 	const getDisplayName = useSubtitleName(); | ||||||
|  | 
 | ||||||
|  | 	const rerender = useForceRerender(); | ||||||
|  | 	useEvent(player, "onTrackChange", rerender); | ||||||
|  | 
 | ||||||
|  | 	const [slug] = useQueryState<string>("slug", undefined!); | ||||||
|  | 	const { data } = useFetch(Player.infoQuery(slug)); | ||||||
|  | 
 | ||||||
|  | 	if (data?.subtitles.length === 0) return null; | ||||||
|  | 
 | ||||||
|  | 	const selectedIdx = player | ||||||
|  | 		.getAvailableTextTracks() | ||||||
|  | 		.findIndex((x) => x.selected); | ||||||
|  | 
 | ||||||
|  | 	const select = (track: Subtitle | null, idx: number) => { | ||||||
|  | 		if (!track) { | ||||||
|  | 			player.selectTextTrack(null); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// TODO: filter by codec here
 | ||||||
|  | 		const sub = player.getAvailableTextTracks()[idx]; | ||||||
|  | 		player.selectTextTrack(sub); | ||||||
|  | 	}; | ||||||
| 
 | 
 | ||||||
| 	// {subtitles && subtitles.length > 0 && (
 |  | ||||||
| 	// )}
 |  | ||||||
| 	return ( | 	return ( | ||||||
| 		<Menu | 		<Menu | ||||||
| 			Trigger={IconButton} | 			Trigger={IconButton} | ||||||
| @ -23,24 +54,19 @@ export const SubtitleMenu = (props: Partial<MenuProps>) => { | |||||||
| 			{...tooltip(t("player.subtitles"), true)} | 			{...tooltip(t("player.subtitles"), true)} | ||||||
| 			{...props} | 			{...props} | ||||||
| 		> | 		> | ||||||
| 			{/* <Menu.Item */} | 			<Menu.Item | ||||||
| 			{/* 	label={t("player.subtitle-none")} */} | 				label={t("player.subtitle-none")} | ||||||
| 			{/* 	selected={!selectedSubtitle} */} | 				selected={selectedIdx === -1} | ||||||
| 			{/* 	onSelect={() => setSubtitle(null)} */} | 				onSelect={() => select(null, -1)} | ||||||
| 			{/* /> */} | 			/> | ||||||
| 			{/* {subtitles */} | 			{data?.subtitles.map((x, i) => ( | ||||||
| 			{/* 	.filter((x) => !!x.link) */} | 				<Menu.Item | ||||||
| 			{/* 	.map((x, i) => ( */} | 					key={x.index ?? x.link} | ||||||
| 			{/* 		<Menu.Item */} | 					label={getDisplayName(x)} | ||||||
| 			{/* 			key={x.index ?? i} */} | 					selected={i === selectedIdx} | ||||||
| 			{/* 			label={ */} | 					onSelect={() => select(x, i)} | ||||||
| 			{/* 				x.link ? getSubtitleName(x) : `${getSubtitleName(x)} (${x.codec})` */} | 				/> | ||||||
| 			{/* 			} */} | 			))} | ||||||
| 			{/* 			selected={selectedSubtitle === x} */} |  | ||||||
| 			{/* 			disabled={!x.link} */} |  | ||||||
| 			{/* 			onSelect={() => setSubtitle(x)} */} |  | ||||||
| 			{/* 		/> */} |  | ||||||
| 			{/* 	))} */} |  | ||||||
| 		</Menu> | 		</Menu> | ||||||
| 	); | 	); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -62,10 +62,13 @@ export const Player = () => { | |||||||
| 				imageUri: data?.show?.thumbnail?.high ?? undefined, | 				imageUri: data?.show?.thumbnail?.high ?? undefined, | ||||||
| 			}, | 			}, | ||||||
| 			externalSubtitles: info?.subtitles | 			externalSubtitles: info?.subtitles | ||||||
| 				.filter((x) => x.link) | 				.filter( | ||||||
|  | 					(x) => Platform.OS === "web" || playMode === "hls" || x.isExternal, | ||||||
|  | 				) | ||||||
| 				.map((x) => ({ | 				.map((x) => ({ | ||||||
|  | 					// we also add those without link to prevent the order from getting out of sync with `info.subtitles`.
 | ||||||
|  | 					// since we never actually play those this is fine
 | ||||||
| 					uri: x.link!, | 					uri: x.link!, | ||||||
| 					// TODO: translate this `Unknown`
 |  | ||||||
| 					label: x.title ?? "Unknown", | 					label: x.title ?? "Unknown", | ||||||
| 					language: x.language ?? "und", | 					language: x.language ?? "und", | ||||||
| 					type: x.codec, | 					type: x.codec, | ||||||
|  | |||||||
| @ -287,7 +287,7 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) { | |||||||
| 			extension := OrNull(SubtitleExtensions[stream.CodecName]) | 			extension := OrNull(SubtitleExtensions[stream.CodecName]) | ||||||
| 			var link string | 			var link string | ||||||
| 			if extension != nil { | 			if extension != nil { | ||||||
| 				link = fmt.Sprintf("video/%s/subtitle/%d.%s", base64.RawURLEncoding.EncodeToString([]byte(path)), i, *extension) | 				link = fmt.Sprintf("/video/%s/subtitle/%d.%s", base64.RawURLEncoding.EncodeToString([]byte(path)), i, *extension) | ||||||
| 			} | 			} | ||||||
| 			lang, _ := language.Parse(stream.Tags.Language) | 			lang, _ := language.Parse(stream.Tags.Language) | ||||||
| 			idx := uint32(i) | 			idx := uint32(i) | ||||||
| @ -315,7 +315,7 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) { | |||||||
| 		}), | 		}), | ||||||
| 		Fonts: MapStream(mi.Streams, ffprobe.StreamAttachment, func(stream *ffprobe.Stream, i uint32) string { | 		Fonts: MapStream(mi.Streams, ffprobe.StreamAttachment, func(stream *ffprobe.Stream, i uint32) string { | ||||||
| 			font, _ := stream.TagList.GetString("filename") | 			font, _ := stream.TagList.GetString("filename") | ||||||
| 			return fmt.Sprintf("video/%s/attachment/%s", base64.RawURLEncoding.EncodeToString([]byte(path)), font) | 			return fmt.Sprintf("/video/%s/attachment/%s", base64.RawURLEncoding.EncodeToString([]byte(path)), font) | ||||||
| 		}), | 		}), | ||||||
| 	} | 	} | ||||||
| 	var codecs []string | 	var codecs []string | ||||||
|  | |||||||
| @ -174,7 +174,7 @@ func (s *MetadataService) GetMetadata(ctx context.Context, path string, sha stri | |||||||
| 		tx.Exec(`update info set ver_keyframes = 0 where sha = $1`, sha) | 		tx.Exec(`update info set ver_keyframes = 0 where sha = $1`, sha) | ||||||
| 		err = tx.Commit() | 		err = tx.Commit() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			fmt.Printf("error deleteing old keyframes from database: %v", err) | 			fmt.Printf("error deleting old keyframes from database: %v", err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -256,7 +256,7 @@ func (s *MetadataService) getMetadata(path string, sha string) (*MediaInfo, erro | |||||||
| 		} | 		} | ||||||
| 		if s.Extension != nil { | 		if s.Extension != nil { | ||||||
| 			link := fmt.Sprintf( | 			link := fmt.Sprintf( | ||||||
| 				"video/%s/subtitle/%d.%s", | 				"/video/%s/subtitle/%d.%s", | ||||||
| 				base64.RawURLEncoding.EncodeToString([]byte(ret.Path)), | 				base64.RawURLEncoding.EncodeToString([]byte(ret.Path)), | ||||||
| 				*s.Index, | 				*s.Index, | ||||||
| 				*s.Extension, | 				*s.Extension, | ||||||
| @ -391,5 +391,10 @@ func (s *MetadataService) storeFreshMetadata(path string, sha string) (*MediaInf | |||||||
| 		return set(ret, err) | 		return set(ret, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	err = ret.SearchExternalSubtitles() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return set(ret, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return set(ret, nil) | 	return set(ret, nil) | ||||||
| } | } | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ outer: | |||||||
| 		for codec, ext := range SubtitleExtensions { | 		for codec, ext := range SubtitleExtensions { | ||||||
| 			if strings.HasSuffix(match, ext) { | 			if strings.HasSuffix(match, ext) { | ||||||
| 				link := fmt.Sprintf( | 				link := fmt.Sprintf( | ||||||
| 					"video/%s/direct/%s", | 					"/video/%s/direct/%s", | ||||||
| 					base64.RawURLEncoding.EncodeToString([]byte(match)), | 					base64.RawURLEncoding.EncodeToString([]byte(match)), | ||||||
| 					filepath.Base(match), | 					filepath.Base(match), | ||||||
| 				) | 				) | ||||||
|  | |||||||
| @ -145,7 +145,7 @@ func (s *MetadataService) extractThumbnail(ctx context.Context, path string, sha | |||||||
| 		timestamps := ts | 		timestamps := ts | ||||||
| 		ts += interval | 		ts += interval | ||||||
| 		vtt += fmt.Sprintf( | 		vtt += fmt.Sprintf( | ||||||
| 			"%s --> %s\nvideo/%s/thumbnails.png#xywh=%d,%d,%d,%d\n\n", | 			"%s --> %s\n/video/%s/thumbnails.png#xywh=%d,%d,%d,%d\n\n", | ||||||
| 			tsToVttTime(timestamps), | 			tsToVttTime(timestamps), | ||||||
| 			tsToVttTime(ts), | 			tsToVttTime(ts), | ||||||
| 			base64.RawURLEncoding.EncodeToString([]byte(path)), | 			base64.RawURLEncoding.EncodeToString([]byte(path)), | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user