mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 18:13:25 +00:00
Compare commits
1 Commits
f6a265f17b
...
2d62ee0c34
Author | SHA1 | Date | |
---|---|---|---|
|
2d62ee0c34 |
@@ -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 {
|
||||||
|
@@ -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;
|
||||||
|
@@ -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);
|
||||||
|
@@ -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 = () => {
|
||||||
|
@@ -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 = () => {
|
||||||
|
@@ -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 }),
|
||||||
}),
|
}),
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
|
@@ -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
589
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
Reference in New Issue
Block a user