Skip to content

Commit

Permalink
Move Netflix metadata fetching into a custom hook
Browse files Browse the repository at this point in the history
  • Loading branch information
mwhirls committed Feb 17, 2024
1 parent 854381d commit 450bb5a
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 71 deletions.
75 changes: 4 additions & 71 deletions src/content-scripts/components/VideoContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,29 @@
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<string, SubtitleData>;

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<SubtitleData | null> {
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;
}

function VideoContainer({ dbStatus }: VideoContainerProps) {
const [currMovie] = useStorage<MovieId | null>(MOVIE_KEY, StorageType.Session, null);
const [enabled] = useExtensionEnabled(false);
const [subtitleData, setSubtitleData] = useState(new Map<MovieId, SubtitleTracks>);
const [currTrack, setCurrTrack] = useState("");
const [subtitleData, currTrack] = useNetflixMetadata();
const [netflixPlayer, setNetflixPlayer] = useState<Element | null>(document.querySelector(`.${NETFLIX_PLAYER_CLASS}`));
const [videoElem, setVideoElem] = useState<HTMLVideoElement | null>(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);
Expand All @@ -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();
};
}, []);
Expand Down
82 changes: 82 additions & 0 deletions src/content-scripts/hooks/useNetflixMetadata.ts
Original file line number Diff line number Diff line change
@@ -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<string, SubtitleData>;

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<SubtitleData | null> {
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<MovieId, SubtitleTracks>, string] {
const [subtitleData, setSubtitleData] = useState(new Map<MovieId, SubtitleTracks>);
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];
}

0 comments on commit 450bb5a

Please sign in to comment.