From 450bb5a2b2c422bff0bb00651a9aadf5bec4ec0e Mon Sep 17 00:00:00 2001 From: Megan Worley Date: Sat, 17 Feb 2024 14:54:19 -0800 Subject: [PATCH] Move Netflix metadata fetching into a custom hook --- .../components/VideoContainer.tsx | 75 +---------------- .../hooks/useNetflixMetadata.ts | 82 +++++++++++++++++++ 2 files changed, 86 insertions(+), 71 deletions(-) create mode 100644 src/content-scripts/hooks/useNetflixMetadata.ts diff --git a/src/content-scripts/components/VideoContainer.tsx b/src/content-scripts/components/VideoContainer.tsx index 5b29f25..6164337 100644 --- a/src/content-scripts/components/VideoContainer.tsx +++ b/src/content-scripts/components/VideoContainer.tsx @@ -1,55 +1,17 @@ import React, { useState, useEffect } from "react"; import { createPortal } from "react-dom"; -import { RuntimeEvent } from "../../common/events"; -import { TimedTextTrack, NetflixMetadata, TimedTextSwitch } from "../../common/netflix-types"; import { StorageType } from "../../storage/storage"; -import { WEBVTT_FORMAT, querySelectorMutation, ChildMutationType } from "../util/util"; -import Video, { WebvttSubtitles } from "./Video"; +import { querySelectorMutation, ChildMutationType } from "../util/util"; +import Video from "./Video"; import { DBStatusResult } from "../../service-worker/database/dbstatus"; import { useStorage } from "../../common/hooks/useStorage"; import { useExtensionEnabled } from "../../common/hooks/useExtensionEnabled"; +import { MovieId, useNetflixMetadata } from "../hooks/useNetflixMetadata"; const NETFLIX_PLAYER_CLASS = "watch-video--player-view"; const NETFLIX_VIDEO_CLASS = `${NETFLIX_PLAYER_CLASS} video` const MOVIE_KEY = 'lastMovieId'; -type MovieId = number; -type SubtitleTracks = Map; - -class SubtitleData implements WebvttSubtitles { - webvttUrl: string; - bcp47: string; - - constructor(webvttUrl: string, bcp47: string) { - this.webvttUrl = webvttUrl; - this.bcp47 = bcp47; - } -} - -async function fetchSubtitlesBlob(url: string) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Bad response to request at ${url}`); - } - const blob = await response.blob(); - const webvttBlob = new Blob([blob], { type: 'text/vtt' }); - return URL.createObjectURL(await webvttBlob); -} - -async function downloadSubtitles(track: TimedTextTrack): Promise { - if (track.language !== 'ja' || track.languageDescription !== 'Japanese') { - return null; - } - const downloadable = track.ttDownloadables[WEBVTT_FORMAT]; - const url = downloadable?.urls.length ? downloadable.urls[0].url : null; - if (!url) { - console.warn(`[JIMAKUN] Failed to find a suitable subtitle URL for track ${track.new_track_id}`); - return null; - } - const blobUrl = await fetchSubtitlesBlob(url); - return new SubtitleData(blobUrl, track.language); -} - interface VideoContainerProps { dbStatus: DBStatusResult | null; } @@ -57,38 +19,11 @@ interface VideoContainerProps { function VideoContainer({ dbStatus }: VideoContainerProps) { const [currMovie] = useStorage(MOVIE_KEY, StorageType.Session, null); const [enabled] = useExtensionEnabled(false); - const [subtitleData, setSubtitleData] = useState(new Map); - const [currTrack, setCurrTrack] = useState(""); + const [subtitleData, currTrack] = useNetflixMetadata(); const [netflixPlayer, setNetflixPlayer] = useState(document.querySelector(`.${NETFLIX_PLAYER_CLASS}`)); const [videoElem, setVideoElem] = useState(document.querySelector(`.${NETFLIX_VIDEO_CLASS}`) as HTMLVideoElement | null); useEffect(() => { - const metadataListener = async (event: Event) => { - const metadata = (event as CustomEvent).detail as NetflixMetadata; - const movieId = metadata.movieId; - for (const track of metadata.timedtexttracks) { - try { - const subtitles = await downloadSubtitles(track); - if (subtitles) { - setSubtitleData(prev => { - const previousTracks = prev.get(movieId) ?? []; - const tracks = new Map([...previousTracks, [track.new_track_id, subtitles]]); - return new Map([...prev, [movieId, tracks]]) - }); - } - } catch (error) { - console.error('[JIMAKUN] Failed to fetch WebVTT file', error); - } - } - setCurrTrack(metadata.recommendedMedia.timedTextTrackId); - }; - const trackSwitchedListener = async (event: Event) => { - const data = (event as CustomEvent).detail as TimedTextSwitch; - setCurrTrack(data.track.trackId); - }; - window.addEventListener(RuntimeEvent.enum.MetadataDetected, metadataListener); - window.addEventListener(RuntimeEvent.enum.SubtitleTrackSwitched, trackSwitchedListener); - // We insert our components into the Netflix DOM, but they constantly // mutate it. Watch for changes so we know when to re-render. const netflixObserver = new MutationObserver(mutationCallback); @@ -109,8 +44,6 @@ function VideoContainer({ dbStatus }: VideoContainerProps) { netflixObserver.observe(document.body, config); return () => { - window.removeEventListener(RuntimeEvent.enum.MetadataDetected, metadataListener); - window.removeEventListener(RuntimeEvent.enum.SubtitleTrackSwitched, trackSwitchedListener); netflixObserver.disconnect(); }; }, []); diff --git a/src/content-scripts/hooks/useNetflixMetadata.ts b/src/content-scripts/hooks/useNetflixMetadata.ts new file mode 100644 index 0000000..211e1eb --- /dev/null +++ b/src/content-scripts/hooks/useNetflixMetadata.ts @@ -0,0 +1,82 @@ +import { useEffect, useState } from "react"; +import { RuntimeEvent } from "../../common/events"; +import { TimedTextTrack, NetflixMetadata, TimedTextSwitch } from "../../common/netflix-types"; +import { WebvttSubtitles } from "../components/Video"; +import { WEBVTT_FORMAT } from "../util/util"; + +export type MovieId = number; +export type SubtitleTracks = Map; + +class SubtitleData implements WebvttSubtitles { + webvttUrl: string; + bcp47: string; + + constructor(webvttUrl: string, bcp47: string) { + this.webvttUrl = webvttUrl; + this.bcp47 = bcp47; + } +} + +async function fetchSubtitlesBlob(url: string) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Bad response to request at ${url}`); + } + const blob = await response.blob(); + const webvttBlob = new Blob([blob], { type: 'text/vtt' }); + return URL.createObjectURL(await webvttBlob); +} + +async function downloadSubtitles(track: TimedTextTrack): Promise { + if (track.language !== 'ja' || track.languageDescription !== 'Japanese') { + return null; + } + const downloadable = track.ttDownloadables[WEBVTT_FORMAT]; + const url = downloadable?.urls.length ? downloadable.urls[0].url : null; + if (!url) { + console.warn(`[JIMAKUN] Failed to find a suitable subtitle URL for track ${track.new_track_id}`); + return null; + } + const blobUrl = await fetchSubtitlesBlob(url); + return new SubtitleData(blobUrl, track.language); +} + +export function useNetflixMetadata(): [Map, string] { + const [subtitleData, setSubtitleData] = useState(new Map); + const [currTrack, setCurrTrack] = useState(""); + + useEffect(() => { + const metadataListener = async (event: Event) => { + const metadata = (event as CustomEvent).detail as NetflixMetadata; + const movieId = metadata.movieId; + for (const track of metadata.timedtexttracks) { + try { + const subtitles = await downloadSubtitles(track); + if (subtitles) { + setSubtitleData(prev => { + const previousTracks = prev.get(movieId) ?? []; + const tracks = new Map([...previousTracks, [track.new_track_id, subtitles]]); + return new Map([...prev, [movieId, tracks]]) + }); + } + } catch (error) { + console.error('[JIMAKUN] Failed to fetch WebVTT file', error); + } + } + setCurrTrack(metadata.recommendedMedia.timedTextTrackId); + }; + const trackSwitchedListener = async (event: Event) => { + const data = (event as CustomEvent).detail as TimedTextSwitch; // todo: validate + setCurrTrack(data.track.trackId); + }; + window.addEventListener(RuntimeEvent.enum.MetadataDetected, metadataListener); + window.addEventListener(RuntimeEvent.enum.SubtitleTrackSwitched, trackSwitchedListener); + + return () => { + window.removeEventListener(RuntimeEvent.enum.MetadataDetected, metadataListener); + window.removeEventListener(RuntimeEvent.enum.SubtitleTrackSwitched, trackSwitchedListener); + }; + }, []); + + return [subtitleData, currTrack]; +} \ No newline at end of file