Compare commits

..

1 Commits

Author SHA1 Message Date
Adrian Castro
2d62ee0c34 Merge 45d12bbf41 into a3f184979e 2024-04-08 20:26:45 +00:00
10 changed files with 103 additions and 660 deletions

View File

@@ -91,7 +91,7 @@ export default function SettingsScreen() {
}; };
const clearCacheDirectory = async () => { const clearCacheDirectory = async () => {
const cacheDirectory = `${FileSystem.cacheDirectory}movie-web`; const cacheDirectory = FileSystem.cacheDirectory + "movie-web";
if (!cacheDirectory) return; if (!cacheDirectory) return;
try { try {

View File

@@ -1,7 +1,7 @@
import { MaterialCommunityIcons } from "@expo/vector-icons"; import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useTheme } from "tamagui"; import { useTheme } from "tamagui";
import { findQuality } from "@movie-web/provider-utils"; import { findHighestQuality } from "@movie-web/provider-utils";
import { useDownloadManager } from "~/hooks/useDownloadManager"; import { useDownloadManager } from "~/hooks/useDownloadManager";
import { convertMetaToScrapeMedia } from "~/lib/meta"; import { convertMetaToScrapeMedia } from "~/lib/meta";
@@ -21,7 +21,7 @@ export const DownloadButton = () => {
let url: string | undefined | null = null; let url: string | undefined | null = null;
if (stream?.type === "file") { if (stream?.type === "file") {
const highestQuality = findQuality(stream); const highestQuality = findHighestQuality(stream);
url = highestQuality ? stream.qualities[highestQuality]?.url : null; url = highestQuality ? stream.qualities[highestQuality]?.url : null;
} else if (stream?.type === "hls") { } else if (stream?.type === "hls") {
url = stream.playlist; url = stream.playlist;

View File

@@ -4,14 +4,19 @@ import { ScrollView } from "react-native-gesture-handler";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { View } from "tamagui"; import { View } from "tamagui";
import type { RunOutput, ScrapeMedia } from "@movie-web/provider-utils"; import type {
HlsBasedStream,
RunOutput,
ScrapeMedia,
} from "@movie-web/provider-utils";
import { import {
constructFullUrl,
extractTracksFromHLS, extractTracksFromHLS,
filterAudioTracks, findHighestQuality,
findQuality,
} from "@movie-web/provider-utils"; } from "@movie-web/provider-utils";
import type { ItemData } from "../item/item"; import type { ItemData } from "../item/item";
import type { AudioTrack } from "./AudioTrackSelector";
import type { PlayerMeta } from "~/stores/player/slices/video"; import type { PlayerMeta } from "~/stores/player/slices/video";
import { useMeta } from "~/hooks/player/useMeta"; import { useMeta } from "~/hooks/player/useMeta";
import { useScrape } from "~/hooks/player/useSourceScrape"; import { useScrape } from "~/hooks/player/useSourceScrape";
@@ -71,9 +76,9 @@ export const ScraperProcess = ({
if (!streamResult) return router.back(); if (!streamResult) return router.back();
if (download) { if (download) {
if (streamResult.stream.type === "file") { if (streamResult.stream.type === "file") {
const quality = findQuality(streamResult.stream); const highestQuality = findHighestQuality(streamResult.stream);
const url = quality const url = highestQuality
? streamResult.stream.qualities[quality]?.url ? streamResult.stream.qualities[highestQuality]?.url
: null; : null;
if (!url) return; if (!url) return;
startDownload(url, "mp4", scrapeMedia).catch(console.error); startDownload(url, "mp4", scrapeMedia).catch(console.error);
@@ -84,7 +89,6 @@ export const ScraperProcess = ({
} }
return router.back(); return router.back();
} }
setStream(streamResult.stream); setStream(streamResult.stream);
if (streamResult.stream.type === "hls") { if (streamResult.stream.type === "hls") {
@@ -99,9 +103,28 @@ export const ScraperProcess = ({
if (tracks) setHlsTracks(tracks); if (tracks) setHlsTracks(tracks);
if (tracks?.audio.length) { if (tracks?.audio.length) {
setAudioTracks( const audioTracks: AudioTrack[] = tracks.audio.map((track) => ({
filterAudioTracks(tracks, streamResult.stream.playlist), uri: constructFullUrl(
); (streamResult?.stream as HlsBasedStream).playlist,
track.uri,
),
name: track.properties[0]?.attributes.name?.toString() ?? "Unknown",
language:
track.properties[0]?.attributes.language?.toString() ?? "Unknown",
active: Boolean(track.properties[0]?.attributes.default) ?? false,
}));
const uniqueTracks = new Set(audioTracks.map((t) => t.language));
const filteredAudioTracks = audioTracks.filter((track) => {
if (uniqueTracks.has(track.language)) {
uniqueTracks.delete(track.language);
return true;
}
return false;
});
setAudioTracks(filteredAudioTracks);
} }
} }
setPlayerStatus(PlayerStatus.READY); setPlayerStatus(PlayerStatus.READY);

View File

@@ -12,13 +12,12 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ResizeMode, Video } from "expo-av"; import { ResizeMode, Video } from "expo-av";
import * as Haptics from "expo-haptics"; import * as Haptics from "expo-haptics";
import * as NavigationBar from "expo-navigation-bar"; import * as NavigationBar from "expo-navigation-bar";
import * as Network from "expo-network";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import * as StatusBar from "expo-status-bar"; import * as StatusBar from "expo-status-bar";
import { Feather } from "@expo/vector-icons"; import { Feather } from "@expo/vector-icons";
import { Spinner, useTheme, View } from "tamagui"; import { Spinner, useTheme, View } from "tamagui";
import { findHLSQuality, findQuality } from "@movie-web/provider-utils"; import { findHighestQuality } from "@movie-web/provider-utils";
import { useAudioTrack } from "~/hooks/player/useAudioTrack"; import { useAudioTrack } from "~/hooks/player/useAudioTrack";
import { useBrightness } from "~/hooks/player/useBrightness"; import { useBrightness } from "~/hooks/player/useBrightness";
@@ -33,8 +32,6 @@ import {
import { useAudioTrackStore } from "~/stores/audio"; import { useAudioTrackStore } from "~/stores/audio";
import { usePlayerStore } from "~/stores/player/store"; import { usePlayerStore } from "~/stores/player/store";
import { import {
DefaultQuality,
useNetworkSettingsStore,
usePlayerSettingsStore, usePlayerSettingsStore,
useWatchHistoryStore, useWatchHistoryStore,
} from "~/stores/settings"; } from "~/stores/settings";
@@ -79,8 +76,6 @@ export const VideoPlayer = () => {
const { gestureControls, autoPlay } = usePlayerSettingsStore(); const { gestureControls, autoPlay } = usePlayerSettingsStore();
const { updateWatchHistory, removeFromWatchHistory, getWatchHistoryItem } = const { updateWatchHistory, removeFromWatchHistory, getWatchHistoryItem } =
useWatchHistoryStore(); useWatchHistoryStore();
const { wifiDefaultQuality, mobileDataDefaultQuality } =
useNetworkSettingsStore();
const updateResizeMode = (newMode: ResizeMode) => { const updateResizeMode = (newMode: ResizeMode) => {
setResizeMode(newMode); setResizeMode(newMode);
@@ -163,22 +158,15 @@ export const VideoPlayer = () => {
} }
setIsLoading(true); setIsLoading(true);
const { type: networkType } = await Network.getNetworkStateAsync();
const defaultQuality =
networkType === Network.NetworkStateType.WIFI
? wifiDefaultQuality
: mobileDataDefaultQuality;
const highest = defaultQuality === DefaultQuality.Highest;
let url = null; let url = null;
if (stream.type === "hls") { if (stream.type === "hls") {
url = await findHLSQuality(stream.playlist, stream.headers, highest); url = stream.playlist;
} }
if (stream.type === "file") { if (stream.type === "file") {
const chosenQuality = findQuality(stream, highest); const highestQuality = findHighestQuality(stream);
url = chosenQuality ? stream.qualities[chosenQuality]?.url : null; url = highestQuality ? stream.qualities[highestQuality]?.url : null;
} }
if (!url) { if (!url) {
@@ -231,8 +219,6 @@ export const VideoPlayer = () => {
updateWatchHistory, updateWatchHistory,
videoRef?.props.positionMillis, videoRef?.props.positionMillis,
videoSrc?.uri, videoSrc?.uri,
wifiDefaultQuality,
mobileDataDefaultQuality,
]); ]);
const onVideoLoadStart = () => { const onVideoLoadStart = () => {

View File

@@ -184,11 +184,11 @@ export const useDownloadManager = () => {
lastTimestamp = currentTime; lastTimestamp = currentTime;
}; };
const fileUri = `${FileSystem.cacheDirectory}movie-web${url.split("/").pop()}`; const fileUri =
if ( FileSystem.cacheDirectory + "movie-web"
!(await FileSystem.getInfoAsync(`${FileSystem.cacheDirectory}movie-web`)) ? FileSystem.cacheDirectory + "movie-web" + url.split("/").pop()
.exists : null;
) { if (!fileUri) {
console.error("Cache directory is unavailable"); console.error("Cache directory is unavailable");
return; return;
} }
@@ -238,7 +238,7 @@ export const useDownloadManager = () => {
const totalSegments = segments.length; const totalSegments = segments.length;
let segmentsDownloaded = 0; let segmentsDownloaded = 0;
const segmentDir = `${FileSystem.cacheDirectory}movie-web/segments/`; const segmentDir = FileSystem.cacheDirectory + "movie-web/segments/";
await ensureDirExists(segmentDir); await ensureDirExists(segmentDir);
const updateProgress = () => { const updateProgress = () => {

View File

@@ -223,18 +223,13 @@ export const useWatchHistoryStore = create<
), ),
); );
export enum DefaultQuality {
Lowest = "Lowest",
Highest = "Highest",
}
interface NetworkSettingsStoreState { interface NetworkSettingsStoreState {
allowMobileData: boolean; allowMobileData: boolean;
setAllowMobileData: (enabled: boolean) => void; setAllowMobileData: (enabled: boolean) => void;
wifiDefaultQuality: DefaultQuality; wifiDefaultQuality: string;
setWifiDefaultQuality: (quality: DefaultQuality) => void; setWifiDefaultQuality: (quality: string) => void;
mobileDataDefaultQuality: DefaultQuality; mobileDataDefaultQuality: string;
setMobileDataDefaultQuality: (quality: DefaultQuality) => void; setMobileDataDefaultQuality: (quality: string) => void;
} }
export const useNetworkSettingsStore = create< export const useNetworkSettingsStore = create<
@@ -245,9 +240,9 @@ export const useNetworkSettingsStore = create<
(set) => ({ (set) => ({
allowMobileData: false, allowMobileData: false,
setAllowMobileData: (enabled) => set({ allowMobileData: enabled }), setAllowMobileData: (enabled) => set({ allowMobileData: enabled }),
wifiDefaultQuality: DefaultQuality.Highest, wifiDefaultQuality: "Highest",
setWifiDefaultQuality: (quality) => set({ wifiDefaultQuality: quality }), setWifiDefaultQuality: (quality) => set({ wifiDefaultQuality: quality }),
mobileDataDefaultQuality: DefaultQuality.Lowest, mobileDataDefaultQuality: "Lowest",
setMobileDataDefaultQuality: (quality) => setMobileDataDefaultQuality: (quality) =>
set({ mobileDataDefaultQuality: quality }), set({ mobileDataDefaultQuality: quality }),
}), }),

View File

@@ -2,8 +2,7 @@ import type { AppendToResponse, MovieDetails, TvShowDetails } from "tmdb-ts";
import type { ScrapeMedia } from "@movie-web/providers"; import type { ScrapeMedia } from "@movie-web/providers";
import type { HLSTracks } from "./video"; import { providers } from "./video";
import { constructFullUrl, providers } from "./video";
export function getMetaData() { export function getMetaData() {
return [...providers.listSources(), ...providers.listEmbeds()]; return [...providers.listSources(), ...providers.listEmbeds()];
@@ -60,31 +59,3 @@ export function transformSearchResultToScrapeMedia<T extends "tv" | "movie">(
throw new Error("Invalid type parameter"); throw new Error("Invalid type parameter");
} }
interface AudioTrack {
uri: string;
name: string;
language: string;
active?: boolean;
}
export function filterAudioTracks(tracks: HLSTracks, playlist: string) {
const audioTracks: AudioTrack[] = tracks.audio.map((track) => ({
uri: constructFullUrl(playlist, track.uri),
name: track.properties[0]?.attributes.name?.toString() ?? "Unknown",
language: track.properties[0]?.attributes.language?.toString() ?? "Unknown",
active: Boolean(track.properties[0]?.attributes.default) ?? false,
}));
const uniqueTracks = new Set(audioTracks.map((t) => t.language));
const filteredAudioTracks = audioTracks.filter((track) => {
if (uniqueTracks.has(track.language)) {
uniqueTracks.delete(track.language);
return true;
}
return false;
});
return filteredAudioTracks;
}

View File

@@ -149,9 +149,8 @@ export async function getVideoStreamFromEmbed({
} }
} }
export function findQuality( export function findHighestQuality(
stream: FileBasedStream, stream: FileBasedStream,
highest = true,
): Qualities | undefined { ): Qualities | undefined {
const qualityOrder: Qualities[] = [ const qualityOrder: Qualities[] = [
"4k", "4k",
@@ -161,9 +160,6 @@ export function findQuality(
"360", "360",
"unknown", "unknown",
]; ];
if (!highest) {
qualityOrder.reverse();
}
for (const quality of qualityOrder) { for (const quality of qualityOrder) {
if (stream.qualities[quality]) { if (stream.qualities[quality]) {
return quality; return quality;
@@ -197,10 +193,9 @@ export async function extractTracksFromHLS(
} }
} }
export async function findHLSQuality( export async function extractSegmentsFromHLS(
playlistUrl: string, playlistUrl: string,
headers?: Record<string, string>, headers: Record<string, string>,
highest = true,
) { ) {
try { try {
const response = await fetch(playlistUrl, { headers }).then((res) => const response = await fetch(playlistUrl, { headers }).then((res) =>
@@ -225,29 +220,17 @@ export async function findHLSQuality(
return widthB * heightB - widthA * heightA; return widthB * heightB - widthA * heightA;
}); });
const chosenQuality = sortedStreams[highest ? 0 : sortedStreams.length - 1]; const highestQuality = sortedStreams[0];
if (!chosenQuality) return null; if (!highestQuality) return null;
return chosenQuality.uri; const highestQualityUri = constructFullUrl(playlistUrl, highestQuality.uri);
} catch (e) { const highestQualityResponse = await fetch(highestQualityUri, {
return null; headers,
} }).then((res) => res.text());
} const highestQualityPlaylist = hls.parse(highestQualityResponse);
export async function extractSegmentsFromHLS( return highestQualityPlaylist.segments.map((segment) =>
playlistUrl: string, constructFullUrl(highestQualityUri, segment.uri),
headers: Record<string, string>,
) {
try {
const highestQualityUrl = await findHLSQuality(playlistUrl, headers);
if (!highestQualityUrl) return null;
const response = await fetch(highestQualityUrl, { headers }).then((res) =>
res.text(),
);
const playlist = hls.parse(response);
return playlist.segments.map((segment) =>
constructFullUrl(highestQualityUrl, segment.uri),
); );
} catch (e) { } catch (e) {
return null; return null;

589
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,8 +32,6 @@ const config = {
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-enum-comparison": "off", "@typescript-eslint/no-unsafe-enum-comparison": "off",
"@typescript-eslint/no-useless-template-literals": "error",
"prefer-template": "error",
}, },
ignorePatterns: [ ignorePatterns: [
"**/*.config.js", "**/*.config.js",