mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 12:14:46 -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-displaynames": "^6.6.4",
|
||||||
"@formatjs/intl-locale": "^3.4.3",
|
"@formatjs/intl-locale": "^3.4.3",
|
||||||
"@gorhom/portal": "^1.0.14",
|
"@gorhom/portal": "^1.0.14",
|
||||||
|
"@kesha-antonov/react-native-background-downloader": "^2.10.0",
|
||||||
"@kyoo/ui": "workspace:^",
|
"@kyoo/ui": "workspace:^",
|
||||||
"@material-symbols/svg-400": "^0.14.1",
|
"@material-symbols/svg-400": "^0.14.1",
|
||||||
"@shopify/flash-list": "1.4.3",
|
"@shopify/flash-list": "1.4.3",
|
||||||
|
@ -22,7 +22,7 @@ import { z } from "zod";
|
|||||||
import { Account, AccountP } from "./accounts";
|
import { Account, AccountP } from "./accounts";
|
||||||
import { MMKV } from "react-native-mmkv";
|
import { MMKV } from "react-native-mmkv";
|
||||||
|
|
||||||
const storage = new MMKV();
|
export const storage = new MMKV();
|
||||||
|
|
||||||
const readAccounts = () => {
|
const readAccounts = () => {
|
||||||
const acc = storage.getString("accounts");
|
const acc = storage.getString("accounts");
|
||||||
|
@ -40,7 +40,7 @@ export let kyooApiUrl = kyooUrl;
|
|||||||
|
|
||||||
export const queryFn = async <Data,>(
|
export const queryFn = async <Data,>(
|
||||||
context:
|
context:
|
||||||
| (QueryFunctionContext & { timeout?: number })
|
| (QueryFunctionContext & { timeout?: number, apiUrl?: string })
|
||||||
| {
|
| {
|
||||||
path: (string | false | undefined | null)[];
|
path: (string | false | undefined | null)[];
|
||||||
body?: object;
|
body?: object;
|
||||||
@ -52,7 +52,6 @@ export const queryFn = async <Data,>(
|
|||||||
type?: z.ZodType<Data>,
|
type?: z.ZodType<Data>,
|
||||||
token?: string | null,
|
token?: string | null,
|
||||||
): Promise<Data> => {
|
): Promise<Data> => {
|
||||||
// @ts-ignore
|
|
||||||
const url = context.apiUrl ?? (Platform.OS === "web" ? kyooUrl : getCurrentAccount()!.apiUrl);
|
const url = context.apiUrl ?? (Platform.OS === "web" ? kyooUrl : getCurrentAccount()!.apiUrl);
|
||||||
kyooApiUrl = url;
|
kyooApiUrl = url;
|
||||||
|
|
||||||
@ -166,7 +165,7 @@ export type QueryPage<Props = {}, Items = unknown> = ComponentType<
|
|||||||
randomItems?: Items[];
|
randomItems?: Items[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const toQueryKey = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => {
|
export const toQueryKey = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => {
|
||||||
if (query.params) {
|
if (query.params) {
|
||||||
return [
|
return [
|
||||||
...query.path,
|
...query.path,
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"typescript": "^5.3.2"
|
"typescript": "^5.3.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@kesha-antonov/react-native-background-downloader": "*",
|
||||||
"@material-symbols/svg-400": "*",
|
"@material-symbols/svg-400": "*",
|
||||||
"@shopify/flash-list": "^1.3.1",
|
"@shopify/flash-list": "^1.3.1",
|
||||||
"@tanstack/react-query": "*",
|
"@tanstack/react-query": "*",
|
||||||
@ -26,5 +27,13 @@
|
|||||||
"react-native-reanimated": "*",
|
"react-native-reanimated": "*",
|
||||||
"react-native-svg": "*",
|
"react-native-svg": "*",
|
||||||
"yoshiki": "*"
|
"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,
|
Movie,
|
||||||
MovieP,
|
MovieP,
|
||||||
QueryIdentifier,
|
QueryIdentifier,
|
||||||
QueryPage,
|
|
||||||
WatchInfo,
|
WatchInfo,
|
||||||
WatchInfoP,
|
WatchInfoP,
|
||||||
useFetch,
|
useFetch,
|
||||||
@ -46,24 +45,6 @@ import { WatchStatusObserver } from "./watch-status-observer";
|
|||||||
|
|
||||||
type Item = (Movie & { type: "movie" }) | (Episode & { type: "episode" });
|
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 = (
|
const mapData = (
|
||||||
data: Item | undefined,
|
data: Item | undefined,
|
||||||
info: WatchInfo | 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 { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [playbackError, setPlaybackError] = useState<string | undefined>(undefined);
|
const [playbackError, setPlaybackError] = useState<string | undefined>(undefined);
|
||||||
const { data, error } = useFetch(query(type, slug));
|
const { data, error } = useFetch(Player.query(type, slug));
|
||||||
const { data: info, error: infoError } = useFetch(infoQuery(type, slug));
|
const { data: info, error: infoError } = useFetch(Player.infoQuery(type, slug));
|
||||||
const previous =
|
const previous =
|
||||||
data && data.type === "episode" && data.previousEpisode
|
data && data.type === "episode" && data.previousEpisode
|
||||||
? `/watch/${data.previousEpisode.slug}`
|
? `/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
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@kyoo/models@workspace:^, @kyoo/models@workspace:packages/models":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@kyoo/models@workspace:packages/models"
|
resolution: "@kyoo/models@workspace:packages/models"
|
||||||
@ -2869,6 +2878,9 @@ __metadata:
|
|||||||
"@tanstack/react-query": "*"
|
"@tanstack/react-query": "*"
|
||||||
react: "*"
|
react: "*"
|
||||||
react-native: "*"
|
react-native: "*"
|
||||||
|
dependenciesMeta:
|
||||||
|
"@kesha-antonov/react-native-background-downloader":
|
||||||
|
optional: true
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
react-native-web:
|
react-native-web:
|
||||||
optional: true
|
optional: true
|
||||||
@ -2935,6 +2947,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@kyoo/ui@workspace:packages/ui"
|
resolution: "@kyoo/ui@workspace:packages/ui"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@kesha-antonov/react-native-background-downloader": ^2.10.0
|
||||||
"@kyoo/models": "workspace:^"
|
"@kyoo/models": "workspace:^"
|
||||||
"@kyoo/primitives": "workspace:^"
|
"@kyoo/primitives": "workspace:^"
|
||||||
"@shopify/flash-list": ^1.6.3
|
"@shopify/flash-list": ^1.6.3
|
||||||
@ -2942,6 +2955,7 @@ __metadata:
|
|||||||
react-native-uuid: ^2.0.1
|
react-native-uuid: ^2.0.1
|
||||||
typescript: ^5.3.2
|
typescript: ^5.3.2
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
"@kesha-antonov/react-native-background-downloader": "*"
|
||||||
"@material-symbols/svg-400": "*"
|
"@material-symbols/svg-400": "*"
|
||||||
"@shopify/flash-list": ^1.3.1
|
"@shopify/flash-list": ^1.3.1
|
||||||
"@tanstack/react-query": "*"
|
"@tanstack/react-query": "*"
|
||||||
@ -2954,6 +2968,12 @@ __metadata:
|
|||||||
react-native-reanimated: "*"
|
react-native-reanimated: "*"
|
||||||
react-native-svg: "*"
|
react-native-svg: "*"
|
||||||
yoshiki: "*"
|
yoshiki: "*"
|
||||||
|
dependenciesMeta:
|
||||||
|
"@kesha-antonov/react-native-background-downloader":
|
||||||
|
optional: true
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@kesha-antonov/react-native-background-downloader":
|
||||||
|
optional: true
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@ -11269,6 +11289,7 @@ __metadata:
|
|||||||
"@formatjs/intl-displaynames": ^6.6.4
|
"@formatjs/intl-displaynames": ^6.6.4
|
||||||
"@formatjs/intl-locale": ^3.4.3
|
"@formatjs/intl-locale": ^3.4.3
|
||||||
"@gorhom/portal": ^1.0.14
|
"@gorhom/portal": ^1.0.14
|
||||||
|
"@kesha-antonov/react-native-background-downloader": ^2.10.0
|
||||||
"@kyoo/ui": "workspace:^"
|
"@kyoo/ui": "workspace:^"
|
||||||
"@material-symbols/svg-400": ^0.14.1
|
"@material-symbols/svg-400": ^0.14.1
|
||||||
"@shopify/flash-list": 1.4.3
|
"@shopify/flash-list": 1.4.3
|
||||||
|
Loading…
x
Reference in New Issue
Block a user