mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-26 08:51:51 +01:00
the start on a docs page + error pages
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com> Co-authored-by: William Oldham <github@binaryoverload.co.uk>
This commit is contained in:
parent
109d9054d6
commit
cec0744907
4
.docs/.eslintignore
Normal file
4
.docs/.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.output
|
||||||
|
.nuxt
|
8
.docs/.eslintrc.cjs
Normal file
8
.docs/.eslintrc.cjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: '@nuxt/eslint-config',
|
||||||
|
rules: {
|
||||||
|
'vue/max-attributes-per-line': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off'
|
||||||
|
}
|
||||||
|
}
|
12
.docs/.gitignore
vendored
Executable file
12
.docs/.gitignore
vendored
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
node_modules
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
*.log*
|
||||||
|
.nuxt
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
sw.*
|
||||||
|
.env
|
||||||
|
.output
|
1
.docs/.npmrc
Normal file
1
.docs/.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
shamefully-hoist=true
|
57
.docs/README.md
Executable file
57
.docs/README.md
Executable file
@ -0,0 +1,57 @@
|
|||||||
|
# Docus Starter
|
||||||
|
|
||||||
|
Starter template for [Docus](https://docus.dev).
|
||||||
|
|
||||||
|
## Clone
|
||||||
|
|
||||||
|
Clone the repository (using `nuxi`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx nuxi init -t themes/docus
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Edge Side Rendering
|
||||||
|
|
||||||
|
Can be deployed to Vercel Functions, Netlify Functions, AWS, and most Node-compatible environments.
|
||||||
|
|
||||||
|
Look at all the available presets [here](https://v3.nuxtjs.org/guide/deploy/presets).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Static Generation
|
||||||
|
|
||||||
|
Use the `generate` command to build your application.
|
||||||
|
|
||||||
|
The HTML files will be generated in the .output/public directory and ready to be deployed to any static compatible hosting.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn generate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preview build
|
||||||
|
|
||||||
|
You might want to preview the result of your build locally, to do so, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn preview
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For a detailed explanation of how things work, check out [Docus](https://docus.dev).
|
18
.docs/app.config.ts
Normal file
18
.docs/app.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export default defineAppConfig({
|
||||||
|
docus: {
|
||||||
|
title: 'movie-web',
|
||||||
|
description: 'For all your media scraping needs',
|
||||||
|
socials: {
|
||||||
|
github: 'movie-web/providers',
|
||||||
|
},
|
||||||
|
image: '',
|
||||||
|
aside: {
|
||||||
|
level: 0,
|
||||||
|
exclude: [],
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
logo: false,
|
||||||
|
title: "movie-web"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
50
.docs/content/0.index.md
Normal file
50
.docs/content/0.index.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
title: "movie-web | For all your movie and TV show needs"
|
||||||
|
navigation: false
|
||||||
|
layout: page
|
||||||
|
---
|
||||||
|
|
||||||
|
::block-hero
|
||||||
|
---
|
||||||
|
cta:
|
||||||
|
- Get Started
|
||||||
|
- /guide/usage
|
||||||
|
secondary:
|
||||||
|
- Open on GitHub →
|
||||||
|
- https://github.com/movie-web/movie-web
|
||||||
|
---
|
||||||
|
|
||||||
|
#title
|
||||||
|
movie-web
|
||||||
|
|
||||||
|
#description
|
||||||
|
A simple and no-BS app for watching movies and TV shows (Binary is a silly goose)
|
||||||
|
::
|
||||||
|
|
||||||
|
::card-grid
|
||||||
|
#title
|
||||||
|
What's all the fuss?
|
||||||
|
|
||||||
|
#root
|
||||||
|
:ellipsis
|
||||||
|
|
||||||
|
#default
|
||||||
|
::card{icon="mdi:server-network"}
|
||||||
|
#title
|
||||||
|
Easy to host
|
||||||
|
#description
|
||||||
|
movie-web can be easily hosted on any static website host.
|
||||||
|
::
|
||||||
|
::card{icon="material-symbols:hangout-video-off"}
|
||||||
|
#title
|
||||||
|
No ADs
|
||||||
|
#description
|
||||||
|
movie-web will never show ADs, enjoy watching without interruptions.
|
||||||
|
::
|
||||||
|
::card{icon="ic:baseline-ondemand-video"}
|
||||||
|
#title
|
||||||
|
Custom Player
|
||||||
|
#description
|
||||||
|
Enjoy a fully custom video player including streaming integration, subtitle customisation and easy TV season navigation.
|
||||||
|
::
|
||||||
|
::
|
2
.docs/content/1.introduction/1.getting-started.md
Normal file
2
.docs/content/1.introduction/1.getting-started.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
2
.docs/content/1.introduction/_dir.yml
Normal file
2
.docs/content/1.introduction/_dir.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
icon: ph:star-duotone
|
||||||
|
navigation.redirect: /introduction/getting-started
|
9
.docs/content/2.self-hosting/1.self-hosting.md
Normal file
9
.docs/content/2.self-hosting/1.self-hosting.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# How to self host
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
We **do not** provide support on how to self-host. If you can't figure it out then tough luck. Please do not make GitHub issues or ask in our Discord server for support on how to self-host.
|
||||||
|
::
|
||||||
|
|
||||||
|
The movie-web application is made of two parts: the proxy and the client. Click the following links to find out more:
|
||||||
|
- [Setup the Proxy](2.proxy.md)
|
||||||
|
- [Setup the Client](3.client.md)
|
36
.docs/content/2.self-hosting/2.proxy.md
Normal file
36
.docs/content/2.self-hosting/2.proxy.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Setting up the proxy
|
||||||
|
|
||||||
|
Our proxy is used to bypass CORS-protected URLs on the client side, allowing users to make requests to protected endpoints without a backend server.
|
||||||
|
|
||||||
|
The proxy is made using [Nitro by UnJS](https://nitro.unjs.io/) which supports building the proxy to work on multiple providers including Cloudflare Workers, AWS Lambda and [more...](https://nitro.unjs.io/deploy)
|
||||||
|
|
||||||
|
Our recommended provider is Cloudflare due to its [generous free plan](https://www.cloudflare.com/en-gb/plans/developer-platform/).
|
||||||
|
|
||||||
|
## Cloudflare Workers
|
||||||
|
|
||||||
|
The proxy is made as a Cloudflare worker. Cloudflare has a generous free plan, so you don't need to pay anything unless you get hundreds of users.
|
||||||
|
|
||||||
|
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/movie-web/simple-proxy)
|
||||||
|
|
||||||
|
1. Create a GitHub account at https://github.com if you don't have one.
|
||||||
|
1. Click the "Deploy with workers" button above.
|
||||||
|
1. Click the "Authorize Workers" button to authorise Cloudflare to talk to GitHub.
|
||||||
|
1. Authorize Cloudflare Workers in the GitHub page that pops up.
|
||||||
|
1. Follow the instructions to configure your Cloudflare account. Select "I have an account" if you have a Cloudflare account already, otherwise follow the link to create one.
|
||||||
|
1. Click the link to "Workers Dashboard" to find your account ID.
|
||||||
|
1. If you have used workers in the past, there will be a place on the right hand side to copy your account ID.
|
||||||
|
1. If you haven't used workers before, you can copy your account ID from the URL e.g. https://dash.cloudflare.com/ab7cb454c93987b6343350d4e84c16c7/workers-and-pages/create where `ab7cb454c93987b6343350d4e84c16c7` is the account ID.
|
||||||
|
1. Paste the account ID into the text box on the original Cloudflare workers page.
|
||||||
|
1. Click the link to "My Profile" to create an API token.
|
||||||
|
1. Click "Create Token".
|
||||||
|
1. Select "Use template" next to "Edit Cloudflare Workers".
|
||||||
|
1. Under "Account Resources", select "Include" and your account under the dropdown.
|
||||||
|
1. Under "Zone Resources", select "All zones" (You can select a more specific zone if you have the zones available).
|
||||||
|
1. At the bottom of the page, click "Continue to summary".
|
||||||
|
1. On the next screen, click "Create token".
|
||||||
|
1. Copy the API token and **save it in a safe place, it won't be shown again**.
|
||||||
|
1. Paste the API token into the Cloudflare Workers API Token text box.
|
||||||
|
1. Click "Fork the Repository" and follow the instructions to enable workflows.
|
||||||
|
1. Click "Deploy" to deploy to Cloudflare Workers.
|
||||||
|
1. Congratulations! Your worker is now deploying. Please wait for the GitHub Action to build and publish your worker.
|
||||||
|
1. You can click the "Worker dash" and "GitHub repo" buttons to see the status of the deploy.
|
0
.docs/content/2.self-hosting/3.client.md
Normal file
0
.docs/content/2.self-hosting/3.client.md
Normal file
2
.docs/content/2.self-hosting/_dir.yml
Normal file
2
.docs/content/2.self-hosting/_dir.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
title: 'Self-Hosting'
|
||||||
|
icon: mdi:server-network
|
11
.docs/nuxt.config.ts
Executable file
11
.docs/nuxt.config.ts
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
export default defineNuxtConfig({
|
||||||
|
// https://github.com/nuxt-themes/docus
|
||||||
|
extends: '@nuxt-themes/docus',
|
||||||
|
devtools: { enabled: true },
|
||||||
|
|
||||||
|
modules: [
|
||||||
|
// Remove it if you don't use Plausible analytics
|
||||||
|
// https://github.com/nuxt-modules/plausible
|
||||||
|
'@nuxtjs/plausible'
|
||||||
|
]
|
||||||
|
})
|
21
.docs/package.json
Executable file
21
.docs/package.json
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "docus-starter",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nuxi dev",
|
||||||
|
"build": "nuxi build",
|
||||||
|
"generate": "nuxi generate",
|
||||||
|
"preview": "nuxi preview",
|
||||||
|
"lint": "eslint ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxt-themes/docus": "latest",
|
||||||
|
"@nuxt/devtools": "^0.8.5",
|
||||||
|
"@nuxt/eslint-config": "^0.2.0",
|
||||||
|
"@nuxtjs/plausible": "^0.2.3",
|
||||||
|
"@types/node": "^20.8.2",
|
||||||
|
"eslint": "^8.50.0",
|
||||||
|
"nuxt": "^3.7.4"
|
||||||
|
}
|
||||||
|
}
|
9763
.docs/pnpm-lock.yaml
generated
Normal file
9763
.docs/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.docs/public/cover.png
Normal file
BIN
.docs/public/cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 214 KiB |
BIN
.docs/public/favicon.ico
Normal file
BIN
.docs/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
8
.docs/renovate.json
Executable file
8
.docs/renovate.json
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"@nuxtjs"
|
||||||
|
],
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
18
.docs/tokens.config.ts
Normal file
18
.docs/tokens.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { defineTheme } from 'pinceau'
|
||||||
|
|
||||||
|
export default defineTheme({
|
||||||
|
color: {
|
||||||
|
primary: {
|
||||||
|
50: "#F5E5FF",
|
||||||
|
100: "#E7CCFF",
|
||||||
|
200: "#D4A9FF",
|
||||||
|
300: "#BE85FF",
|
||||||
|
400: "#A861FF",
|
||||||
|
500: "#8E3DFF",
|
||||||
|
600: "#7F36D4",
|
||||||
|
700: "#662CA6",
|
||||||
|
800: "#552578",
|
||||||
|
900: "#441E49"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
3
.docs/tsconfig.json
Executable file
3
.docs/tsconfig.json
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
|
}
|
48
.github/workflows/docs.yml
vendored
Normal file
48
.github/workflows/docs.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
name: Publish docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
- name: Install packages
|
||||||
|
working-directory: ./.docs
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
working-directory: ./.docs
|
||||||
|
run: npm run generate
|
||||||
|
|
||||||
|
- name: Upload production-ready build files
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
path: ./.docs/.output/public
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
needs: build
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
environment:
|
||||||
|
name: docs
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v2
|
@ -1,4 +1,6 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
@ -6,21 +8,55 @@ interface Props {
|
|||||||
icon?: Icons;
|
icon?: Icons;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
theme?: "white" | "purple" | "secondary";
|
||||||
|
padding?: string;
|
||||||
|
className?: string;
|
||||||
|
href?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button(props: Props) {
|
export function Button(props: Props) {
|
||||||
return (
|
const history = useHistory();
|
||||||
<button
|
|
||||||
type="button"
|
let colorClasses = "bg-white hover:bg-gray-200 text-black";
|
||||||
onClick={props.onClick}
|
if (props.theme === "purple")
|
||||||
className="inline-flex items-center justify-center rounded-lg bg-white px-4 py-3 font-bold text-black transition-[transform,background-color] duration-100 hover:bg-gray-200 active:scale-105 md:px-8"
|
colorClasses =
|
||||||
>
|
"bg-video-buttons-purple hover:bg-video-buttons-purpleHover text-white";
|
||||||
|
if (props.theme === "secondary")
|
||||||
|
colorClasses =
|
||||||
|
"bg-video-buttons-cancel hover:bg-video-buttons-cancelHover transition-colors duration-100 text-white";
|
||||||
|
|
||||||
|
const classes = classNames(
|
||||||
|
"cursor-pointer inline-flex items-center justify-center rounded-lg font-medium transition-[transform,background-color] duration-100 active:scale-105 md:px-8",
|
||||||
|
props.padding ?? "px-4 py-3",
|
||||||
|
props.className,
|
||||||
|
colorClasses
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
{props.icon ? (
|
{props.icon ? (
|
||||||
<span className="mr-3 hidden md:inline-block">
|
<span className="mr-3 hidden md:inline-block">
|
||||||
<Icon icon={props.icon} />
|
<Icon icon={props.icon} />
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{props.children}
|
{props.children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
function goTo(href: string) {
|
||||||
|
history.push(href);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.href)
|
||||||
|
return (
|
||||||
|
<a className={classes} onClick={() => goTo(props.href || "")}>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button type="button" onClick={props.onClick} className={classes}>
|
||||||
|
{content}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,8 @@ export enum Icons {
|
|||||||
MORE_VERTICAL = "more_vertical",
|
MORE_VERTICAL = "more_vertical",
|
||||||
IOS_SHARE = "ios_share",
|
IOS_SHARE = "ios_share",
|
||||||
IOS_FILES = "ios_files",
|
IOS_FILES = "ios_files",
|
||||||
|
WAND = "wand",
|
||||||
|
COPY = "copy",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
@ -103,6 +105,8 @@ const iconList: Record<Icons, string> = {
|
|||||||
more_vertical: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-vertical"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>`,
|
more_vertical: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-vertical"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>`,
|
||||||
ios_share: `<svg width="1em" height="1em" viewBox="0 0 20 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 15.3857C10.4409 15.3857 10.8101 15.0166 10.8101 14.5859V4.05518L10.7485 2.51709L11.4355 3.24512L12.9941 4.90625C13.1377 5.07031 13.353 5.15234 13.5479 5.15234C13.9683 5.15234 14.2964 4.84473 14.2964 4.42432C14.2964 4.20898 14.2041 4.04492 14.0503 3.89111L10.5845 0.54834C10.3794 0.343262 10.2051 0.271484 10 0.271484C9.78467 0.271484 9.61035 0.343262 9.40527 0.54834L5.93945 3.89111C5.78564 4.04492 5.69336 4.20898 5.69336 4.42432C5.69336 4.84473 6.00098 5.15234 6.43164 5.15234C6.62646 5.15234 6.85205 5.07031 6.99561 4.90625L8.5542 3.24512L9.24121 2.51709L9.17969 4.05518V14.5859C9.17969 15.0166 9.55908 15.3857 10 15.3857ZM4.11426 23.4146H15.8755C18.0186 23.4146 19.0952 22.3481 19.0952 20.2358V10.0024C19.0952 7.89014 18.0186 6.82373 15.8755 6.82373H13.0146V8.47461H15.8447C16.8599 8.47461 17.4443 9.02832 17.4443 10.0947V20.1436C17.4443 21.21 16.8599 21.7637 15.8447 21.7637H4.13477C3.10938 21.7637 2.54541 21.21 2.54541 20.1436V10.0947C2.54541 9.02832 3.10938 8.47461 4.13477 8.47461H6.9751V6.82373H4.11426C1.97119 6.82373 0.894531 7.89014 0.894531 10.0024V20.2358C0.894531 22.3481 1.97119 23.4146 4.11426 23.4146Z" fill="currentColor"/></svg>`,
|
ios_share: `<svg width="1em" height="1em" viewBox="0 0 20 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 15.3857C10.4409 15.3857 10.8101 15.0166 10.8101 14.5859V4.05518L10.7485 2.51709L11.4355 3.24512L12.9941 4.90625C13.1377 5.07031 13.353 5.15234 13.5479 5.15234C13.9683 5.15234 14.2964 4.84473 14.2964 4.42432C14.2964 4.20898 14.2041 4.04492 14.0503 3.89111L10.5845 0.54834C10.3794 0.343262 10.2051 0.271484 10 0.271484C9.78467 0.271484 9.61035 0.343262 9.40527 0.54834L5.93945 3.89111C5.78564 4.04492 5.69336 4.20898 5.69336 4.42432C5.69336 4.84473 6.00098 5.15234 6.43164 5.15234C6.62646 5.15234 6.85205 5.07031 6.99561 4.90625L8.5542 3.24512L9.24121 2.51709L9.17969 4.05518V14.5859C9.17969 15.0166 9.55908 15.3857 10 15.3857ZM4.11426 23.4146H15.8755C18.0186 23.4146 19.0952 22.3481 19.0952 20.2358V10.0024C19.0952 7.89014 18.0186 6.82373 15.8755 6.82373H13.0146V8.47461H15.8447C16.8599 8.47461 17.4443 9.02832 17.4443 10.0947V20.1436C17.4443 21.21 16.8599 21.7637 15.8447 21.7637H4.13477C3.10938 21.7637 2.54541 21.21 2.54541 20.1436V10.0947C2.54541 9.02832 3.10938 8.47461 4.13477 8.47461H6.9751V6.82373H4.11426C1.97119 6.82373 0.894531 7.89014 0.894531 10.0024V20.2358C0.894531 22.3481 1.97119 23.4146 4.11426 23.4146Z" fill="currentColor"/></svg>`,
|
||||||
ios_files: `<svg width="1em" height="1em" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.22405 20H21.024C22.9178 20 24 18.8772 24 16.7018V5.33333C24 3.1462 22.9065 2.03509 20.776 2.03509H10.5063C9.72851 2.03509 9.30014 1.85965 8.74777 1.36842L8.12776 0.818713C7.41757 0.187135 6.91029 0 5.85063 0H2.81822C1.01456 0 0 1.04094 0 3.1462V16.7018C0 18.8889 1.0822 20 3.22405 20ZM1.47675 3.22807C1.47675 2.08187 2.04039 1.50877 3.11132 1.50877H5.47863C6.23391 1.50877 6.65101 1.68421 7.21466 2.19883L7.84594 2.74854C8.52231 3.35673 9.06341 3.55556 10.1343 3.55556H20.7534C21.8807 3.55556 22.5233 4.18713 22.5233 5.4152V6.17544H1.47675V3.22807ZM3.24659 18.4795C2.09676 18.4795 1.47675 17.848 1.47675 16.6199V7.61403H22.5233V16.6316C22.5233 17.848 21.8807 18.4795 20.7534 18.4795H3.24659Z" fill="white"/></svg>`,
|
ios_files: `<svg width="1em" height="1em" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.22405 20H21.024C22.9178 20 24 18.8772 24 16.7018V5.33333C24 3.1462 22.9065 2.03509 20.776 2.03509H10.5063C9.72851 2.03509 9.30014 1.85965 8.74777 1.36842L8.12776 0.818713C7.41757 0.187135 6.91029 0 5.85063 0H2.81822C1.01456 0 0 1.04094 0 3.1462V16.7018C0 18.8889 1.0822 20 3.22405 20ZM1.47675 3.22807C1.47675 2.08187 2.04039 1.50877 3.11132 1.50877H5.47863C6.23391 1.50877 6.65101 1.68421 7.21466 2.19883L7.84594 2.74854C8.52231 3.35673 9.06341 3.55556 10.1343 3.55556H20.7534C21.8807 3.55556 22.5233 4.18713 22.5233 5.4152V6.17544H1.47675V3.22807ZM3.24659 18.4795C2.09676 18.4795 1.47675 17.848 1.47675 16.6199V7.61403H22.5233V16.6316C22.5233 17.848 21.8807 18.4795 20.7534 18.4795H3.24659Z" fill="white"/></svg>`,
|
||||||
|
wand: `<svg width="1em" height="1em" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.33437 4.33438L8.15625 4.775C8.0625 4.80937 8 4.9 8 5C8 5.1 8.0625 5.19062 8.15625 5.225L9.33437 5.66563L9.775 6.84375C9.80938 6.9375 9.9 7 10 7C10.1 7 10.1906 6.9375 10.225 6.84375L10.6656 5.66563L11.8438 5.225C11.9375 5.19062 12 5.1 12 5C12 4.9 11.9375 4.80937 11.8438 4.775L10.6656 4.33438L10.225 3.15625C10.1906 3.0625 10.1 3 10 3C9.9 3 9.80938 3.0625 9.775 3.15625L9.33437 4.33438ZM3.44062 15.3562C2.85625 15.9406 2.85625 16.8906 3.44062 17.4781L4.52187 18.5594C5.10625 19.1437 6.05625 19.1437 6.64375 18.5594L18.5594 6.64062C19.1438 6.05625 19.1438 5.10625 18.5594 4.51875L17.4781 3.44063C16.8937 2.85625 15.9437 2.85625 15.3562 3.44063L3.44062 15.3562ZM17.1438 5.58125L13.8625 8.8625L13.1344 8.13438L16.4156 4.85312L17.1438 5.58125ZM2.23438 6.6625C2.09375 6.71562 2 6.85 2 7C2 7.15 2.09375 7.28438 2.23438 7.3375L4 8L4.6625 9.76562C4.71562 9.90625 4.85 10 5 10C5.15 10 5.28438 9.90625 5.3375 9.76562L6 8L7.76562 7.3375C7.90625 7.28438 8 7.15 8 7C8 6.85 7.90625 6.71562 7.76562 6.6625L6 6L5.3375 4.23438C5.28438 4.09375 5.15 4 5 4C4.85 4 4.71562 4.09375 4.6625 4.23438L4 6L2.23438 6.6625ZM13.2344 14.6625C13.0938 14.7156 13 14.85 13 15C13 15.15 13.0938 15.2844 13.2344 15.3375L15 16L15.6625 17.7656C15.7156 17.9062 15.85 18 16 18C16.15 18 16.2844 17.9062 16.3375 17.7656L17 16L18.7656 15.3375C18.9062 15.2844 19 15.15 19 15C19 14.85 18.9062 14.7156 18.7656 14.6625L17 14L16.3375 12.2344C16.2844 12.0938 16.15 12 16 12C15.85 12 15.7156 12.0938 15.6625 12.2344L15 14L13.2344 14.6625Z" fill="currentColor"/></svg>`,
|
||||||
|
copy: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-copy"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChromeCastButton() {
|
function ChromeCastButton() {
|
||||||
|
13
src/components/layout/IconPill.tsx
Normal file
13
src/components/layout/IconPill.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
|
export function IconPill(props: { icon: Icons; children?: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-pill-background bg-opacity-50 px-4 py-2 rounded-full text-white flex justify-center items-center">
|
||||||
|
<Icon
|
||||||
|
icon={props.icon ?? Icons.WAND}
|
||||||
|
className="mr-3 text-xl text-bink-600"
|
||||||
|
/>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -51,5 +51,8 @@ export function usePlayer() {
|
|||||||
setScrapeStatus() {
|
setScrapeStatus() {
|
||||||
setStatus(playerStatus.SCRAPING);
|
setStatus(playerStatus.SCRAPING);
|
||||||
},
|
},
|
||||||
|
setScrapeNotFound() {
|
||||||
|
setStatus(playerStatus.SCRAPE_NOT_FOUND);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
16
src/components/text/HeroTitle.tsx
Normal file
16
src/components/text/HeroTitle.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export interface HeroTitleProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroTitle(props: HeroTitleProps) {
|
||||||
|
return (
|
||||||
|
<h1
|
||||||
|
className={`text-2xl font-bold text-white sm:text-3xl md:text-4xl ${
|
||||||
|
props.className ?? ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
}
|
3
src/components/text/Paragraph.tsx
Normal file
3
src/components/text/Paragraph.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function Paragraph(props: { children: React.ReactNode }) {
|
||||||
|
return <p className="text-errors-type-secondary mt-6">{props.children}</p>;
|
||||||
|
}
|
@ -1,16 +1,17 @@
|
|||||||
export interface TitleProps {
|
import classNames from "classnames";
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Title(props: TitleProps) {
|
export function Title(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<h1
|
<h2
|
||||||
className={`text-2xl font-bold text-white sm:text-3xl md:text-4xl ${
|
className={classNames(
|
||||||
props.className ?? ""
|
"text-white text-3xl font-bold text-opacity-100 mt-6",
|
||||||
}`}
|
props.className
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</h1>
|
</h2>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ export interface ScrapingSegment {
|
|||||||
id: string;
|
id: string;
|
||||||
status: "failure" | "pending" | "notfound" | "success" | "waiting";
|
status: "failure" | "pending" | "notfound" | "success" | "waiting";
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
error?: unknown;
|
||||||
percentage: number;
|
percentage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ export function useScrape() {
|
|||||||
if (s[evt.id]) {
|
if (s[evt.id]) {
|
||||||
s[evt.id].status = evt.status;
|
s[evt.id].status = evt.status;
|
||||||
s[evt.id].reason = evt.reason;
|
s[evt.id].reason = evt.reason;
|
||||||
|
s[evt.id].error = evt.error;
|
||||||
s[evt.id].percentage = evt.percentage;
|
s[evt.id].percentage = evt.percentage;
|
||||||
}
|
}
|
||||||
return { ...s };
|
return { ...s };
|
||||||
|
@ -8,7 +8,7 @@ export default function DeveloperPage() {
|
|||||||
<div className="py-48">
|
<div className="py-48">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<ThinContainer classNames="flex flex-col space-y-4">
|
<ThinContainer classNames="flex flex-col space-y-4">
|
||||||
<Title className="mb-8">Developer tools</Title>
|
<Title>Developer tools</Title>
|
||||||
<ArrowLink to="/dev/video" direction="right" linkText="Video tester" />
|
<ArrowLink to="/dev/video" direction="right" linkText="Video tester" />
|
||||||
<ArrowLink to="/dev/test" direction="right" linkText="Test page" />
|
<ArrowLink to="/dev/test" direction="right" linkText="Test page" />
|
||||||
</ThinContainer>
|
</ThinContainer>
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { RunOutput } from "@movie-web/providers";
|
import { RunOutput } from "@movie-web/providers";
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||||
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
||||||
import { convertRunoutputToSource } from "@/components/player/utils/convertRunoutputToSource";
|
import { convertRunoutputToSource } from "@/components/player/utils/convertRunoutputToSource";
|
||||||
|
import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape";
|
||||||
import { MetaPart } from "@/pages/parts/player/MetaPart";
|
import { MetaPart } from "@/pages/parts/player/MetaPart";
|
||||||
import { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
import { PlayerPart } from "@/pages/parts/player/PlayerPart";
|
||||||
|
import { ScrapeErrorPart } from "@/pages/parts/player/ScrapeErrorPart";
|
||||||
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
||||||
import { useLastNonPlayerLink } from "@/stores/history";
|
import { useLastNonPlayerLink } from "@/stores/history";
|
||||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||||
@ -18,7 +20,11 @@ export function PlayerView() {
|
|||||||
episode?: string;
|
episode?: string;
|
||||||
season?: string;
|
season?: string;
|
||||||
}>();
|
}>();
|
||||||
const { status, playMedia, reset } = usePlayer();
|
const [errorData, setErrorData] = useState<{
|
||||||
|
sources: Record<string, ScrapingSegment>;
|
||||||
|
sourceOrder: ScrapingItems[];
|
||||||
|
} | null>(null);
|
||||||
|
const { status, playMedia, reset, setScrapeNotFound } = usePlayer();
|
||||||
const { setPlayerMeta, scrapeMedia } = usePlayerMeta();
|
const { setPlayerMeta, scrapeMedia } = usePlayerMeta();
|
||||||
const backUrl = useLastNonPlayerLink();
|
const backUrl = useLastNonPlayerLink();
|
||||||
|
|
||||||
@ -56,7 +62,20 @@ export function PlayerView() {
|
|||||||
<MetaPart onGetMeta={setPlayerMeta} />
|
<MetaPart onGetMeta={setPlayerMeta} />
|
||||||
) : null}
|
) : null}
|
||||||
{status === playerStatus.SCRAPING && scrapeMedia ? (
|
{status === playerStatus.SCRAPING && scrapeMedia ? (
|
||||||
<ScrapingPart media={scrapeMedia} onGetStream={playAfterScrape} />
|
<ScrapingPart
|
||||||
|
media={scrapeMedia}
|
||||||
|
onResult={(sources, sourceOrder) => {
|
||||||
|
setErrorData({
|
||||||
|
sourceOrder,
|
||||||
|
sources,
|
||||||
|
});
|
||||||
|
setScrapeNotFound();
|
||||||
|
}}
|
||||||
|
onGetStream={playAfterScrape}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{status === playerStatus.SCRAPE_NOT_FOUND && errorData ? (
|
||||||
|
<ScrapeErrorPart data={errorData} />
|
||||||
) : null}
|
) : null}
|
||||||
</PlayerPart>
|
</PlayerPart>
|
||||||
);
|
);
|
||||||
|
@ -63,7 +63,7 @@ export default function VideoTesterView() {
|
|||||||
<div className="w-full max-w-4xl rounded-xl bg-video-scraping-card p-10 m-4">
|
<div className="w-full max-w-4xl rounded-xl bg-video-scraping-card p-10 m-4">
|
||||||
<div className="flex gap-16 flex-col lg:flex-row">
|
<div className="flex gap-16 flex-col lg:flex-row">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Title className="!text-2xl">Custom stream</Title>
|
<Title>Custom stream</Title>
|
||||||
<div className="grid grid-cols-[1fr,auto] gap-2 items-center">
|
<div className="grid grid-cols-[1fr,auto] gap-2 items-center">
|
||||||
<TextInputControl
|
<TextInputControl
|
||||||
className="bg-video-context-flagBg rounded-md p-2 text-white w-full"
|
className="bg-video-context-flagBg rounded-md p-2 text-white w-full"
|
||||||
@ -91,7 +91,7 @@ export default function VideoTesterView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Title className="!text-2xl mb-8">Preset tests</Title>
|
<Title>Preset tests</Title>
|
||||||
<div className="grid grid-cols-[1fr,1fr] gap-2">
|
<div className="grid grid-cols-[1fr,1fr] gap-2">
|
||||||
<Button onClick={() => start(testStreams.hls, "hls")}>
|
<Button onClick={() => start(testStreams.hls, "hls")}>
|
||||||
HLS test
|
HLS test
|
||||||
|
26
src/pages/layouts/ErrorLayout.tsx
Normal file
26
src/pages/layouts/ErrorLayout.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
export function ErrorContainer(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
maxWidth?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"w-full p-6 text-center flex flex-col items-center",
|
||||||
|
props.maxWidth ?? "max-w-[28rem]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorLayout(props: { children?: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex justify-center items-center flex-col">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
|
||||||
import { Icons } from "@/components/Icon";
|
|
||||||
import { ArrowLink } from "@/components/text/ArrowLink";
|
|
||||||
import { Title } from "@/components/text/Title";
|
|
||||||
|
|
||||||
export function MediaNotFoundPart() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
|
|
||||||
<IconPatch
|
|
||||||
icon={Icons.EYE_SLASH}
|
|
||||||
className="mb-6 text-xl text-bink-600"
|
|
||||||
/>
|
|
||||||
<Title>{t("notFound.media.title")}</Title>
|
|
||||||
<p className="mb-12 mt-5 max-w-sm">{t("notFound.media.description")}</p>
|
|
||||||
<ArrowLink to="/" linkText={t("notFound.backArrow")} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
|
||||||
import { Icons } from "@/components/Icon";
|
|
||||||
import { ArrowLink } from "@/components/text/ArrowLink";
|
|
||||||
import { Title } from "@/components/text/Title";
|
|
||||||
|
|
||||||
export function ProviderNotFoundPart() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
|
|
||||||
<IconPatch
|
|
||||||
icon={Icons.EYE_SLASH}
|
|
||||||
className="mb-6 text-xl text-bink-600"
|
|
||||||
/>
|
|
||||||
<Title>{t("notFound.provider.title")}</Title>
|
|
||||||
<p className="mb-12 mt-5 max-w-sm">
|
|
||||||
{t("notFound.provider.description")}
|
|
||||||
</p>
|
|
||||||
<ArrowLink to="/" linkText={t("notFound.backArrow")} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -4,7 +4,7 @@ import Sticky from "react-stickynode";
|
|||||||
|
|
||||||
import { ThinContainer } from "@/components/layout/ThinContainer";
|
import { ThinContainer } from "@/components/layout/ThinContainer";
|
||||||
import { SearchBarInput } from "@/components/SearchBar";
|
import { SearchBarInput } from "@/components/SearchBar";
|
||||||
import { Title } from "@/components/text/Title";
|
import { HeroTitle } from "@/components/text/HeroTitle";
|
||||||
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||||
import { useBannerSize } from "@/stores/banner";
|
import { useBannerSize } from "@/stores/banner";
|
||||||
|
|
||||||
@ -31,7 +31,9 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
|
|||||||
<ThinContainer>
|
<ThinContainer>
|
||||||
<div className="mt-44 space-y-16 text-center">
|
<div className="mt-44 space-y-16 text-center">
|
||||||
<div className="relative z-10 mb-16">
|
<div className="relative z-10 mb-16">
|
||||||
<Title className="mx-auto max-w-xs">{t("search.title")}</Title>
|
<HeroTitle className="mx-auto max-w-xs">
|
||||||
|
{t("search.title")}
|
||||||
|
</HeroTitle>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative z-30">
|
<div className="relative z-30">
|
||||||
<Sticky
|
<Sticky
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
|
import type { AsyncReturnType } from "type-fest";
|
||||||
|
|
||||||
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
import { decodeTMDBId } from "@/backend/metadata/tmdb";
|
import { decodeTMDBId } from "@/backend/metadata/tmdb";
|
||||||
import { MWMediaType } from "@/backend/metadata/types/mw";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { IconPill } from "@/components/layout/IconPill";
|
||||||
|
import { Loading } from "@/components/layout/Loading";
|
||||||
|
import { Paragraph } from "@/components/text/Paragraph";
|
||||||
|
import { Title } from "@/components/text/Title";
|
||||||
|
import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout";
|
||||||
|
|
||||||
export interface MetaPartProps {
|
export interface MetaPartProps {
|
||||||
onGetMeta?: (meta: DetailedMeta, episodeId?: string) => void;
|
onGetMeta?: (meta: DetailedMeta, episodeId?: string) => void;
|
||||||
@ -17,12 +25,25 @@ export function MetaPart(props: MetaPartProps) {
|
|||||||
}>();
|
}>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const { loading, error } = useAsync(async () => {
|
const { error, value, loading } = useAsync(async () => {
|
||||||
const data = decodeTMDBId(params.media);
|
let data: ReturnType<typeof decodeTMDBId> = null;
|
||||||
if (!data) return;
|
try {
|
||||||
|
data = decodeTMDBId(params.media);
|
||||||
|
} catch {
|
||||||
|
// error dont matter, itll just be a 404
|
||||||
|
}
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
const meta = await getMetaFromId(data.type, data.id, params.season);
|
let meta: AsyncReturnType<typeof getMetaFromId> = null;
|
||||||
if (!meta) return;
|
try {
|
||||||
|
meta = await getMetaFromId(data.type, data.id, params.season);
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as any).status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (!meta) return null;
|
||||||
|
|
||||||
// replace link with new link if youre not already on the right link
|
// replace link with new link if youre not already on the right link
|
||||||
let epId = params.episode;
|
let epId = params.episode;
|
||||||
@ -45,10 +66,61 @@ export function MetaPart(props: MetaPartProps) {
|
|||||||
props.onGetMeta?.(meta, epId);
|
props.onGetMeta?.(meta, epId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
<ErrorLayout>
|
||||||
|
<ErrorContainer>
|
||||||
|
<IconPill icon={Icons.WAND}>Failed to load</IconPill>
|
||||||
|
<Title>Failed to load meta data</Title>
|
||||||
|
<Paragraph>
|
||||||
|
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
|
||||||
|
bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`)
|
||||||
|
Please don't be angwy, wittle movie-web ish twying so hard. Can
|
||||||
|
you find it in your heart to forgive? UwU 💖
|
||||||
|
</Paragraph>
|
||||||
|
<Button
|
||||||
|
href="/"
|
||||||
|
theme="purple"
|
||||||
|
padding="md:px-12 p-2.5"
|
||||||
|
className="mt-6"
|
||||||
|
>
|
||||||
|
Go home
|
||||||
|
</Button>
|
||||||
|
</ErrorContainer>
|
||||||
|
</ErrorLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value && !loading) {
|
||||||
|
return (
|
||||||
|
<ErrorLayout>
|
||||||
|
<ErrorContainer>
|
||||||
|
<IconPill icon={Icons.WAND}>Not found</IconPill>
|
||||||
|
<Title>This media doesnt exist</Title>
|
||||||
|
<Paragraph>
|
||||||
|
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
|
||||||
|
bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`)
|
||||||
|
Please don't be angwy, wittle movie-web ish twying so hard. Can
|
||||||
|
you find it in your heart to forgive? UwU 💖
|
||||||
|
</Paragraph>
|
||||||
|
<Button
|
||||||
|
href="/"
|
||||||
|
theme="purple"
|
||||||
|
padding="md:px-12 p-2.5"
|
||||||
|
className="mt-6"
|
||||||
|
>
|
||||||
|
Go home
|
||||||
|
</Button>
|
||||||
|
</ErrorContainer>
|
||||||
|
</ErrorLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorLayout>
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
{loading ? <p>loading meta...</p> : null}
|
<Loading />
|
||||||
{error ? <p>failed to load meta!</p> : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
</ErrorLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,9 @@ export function PlayerPart(props: PlayerPartProps) {
|
|||||||
return (
|
return (
|
||||||
<Player.Container onLoad={props.onLoad} showingControls={showTargets}>
|
<Player.Container onLoad={props.onLoad} showingControls={showTargets}>
|
||||||
{props.children}
|
{props.children}
|
||||||
<Player.BlackOverlay show={showTargets} />
|
<Player.BlackOverlay
|
||||||
|
show={showTargets && status === playerStatus.PLAYING}
|
||||||
|
/>
|
||||||
<Player.EpisodesRouter onChange={props.onMetaChange} />
|
<Player.EpisodesRouter onChange={props.onMetaChange} />
|
||||||
<Player.SettingsRouter />
|
<Player.SettingsRouter />
|
||||||
<Player.SubtitleView controlsShown={showTargets} />
|
<Player.SubtitleView controlsShown={showTargets} />
|
||||||
|
78
src/pages/parts/player/ScrapeErrorPart.tsx
Normal file
78
src/pages/parts/player/ScrapeErrorPart.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { IconPill } from "@/components/layout/IconPill";
|
||||||
|
import { Paragraph } from "@/components/text/Paragraph";
|
||||||
|
import { Title } from "@/components/text/Title";
|
||||||
|
import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape";
|
||||||
|
import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout";
|
||||||
|
|
||||||
|
export interface ScrapeErrorPartProps {
|
||||||
|
data: {
|
||||||
|
sources: Record<string, ScrapingSegment>;
|
||||||
|
sourceOrder: ScrapingItems[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
|
||||||
|
const error = useMemo(() => {
|
||||||
|
const data = props.data;
|
||||||
|
const amountError = Object.values(data.sources).filter(
|
||||||
|
(v) => v.status === "failure"
|
||||||
|
);
|
||||||
|
if (amountError.length === 0) return null;
|
||||||
|
let str = "";
|
||||||
|
Object.values(data.sources).forEach((v) => {
|
||||||
|
str += `${v.id}: ${v.status}\n`;
|
||||||
|
if (v.reason) str += `${v.reason}\n`;
|
||||||
|
if (v.error) str += `${v.error.toString()}\n`;
|
||||||
|
});
|
||||||
|
return str;
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorLayout>
|
||||||
|
<ErrorContainer>
|
||||||
|
<IconPill icon={Icons.WAND}>Not found</IconPill>
|
||||||
|
<Title>Goo goo gaa gaa</Title>
|
||||||
|
<Paragraph>
|
||||||
|
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
|
||||||
|
bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`)
|
||||||
|
Please don't be angwy, wittle movie-web ish twying so hard. Can
|
||||||
|
you find it in your heart to forgive? UwU 💖
|
||||||
|
</Paragraph>
|
||||||
|
<Button
|
||||||
|
href="/"
|
||||||
|
theme="purple"
|
||||||
|
padding="md:px-12 p-2.5"
|
||||||
|
className="mt-6"
|
||||||
|
>
|
||||||
|
Go home
|
||||||
|
</Button>
|
||||||
|
</ErrorContainer>
|
||||||
|
<ErrorContainer maxWidth="max-w-[45rem]">
|
||||||
|
{/* Error */}
|
||||||
|
{error ? (
|
||||||
|
<div className="w-full bg-errors-card p-6 rounded-lg">
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b border-errors-border">
|
||||||
|
<span className="text-white font-medium">Error details</span>
|
||||||
|
<div className="flex justify-center items-center gap-3">
|
||||||
|
<Button theme="secondary" padding="p-2 md:px-4">
|
||||||
|
<Icon icon={Icons.COPY} className="text-2xl mr-3" />
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
<Button theme="secondary" padding="p-2 md:px-2">
|
||||||
|
<Icon icon={Icons.X} className="text-2xl" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 h-60 overflow-y-auto text-left whitespace-pre pointer-events-auto">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</ErrorContainer>
|
||||||
|
</ErrorLayout>
|
||||||
|
);
|
||||||
|
}
|
@ -8,11 +8,20 @@ import {
|
|||||||
ScrapeCard,
|
ScrapeCard,
|
||||||
ScrapeItem,
|
ScrapeItem,
|
||||||
} from "@/components/player/internals/ScrapeCard";
|
} from "@/components/player/internals/ScrapeCard";
|
||||||
import { useListCenter, useScrape } from "@/hooks/useProviderScrape";
|
import {
|
||||||
|
ScrapingItems,
|
||||||
|
ScrapingSegment,
|
||||||
|
useListCenter,
|
||||||
|
useScrape,
|
||||||
|
} from "@/hooks/useProviderScrape";
|
||||||
|
|
||||||
export interface ScrapingProps {
|
export interface ScrapingProps {
|
||||||
media: ScrapeMedia;
|
media: ScrapeMedia;
|
||||||
onGetStream?: (stream: AsyncReturnType<ProviderControls["runAll"]>) => void;
|
onGetStream?: (stream: AsyncReturnType<ProviderControls["runAll"]>) => void;
|
||||||
|
onResult?: (
|
||||||
|
sources: Record<string, ScrapingSegment>,
|
||||||
|
sourceOrder: ScrapingItems[]
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScrapingPart(props: ScrapingProps) {
|
export function ScrapingPart(props: ScrapingProps) {
|
||||||
@ -28,12 +37,27 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
currentSource
|
currentSource
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const resultRef = useRef({
|
||||||
|
sourceOrder,
|
||||||
|
sources,
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
resultRef.current = {
|
||||||
|
sourceOrder,
|
||||||
|
sources,
|
||||||
|
};
|
||||||
|
}, [sourceOrder, sources]);
|
||||||
|
|
||||||
const started = useRef(false);
|
const started = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (started.current) return;
|
if (started.current) return;
|
||||||
started.current = true;
|
started.current = true;
|
||||||
(async () => {
|
(async () => {
|
||||||
const output = await startScraping(props.media);
|
const output = await startScraping(props.media);
|
||||||
|
props.onResult?.(
|
||||||
|
resultRef.current.sources,
|
||||||
|
resultRef.current.sourceOrder
|
||||||
|
);
|
||||||
props.onGetStream?.(output);
|
props.onGetStream?.(output);
|
||||||
})();
|
})();
|
||||||
}, [startScraping, props, playMedia]);
|
}, [startScraping, props, playMedia]);
|
||||||
|
@ -13,6 +13,7 @@ export const playerStatus = {
|
|||||||
IDLE: "idle",
|
IDLE: "idle",
|
||||||
SCRAPING: "scraping",
|
SCRAPING: "scraping",
|
||||||
PLAYING: "playing",
|
PLAYING: "playing",
|
||||||
|
SCRAPE_NOT_FOUND: "scrapeNotFound",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type PlayerStatus = ValuesOf<typeof playerStatus>;
|
export type PlayerStatus = ValuesOf<typeof playerStatus>;
|
||||||
|
@ -109,6 +109,16 @@ module.exports = {
|
|||||||
badgeText: "#5F5F7A"
|
badgeText: "#5F5F7A"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Error page
|
||||||
|
errors: {
|
||||||
|
card: "#12121B",
|
||||||
|
border: "#252534",
|
||||||
|
|
||||||
|
type: {
|
||||||
|
secondary: "#62627D"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// video player
|
// video player
|
||||||
video: {
|
video: {
|
||||||
buttonBackground: "#444B5C",
|
buttonBackground: "#444B5C",
|
||||||
@ -137,7 +147,11 @@ module.exports = {
|
|||||||
secondaryHover: "#1B262E",
|
secondaryHover: "#1B262E",
|
||||||
primary: "#fff",
|
primary: "#fff",
|
||||||
primaryText: "#000",
|
primaryText: "#000",
|
||||||
primaryHover: "#dedede"
|
primaryHover: "#dedede",
|
||||||
|
purple: "#6b298a",
|
||||||
|
purpleHover: "#7f35a1",
|
||||||
|
cancel: "#252533",
|
||||||
|
cancelHover: "#3C3C4A"
|
||||||
},
|
},
|
||||||
|
|
||||||
context: {
|
context: {
|
||||||
@ -155,11 +169,6 @@ module.exports = {
|
|||||||
sliderFilled: "#A75FC9",
|
sliderFilled: "#A75FC9",
|
||||||
error: "#E44F4F",
|
error: "#E44F4F",
|
||||||
|
|
||||||
download: {
|
|
||||||
button: "#6b298a",
|
|
||||||
hover: "#7f35a1"
|
|
||||||
},
|
|
||||||
|
|
||||||
buttons: {
|
buttons: {
|
||||||
list: "#161C26",
|
list: "#161C26",
|
||||||
active: "#0D1317"
|
active: "#0D1317"
|
||||||
|
@ -37,6 +37,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
|
disable: process.env.VITE_PWA_ENABLED !== "yes",
|
||||||
registerType: "autoUpdate",
|
registerType: "autoUpdate",
|
||||||
workbox: {
|
workbox: {
|
||||||
globIgnores: ["**ping.txt**"],
|
globIgnores: ["**ping.txt**"],
|
||||||
|
Loading…
Reference in New Issue
Block a user