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:
mrjvs 2023-10-23 23:06:24 +02:00
parent 109d9054d6
commit cec0744907
44 changed files with 10426 additions and 87 deletions

4
.docs/.eslintignore Normal file
View File

@ -0,0 +1,4 @@
dist
node_modules
.output
.nuxt

8
.docs/.eslintrc.cjs Normal file
View 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
View File

@ -0,0 +1,12 @@
node_modules
*.iml
.idea
*.log*
.nuxt
.vscode
.DS_Store
coverage
dist
sw.*
.env
.output

1
.docs/.npmrc Normal file
View File

@ -0,0 +1 @@
shamefully-hoist=true

57
.docs/README.md Executable file
View 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
View 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
View 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.
::
::

View File

@ -0,0 +1,2 @@
# Getting Started

View File

@ -0,0 +1,2 @@
icon: ph:star-duotone
navigation.redirect: /introduction/getting-started

View 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)

View 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.

View File

View File

@ -0,0 +1,2 @@
title: 'Self-Hosting'
icon: mdi:server-network

11
.docs/nuxt.config.ts Executable file
View 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
View 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

File diff suppressed because it is too large Load Diff

BIN
.docs/public/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
.docs/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

8
.docs/renovate.json Executable file
View File

@ -0,0 +1,8 @@
{
"extends": [
"@nuxtjs"
],
"lockFileMaintenance": {
"enabled": true
}
}

18
.docs/tokens.config.ts Normal file
View 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
View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

48
.github/workflows/docs.yml vendored Normal file
View 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

View File

@ -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>
); );
} }

View File

@ -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() {

View 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>
);
}

View File

@ -51,5 +51,8 @@ export function usePlayer() {
setScrapeStatus() { setScrapeStatus() {
setStatus(playerStatus.SCRAPING); setStatus(playerStatus.SCRAPING);
}, },
setScrapeNotFound() {
setStatus(playerStatus.SCRAPE_NOT_FOUND);
},
}; };
} }

View 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>
);
}

View File

@ -0,0 +1,3 @@
export function Paragraph(props: { children: React.ReactNode }) {
return <p className="text-errors-type-secondary mt-6">{props.children}</p>;
}

View File

@ -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>
); );
} }

View File

@ -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 };

View File

@ -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>

View File

@ -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>
); );

View File

@ -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

View 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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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

View File

@ -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 (
<div className="flex items-center justify-center"> <ErrorLayout>
{loading ? <p>loading meta...</p> : null} <ErrorContainer>
{error ? <p>failed to load meta!</p> : null} <IconPill icon={Icons.WAND}>Failed to load</IconPill>
</div> <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&apos;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&apos;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">
<Loading />
</div>
</ErrorLayout>
); );
} }

View File

@ -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} />

View 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&apos;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>
);
}

View File

@ -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]);

View File

@ -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>;

View File

@ -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"

View File

@ -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**"],