mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 07:31:50 +01:00
Merge branch 'dev' into feature/i18n
This commit is contained in:
commit
f0b8724763
5
.env
5
.env
@ -1,5 +0,0 @@
|
|||||||
# this is to prevent warnings in webpack builds
|
|
||||||
GENERATE_SOURCEMAP=false
|
|
||||||
|
|
||||||
# uncomment and add the following line to `.env.local` if you are running behind a proxy or on a subdirectory
|
|
||||||
# PUBLIC_URL=https://your-domain.com/directory-here
|
|
49
index.html
Normal file
49
index.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||||
|
<script
|
||||||
|
async
|
||||||
|
src="https://www.googletagmanager.com/gtag/js?id=G-44YVXRL61C"
|
||||||
|
></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag() {
|
||||||
|
dataLayer.push(arguments);
|
||||||
|
}
|
||||||
|
gtag("js", new Date());
|
||||||
|
|
||||||
|
gtag("config", "G-44YVXRL61C");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Because watching movies legally is boring"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#E880C5" />
|
||||||
|
<meta name="msapplication-TileColor" content="#E880C5" />
|
||||||
|
<meta name="theme-color" content="#E880C5" />
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<title>movie-web</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
18
package.json
18
package.json
@ -13,13 +13,13 @@
|
|||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"srt-webvtt": "^2.0.0",
|
"srt-webvtt": "^2.0.0",
|
||||||
"unpacker": "^1.0.1"
|
"unpacker": "^1.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"dev": "vite",
|
||||||
"build": "react-scripts build",
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
"lint": "eslint --ext .tsx,.ts src",
|
"lint": "eslint --ext .tsx,.ts src",
|
||||||
"lint:strict": "eslint --ext .tsx,.ts --max-warnings 0 src"
|
"lint:strict": "eslint --ext .tsx,.ts --max-warnings 0 src"
|
||||||
},
|
},
|
||||||
@ -44,7 +44,8 @@
|
|||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||||
"@typescript-eslint/parser": "^5.13.0",
|
"@typescript-eslint/parser": "^5.13.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||||
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
"eslint-config-airbnb": "19.0.4",
|
"eslint-config-airbnb": "19.0.4",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
@ -53,11 +54,12 @@
|
|||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
"eslint-plugin-react": "7.29.4",
|
"eslint-plugin-react": "7.29.4",
|
||||||
"eslint-plugin-react-hooks": "4.3.0",
|
"eslint-plugin-react-hooks": "4.3.0",
|
||||||
"postcss": "^8.4.6",
|
"postcss": "^8.4.20",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.7",
|
"prettier-plugin-tailwindcss": "^0.1.7",
|
||||||
"tailwind-scrollbar": "^1.3.1",
|
"tailwind-scrollbar": "^2.0.1",
|
||||||
"tailwindcss": "^3.0.20",
|
"tailwindcss": "^3.2.4",
|
||||||
"typescript": "^4.6.4"
|
"typescript": "^4.6.4",
|
||||||
|
"vite": "^4.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-44YVXRL61C"></script>
|
|
||||||
<script>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag(){dataLayer.push(arguments);}
|
|
||||||
gtag('js', new Date());
|
|
||||||
|
|
||||||
gtag('config', 'G-44YVXRL61C');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Because watching movies legally is boring"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png">
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest">
|
|
||||||
<link rel="mask-icon" href="%PUBLIC_URL%/safari-pinned-tab.svg" color="#E880C5">
|
|
||||||
<meta name="msapplication-TileColor" content="#E880C5">
|
|
||||||
<meta name="theme-color" content="#E880C5">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<title>movie-web</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,8 +1,8 @@
|
|||||||
import { MWMediaType } from "providers";
|
|
||||||
import { Redirect, Route, Switch } from "react-router-dom";
|
import { Redirect, Route, Switch } from "react-router-dom";
|
||||||
import { BookmarkContextProvider } from "state/bookmark";
|
import { MWMediaType } from "@/providers";
|
||||||
import { WatchedContextProvider } from "state/watched";
|
import { BookmarkContextProvider } from "@/state/bookmark";
|
||||||
import { NotFoundPage } from "views/notfound/NotFoundView";
|
import { WatchedContextProvider } from "@/state/watched";
|
||||||
|
import { NotFoundPage } from "@/views/notfound/NotFoundView";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { MediaView } from "./views/MediaView";
|
import { MediaView } from "./views/MediaView";
|
||||||
import { SearchView } from "./views/SearchView";
|
import { SearchView } from "./views/SearchView";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Icon, Icons } from "components/Icon";
|
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
|
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
export interface OptionItem {
|
export interface OptionItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -14,48 +14,46 @@ interface DropdownProps {
|
|||||||
options: Array<OptionItem>;
|
options: Array<OptionItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>(
|
export function Dropdown(props: DropdownProps) {
|
||||||
(props: DropdownProps) => (
|
<div className="relative my-4 max-w-[18rem]">
|
||||||
<div className="relative my-4 max-w-[18rem]">
|
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}>
|
||||||
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}>
|
{({ open }) => (
|
||||||
{({ open }) => (
|
<>
|
||||||
<>
|
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-denim-500 py-2 pl-3 pr-10 text-left text-white shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-bink-500 focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-bink-300 sm:text-sm">
|
||||||
<Listbox.Button className="bg-denim-500 focus-visible:ring-bink-500 focus-visible:ring-offset-bink-300 relative w-full cursor-default rounded-lg py-2 pl-3 pr-10 text-left text-white shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-offset-2 sm:text-sm">
|
<span className="block truncate">{props.selectedItem.name}</span>
|
||||||
<span className="block truncate">{props.selectedItem.name}</span>
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<Icon
|
||||||
<Icon
|
icon={Icons.CHEVRON_DOWN}
|
||||||
icon={Icons.CHEVRON_DOWN}
|
className={`transform transition-transform ${
|
||||||
className={`transform transition-transform ${
|
open ? "rotate-180" : ""
|
||||||
open ? "rotate-180" : ""
|
}`}
|
||||||
}`}
|
/>
|
||||||
/>
|
</span>
|
||||||
</span>
|
</Listbox.Button>
|
||||||
</Listbox.Button>
|
<Transition
|
||||||
<Transition
|
as={Fragment}
|
||||||
as={Fragment}
|
leave="transition ease-in duration-100"
|
||||||
leave="transition ease-in duration-100"
|
leaveFrom="opacity-100"
|
||||||
leaveFrom="opacity-100"
|
leaveTo="opacity-0"
|
||||||
leaveTo="opacity-0"
|
>
|
||||||
>
|
<Listbox.Options className="absolute bottom-11 left-0 right-0 z-10 mt-1 max-h-60 overflow-auto rounded-md bg-denim-500 py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 focus:outline-none sm:bottom-10 sm:text-sm">
|
||||||
<Listbox.Options className="bg-denim-500 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 absolute bottom-11 left-0 right-0 z-10 mt-1 max-h-60 overflow-auto rounded-md py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:bottom-10 sm:text-sm">
|
{props.options.map((opt) => (
|
||||||
{props.options.map((opt) => (
|
<Listbox.Option
|
||||||
<Listbox.Option
|
className={({ active }) =>
|
||||||
className={({ active }) =>
|
`relative cursor-default select-none py-2 pl-10 pr-4 ${
|
||||||
`relative cursor-default select-none py-2 pl-10 pr-4 ${
|
active ? "bg-denim-400 text-bink-700" : "text-white"
|
||||||
active ? "bg-denim-400 text-bink-700" : "text-white"
|
}`
|
||||||
}`
|
}
|
||||||
}
|
key={opt.id}
|
||||||
key={opt.id}
|
value={opt}
|
||||||
value={opt}
|
>
|
||||||
>
|
{opt.name}
|
||||||
{opt.name}
|
</Listbox.Option>
|
||||||
</Listbox.Option>
|
))}
|
||||||
))}
|
</Listbox.Options>
|
||||||
</Listbox.Options>
|
</Transition>
|
||||||
</Transition>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</Listbox>
|
||||||
</Listbox>
|
</div>;
|
||||||
</div>
|
}
|
||||||
)
|
|
||||||
);
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { MWMediaType, MWQuery } from "providers";
|
import { MWMediaType, MWQuery } from "@/providers";
|
||||||
import { DropdownButton } from "./buttons/DropdownButton";
|
import { DropdownButton } from "./buttons/DropdownButton";
|
||||||
import { Icons } from "./Icon";
|
import { Icons } from "./Icon";
|
||||||
import { TextInputControl } from "./text-inputs/TextInputControl";
|
import { TextInputControl } from "./text-inputs/TextInputControl";
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Icon, Icons } from "components/Icon";
|
|
||||||
import React, {
|
import React, {
|
||||||
MouseEventHandler,
|
MouseEventHandler,
|
||||||
SyntheticEvent,
|
SyntheticEvent,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
import { Backdrop, useBackdrop } from "components/layout/Backdrop";
|
import { Backdrop, useBackdrop } from "@/components/layout/Backdrop";
|
||||||
import { ButtonControlProps, ButtonControl } from "./ButtonControl";
|
import { ButtonControlProps, ButtonControl } from "./ButtonControl";
|
||||||
|
|
||||||
export interface OptionItem {
|
export interface OptionItem {
|
||||||
@ -33,7 +33,7 @@ export interface OptionProps {
|
|||||||
function Option({ option, onClick, tabIndex }: OptionProps) {
|
function Option({ option, onClick, tabIndex }: OptionProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="text-denim-700 flex h-10 cursor-pointer items-center space-x-2 px-4 py-2 text-left transition-colors hover:text-white"
|
className="flex h-10 cursor-pointer items-center space-x-2 px-4 py-2 text-left text-denim-700 transition-colors hover:text-white"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
>
|
>
|
||||||
@ -95,7 +95,7 @@ export const DropdownButton = React.forwardRef<
|
|||||||
>
|
>
|
||||||
<ButtonControl
|
<ButtonControl
|
||||||
{...props}
|
{...props}
|
||||||
className="sm:justify-left bg-bink-200 hover:bg-bink-300 relative z-20 flex h-10 w-full items-center justify-center space-x-2 rounded-[20px] px-4 py-2 text-white"
|
className="sm:justify-left relative z-20 flex h-10 w-full items-center justify-center space-x-2 rounded-[20px] bg-bink-200 px-4 py-2 text-white hover:bg-bink-300"
|
||||||
>
|
>
|
||||||
<Icon icon={selectedItem.icon} />
|
<Icon icon={selectedItem.icon} />
|
||||||
<span className="flex-1">{selectedItem.name}</span>
|
<span className="flex-1">{selectedItem.name}</span>
|
||||||
@ -105,7 +105,7 @@ export const DropdownButton = React.forwardRef<
|
|||||||
/>
|
/>
|
||||||
</ButtonControl>
|
</ButtonControl>
|
||||||
<div
|
<div
|
||||||
className={`bg-denim-300 absolute top-0 z-10 w-full rounded-[20px] pt-[40px] transition-all duration-200 ${
|
className={`absolute top-0 z-10 w-full rounded-[20px] bg-denim-300 pt-[40px] transition-all duration-200 ${
|
||||||
props.open
|
props.open
|
||||||
? "block max-h-60 opacity-100"
|
? "block max-h-60 opacity-100"
|
||||||
: "invisible max-h-0 opacity-0"
|
: "invisible max-h-0 opacity-0"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Icon, Icons } from "components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { ButtonControlProps, ButtonControl } from "./ButtonControl";
|
import { ButtonControlProps, ButtonControl } from "./ButtonControl";
|
||||||
|
|
||||||
export interface IconButtonProps extends ButtonControlProps {
|
export interface IconButtonProps extends ButtonControlProps {
|
||||||
@ -9,7 +9,7 @@ export function IconButton(props: IconButtonProps) {
|
|||||||
return (
|
return (
|
||||||
<ButtonControl
|
<ButtonControl
|
||||||
{...props}
|
{...props}
|
||||||
className="flex items-center px-4 py-2 space-x-2 bg-bink-200 hover:bg-bink-300 text-white rounded-full"
|
className="flex items-center space-x-2 rounded-full bg-bink-200 px-4 py-2 text-white hover:bg-bink-300"
|
||||||
>
|
>
|
||||||
<Icon icon={props.icon} />
|
<Icon icon={props.icon} />
|
||||||
<span>{props.children}</span>
|
<span>{props.children}</span>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Icon, Icons } from "components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
export interface IconPatchProps {
|
export interface IconPatchProps {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
@ -12,11 +12,11 @@ export function IconPatch(props: IconPatchProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={props.className || undefined} onClick={props.onClick}>
|
<div className={props.className || undefined} onClick={props.onClick}>
|
||||||
<div
|
<div
|
||||||
className={`bg-denim-300 flex h-12 w-12 items-center justify-center rounded-full border-2 border-transparent transition-[color,transform,border-color] duration-75 ${
|
className={`flex h-12 w-12 items-center justify-center rounded-full border-2 border-transparent bg-denim-300 transition-[color,transform,border-color] duration-75 ${
|
||||||
props.clickable
|
props.clickable
|
||||||
? "hover:bg-denim-400 cursor-pointer hover:scale-110 hover:text-white active:scale-125"
|
? "cursor-pointer hover:scale-110 hover:bg-denim-400 hover:text-white active:scale-125"
|
||||||
: ""
|
: ""
|
||||||
} ${props.active ? "text-bink-600 border-bink-600 bg-bink-100" : ""}`}
|
} ${props.active ? "border-bink-600 bg-bink-100 text-bink-600" : ""}`}
|
||||||
>
|
>
|
||||||
<Icon icon={props.icon} />
|
<Icon icon={props.icon} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useFade } from "hooks/useFade";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useFade } from "@/hooks/useFade";
|
||||||
|
|
||||||
interface BackdropProps {
|
interface BackdropProps {
|
||||||
onClick?: (e: MouseEvent) => void;
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Icon, Icons } from "components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
export function BrandPill(props: { clickable?: boolean }) {
|
export function BrandPill(props: { clickable?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`bg-bink-100 text-bink-600 flex items-center space-x-2 rounded-full bg-opacity-50 px-4 py-2 ${
|
className={`flex items-center space-x-2 rounded-full bg-bink-100 bg-opacity-50 px-4 py-2 text-bink-600 ${
|
||||||
props.clickable
|
props.clickable
|
||||||
? "hover:bg-bink-200 hover:text-bink-700 transition-[transform,background-color] hover:scale-105 active:scale-95"
|
? "transition-[transform,background-color] hover:scale-105 hover:bg-bink-200 hover:text-bink-700 active:scale-95"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { IconPatch } from "components/buttons/IconPatch";
|
|
||||||
import { Icons } from "components/Icon";
|
|
||||||
import { Link } from "components/text/Link";
|
|
||||||
import { Title } from "components/text/Title";
|
|
||||||
import { DISCORD_LINK, GITHUB_LINK } from "mw_constants";
|
|
||||||
import { Component } from "react";
|
import { Component } from "react";
|
||||||
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { Link } from "@/components/text/Link";
|
||||||
|
import { Title } from "@/components/text/Title";
|
||||||
|
import { DISCORD_LINK, GITHUB_LINK } from "@/mw_constants";
|
||||||
|
|
||||||
interface ErrorBoundaryState {
|
interface ErrorBoundaryState {
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { IconPatch } from "components/buttons/IconPatch";
|
|
||||||
import { Icons } from "components/Icon";
|
|
||||||
import { DISCORD_LINK, GITHUB_LINK } from "mw_constants";
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { DISCORD_LINK, GITHUB_LINK } from "@/mw_constants";
|
||||||
import { BrandPill } from "./BrandPill";
|
import { BrandPill } from "./BrandPill";
|
||||||
|
|
||||||
export interface NavigationProps {
|
export interface NavigationProps {
|
||||||
@ -11,8 +11,8 @@ export interface NavigationProps {
|
|||||||
|
|
||||||
export function Navigation(props: NavigationProps) {
|
export function Navigation(props: NavigationProps) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute left-0 right-0 top-0 flex items-center justify-between py-5 px-7 min-h-[88px]">
|
<div className="absolute left-0 right-0 top-0 flex min-h-[88px] items-center justify-between py-5 px-7">
|
||||||
<div className="flex items-center justify-center w-full sm:w-fit">
|
<div className="flex w-full items-center justify-center sm:w-fit">
|
||||||
<div className="mr-auto sm:mr-6">
|
<div className="mr-auto sm:mr-6">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<BrandPill clickable />
|
<BrandPill clickable />
|
||||||
@ -20,7 +20,11 @@ export function Navigation(props: NavigationProps) {
|
|||||||
</div>
|
</div>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
<div className={`${props.children ? "hidden sm:flex" : "flex"} flex-row gap-4`}>
|
<div
|
||||||
|
className={`${
|
||||||
|
props.children ? "hidden sm:flex" : "flex"
|
||||||
|
} flex-row gap-4`}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href={DISCORD_LINK}
|
href={DISCORD_LINK}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { IconPatch } from "components/buttons/IconPatch";
|
import { useEffect, useState } from "react";
|
||||||
import { Dropdown, OptionItem } from "components/Dropdown";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Icons } from "components/Icon";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { WatchedEpisode } from "components/media/WatchedEpisodeButton";
|
import { Dropdown, OptionItem } from "@/components/Dropdown";
|
||||||
import { useLoading } from "hooks/useLoading";
|
import { Icons } from "@/components/Icon";
|
||||||
import { serializePortableMedia } from "hooks/usePortableMedia";
|
import { WatchedEpisode } from "@/components/media/WatchedEpisodeButton";
|
||||||
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
|
import { serializePortableMedia } from "@/hooks/usePortableMedia";
|
||||||
import {
|
import {
|
||||||
convertMediaToPortable,
|
convertMediaToPortable,
|
||||||
MWMedia,
|
MWMedia,
|
||||||
MWMediaSeasons,
|
MWMediaSeasons,
|
||||||
MWMediaSeason,
|
MWMediaSeason,
|
||||||
MWPortableMedia,
|
MWPortableMedia,
|
||||||
} from "providers";
|
} from "@/providers";
|
||||||
import { getSeasonDataFromMedia } from "providers/methods/seasons";
|
import { getSeasonDataFromMedia } from "@/providers/methods/seasons";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
|
|
||||||
export interface SeasonsProps {
|
export interface SeasonsProps {
|
||||||
media: MWMedia;
|
media: MWMedia;
|
||||||
@ -23,13 +23,13 @@ export function LoadingSeasons(props: { error?: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-denim-400 mb-3 mt-5 h-10 w-56 rounded opacity-50" />
|
<div className="mb-3 mt-5 h-10 w-56 rounded bg-denim-400 opacity-50" />
|
||||||
</div>
|
</div>
|
||||||
{!props.error ? (
|
{!props.error ? (
|
||||||
<>
|
<>
|
||||||
<div className="bg-denim-400 mr-3 mb-3 inline-block h-10 w-10 rounded opacity-50" />
|
<div className="mr-3 mb-3 inline-block h-10 w-10 rounded bg-denim-400 opacity-50" />
|
||||||
<div className="bg-denim-400 mr-3 mb-3 inline-block h-10 w-10 rounded opacity-50" />
|
<div className="mr-3 mb-3 inline-block h-10 w-10 rounded bg-denim-400 opacity-50" />
|
||||||
<div className="bg-denim-400 mr-3 mb-3 inline-block h-10 w-10 rounded opacity-50" />
|
<div className="mr-3 mb-3 inline-block h-10 w-10 rounded bg-denim-400 opacity-50" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Icon, Icons } from "components/Icon";
|
|
||||||
import { ArrowLink } from "components/text/ArrowLink";
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { ArrowLink } from "@/components/text/ArrowLink";
|
||||||
|
|
||||||
interface SectionHeadingProps {
|
interface SectionHeadingProps {
|
||||||
icon?: Icons;
|
icon?: Icons;
|
||||||
@ -15,7 +15,7 @@ export function SectionHeading(props: SectionHeadingProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={`mt-12 ${props.className}`}>
|
<div className={`mt-12 ${props.className}`}>
|
||||||
<div className="mb-4 flex items-end">
|
<div className="mb-4 flex items-end">
|
||||||
<p className="text-denim-700 flex flex-1 items-center font-bold uppercase">
|
<p className="flex flex-1 items-center font-bold uppercase text-denim-700">
|
||||||
{props.icon ? (
|
{props.icon ? (
|
||||||
<span className="mr-2 text-xl">
|
<span className="mr-2 text-xl">
|
||||||
<Icon icon={props.icon} />
|
<Icon icon={props.icon} />
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
convertMediaToPortable,
|
convertMediaToPortable,
|
||||||
getProviderFromId,
|
getProviderFromId,
|
||||||
MWMediaMeta,
|
MWMediaMeta,
|
||||||
MWMediaType,
|
MWMediaType,
|
||||||
} from "providers";
|
} from "@/providers";
|
||||||
import { Link } from "react-router-dom";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Icon, Icons } from "components/Icon";
|
import { serializePortableMedia } from "@/hooks/usePortableMedia";
|
||||||
import { serializePortableMedia } from "hooks/usePortableMedia";
|
import { DotList } from "@/components/text/DotList";
|
||||||
import { DotList } from "components/text/DotList";
|
|
||||||
|
|
||||||
export interface MediaCardProps {
|
export interface MediaCardProps {
|
||||||
media: MWMediaMeta;
|
media: MWMediaMeta;
|
||||||
@ -30,7 +30,7 @@ function MediaCardContent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
className={`bg-denim-300 group relative mb-4 flex overflow-hidden rounded py-4 px-5 ${
|
className={`group relative mb-4 flex overflow-hidden rounded bg-denim-300 py-4 px-5 ${
|
||||||
linkable ? "hover:bg-denim-400" : ""
|
linkable ? "hover:bg-denim-400" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -38,12 +38,12 @@ function MediaCardContent({
|
|||||||
{watchedPercentage > 0 ? (
|
{watchedPercentage > 0 ? (
|
||||||
<div className="absolute top-0 left-0 right-0 bottom-0">
|
<div className="absolute top-0 left-0 right-0 bottom-0">
|
||||||
<div
|
<div
|
||||||
className="bg-bink-300 relative h-full bg-opacity-30"
|
className="relative h-full bg-bink-300 bg-opacity-30"
|
||||||
style={{
|
style={{
|
||||||
width: `${watchedPercentage}%`,
|
width: `${watchedPercentage}%`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="from-bink-400 absolute right-0 top-0 bottom-0 ml-auto w-40 bg-gradient-to-l to-transparent opacity-40" />
|
<div className="absolute right-0 top-0 bottom-0 ml-auto w-40 bg-gradient-to-l from-bink-400 to-transparent opacity-40" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@ -54,7 +54,7 @@ function MediaCardContent({
|
|||||||
<h1 className="mb-1 font-bold text-white">
|
<h1 className="mb-1 font-bold text-white">
|
||||||
{media.title}
|
{media.title}
|
||||||
{series && media.seasonId && media.episodeId ? (
|
{series && media.seasonId && media.episodeId ? (
|
||||||
<span className="text-denim-700 ml-2 text-xs">
|
<span className="ml-2 text-xs text-denim-700">
|
||||||
S{media.seasonId} E{media.episodeId}
|
S{media.seasonId} E{media.episodeId}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { IconPatch } from "components/buttons/IconPatch";
|
|
||||||
import { Icons } from "components/Icon";
|
|
||||||
import { Loading } from "components/layout/Loading";
|
|
||||||
import { MWMediaCaption, MWMediaStream } from "providers";
|
|
||||||
import { ReactElement, useEffect, useRef, useState } from "react";
|
import { ReactElement, useEffect, useRef, useState } from "react";
|
||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { Loading } from "@/components/layout/Loading";
|
||||||
|
import { MWMediaCaption, MWMediaStream } from "@/providers";
|
||||||
|
|
||||||
export interface VideoPlayerProps {
|
export interface VideoPlayerProps {
|
||||||
source: MWMediaStream;
|
source: MWMediaStream;
|
||||||
@ -14,7 +14,7 @@ export interface VideoPlayerProps {
|
|||||||
|
|
||||||
export function SkeletonVideoPlayer(props: { error?: boolean }) {
|
export function SkeletonVideoPlayer(props: { error?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-denim-200 flex aspect-video w-full items-center justify-center lg:rounded-xl">
|
<div className="flex aspect-video w-full items-center justify-center bg-denim-200 lg:rounded-xl">
|
||||||
{props.error ? (
|
{props.error ? (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<IconPatch icon={Icons.WARNING} className="text-red-400" />
|
<IconPatch icon={Icons.WARNING} className="text-red-400" />
|
||||||
@ -44,8 +44,7 @@ export function VideoPlayer(props: VideoPlayerProps) {
|
|||||||
|
|
||||||
// hls support
|
// hls support
|
||||||
if (mustUseHls) {
|
if (mustUseHls) {
|
||||||
if (!videoRef.current)
|
if (!videoRef.current) return;
|
||||||
return;
|
|
||||||
|
|
||||||
if (!Hls.isSupported()) {
|
if (!Hls.isSupported()) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -55,7 +54,7 @@ export function VideoPlayer(props: VideoPlayerProps) {
|
|||||||
|
|
||||||
const hls = new Hls();
|
const hls = new Hls();
|
||||||
|
|
||||||
if (videoRef.current.canPlayType('application/vnd.apple.mpegurl')) {
|
if (videoRef.current.canPlayType("application/vnd.apple.mpegurl")) {
|
||||||
videoRef.current.src = props.source.url;
|
videoRef.current.src = props.source.url;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -81,8 +80,7 @@ export function VideoPlayer(props: VideoPlayerProps) {
|
|||||||
<>
|
<>
|
||||||
{skeletonUi}
|
{skeletonUi}
|
||||||
<video
|
<video
|
||||||
className={`bg-black w-full rounded-xl ${!showVideo ? "hidden" : ""
|
className={`w-full rounded-xl bg-black ${!showVideo ? "hidden" : ""}`}
|
||||||
}`}
|
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
onProgress={(e) =>
|
onProgress={(e) =>
|
||||||
props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent)
|
props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { getEpisodeFromMedia, MWMedia } from "providers";
|
import { getEpisodeFromMedia, MWMedia } from "@/providers";
|
||||||
import { useWatchedContext, getWatchedFromPortable } from "state/watched";
|
import { useWatchedContext, getWatchedFromPortable } from "@/state/watched";
|
||||||
import { Episode } from "./EpisodeButton";
|
import { Episode } from "./EpisodeButton";
|
||||||
|
|
||||||
export interface WatchedEpisodeProps {
|
export interface WatchedEpisodeProps {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MWMediaMeta } from "providers";
|
import { MWMediaMeta } from "@/providers";
|
||||||
import { useWatchedContext, getWatchedFromPortable } from "state/watched";
|
import { useWatchedContext, getWatchedFromPortable } from "@/state/watched";
|
||||||
import { MediaCard } from "./MediaCard";
|
import { MediaCard } from "./MediaCard";
|
||||||
|
|
||||||
export interface WatchedMediaCardProps {
|
export interface WatchedMediaCardProps {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Icon, Icons } from "components/Icon";
|
|
||||||
import { Link as LinkRouter } from "react-router-dom";
|
import { Link as LinkRouter } from "react-router-dom";
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
interface IArrowLinkPropsBase {
|
interface IArrowLinkPropsBase {
|
||||||
linkText: string;
|
linkText: string;
|
||||||
@ -26,7 +26,7 @@ export function ArrowLink(props: ArrowLinkProps) {
|
|||||||
const isExternal = !!(props as IArrowLinkPropsExternal).url;
|
const isExternal = !!(props as IArrowLinkPropsExternal).url;
|
||||||
const isInternal = !!(props as IArrowLinkPropsInternal).to;
|
const isInternal = !!(props as IArrowLinkPropsInternal).to;
|
||||||
const content = (
|
const content = (
|
||||||
<span className="text-bink-600 hover:text-bink-700 group inline-flex cursor-pointer items-center space-x-1 font-bold active:scale-95 mt-1 pr-1">
|
<span className="group mt-1 inline-flex cursor-pointer items-center space-x-1 pr-1 font-bold text-bink-600 hover:text-bink-700 active:scale-95">
|
||||||
{direction === "left" ? (
|
{direction === "left" ? (
|
||||||
<span className="text-xl transition-transform group-hover:-translate-x-1">
|
<span className="text-xl transition-transform group-hover:-translate-x-1">
|
||||||
<Icon icon={Icons.ARROW_LEFT} />
|
<Icon icon={Icons.ARROW_LEFT} />
|
||||||
@ -45,7 +45,9 @@ export function ArrowLink(props: ArrowLinkProps) {
|
|||||||
return <a href={(props as IArrowLinkPropsExternal).url}>{content}</a>;
|
return <a href={(props as IArrowLinkPropsExternal).url}>{content}</a>;
|
||||||
if (isInternal)
|
if (isInternal)
|
||||||
return (
|
return (
|
||||||
<LinkRouter to={(props as IArrowLinkPropsInternal).to}>{content}</LinkRouter>
|
<LinkRouter to={(props as IArrowLinkPropsInternal).to}>
|
||||||
|
{content}
|
||||||
|
</LinkRouter>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<span onClick={() => props.onClick && props.onClick()}>{content}</span>
|
<span onClick={() => props.onClick && props.onClick()}>{content}</span>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { MWPortableMedia } from "providers";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
import { MWPortableMedia } from "@/providers";
|
||||||
|
|
||||||
export function deserializePortableMedia(media: string): MWPortableMedia {
|
export function deserializePortableMedia(media: string): MWPortableMedia {
|
||||||
return JSON.parse(atob(decodeURIComponent(media)));
|
return JSON.parse(atob(decodeURIComponent(media)));
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { MWMediaType, MWQuery } from "providers";
|
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
|
import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
|
||||||
|
import { MWMediaType, MWQuery } from "@/providers";
|
||||||
|
|
||||||
export function useSearchQuery(): [
|
export function useSearchQuery(): [
|
||||||
MWQuery,
|
MWQuery,
|
||||||
|
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { HashRouter } from "react-router-dom";
|
import { HashRouter } from "react-router-dom";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { ErrorBoundary } from "components/layout/ErrorBoundary";
|
import { ErrorBoundary } from "@/components/layout/ErrorBoundary";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
@ -4,10 +4,10 @@ import {
|
|||||||
MWPortableMedia,
|
MWPortableMedia,
|
||||||
MWMediaStream,
|
MWMediaStream,
|
||||||
MWQuery,
|
MWQuery,
|
||||||
MWProviderMediaResult
|
MWProviderMediaResult,
|
||||||
} from "providers/types";
|
} from "@/providers/types";
|
||||||
|
|
||||||
import { CORS_PROXY_URL } from "mw_constants";
|
import { CORS_PROXY_URL } from "@/mw_constants";
|
||||||
|
|
||||||
export const flixhqProvider: MWMediaProvider = {
|
export const flixhqProvider: MWMediaProvider = {
|
||||||
id: "flixhq",
|
id: "flixhq",
|
||||||
@ -15,9 +15,13 @@ export const flixhqProvider: MWMediaProvider = {
|
|||||||
type: [MWMediaType.MOVIE],
|
type: [MWMediaType.MOVIE],
|
||||||
displayName: "flixhq",
|
displayName: "flixhq",
|
||||||
|
|
||||||
async getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult> {
|
async getMediaFromPortable(
|
||||||
|
media: MWPortableMedia
|
||||||
|
): Promise<MWProviderMediaResult> {
|
||||||
const searchRes = await fetch(
|
const searchRes = await fetch(
|
||||||
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/info?id=${encodeURIComponent(media.mediaId)}`
|
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/info?id=${encodeURIComponent(
|
||||||
|
media.mediaId
|
||||||
|
)}`
|
||||||
).then((d) => d.json());
|
).then((d) => d.json());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -29,39 +33,49 @@ export const flixhqProvider: MWMediaProvider = {
|
|||||||
|
|
||||||
async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> {
|
async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> {
|
||||||
const searchRes = await fetch(
|
const searchRes = await fetch(
|
||||||
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/${encodeURIComponent(query.searchQuery)}`
|
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/${encodeURIComponent(
|
||||||
|
query.searchQuery
|
||||||
|
)}`
|
||||||
).then((d) => d.json());
|
).then((d) => d.json());
|
||||||
|
|
||||||
const results: MWProviderMediaResult[] = (searchRes || []).results.map((item: any) => ({
|
const results: MWProviderMediaResult[] = (searchRes || []).results.map(
|
||||||
title: item.title,
|
(item: any) => ({
|
||||||
year: item.releaseDate,
|
title: item.title,
|
||||||
mediaId: item.id,
|
year: item.releaseDate,
|
||||||
type: MWMediaType.MOVIE,
|
mediaId: item.id,
|
||||||
}));
|
type: MWMediaType.MOVIE,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
|
async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
|
||||||
const searchRes = await fetch(
|
const searchRes = await fetch(
|
||||||
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/info?id=${encodeURIComponent(media.mediaId)}`
|
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/info?id=${encodeURIComponent(
|
||||||
|
media.mediaId
|
||||||
|
)}`
|
||||||
).then((d) => d.json());
|
).then((d) => d.json());
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
episodeId: searchRes.episodes[0].id,
|
episodeId: searchRes.episodes[0].id,
|
||||||
mediaId: media.mediaId
|
mediaId: media.mediaId,
|
||||||
})
|
});
|
||||||
|
|
||||||
const watchRes = await fetch(
|
const watchRes = await fetch(
|
||||||
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/watch?${encodeURIComponent(params.toString())}`
|
`${CORS_PROXY_URL}https://api.consumet.org/movies/flixhq/watch?${encodeURIComponent(
|
||||||
|
params.toString()
|
||||||
|
)}`
|
||||||
).then((d) => d.json());
|
).then((d) => d.json());
|
||||||
|
|
||||||
const source = watchRes.sources.reduce((p: any, c: any) => (c.quality > p.quality) ? c : p);
|
const source = watchRes.sources.reduce((p: any, c: any) =>
|
||||||
|
c.quality > p.quality ? c : p
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: source.url,
|
url: source.url,
|
||||||
type: source.isM3U8 ? "m3u8" : "mp4",
|
type: source.isM3U8 ? "m3u8" : "mp4",
|
||||||
captions: []
|
captions: [],
|
||||||
} as MWMediaStream;
|
} as MWMediaStream;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
|
import { unpack } from "unpacker";
|
||||||
|
import CryptoJS from "crypto-js";
|
||||||
import {
|
import {
|
||||||
MWMediaProvider,
|
MWMediaProvider,
|
||||||
MWMediaType,
|
MWMediaType,
|
||||||
MWPortableMedia,
|
MWPortableMedia,
|
||||||
MWMediaStream,
|
MWMediaStream,
|
||||||
MWQuery,
|
MWQuery,
|
||||||
MWProviderMediaResult
|
MWProviderMediaResult,
|
||||||
} from "providers/types";
|
} from "@/providers/types";
|
||||||
|
|
||||||
import { CORS_PROXY_URL } from "mw_constants";
|
import { CORS_PROXY_URL } from "@/mw_constants";
|
||||||
import { unpack } from "unpacker";
|
|
||||||
import CryptoJS from "crypto-js";
|
|
||||||
|
|
||||||
const format = {
|
const format = {
|
||||||
stringify: (cipher: any) => {
|
stringify: (cipher: any) => {
|
||||||
@ -34,7 +34,7 @@ const format = {
|
|||||||
salt,
|
salt,
|
||||||
});
|
});
|
||||||
return cipher;
|
return cipher;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gDrivePlayerScraper: MWMediaProvider = {
|
export const gDrivePlayerScraper: MWMediaProvider = {
|
||||||
@ -43,8 +43,12 @@ export const gDrivePlayerScraper: MWMediaProvider = {
|
|||||||
type: [MWMediaType.MOVIE],
|
type: [MWMediaType.MOVIE],
|
||||||
displayName: "gdriveplayer",
|
displayName: "gdriveplayer",
|
||||||
|
|
||||||
async getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult> {
|
async getMediaFromPortable(
|
||||||
const res = await fetch(`${CORS_PROXY_URL}https://api.gdriveplayer.us/v1/imdb/${media.mediaId}`).then((d) => d.json());
|
media: MWPortableMedia
|
||||||
|
): Promise<MWProviderMediaResult> {
|
||||||
|
const res = await fetch(
|
||||||
|
`${CORS_PROXY_URL}https://api.gdriveplayer.us/v1/imdb/${media.mediaId}`
|
||||||
|
).then((d) => d.json());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...media,
|
...media,
|
||||||
@ -54,19 +58,25 @@ export const gDrivePlayerScraper: MWMediaProvider = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> {
|
async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> {
|
||||||
const searchRes = await fetch(`${CORS_PROXY_URL}https://api.gdriveplayer.us/v1/movie/search?title=${query.searchQuery}`).then((d) => d.json());
|
const searchRes = await fetch(
|
||||||
|
`${CORS_PROXY_URL}https://api.gdriveplayer.us/v1/movie/search?title=${query.searchQuery}`
|
||||||
|
).then((d) => d.json());
|
||||||
|
|
||||||
const results: MWProviderMediaResult[] = (searchRes || []).map((item: any) => ({
|
const results: MWProviderMediaResult[] = (searchRes || []).map(
|
||||||
title: item.title,
|
(item: any) => ({
|
||||||
year: item.year,
|
title: item.title,
|
||||||
mediaId: item.imdb,
|
year: item.year,
|
||||||
}));
|
mediaId: item.imdb,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
|
async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
|
||||||
const streamRes = await fetch(`${CORS_PROXY_URL}https://database.gdriveplayer.us/player.php?imdb=${media.mediaId}`).then((d) => d.text());
|
const streamRes = await fetch(
|
||||||
|
`${CORS_PROXY_URL}https://database.gdriveplayer.us/player.php?imdb=${media.mediaId}`
|
||||||
|
).then((d) => d.text());
|
||||||
const page = new DOMParser().parseFromString(streamRes, "text/html");
|
const page = new DOMParser().parseFromString(streamRes, "text/html");
|
||||||
|
|
||||||
const script: HTMLElement | undefined = Array.from(
|
const script: HTMLElement | undefined = Array.from(
|
||||||
@ -78,10 +88,29 @@ export const gDrivePlayerScraper: MWMediaProvider = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// NOTE: this code requires re-write, it's not safe
|
/// NOTE: this code requires re-write, it's not safe
|
||||||
const data = unpack(script.textContent).split("var data=\\'")[1].split("\\'")[0].replace(/\\/g, "");
|
const data = unpack(script.textContent)
|
||||||
const decryptedData = unpack(CryptoJS.AES.decrypt(data, "alsfheafsjklNIWORNiolNIOWNKLNXakjsfwnBdwjbwfkjbJjkopfjweopjASoiwnrflakefneiofrt", { format }).toString(CryptoJS.enc.Utf8));
|
.split("var data=\\'")[1]
|
||||||
|
.split("\\'")[0]
|
||||||
|
.replace(/\\/g, "");
|
||||||
|
const decryptedData = unpack(
|
||||||
|
CryptoJS.AES.decrypt(
|
||||||
|
data,
|
||||||
|
"alsfheafsjklNIWORNiolNIOWNKLNXakjsfwnBdwjbwfkjbJjkopfjweopjASoiwnrflakefneiofrt",
|
||||||
|
{ format }
|
||||||
|
).toString(CryptoJS.enc.Utf8)
|
||||||
|
);
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const sources = JSON.parse(JSON.stringify(eval(decryptedData.split("sources:")[1].split(",image")[0].replace(/\\/g, "").replace(/document\.referrer/g, "\"\""))));
|
const sources = JSON.parse(
|
||||||
|
JSON.stringify(
|
||||||
|
eval(
|
||||||
|
decryptedData
|
||||||
|
.split("sources:")[1]
|
||||||
|
.split(",image")[0]
|
||||||
|
.replace(/\\/g, "")
|
||||||
|
.replace(/document\.referrer/g, '""')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
const source = sources[sources.length - 1];
|
const source = sources[sources.length - 1];
|
||||||
/// END
|
/// END
|
||||||
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
|
import { unpack } from "unpacker";
|
||||||
|
import json5 from "json5";
|
||||||
import {
|
import {
|
||||||
MWMediaProvider,
|
MWMediaProvider,
|
||||||
MWMediaType,
|
MWMediaType,
|
||||||
MWPortableMedia,
|
MWPortableMedia,
|
||||||
MWMediaStream,
|
MWMediaStream,
|
||||||
MWQuery,
|
MWQuery,
|
||||||
MWProviderMediaResult
|
MWProviderMediaResult,
|
||||||
} from "providers/types";
|
} from "@/providers/types";
|
||||||
|
|
||||||
import { CORS_PROXY_URL, OMDB_API_KEY } from "mw_constants";
|
import { CORS_PROXY_URL, OMDB_API_KEY } from "@/mw_constants";
|
||||||
import { unpack } from "unpacker";
|
|
||||||
import json5 from "json5";
|
|
||||||
|
|
||||||
export const gomostreamScraper: MWMediaProvider = {
|
export const gomostreamScraper: MWMediaProvider = {
|
||||||
id: "gomostream",
|
id: "gomostream",
|
||||||
@ -17,21 +17,25 @@ export const gomostreamScraper: MWMediaProvider = {
|
|||||||
type: [MWMediaType.MOVIE],
|
type: [MWMediaType.MOVIE],
|
||||||
displayName: "gomostream",
|
displayName: "gomostream",
|
||||||
|
|
||||||
async getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult> {
|
async getMediaFromPortable(
|
||||||
|
media: MWPortableMedia
|
||||||
|
): Promise<MWProviderMediaResult> {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
apikey: OMDB_API_KEY,
|
apikey: OMDB_API_KEY,
|
||||||
i: media.mediaId,
|
i: media.mediaId,
|
||||||
type: media.mediaType
|
type: media.mediaType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${CORS_PROXY_URL}http://www.omdbapi.com/?${encodeURIComponent(params.toString())}`,
|
`${CORS_PROXY_URL}http://www.omdbapi.com/?${encodeURIComponent(
|
||||||
).then(d => d.json())
|
params.toString()
|
||||||
|
)}`
|
||||||
|
).then((d) => d.json());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...media,
|
...media,
|
||||||
title: res.Title,
|
title: res.Title,
|
||||||
year: res.Year
|
year: res.Year,
|
||||||
} as MWProviderMediaResult;
|
} as MWProviderMediaResult;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -41,51 +45,69 @@ export const gomostreamScraper: MWMediaProvider = {
|
|||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
apikey: OMDB_API_KEY,
|
apikey: OMDB_API_KEY,
|
||||||
s: term,
|
s: term,
|
||||||
type: query.type
|
type: query.type,
|
||||||
});
|
});
|
||||||
const searchRes = await fetch(
|
const searchRes = await fetch(
|
||||||
`${CORS_PROXY_URL}http://www.omdbapi.com/?${encodeURIComponent(params.toString())}`,
|
`${CORS_PROXY_URL}http://www.omdbapi.com/?${encodeURIComponent(
|
||||||
).then(d => d.json())
|
params.toString()
|
||||||
|
)}`
|
||||||
|
).then((d) => d.json());
|
||||||
|
|
||||||
const results: MWProviderMediaResult[] = (searchRes.Search || []).map((d: any) => ({
|
const results: MWProviderMediaResult[] = (searchRes.Search || []).map(
|
||||||
title: d.Title,
|
(d: any) =>
|
||||||
year: d.Year,
|
({
|
||||||
mediaId: d.imdbID
|
title: d.Title,
|
||||||
} as MWProviderMediaResult));
|
year: d.Year,
|
||||||
|
mediaId: d.imdbID,
|
||||||
|
} as MWProviderMediaResult)
|
||||||
|
);
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
|
async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
|
||||||
const type = media.mediaType === MWMediaType.SERIES ? 'show' : media.mediaType;
|
const type =
|
||||||
const res1 = await fetch(`${CORS_PROXY_URL}https://gomo.to/${type}/${media.mediaId}`).then((d) => d.text());
|
media.mediaType === MWMediaType.SERIES ? "show" : media.mediaType;
|
||||||
if (res1 === "Movie not available." || res1 === "Episode not available.") throw new Error(res1);
|
const res1 = await fetch(
|
||||||
|
`${CORS_PROXY_URL}https://gomo.to/${type}/${media.mediaId}`
|
||||||
|
).then((d) => d.text());
|
||||||
|
if (res1 === "Movie not available." || res1 === "Episode not available.")
|
||||||
|
throw new Error(res1);
|
||||||
|
|
||||||
const tc = res1.match(/var tc = '(.+)';/)?.[1] || "";
|
const tc = res1.match(/var tc = '(.+)';/)?.[1] || "";
|
||||||
const _token = res1.match(/"_token": "(.+)",/)?.[1] || "";
|
const _token = res1.match(/"_token": "(.+)",/)?.[1] || "";
|
||||||
|
|
||||||
const fd = new FormData()
|
const fd = new FormData();
|
||||||
fd.append('tokenCode', tc)
|
fd.append("tokenCode", tc);
|
||||||
fd.append('_token', _token)
|
fd.append("_token", _token);
|
||||||
|
|
||||||
const src = await fetch(`${CORS_PROXY_URL}https://gomo.to/decoding_v3.php`, {
|
const src = await fetch(
|
||||||
method: "POST",
|
`${CORS_PROXY_URL}https://gomo.to/decoding_v3.php`,
|
||||||
body: fd,
|
{
|
||||||
headers: {
|
method: "POST",
|
||||||
'x-token': `${tc.slice(5, 13).split("").reverse().join("")}13574199`
|
body: fd,
|
||||||
|
headers: {
|
||||||
|
"x-token": `${tc.slice(5, 13).split("").reverse().join("")}13574199`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}).then((d) => d.json());
|
).then((d) => d.json());
|
||||||
const embeds = src.filter((url: string) => url.includes('gomo.to'));
|
const embeds = src.filter((url: string) => url.includes("gomo.to"));
|
||||||
|
|
||||||
// maybe try all embeds in the future
|
// maybe try all embeds in the future
|
||||||
const embedUrl = embeds[1];
|
const embedUrl = embeds[1];
|
||||||
const res2 = await fetch(`${CORS_PROXY_URL}${embedUrl}`).then((d) => d.text());
|
const res2 = await fetch(`${CORS_PROXY_URL}${embedUrl}`).then((d) =>
|
||||||
|
d.text()
|
||||||
|
);
|
||||||
|
|
||||||
const res2DOM = new DOMParser().parseFromString(res2, "text/html");
|
const res2DOM = new DOMParser().parseFromString(res2, "text/html");
|
||||||
if (res2DOM.body.innerText === "File was deleted") throw new Error("File was deleted");
|
if (res2DOM.body.innerText === "File was deleted")
|
||||||
|
throw new Error("File was deleted");
|
||||||
|
|
||||||
const script = Array.from(res2DOM.querySelectorAll("script")).find((s: HTMLScriptElement) => s.innerHTML.includes("eval(function(p,a,c,k,e,d"))?.innerHTML;
|
const script = Array.from(res2DOM.querySelectorAll("script")).find(
|
||||||
if (!script) throw new Error("Could not get packed data")
|
(s: HTMLScriptElement) =>
|
||||||
|
s.innerHTML.includes("eval(function(p,a,c,k,e,d")
|
||||||
|
)?.innerHTML;
|
||||||
|
if (!script) throw new Error("Could not get packed data");
|
||||||
|
|
||||||
const unpacked = unpack(script);
|
const unpacked = unpack(script);
|
||||||
const rawSources = /sources:(\[.*?\])/.exec(unpacked);
|
const rawSources = /sources:(\[.*?\])/.exec(unpacked);
|
||||||
@ -94,9 +116,10 @@ export const gomostreamScraper: MWMediaProvider = {
|
|||||||
const sources = json5.parse(rawSources[1]);
|
const sources = json5.parse(rawSources[1]);
|
||||||
const streamUrl = sources[0].file;
|
const streamUrl = sources[0].file;
|
||||||
|
|
||||||
const streamType = streamUrl.split('.').at(-1);
|
const streamType = streamUrl.split(".").at(-1);
|
||||||
if (streamType !== "mp4" && streamType !== "m3u8") throw new Error("Unsupported stream type");
|
if (streamType !== "mp4" && streamType !== "m3u8")
|
||||||
|
throw new Error("Unsupported stream type");
|
||||||
|
|
||||||
return { url: streamUrl, type: streamType, captions: [] };
|
return { url: streamUrl, type: streamType, captions: [] };
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
// this is derived from https://github.com/recloudstream/cloudstream-extensions
|
// this is derived from https://github.com/recloudstream/cloudstream-extensions
|
||||||
// for more info please check the LICENSE file in the same directory
|
// for more info please check the LICENSE file in the same directory
|
||||||
|
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
import toWebVTT from "srt-webvtt";
|
||||||
|
import CryptoJS from "crypto-js";
|
||||||
|
import { CORS_PROXY_URL, TMDB_API_KEY } from "@/mw_constants";
|
||||||
import {
|
import {
|
||||||
MWMediaProvider,
|
MWMediaProvider,
|
||||||
MWMediaType,
|
MWMediaType,
|
||||||
@ -9,11 +13,7 @@ import {
|
|||||||
MWQuery,
|
MWQuery,
|
||||||
MWMediaSeasons,
|
MWMediaSeasons,
|
||||||
MWProviderMediaResult,
|
MWProviderMediaResult,
|
||||||
} from "providers/types";
|
} from "@/providers/types";
|
||||||
import { CORS_PROXY_URL, TMDB_API_KEY } from "mw_constants";
|
|
||||||
import { customAlphabet } from "nanoid";
|
|
||||||
import toWebVTT from "srt-webvtt";
|
|
||||||
import CryptoJS from "crypto-js";
|
|
||||||
|
|
||||||
const nanoid = customAlphabet("0123456789abcdef", 32);
|
const nanoid = customAlphabet("0123456789abcdef", 32);
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ const crypto = {
|
|||||||
getVerify(str: string, str2: string, str3: string) {
|
getVerify(str: string, str2: string, str3: string) {
|
||||||
if (str) {
|
if (str) {
|
||||||
return CryptoJS.MD5(
|
return CryptoJS.MD5(
|
||||||
CryptoJS.MD5(str2).toString() + str3 + str,
|
CryptoJS.MD5(str2).toString() + str3 + str
|
||||||
).toString();
|
).toString();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -66,7 +66,7 @@ const get = (data: object, altApi = false) => {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
...defaultData,
|
...defaultData,
|
||||||
...data,
|
...data,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
const appKeyHash = CryptoJS.MD5(appKey).toString();
|
const appKeyHash = CryptoJS.MD5(appKey).toString();
|
||||||
const verify = crypto.getVerify(encryptedData, appKey, key);
|
const verify = crypto.getVerify(encryptedData, appKey, key);
|
||||||
@ -102,7 +102,7 @@ export const superStreamScraper: MWMediaProvider = {
|
|||||||
displayName: "SuperStream",
|
displayName: "SuperStream",
|
||||||
|
|
||||||
async getMediaFromPortable(
|
async getMediaFromPortable(
|
||||||
media: MWPortableMedia,
|
media: MWPortableMedia
|
||||||
): Promise<MWProviderMediaResult> {
|
): Promise<MWProviderMediaResult> {
|
||||||
let apiQuery: any;
|
let apiQuery: any;
|
||||||
if (media.mediaType === MWMediaType.SERIES) {
|
if (media.mediaType === MWMediaType.SERIES) {
|
||||||
@ -174,10 +174,18 @@ export const superStreamScraper: MWMediaProvider = {
|
|||||||
};
|
};
|
||||||
const mediaRes = (await get(apiQuery).then((r) => r.json())).data;
|
const mediaRes = (await get(apiQuery).then((r) => r.json())).data;
|
||||||
const hdQuality =
|
const hdQuality =
|
||||||
mediaRes.list.find((quality: any) => (quality.quality === "1080p" && quality.path)) ??
|
mediaRes.list.find(
|
||||||
mediaRes.list.find((quality: any) => (quality.quality === "720p" && quality.path)) ??
|
(quality: any) => quality.quality === "1080p" && quality.path
|
||||||
mediaRes.list.find((quality: any) => (quality.quality === "480p" && quality.path)) ??
|
) ??
|
||||||
mediaRes.list.find((quality: any) => (quality.quality === "360p" && quality.path));
|
mediaRes.list.find(
|
||||||
|
(quality: any) => quality.quality === "720p" && quality.path
|
||||||
|
) ??
|
||||||
|
mediaRes.list.find(
|
||||||
|
(quality: any) => quality.quality === "480p" && quality.path
|
||||||
|
) ??
|
||||||
|
mediaRes.list.find(
|
||||||
|
(quality: any) => quality.quality === "360p" && quality.path
|
||||||
|
);
|
||||||
|
|
||||||
if (!hdQuality) throw new Error("No quality could be found.");
|
if (!hdQuality) throw new Error("No quality could be found.");
|
||||||
|
|
||||||
@ -192,7 +200,7 @@ export const superStreamScraper: MWMediaProvider = {
|
|||||||
const mappedCaptions = await Promise.all(
|
const mappedCaptions = await Promise.all(
|
||||||
subtitleRes.list.map(async (subtitle: any) => {
|
subtitleRes.list.map(async (subtitle: any) => {
|
||||||
const captionBlob = await fetch(
|
const captionBlob = await fetch(
|
||||||
`${CORS_PROXY_URL}${subtitle.subtitles[0].file_path}`,
|
`${CORS_PROXY_URL}${subtitle.subtitles[0].file_path}`
|
||||||
).then((captionRes) => captionRes.blob()); // cross-origin bypass
|
).then((captionRes) => captionRes.blob()); // cross-origin bypass
|
||||||
const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable
|
const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable
|
||||||
return {
|
return {
|
||||||
@ -200,7 +208,7 @@ export const superStreamScraper: MWMediaProvider = {
|
|||||||
url: captionUrl,
|
url: captionUrl,
|
||||||
label: subtitle.language,
|
label: subtitle.language,
|
||||||
};
|
};
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return { url: hdQuality.path, type: "mp4", captions: mappedCaptions };
|
return { url: hdQuality.path, type: "mp4", captions: mappedCaptions };
|
||||||
@ -217,10 +225,18 @@ export const superStreamScraper: MWMediaProvider = {
|
|||||||
};
|
};
|
||||||
const mediaRes = (await get(apiQuery).then((r) => r.json())).data;
|
const mediaRes = (await get(apiQuery).then((r) => r.json())).data;
|
||||||
const hdQuality =
|
const hdQuality =
|
||||||
mediaRes.list.find((quality: any) => (quality.quality === "1080p" && quality.path)) ??
|
mediaRes.list.find(
|
||||||
mediaRes.list.find((quality: any) => (quality.quality === "720p" && quality.path)) ??
|
(quality: any) => quality.quality === "1080p" && quality.path
|
||||||
mediaRes.list.find((quality: any) => (quality.quality === "480p" && quality.path)) ??
|
) ??
|
||||||
mediaRes.list.find((quality: any) => (quality.quality === "360p" && quality.path));
|
mediaRes.list.find(
|
||||||
|
(quality: any) => quality.quality === "720p" && quality.path
|
||||||
|
) ??
|
||||||
|
mediaRes.list.find(
|
||||||
|
(quality: any) => quality.quality === "480p" && quality.path
|
||||||
|
) ??
|
||||||
|
mediaRes.list.find(
|
||||||
|
(quality: any) => quality.quality === "360p" && quality.path
|
||||||
|
);
|
||||||
|
|
||||||
if (!hdQuality) throw new Error("No quality could be found.");
|
if (!hdQuality) throw new Error("No quality could be found.");
|
||||||
|
|
||||||
@ -237,7 +253,7 @@ export const superStreamScraper: MWMediaProvider = {
|
|||||||
const mappedCaptions = await Promise.all(
|
const mappedCaptions = await Promise.all(
|
||||||
subtitleRes.list.map(async (subtitle: any) => {
|
subtitleRes.list.map(async (subtitle: any) => {
|
||||||
const captionBlob = await fetch(
|
const captionBlob = await fetch(
|
||||||
`${CORS_PROXY_URL}${subtitle.subtitles[0].file_path}`,
|
`${CORS_PROXY_URL}${subtitle.subtitles[0].file_path}`
|
||||||
).then((captionRes) => captionRes.blob()); // cross-origin bypass
|
).then((captionRes) => captionRes.blob()); // cross-origin bypass
|
||||||
const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable
|
const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable
|
||||||
return {
|
return {
|
||||||
@ -245,13 +261,13 @@ export const superStreamScraper: MWMediaProvider = {
|
|||||||
url: captionUrl,
|
url: captionUrl,
|
||||||
label: subtitle.language,
|
label: subtitle.language,
|
||||||
};
|
};
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return { url: hdQuality.path, type: "mp4", captions: mappedCaptions };
|
return { url: hdQuality.path, type: "mp4", captions: mappedCaptions };
|
||||||
},
|
},
|
||||||
async getSeasonDataFromMedia(
|
async getSeasonDataFromMedia(
|
||||||
media: MWPortableMedia,
|
media: MWPortableMedia
|
||||||
): Promise<MWMediaSeasons> {
|
): Promise<MWMediaSeasons> {
|
||||||
const apiQuery = {
|
const apiQuery = {
|
||||||
module: "TV_detail_1",
|
module: "TV_detail_1",
|
||||||
@ -261,11 +277,11 @@ export const superStreamScraper: MWMediaProvider = {
|
|||||||
const detailRes = (await get(apiQuery, true).then((r) => r.json())).data;
|
const detailRes = (await get(apiQuery, true).then((r) => r.json())).data;
|
||||||
const firstSearchResult = (
|
const firstSearchResult = (
|
||||||
await fetch(
|
await fetch(
|
||||||
`https://api.themoviedb.org/3/search/tv?api_key=${TMDB_API_KEY}&language=en-US&page=1&query=${detailRes.title}&include_adult=false`,
|
`https://api.themoviedb.org/3/search/tv?api_key=${TMDB_API_KEY}&language=en-US&page=1&query=${detailRes.title}&include_adult=false`
|
||||||
).then((r) => r.json())
|
).then((r) => r.json())
|
||||||
).results[0];
|
).results[0];
|
||||||
const showDetails = await fetch(
|
const showDetails = await fetch(
|
||||||
`https://api.themoviedb.org/3/tv/${firstSearchResult.id}?api_key=${TMDB_API_KEY}`,
|
`https://api.themoviedb.org/3/tv/${firstSearchResult.id}?api_key=${TMDB_API_KEY}`
|
||||||
).then((r) => r.json());
|
).then((r) => r.json());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -279,7 +295,7 @@ export const superStreamScraper: MWMediaProvider = {
|
|||||||
sort: epNum + 1,
|
sort: epNum + 1,
|
||||||
id: (epNum + 1).toString(),
|
id: (epNum + 1).toString(),
|
||||||
episodeNumber: epNum + 1,
|
episodeNumber: epNum + 1,
|
||||||
}),
|
})
|
||||||
),
|
),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
@ -5,17 +5,17 @@ import {
|
|||||||
MWMediaStream,
|
MWMediaStream,
|
||||||
MWQuery,
|
MWQuery,
|
||||||
MWMediaSeasons,
|
MWMediaSeasons,
|
||||||
MWProviderMediaResult
|
MWProviderMediaResult,
|
||||||
} from "providers/types";
|
} from "@/providers/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
searchTheFlix,
|
searchTheFlix,
|
||||||
getDataFromSearch,
|
getDataFromSearch,
|
||||||
turnDataIntoMedia,
|
turnDataIntoMedia,
|
||||||
} from "providers/list/theflix/search";
|
} from "@/providers/list/theflix/search";
|
||||||
|
|
||||||
import { getDataFromPortableSearch } from "providers/list/theflix/portableToMedia";
|
import { getDataFromPortableSearch } from "@/providers/list/theflix/portableToMedia";
|
||||||
import { CORS_PROXY_URL } from "mw_constants";
|
import { CORS_PROXY_URL } from "@/mw_constants";
|
||||||
|
|
||||||
export const theFlixScraper: MWMediaProvider = {
|
export const theFlixScraper: MWMediaProvider = {
|
||||||
id: "theflix",
|
id: "theflix",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CORS_PROXY_URL } from "mw_constants";
|
import { CORS_PROXY_URL } from "@/mw_constants";
|
||||||
import { MWMediaType, MWPortableMedia } from "providers/types";
|
import { MWMediaType, MWPortableMedia } from "@/providers/types";
|
||||||
|
|
||||||
const getTheFlixUrl = (media: MWPortableMedia, params?: URLSearchParams) => {
|
const getTheFlixUrl = (media: MWPortableMedia, params?: URLSearchParams) => {
|
||||||
if (media.mediaType === MWMediaType.MOVIE) {
|
if (media.mediaType === MWMediaType.MOVIE) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CORS_PROXY_URL } from "mw_constants";
|
import { CORS_PROXY_URL } from "@/mw_constants";
|
||||||
import { MWMediaType, MWProviderMediaResult, MWQuery } from "providers";
|
import { MWMediaType, MWProviderMediaResult, MWQuery } from "@/providers";
|
||||||
|
|
||||||
const getTheFlixUrl = (type: "tv-shows" | "movies", params: URLSearchParams) =>
|
const getTheFlixUrl = (type: "tv-shows" | "movies", params: URLSearchParams) =>
|
||||||
`https://theflix.to/${type}/trending?${params}`;
|
`https://theflix.to/${type}/trending?${params}`;
|
||||||
|
@ -5,10 +5,10 @@ import {
|
|||||||
MWMediaStream,
|
MWMediaStream,
|
||||||
MWQuery,
|
MWQuery,
|
||||||
MWProviderMediaResult,
|
MWProviderMediaResult,
|
||||||
MWMediaCaption
|
MWMediaCaption,
|
||||||
} from "providers/types";
|
} from "@/providers/types";
|
||||||
|
|
||||||
import { CORS_PROXY_URL } from "mw_constants";
|
import { CORS_PROXY_URL } from "@/mw_constants";
|
||||||
|
|
||||||
export const xemovieScraper: MWMediaProvider = {
|
export const xemovieScraper: MWMediaProvider = {
|
||||||
id: "xemovie",
|
id: "xemovie",
|
||||||
@ -16,15 +16,21 @@ export const xemovieScraper: MWMediaProvider = {
|
|||||||
type: [MWMediaType.MOVIE],
|
type: [MWMediaType.MOVIE],
|
||||||
displayName: "xemovie",
|
displayName: "xemovie",
|
||||||
|
|
||||||
async getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult> {
|
async getMediaFromPortable(
|
||||||
|
media: MWPortableMedia
|
||||||
|
): Promise<MWProviderMediaResult> {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${CORS_PROXY_URL}https://xemovie.co/movies/${media.mediaId}/watch`,
|
`${CORS_PROXY_URL}https://xemovie.co/movies/${media.mediaId}/watch`
|
||||||
).then(d => d.text());
|
).then((d) => d.text());
|
||||||
|
|
||||||
const DOM = new DOMParser().parseFromString(res, "text/html");
|
const DOM = new DOMParser().parseFromString(res, "text/html");
|
||||||
|
|
||||||
const title = DOM.querySelector(".text-primary.text-lg.font-extrabold")?.textContent || "";
|
const title =
|
||||||
const year = DOM.querySelector("div.justify-between:nth-child(3) > div:nth-child(2)")?.textContent || "";
|
DOM.querySelector(".text-primary.text-lg.font-extrabold")?.textContent ||
|
||||||
|
"";
|
||||||
|
const year =
|
||||||
|
DOM.querySelector("div.justify-between:nth-child(3) > div:nth-child(2)")
|
||||||
|
?.textContent || "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...media,
|
...media,
|
||||||
@ -36,68 +42,99 @@ export const xemovieScraper: MWMediaProvider = {
|
|||||||
async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> {
|
async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> {
|
||||||
const term = query.searchQuery.toLowerCase();
|
const term = query.searchQuery.toLowerCase();
|
||||||
|
|
||||||
const searchUrl = `${CORS_PROXY_URL}https://xemovie.co/search?q=${encodeURIComponent(term)}`;
|
const searchUrl = `${CORS_PROXY_URL}https://xemovie.co/search?q=${encodeURIComponent(
|
||||||
|
term
|
||||||
|
)}`;
|
||||||
const searchRes = await fetch(searchUrl).then((d) => d.text());
|
const searchRes = await fetch(searchUrl).then((d) => d.text());
|
||||||
|
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(searchRes, "text/html");
|
const doc = parser.parseFromString(searchRes, "text/html");
|
||||||
|
|
||||||
const movieContainer = doc.querySelectorAll(".py-10")[0].querySelector(".grid");
|
const movieContainer = doc
|
||||||
|
.querySelectorAll(".py-10")[0]
|
||||||
|
.querySelector(".grid");
|
||||||
if (!movieContainer) return [];
|
if (!movieContainer) return [];
|
||||||
const movieNodes = Array.from(movieContainer.querySelectorAll("a")).filter(link => !link.className);
|
const movieNodes = Array.from(movieContainer.querySelectorAll("a")).filter(
|
||||||
|
(link) => !link.className
|
||||||
|
);
|
||||||
|
|
||||||
const results: MWProviderMediaResult[] = movieNodes.map((node) => {
|
const results: MWProviderMediaResult[] = movieNodes
|
||||||
const parent = node.parentElement;
|
.map((node) => {
|
||||||
if (!parent) return;
|
const parent = node.parentElement;
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
const aElement = parent.querySelector("a");
|
const aElement = parent.querySelector("a");
|
||||||
if (!aElement) return;
|
if (!aElement) return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: parent.querySelector("div > div > a > h6")?.textContent,
|
title: parent.querySelector("div > div > a > h6")?.textContent,
|
||||||
year: parent.querySelector("div.float-right")?.textContent,
|
year: parent.querySelector("div.float-right")?.textContent,
|
||||||
mediaId: aElement.href.split('/').pop() || "",
|
mediaId: aElement.href.split("/").pop() || "",
|
||||||
}
|
};
|
||||||
}).filter((d): d is MWProviderMediaResult => !!d);
|
})
|
||||||
|
.filter((d): d is MWProviderMediaResult => !!d);
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
|
async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
|
||||||
if (media.mediaType !== MWMediaType.MOVIE) throw new Error("Incorrect type")
|
if (media.mediaType !== MWMediaType.MOVIE)
|
||||||
|
throw new Error("Incorrect type");
|
||||||
|
|
||||||
const url = `${CORS_PROXY_URL}https://xemovie.co/movies/${media.mediaId}/watch`;
|
const url = `${CORS_PROXY_URL}https://xemovie.co/movies/${media.mediaId}/watch`;
|
||||||
|
|
||||||
let streamUrl = "";
|
let streamUrl = "";
|
||||||
const subtitles: MWMediaCaption[] = [];
|
const subtitles: MWMediaCaption[] = [];
|
||||||
|
|
||||||
const res = await fetch(url).then(d => d.text());
|
const res = await fetch(url).then((d) => d.text());
|
||||||
const scripts = Array.from(new DOMParser().parseFromString(res, "text/html").querySelectorAll("script"));
|
const scripts = Array.from(
|
||||||
|
new DOMParser()
|
||||||
|
.parseFromString(res, "text/html")
|
||||||
|
.querySelectorAll("script")
|
||||||
|
);
|
||||||
|
|
||||||
for (const script of scripts) {
|
for (const script of scripts) {
|
||||||
if (!script.textContent) continue;
|
if (!script.textContent) continue;
|
||||||
|
|
||||||
if (script.textContent.match(/https:\/\/[a-z][0-9]\.xemovie\.com/)) {
|
if (script.textContent.match(/https:\/\/[a-z][0-9]\.xemovie\.com/)) {
|
||||||
const data = JSON.parse(JSON.stringify(eval(`(${script.textContent.replace("const data = ", "").split("};")[0]}})`)));
|
const data = JSON.parse(
|
||||||
|
JSON.stringify(
|
||||||
|
eval(
|
||||||
|
`(${
|
||||||
|
script.textContent.replace("const data = ", "").split("};")[0]
|
||||||
|
}})`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
streamUrl = data.playlist[0].file;
|
streamUrl = data.playlist[0].file;
|
||||||
|
|
||||||
for (const [index, subtitleTrack] of data.playlist[0].tracks.entries()) {
|
for (const [
|
||||||
|
index,
|
||||||
|
subtitleTrack,
|
||||||
|
] of data.playlist[0].tracks.entries()) {
|
||||||
const subtitleBlob = URL.createObjectURL(
|
const subtitleBlob = URL.createObjectURL(
|
||||||
await fetch(`${CORS_PROXY_URL}${subtitleTrack.file}`).then((captionRes) => captionRes.blob())
|
await fetch(`${CORS_PROXY_URL}${subtitleTrack.file}`).then(
|
||||||
|
(captionRes) => captionRes.blob()
|
||||||
|
)
|
||||||
); // do this so no need for CORS errors
|
); // do this so no need for CORS errors
|
||||||
|
|
||||||
subtitles.push({
|
subtitles.push({
|
||||||
id: index,
|
id: index,
|
||||||
url: subtitleBlob,
|
url: subtitleBlob,
|
||||||
label: subtitleTrack.label
|
label: subtitleTrack.label,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamType = streamUrl.split('.').at(-1);
|
const streamType = streamUrl.split(".").at(-1);
|
||||||
if (streamType !== "mp4" && streamType !== "m3u8") throw new Error("Unsupported stream type");
|
if (streamType !== "mp4" && streamType !== "m3u8")
|
||||||
|
throw new Error("Unsupported stream type");
|
||||||
|
|
||||||
return { url: streamUrl, type: streamType, captions: subtitles } as MWMediaStream;
|
return {
|
||||||
}
|
url: streamUrl,
|
||||||
|
type: streamType,
|
||||||
|
captions: subtitles,
|
||||||
|
} as MWMediaStream;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { SimpleCache } from "utils/cache";
|
import { SimpleCache } from "@/utils/cache";
|
||||||
import { MWPortableMedia, MWMedia } from "providers";
|
import { MWPortableMedia, MWMedia } from "@/providers";
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
const contentCache = new SimpleCache<MWPortableMedia, MWMedia>();
|
const contentCache = new SimpleCache<MWPortableMedia, MWMedia>();
|
||||||
contentCache.setCompare((a,b) => a.mediaId === b.mediaId && a.providerId === b.providerId);
|
contentCache.setCompare(
|
||||||
|
(a, b) => a.mediaId === b.mediaId && a.providerId === b.providerId
|
||||||
|
);
|
||||||
contentCache.initialize();
|
contentCache.initialize();
|
||||||
|
|
||||||
export default contentCache;
|
export default contentCache;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MWMediaType, MWMediaProviderMetadata } from "providers";
|
import { MWMediaType, MWMediaProviderMetadata } from "@/providers";
|
||||||
import { MWMedia, MWMediaEpisode, MWMediaSeason } from "providers/types";
|
import { MWMedia, MWMediaEpisode, MWMediaSeason } from "@/providers/types";
|
||||||
import { mediaProviders, mediaProvidersUnchecked } from "./providers";
|
import { mediaProviders, mediaProvidersUnchecked } from "./providers";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { theFlixScraper } from "providers/list/theflix";
|
import { theFlixScraper } from "@/providers/list/theflix";
|
||||||
import { gDrivePlayerScraper } from "providers/list/gdriveplayer";
|
import { gDrivePlayerScraper } from "@/providers/list/gdriveplayer";
|
||||||
import { MWWrappedMediaProvider, WrapProvider } from "providers/wrapper";
|
import { MWWrappedMediaProvider, WrapProvider } from "@/providers/wrapper";
|
||||||
import { gomostreamScraper } from "providers/list/gomostream";
|
import { gomostreamScraper } from "@/providers/list/gomostream";
|
||||||
import { xemovieScraper } from "providers/list/xemovie";
|
import { xemovieScraper } from "@/providers/list/xemovie";
|
||||||
import { flixhqProvider } from "providers/list/flixhq";
|
import { flixhqProvider } from "@/providers/list/flixhq";
|
||||||
import { superStreamScraper } from "providers/list/superstream";
|
import { superStreamScraper } from "@/providers/list/superstream";
|
||||||
|
|
||||||
export const mediaProvidersUnchecked: MWWrappedMediaProvider[] = [
|
export const mediaProvidersUnchecked: MWWrappedMediaProvider[] = [
|
||||||
WrapProvider(superStreamScraper),
|
WrapProvider(superStreamScraper),
|
||||||
WrapProvider(theFlixScraper),
|
WrapProvider(theFlixScraper),
|
||||||
WrapProvider(gDrivePlayerScraper),
|
WrapProvider(gDrivePlayerScraper),
|
||||||
WrapProvider(gomostreamScraper),
|
WrapProvider(gomostreamScraper),
|
||||||
WrapProvider(xemovieScraper),
|
WrapProvider(xemovieScraper),
|
||||||
WrapProvider(flixhqProvider),
|
WrapProvider(flixhqProvider),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mediaProviders: MWWrappedMediaProvider[] =
|
export const mediaProviders: MWWrappedMediaProvider[] =
|
||||||
mediaProvidersUnchecked.filter((v) => v.enabled);
|
mediaProvidersUnchecked.filter((v) => v.enabled);
|
||||||
|
@ -4,8 +4,8 @@ import {
|
|||||||
MWMedia,
|
MWMedia,
|
||||||
MWQuery,
|
MWQuery,
|
||||||
convertMediaToPortable,
|
convertMediaToPortable,
|
||||||
} from "providers";
|
} from "@/providers";
|
||||||
import { SimpleCache } from "utils/cache";
|
import { SimpleCache } from "@/utils/cache";
|
||||||
import { GetProvidersForType } from "./helpers";
|
import { GetProvidersForType } from "./helpers";
|
||||||
import contentCache from "./contentCache";
|
import contentCache from "./contentCache";
|
||||||
|
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { SimpleCache } from "utils/cache";
|
import { SimpleCache } from "@/utils/cache";
|
||||||
import { MWPortableMedia } from "providers";
|
import { MWPortableMedia } from "@/providers";
|
||||||
import { MWMediaSeasons, MWMediaType, MWMediaProviderSeries } from "providers/types";
|
import {
|
||||||
|
MWMediaSeasons,
|
||||||
|
MWMediaType,
|
||||||
|
MWMediaProviderSeries,
|
||||||
|
} from "@/providers/types";
|
||||||
import { getProviderFromId } from "./helpers";
|
import { getProviderFromId } from "./helpers";
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
@ -23,7 +27,10 @@ export async function getSeasonDataFromMedia(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!provider.type.includes(MWMediaType.SERIES) && !provider.type.includes(MWMediaType.ANIME)) {
|
if (
|
||||||
|
!provider.type.includes(MWMediaType.SERIES) &&
|
||||||
|
!provider.type.includes(MWMediaType.ANIME)
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
seasons: [],
|
seasons: [],
|
||||||
};
|
};
|
||||||
|
1
src/react-app-env.d.ts
vendored
1
src/react-app-env.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
/// <reference types="react-scripts" />
|
|
@ -1,4 +1,3 @@
|
|||||||
import { getProviderMetadata, MWMediaMeta } from "providers";
|
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
@ -7,6 +6,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { getProviderMetadata, MWMediaMeta } from "@/providers";
|
||||||
import { BookmarkStore } from "./store";
|
import { BookmarkStore } from "./store";
|
||||||
|
|
||||||
interface BookmarkStoreData {
|
interface BookmarkStoreData {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { versionedStoreBuilder } from 'utils/storage';
|
import { versionedStoreBuilder } from "@/utils/storage";
|
||||||
|
|
||||||
export const BookmarkStore = versionedStoreBuilder()
|
export const BookmarkStore = versionedStoreBuilder()
|
||||||
.setKey('mw-bookmarks')
|
.setKey("mw-bookmarks")
|
||||||
.addVersion({
|
.addVersion({
|
||||||
version: 0,
|
version: 0,
|
||||||
create() {
|
create() {
|
||||||
return {
|
return {
|
||||||
bookmarks: []
|
bookmarks: [],
|
||||||
}
|
};
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.build()
|
.build();
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { MWMediaMeta, getProviderMetadata, MWMediaType } from "providers";
|
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
@ -7,6 +6,7 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { MWMediaMeta, getProviderMetadata, MWMediaType } from "@/providers";
|
||||||
import { VideoProgressStore } from "./store";
|
import { VideoProgressStore } from "./store";
|
||||||
|
|
||||||
interface WatchedStoreItem extends MWMediaMeta {
|
interface WatchedStoreItem extends MWMediaMeta {
|
||||||
@ -38,7 +38,7 @@ export function getWatchedFromPortable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WatchedContext = createContext<WatchedStoreDataWrapper>({
|
const WatchedContext = createContext<WatchedStoreDataWrapper>({
|
||||||
updateProgress: () => { },
|
updateProgress: () => {},
|
||||||
getFilteredWatched: () => [],
|
getFilteredWatched: () => [],
|
||||||
watched: {
|
watched: {
|
||||||
items: [],
|
items: [],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MWMediaType } from "providers";
|
import { MWMediaType } from "@/providers";
|
||||||
import { versionedStoreBuilder } from "utils/storage";
|
import { versionedStoreBuilder } from "@/utils/storage";
|
||||||
import { WatchedStoreData } from "./context";
|
import { WatchedStoreData } from "./context";
|
||||||
|
|
||||||
export const VideoProgressStore = versionedStoreBuilder()
|
export const VideoProgressStore = versionedStoreBuilder()
|
||||||
@ -12,35 +12,48 @@ export const VideoProgressStore = versionedStoreBuilder()
|
|||||||
migrate(data: any) {
|
migrate(data: any) {
|
||||||
const output: WatchedStoreData = { items: [] };
|
const output: WatchedStoreData = { items: [] };
|
||||||
|
|
||||||
if (!data || data.constructor !== Object)
|
if (!data || data.constructor !== Object) return output;
|
||||||
return output;
|
|
||||||
|
|
||||||
Object.keys(data).forEach((scraperId) => {
|
Object.keys(data).forEach((scraperId) => {
|
||||||
if (scraperId === "--version") return;
|
if (scraperId === "--version") return;
|
||||||
if (scraperId === "save") return;
|
if (scraperId === "save") return;
|
||||||
|
|
||||||
if (data[scraperId].movie && data[scraperId].movie.constructor === Object) {
|
if (
|
||||||
|
data[scraperId].movie &&
|
||||||
|
data[scraperId].movie.constructor === Object
|
||||||
|
) {
|
||||||
Object.keys(data[scraperId].movie).forEach((movieId) => {
|
Object.keys(data[scraperId].movie).forEach((movieId) => {
|
||||||
try {
|
try {
|
||||||
output.items.push({
|
output.items.push({
|
||||||
mediaId: movieId.includes("player.php") ? movieId.split("player.php%3Fimdb%3D")[1] : movieId,
|
mediaId: movieId.includes("player.php")
|
||||||
|
? movieId.split("player.php%3Fimdb%3D")[1]
|
||||||
|
: movieId,
|
||||||
mediaType: MWMediaType.MOVIE,
|
mediaType: MWMediaType.MOVIE,
|
||||||
providerId: scraperId,
|
providerId: scraperId,
|
||||||
title: data[scraperId].movie[movieId].full.meta.title,
|
title: data[scraperId].movie[movieId].full.meta.title,
|
||||||
year: data[scraperId].movie[movieId].full.meta.year,
|
year: data[scraperId].movie[movieId].full.meta.year,
|
||||||
progress: data[scraperId].movie[movieId].full.currentlyAt,
|
progress: data[scraperId].movie[movieId].full.currentlyAt,
|
||||||
percentage: Math.round((data[scraperId].movie[movieId].full.currentlyAt / data[scraperId].movie[movieId].full.totalDuration) * 100)
|
percentage: Math.round(
|
||||||
|
(data[scraperId].movie[movieId].full.currentlyAt /
|
||||||
|
data[scraperId].movie[movieId].full.totalDuration) *
|
||||||
|
100
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to migrate movie: ${scraperId}/${movieId}`, data[scraperId].movie[movieId]);
|
console.error(
|
||||||
|
`Failed to migrate movie: ${scraperId}/${movieId}`,
|
||||||
|
data[scraperId].movie[movieId]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data[scraperId].show && data[scraperId].show.constructor === Object) {
|
if (
|
||||||
|
data[scraperId].show &&
|
||||||
|
data[scraperId].show.constructor === Object
|
||||||
|
) {
|
||||||
Object.keys(data[scraperId].show).forEach((showId) => {
|
Object.keys(data[scraperId].show).forEach((showId) => {
|
||||||
if (data[scraperId].show[showId].constructor !== Object)
|
if (data[scraperId].show[showId].constructor !== Object) return;
|
||||||
return;
|
|
||||||
Object.keys(data[scraperId].show[showId]).forEach((episodeId) => {
|
Object.keys(data[scraperId].show[showId]).forEach((episodeId) => {
|
||||||
try {
|
try {
|
||||||
output.items.push({
|
output.items.push({
|
||||||
@ -49,13 +62,21 @@ export const VideoProgressStore = versionedStoreBuilder()
|
|||||||
providerId: scraperId,
|
providerId: scraperId,
|
||||||
title: data[scraperId].show[showId][episodeId].meta.title,
|
title: data[scraperId].show[showId][episodeId].meta.title,
|
||||||
year: data[scraperId].show[showId][episodeId].meta.year,
|
year: data[scraperId].show[showId][episodeId].meta.year,
|
||||||
percentage: Math.round((data[scraperId].show[showId][episodeId].currentlyAt / data[scraperId].show[showId][episodeId].totalDuration) * 100),
|
percentage: Math.round(
|
||||||
|
(data[scraperId].show[showId][episodeId].currentlyAt /
|
||||||
|
data[scraperId].show[showId][episodeId].totalDuration) *
|
||||||
|
100
|
||||||
|
),
|
||||||
progress: data[scraperId].show[showId][episodeId].currentlyAt,
|
progress: data[scraperId].show[showId][episodeId].currentlyAt,
|
||||||
episodeId: data[scraperId].show[showId][episodeId].show.episode,
|
episodeId:
|
||||||
|
data[scraperId].show[showId][episodeId].show.episode,
|
||||||
seasonId: data[scraperId].show[showId][episodeId].show.season,
|
seasonId: data[scraperId].show[showId][episodeId].show.season,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to migrate series: ${scraperId}/${showId}/${episodeId}`, data[scraperId].show[showId][episodeId]);
|
console.error(
|
||||||
|
`Failed to migrate series: ${scraperId}/${showId}/${episodeId}`,
|
||||||
|
data[scraperId].show[showId][episodeId]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import { IconPatch } from "components/buttons/IconPatch";
|
import { ReactElement, useEffect, useState } from "react";
|
||||||
import { Icons } from "components/Icon";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Navigation } from "components/layout/Navigation";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Paper } from "components/layout/Paper";
|
import { Icons } from "@/components/Icon";
|
||||||
import { LoadingSeasons, Seasons } from "components/layout/Seasons";
|
import { Navigation } from "@/components/layout/Navigation";
|
||||||
import { SkeletonVideoPlayer, VideoPlayer } from "components/media/VideoPlayer";
|
import { Paper } from "@/components/layout/Paper";
|
||||||
import { ArrowLink } from "components/text/ArrowLink";
|
import { LoadingSeasons, Seasons } from "@/components/layout/Seasons";
|
||||||
import { DotList } from "components/text/DotList";
|
import {
|
||||||
import { Title } from "components/text/Title";
|
SkeletonVideoPlayer,
|
||||||
import { useLoading } from "hooks/useLoading";
|
VideoPlayer,
|
||||||
import { usePortableMedia } from "hooks/usePortableMedia";
|
} from "@/components/media/VideoPlayer";
|
||||||
|
import { ArrowLink } from "@/components/text/ArrowLink";
|
||||||
|
import { DotList } from "@/components/text/DotList";
|
||||||
|
import { Title } from "@/components/text/Title";
|
||||||
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
|
import { usePortableMedia } from "@/hooks/usePortableMedia";
|
||||||
import {
|
import {
|
||||||
MWPortableMedia,
|
MWPortableMedia,
|
||||||
getStream,
|
getStream,
|
||||||
@ -18,14 +23,12 @@ import {
|
|||||||
getProviderFromId,
|
getProviderFromId,
|
||||||
MWMediaProvider,
|
MWMediaProvider,
|
||||||
MWMediaType,
|
MWMediaType,
|
||||||
} from "providers";
|
} from "@/providers";
|
||||||
import { ReactElement, useEffect, useState } from "react";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
import {
|
import {
|
||||||
getIfBookmarkedFromPortable,
|
getIfBookmarkedFromPortable,
|
||||||
useBookmarkContext,
|
useBookmarkContext,
|
||||||
} from "state/bookmark";
|
} from "@/state/bookmark";
|
||||||
import { getWatchedFromPortable, useWatchedContext } from "state/watched";
|
import { getWatchedFromPortable, useWatchedContext } from "@/state/watched";
|
||||||
import { NotFoundChecks } from "./notfound/NotFoundChecks";
|
import { NotFoundChecks } from "./notfound/NotFoundChecks";
|
||||||
|
|
||||||
interface StyledMediaViewProps {
|
interface StyledMediaViewProps {
|
||||||
@ -106,10 +109,10 @@ function LoadingMediaFooter(props: { error?: boolean }) {
|
|||||||
<Paper className="mt-5">
|
<Paper className="mt-5">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="bg-denim-500 mb-2 h-4 w-48 rounded-full" />
|
<div className="mb-2 h-4 w-48 rounded-full bg-denim-500" />
|
||||||
<div>
|
<div>
|
||||||
<span className="bg-denim-400 mr-4 inline-block h-2 w-12 rounded-full" />
|
<span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" />
|
||||||
<span className="bg-denim-400 mr-4 inline-block h-2 w-12 rounded-full" />
|
<span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" />
|
||||||
</div>
|
</div>
|
||||||
{props.error ? (
|
{props.error ? (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import { WatchedMediaCard } from "components/media/WatchedMediaCard";
|
|
||||||
import { SearchBarInput } from "components/SearchBar";
|
|
||||||
import { MWMassProviderOutput, MWQuery, SearchProviders } from "providers";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { ThinContainer } from "components/layout/ThinContainer";
|
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||||
import { SectionHeading } from "components/layout/SectionHeading";
|
import { SearchBarInput } from "@/components/SearchBar";
|
||||||
import { Icons } from "components/Icon";
|
import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers";
|
||||||
import { Loading } from "components/layout/Loading";
|
import { ThinContainer } from "@/components/layout/ThinContainer";
|
||||||
import { Tagline } from "components/text/Tagline";
|
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||||
import { Title } from "components/text/Title";
|
import { Icons } from "@/components/Icon";
|
||||||
import { useDebounce } from "hooks/useDebounce";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
import { useLoading } from "hooks/useLoading";
|
import { Tagline } from "@/components/text/Tagline";
|
||||||
import { IconPatch } from "components/buttons/IconPatch";
|
import { Title } from "@/components/text/Title";
|
||||||
import { Navigation } from "components/layout/Navigation";
|
import { useDebounce } from "@/hooks/useDebounce";
|
||||||
import { useSearchQuery } from "hooks/useSearchQuery";
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
import { useWatchedContext } from "state/watched/context";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
|
import { Navigation } from "@/components/layout/Navigation";
|
||||||
|
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||||
|
import { useWatchedContext } from "@/state/watched/context";
|
||||||
import {
|
import {
|
||||||
getIfBookmarkedFromPortable,
|
getIfBookmarkedFromPortable,
|
||||||
useBookmarkContext,
|
useBookmarkContext,
|
||||||
} from "state/bookmark/context";
|
} from "@/state/bookmark/context";
|
||||||
|
|
||||||
function SearchLoading() {
|
function SearchLoading() {
|
||||||
return <Loading className="my-24" text="Fetching your favourite shows..." />;
|
return <Loading className="my-24" text="Fetching your favourite shows..." />;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { getProviderMetadata, MWPortableMedia } from "providers";
|
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { getProviderMetadata, MWPortableMedia } from "@/providers";
|
||||||
import { NotFoundMedia, NotFoundProvider } from "./NotFoundView";
|
import { NotFoundMedia, NotFoundProvider } from "./NotFoundView";
|
||||||
|
|
||||||
export interface NotFoundChecksProps {
|
export interface NotFoundChecksProps {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { IconPatch } from "components/buttons/IconPatch";
|
|
||||||
import { Icons } from "components/Icon";
|
|
||||||
import { Navigation } from "components/layout/Navigation";
|
|
||||||
import { ArrowLink } from "components/text/ArrowLink";
|
|
||||||
import { Title } from "components/text/Title";
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { Navigation } from "@/components/layout/Navigation";
|
||||||
|
import { ArrowLink } from "@/components/text/ArrowLink";
|
||||||
|
import { Title } from "@/components/text/Title";
|
||||||
|
|
||||||
function NotFoundWrapper(props: { children?: ReactNode }) {
|
function NotFoundWrapper(props: { children?: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@ -21,7 +21,7 @@ export function NotFoundMedia() {
|
|||||||
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
|
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
|
||||||
<IconPatch
|
<IconPatch
|
||||||
icon={Icons.EYE_SLASH}
|
icon={Icons.EYE_SLASH}
|
||||||
className="text-bink-600 mb-6 text-xl"
|
className="mb-6 text-xl text-bink-600"
|
||||||
/>
|
/>
|
||||||
<Title>Couldn't find that media</Title>
|
<Title>Couldn't find that media</Title>
|
||||||
<p className="mt-5 mb-12 max-w-sm">
|
<p className="mt-5 mb-12 max-w-sm">
|
||||||
@ -38,7 +38,7 @@ export function NotFoundProvider() {
|
|||||||
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
|
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
|
||||||
<IconPatch
|
<IconPatch
|
||||||
icon={Icons.EYE_SLASH}
|
icon={Icons.EYE_SLASH}
|
||||||
className="text-bink-600 mb-6 text-xl"
|
className="mb-6 text-xl text-bink-600"
|
||||||
/>
|
/>
|
||||||
<Title>This provider has been disabled</Title>
|
<Title>This provider has been disabled</Title>
|
||||||
<p className="mt-5 mb-12 max-w-sm">
|
<p className="mt-5 mb-12 max-w-sm">
|
||||||
@ -55,7 +55,7 @@ export function NotFoundPage() {
|
|||||||
<NotFoundWrapper>
|
<NotFoundWrapper>
|
||||||
<IconPatch
|
<IconPatch
|
||||||
icon={Icons.EYE_SLASH}
|
icon={Icons.EYE_SLASH}
|
||||||
className="text-bink-600 mb-6 text-xl"
|
className="mb-6 text-xl text-bink-600"
|
||||||
/>
|
/>
|
||||||
<Title>Couldn't find that page</Title>
|
<Title>Couldn't find that page</Title>
|
||||||
<p className="mt-5 mb-12 max-w-sm">
|
<p className="mt-5 mb-12 max-w-sm">
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
/* colors */
|
/* colors */
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "ES2020",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
@ -20,8 +16,10 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
},
|
||||||
|
"types": ["vite/client"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"]
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
12
vite.config.ts
Normal file
12
vite.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react-swc";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user