mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add logic to download episodes and movies
This commit is contained in:
parent
b51c77dfa7
commit
3a2e5f5eb1
@ -17,6 +17,7 @@
|
||||
"@formatjs/intl-displaynames": "^6.6.4",
|
||||
"@formatjs/intl-locale": "^3.4.3",
|
||||
"@gorhom/portal": "^1.0.14",
|
||||
"@kesha-antonov/react-native-background-downloader": "^2.10.0",
|
||||
"@kyoo/ui": "workspace:^",
|
||||
"@material-symbols/svg-400": "^0.14.1",
|
||||
"@shopify/flash-list": "1.4.3",
|
||||
|
@ -22,7 +22,7 @@ import { z } from "zod";
|
||||
import { Account, AccountP } from "./accounts";
|
||||
import { MMKV } from "react-native-mmkv";
|
||||
|
||||
const storage = new MMKV();
|
||||
export const storage = new MMKV();
|
||||
|
||||
const readAccounts = () => {
|
||||
const acc = storage.getString("accounts");
|
||||
|
@ -40,7 +40,7 @@ export let kyooApiUrl = kyooUrl;
|
||||
|
||||
export const queryFn = async <Data,>(
|
||||
context:
|
||||
| (QueryFunctionContext & { timeout?: number })
|
||||
| (QueryFunctionContext & { timeout?: number, apiUrl?: string })
|
||||
| {
|
||||
path: (string | false | undefined | null)[];
|
||||
body?: object;
|
||||
@ -52,7 +52,6 @@ export const queryFn = async <Data,>(
|
||||
type?: z.ZodType<Data>,
|
||||
token?: string | null,
|
||||
): Promise<Data> => {
|
||||
// @ts-ignore
|
||||
const url = context.apiUrl ?? (Platform.OS === "web" ? kyooUrl : getCurrentAccount()!.apiUrl);
|
||||
kyooApiUrl = url;
|
||||
|
||||
@ -166,7 +165,7 @@ export type QueryPage<Props = {}, Items = unknown> = ComponentType<
|
||||
randomItems?: Items[];
|
||||
};
|
||||
|
||||
const toQueryKey = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => {
|
||||
export const toQueryKey = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => {
|
||||
if (query.params) {
|
||||
return [
|
||||
...query.path,
|
||||
|
@ -14,6 +14,7 @@
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@kesha-antonov/react-native-background-downloader": "*",
|
||||
"@material-symbols/svg-400": "*",
|
||||
"@shopify/flash-list": "^1.3.1",
|
||||
"@tanstack/react-query": "*",
|
||||
@ -26,5 +27,13 @@
|
||||
"react-native-reanimated": "*",
|
||||
"react-native-svg": "*",
|
||||
"yoshiki": "*"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@kesha-antonov/react-native-background-downloader": "^2.10.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@kesha-antonov/react-native-background-downloader": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
165
front/packages/ui/src/downloads/download.tsx
Normal file
165
front/packages/ui/src/downloads/download.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
download,
|
||||
completeHandler,
|
||||
directories,
|
||||
DownloadTask,
|
||||
checkForExistingDownloads,
|
||||
Network,
|
||||
ensureDownloadsAreRunning,
|
||||
} from "@kesha-antonov/react-native-background-downloader";
|
||||
import {
|
||||
Account,
|
||||
Episode,
|
||||
Movie,
|
||||
QueryIdentifier,
|
||||
WatchInfo,
|
||||
queryFn,
|
||||
toQueryKey,
|
||||
} from "@kyoo/models";
|
||||
import { Player } from "../player";
|
||||
import { atom, useSetAtom, useAtom, Atom, PrimitiveAtom, useStore } from "jotai";
|
||||
import { getCurrentAccount, storage } from "@kyoo/models/src/account-internal";
|
||||
import { useContext, useEffect } from "react";
|
||||
|
||||
type State = {
|
||||
status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED";
|
||||
progress: number;
|
||||
size: number;
|
||||
availableSize: number;
|
||||
error?: string;
|
||||
pause: () => void;
|
||||
resume: () => void;
|
||||
stop: () => void;
|
||||
play: () => void;
|
||||
};
|
||||
|
||||
const downloadAtom = atom<
|
||||
{
|
||||
data: Episode | Movie;
|
||||
info: WatchInfo;
|
||||
path: string;
|
||||
state: PrimitiveAtom<State>;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
const query = <T,>(query: QueryIdentifier<T>, info: Account): Promise<T> =>
|
||||
queryFn(
|
||||
{
|
||||
queryKey: toQueryKey(query),
|
||||
signal: undefined as any,
|
||||
meta: undefined,
|
||||
apiUrl: info.apiUrl,
|
||||
// use current user and current apiUrl to download this meta.
|
||||
},
|
||||
query.parser,
|
||||
info.token.access_token,
|
||||
);
|
||||
|
||||
const listenToTask = (
|
||||
task: DownloadTask,
|
||||
atom: PrimitiveAtom<State>,
|
||||
atomStore: ReturnType<typeof useStore>,
|
||||
) => {
|
||||
task
|
||||
.begin(({ expectedBytes }) => atomStore.set(atom, (x) => ({ ...x, size: expectedBytes })))
|
||||
.progress((percent, availableSize, size) =>
|
||||
atomStore.set(atom, (x) => ({ ...x, percent, size, availableSize })),
|
||||
)
|
||||
.done(() => {
|
||||
atomStore.set(atom, (x) => ({ ...x, percent: 100, status: "DONE" }));
|
||||
// apparently this is needed for ios /shrug i'm totaly gona forget this
|
||||
// if i ever implement ios so keeping this here
|
||||
completeHandler(task.id);
|
||||
})
|
||||
.error((error) => atomStore.set(atom, (x) => ({ ...x, status: "FAILED", error })));
|
||||
};
|
||||
|
||||
export const useDownloader = () => {
|
||||
const setDownloads = useSetAtom(downloadAtom);
|
||||
const atomStore = useStore();
|
||||
|
||||
return async (type: "episode" | "movie", slug: string) => {
|
||||
const account = getCurrentAccount()!;
|
||||
const [data, info] = await Promise.all([
|
||||
query(Player.query(type, slug), account),
|
||||
query(Player.infoQuery(type, slug), account),
|
||||
]);
|
||||
|
||||
// TODO: support custom paths
|
||||
const path = `${directories.documents}/${slug}-${data.id}.${info.extension}`;
|
||||
const task = download({
|
||||
id: data.id,
|
||||
// TODO: support variant qualities
|
||||
url: `${account.apiUrl}/api/video/${type}/${slug}/direct`,
|
||||
destination: path,
|
||||
headers: {
|
||||
Authorization: account.token.access_token,
|
||||
},
|
||||
// TODO: Implement only wifi
|
||||
// network: Network.ALL,
|
||||
});
|
||||
|
||||
const state = atom({
|
||||
status: task.state,
|
||||
progress: task.percent * 100,
|
||||
size: task.totalBytes,
|
||||
availableSize: task.bytesWritten,
|
||||
pause: () => task.pause(),
|
||||
resume: () => task.resume(),
|
||||
stop: () => {
|
||||
task.stop();
|
||||
setDownloads((x) => x.filter((y) => y.data.id !== task.id));
|
||||
},
|
||||
play: () => {
|
||||
// TODO: set useQuery cache
|
||||
// TODO: move to the play page.
|
||||
},
|
||||
});
|
||||
|
||||
// we use the store instead of the onMount because we want to update the state to cache it even if it was not
|
||||
// used during this launch of the app.
|
||||
listenToTask(task, state, atomStore);
|
||||
setDownloads((x) => [...x, { data, info, path, state }]);
|
||||
};
|
||||
};
|
||||
|
||||
export const DownloadProvider = () => {
|
||||
const store = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
const tasks = await checkForExistingDownloads();
|
||||
const downloads = store.get(downloadAtom);
|
||||
for (const t of tasks) {
|
||||
const downAtom = downloads.find((x) => x.data.id === t.id);
|
||||
if (!downAtom) {
|
||||
t.stop();
|
||||
continue;
|
||||
}
|
||||
listenToTask(t, downAtom.state, store);
|
||||
}
|
||||
ensureDownloadsAreRunning();
|
||||
}
|
||||
run();
|
||||
}, [store]);
|
||||
};
|
@ -24,7 +24,6 @@ import {
|
||||
Movie,
|
||||
MovieP,
|
||||
QueryIdentifier,
|
||||
QueryPage,
|
||||
WatchInfo,
|
||||
WatchInfoP,
|
||||
useFetch,
|
||||
@ -46,24 +45,6 @@ import { WatchStatusObserver } from "./watch-status-observer";
|
||||
|
||||
type Item = (Movie & { type: "movie" }) | (Episode & { type: "episode" });
|
||||
|
||||
const query = (type: string, slug: string): QueryIdentifier<Item> =>
|
||||
type === "episode"
|
||||
? {
|
||||
path: ["episode", slug],
|
||||
params: {
|
||||
fields: ["nextEpisode", "previousEpisode", "show"],
|
||||
},
|
||||
parser: EpisodeP.transform((x) => ({ ...x, type: "episode" })),
|
||||
}
|
||||
: {
|
||||
path: ["movie", slug],
|
||||
parser: MovieP.transform((x) => ({ ...x, type: "movie" })),
|
||||
};
|
||||
const infoQuery = (type: string, slug: string): QueryIdentifier<WatchInfo> => ({
|
||||
path: ["video", type, slug, "info"],
|
||||
parser: WatchInfoP,
|
||||
});
|
||||
|
||||
const mapData = (
|
||||
data: Item | undefined,
|
||||
info: WatchInfo | undefined,
|
||||
@ -86,14 +67,14 @@ const mapData = (
|
||||
};
|
||||
};
|
||||
|
||||
export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({ slug, type }) => {
|
||||
export const Player = ({ slug, type }: { slug: string; type: "episode" | "movie" }) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const [playbackError, setPlaybackError] = useState<string | undefined>(undefined);
|
||||
const { data, error } = useFetch(query(type, slug));
|
||||
const { data: info, error: infoError } = useFetch(infoQuery(type, slug));
|
||||
const { data, error } = useFetch(Player.query(type, slug));
|
||||
const { data: info, error: infoError } = useFetch(Player.infoQuery(type, slug));
|
||||
const previous =
|
||||
data && data.type === "episode" && data.previousEpisode
|
||||
? `/watch/${data.previousEpisode.slug}`
|
||||
@ -181,4 +162,27 @@ export const Player: QueryPage<{ slug: string; type: "episode" | "movie" }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
Player.getFetchUrls = ({ slug, type }) => [query(type, slug), infoQuery(type, slug)];
|
||||
Player.query = (type: "episode" | "movie", slug: string): QueryIdentifier<Item> =>
|
||||
type === "episode"
|
||||
? {
|
||||
path: ["episode", slug],
|
||||
params: {
|
||||
fields: ["nextEpisode", "previousEpisode", "show"],
|
||||
},
|
||||
parser: EpisodeP.transform((x) => ({ ...x, type: "episode" })),
|
||||
}
|
||||
: {
|
||||
path: ["movie", slug],
|
||||
parser: MovieP.transform((x) => ({ ...x, type: "movie" })),
|
||||
};
|
||||
|
||||
Player.infoQuery = (type: "episode" | "movie", slug: string): QueryIdentifier<WatchInfo> => ({
|
||||
path: ["video", type, slug, "info"],
|
||||
parser: WatchInfoP,
|
||||
});
|
||||
|
||||
// if more queries are needed, dont forget to update download.tsx to cache those.
|
||||
Player.getFetchUrls = ({ slug, type }: { slug: string; type: "episode" | "movie" }) => [
|
||||
Player.query(type, slug),
|
||||
Player.infoQuery(type, slug),
|
||||
];
|
||||
|
@ -2857,6 +2857,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@kesha-antonov/react-native-background-downloader@npm:^2.10.0":
|
||||
version: 2.10.0
|
||||
resolution: "@kesha-antonov/react-native-background-downloader@npm:2.10.0"
|
||||
peerDependencies:
|
||||
react-native: ">=0.57.0"
|
||||
checksum: f56e4bf5f28b07d8b9e776ce3bdc6ed715ef3d9a95d3ec67b4852dc2992c51283550b5406106454557654a1c2503e920ec62d895aa6240aae97ba4f029df8a32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@kyoo/models@workspace:^, @kyoo/models@workspace:packages/models":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@kyoo/models@workspace:packages/models"
|
||||
@ -2869,6 +2878,9 @@ __metadata:
|
||||
"@tanstack/react-query": "*"
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
dependenciesMeta:
|
||||
"@kesha-antonov/react-native-background-downloader":
|
||||
optional: true
|
||||
peerDependenciesMeta:
|
||||
react-native-web:
|
||||
optional: true
|
||||
@ -2935,6 +2947,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@kyoo/ui@workspace:packages/ui"
|
||||
dependencies:
|
||||
"@kesha-antonov/react-native-background-downloader": ^2.10.0
|
||||
"@kyoo/models": "workspace:^"
|
||||
"@kyoo/primitives": "workspace:^"
|
||||
"@shopify/flash-list": ^1.6.3
|
||||
@ -2942,6 +2955,7 @@ __metadata:
|
||||
react-native-uuid: ^2.0.1
|
||||
typescript: ^5.3.2
|
||||
peerDependencies:
|
||||
"@kesha-antonov/react-native-background-downloader": "*"
|
||||
"@material-symbols/svg-400": "*"
|
||||
"@shopify/flash-list": ^1.3.1
|
||||
"@tanstack/react-query": "*"
|
||||
@ -2954,6 +2968,12 @@ __metadata:
|
||||
react-native-reanimated: "*"
|
||||
react-native-svg: "*"
|
||||
yoshiki: "*"
|
||||
dependenciesMeta:
|
||||
"@kesha-antonov/react-native-background-downloader":
|
||||
optional: true
|
||||
peerDependenciesMeta:
|
||||
"@kesha-antonov/react-native-background-downloader":
|
||||
optional: true
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@ -11269,6 +11289,7 @@ __metadata:
|
||||
"@formatjs/intl-displaynames": ^6.6.4
|
||||
"@formatjs/intl-locale": ^3.4.3
|
||||
"@gorhom/portal": ^1.0.14
|
||||
"@kesha-antonov/react-native-background-downloader": ^2.10.0
|
||||
"@kyoo/ui": "workspace:^"
|
||||
"@material-symbols/svg-400": ^0.14.1
|
||||
"@shopify/flash-list": 1.4.3
|
||||
|
Loading…
x
Reference in New Issue
Block a user