mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-28 03:11:51 +01:00
Merge branch 'dev' into dev
This commit is contained in:
commit
02d94ba411
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
3
.github/workflows/deploying.yml
vendored
3
.github/workflows/deploying.yml
vendored
@ -18,12 +18,13 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: Install Yarn packages
|
- name: Install Yarn packages
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: npm run build
|
run: yarn build
|
||||||
|
|
||||||
- name: Upload production-ready build files
|
- name: Upload production-ready build files
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
48
.github/workflows/linting_annotate.yml
vendored
Normal file
48
.github/workflows/linting_annotate.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
name: Annotate linting
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: read # download artifact
|
||||||
|
checks: write # annotate
|
||||||
|
|
||||||
|
# this is done as a seperate workflow so
|
||||||
|
# the annotater has access to write to checks (to annotate)
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Linting and Testing"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
annotate:
|
||||||
|
name: Annotate linting
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download linting report
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: ${{github.event.workflow_run.id }},
|
||||||
|
});
|
||||||
|
const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
|
return artifact.name == "eslint_report.json"
|
||||||
|
})[0];
|
||||||
|
const download = await github.rest.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: matchArtifact.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
const fs = require('fs');
|
||||||
|
fs.writeFileSync('${{github.workspace}}/eslint_report.zip', Buffer.from(download.data));
|
||||||
|
|
||||||
|
- run: unzip eslint_report.zip
|
||||||
|
|
||||||
|
- name: Annotate linting
|
||||||
|
uses: ataylorme/eslint-annotate-action@v2
|
||||||
|
with:
|
||||||
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
report-json: "eslint_report.json"
|
30
.github/workflows/linting_testing.yml
vendored
30
.github/workflows/linting_testing.yml
vendored
@ -5,8 +5,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- dev
|
- dev
|
||||||
pull_request_target:
|
pull_request:
|
||||||
types: [opened, reopened, synchronize]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linting:
|
linting:
|
||||||
@ -21,6 +20,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: Install Yarn packages
|
- name: Install Yarn packages
|
||||||
run: yarn install
|
run: yarn install
|
||||||
@ -30,11 +30,27 @@ jobs:
|
|||||||
# continue on error, so it still reports it in the next step
|
# continue on error, so it still reports it in the next step
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Annotate Code Linting Results
|
- uses: actions/upload-artifact@v3
|
||||||
uses: ataylorme/eslint-annotate-action@v2
|
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
name: eslint_report.json
|
||||||
report-json: "eslint_report.json"
|
path: eslint_report.json
|
||||||
|
|
||||||
|
building:
|
||||||
|
name: Build project
|
||||||
|
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
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install Yarn packages
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
- name: Build Project
|
- name: Build Project
|
||||||
run: npm run build
|
run: yarn build
|
||||||
|
@ -2,6 +2,7 @@ import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
|
|||||||
import { MWCaption, MWCaptionType } from "@/backend/helpers/streams";
|
import { MWCaption, MWCaptionType } from "@/backend/helpers/streams";
|
||||||
import toWebVTT from "srt-webvtt";
|
import toWebVTT from "srt-webvtt";
|
||||||
|
|
||||||
|
export const CUSTOM_CAPTION_ID = "customCaption";
|
||||||
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
||||||
if (caption.type === MWCaptionType.SRT) {
|
if (caption.type === MWCaptionType.SRT) {
|
||||||
let captionBlob: Blob;
|
let captionBlob: Blob;
|
||||||
@ -32,3 +33,18 @@ export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
|||||||
|
|
||||||
throw new Error("invalid type");
|
throw new Error("invalid type");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function convertCustomCaptionFileToWebVTT(file: File) {
|
||||||
|
const header = await file.slice(0, 6).text();
|
||||||
|
const isWebVTT = header === "WEBVTT";
|
||||||
|
if (!isWebVTT) {
|
||||||
|
return toWebVTT(file);
|
||||||
|
}
|
||||||
|
return URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function revokeCaptionBlob(url: string | undefined) {
|
||||||
|
if (url && url.startsWith("blob:")) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { proxiedFetch } from "../helpers/fetch";
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
import { registerProvider } from "../helpers/register";
|
import { registerProvider } from "../helpers/register";
|
||||||
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
|
import {
|
||||||
|
MWCaptionType,
|
||||||
|
MWStreamQuality,
|
||||||
|
MWStreamType,
|
||||||
|
} from "../helpers/streams";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types";
|
||||||
|
|
||||||
const netfilmBase = "https://net-film.vercel.app";
|
const netfilmBase = "https://net-film.vercel.app";
|
||||||
@ -18,7 +22,6 @@ registerProvider({
|
|||||||
displayName: "NetFilm",
|
displayName: "NetFilm",
|
||||||
rank: 15,
|
rank: 15,
|
||||||
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
||||||
disabled: true, // https://github.com/lamhoang1256/netfilm/issues/25
|
|
||||||
|
|
||||||
async scrape({ media, episode, progress }) {
|
async scrape({ media, episode, progress }) {
|
||||||
// search for relevant item
|
// search for relevant item
|
||||||
@ -48,20 +51,29 @@ registerProvider({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { qualities } = watchInfo.data;
|
const data = watchInfo.data;
|
||||||
|
|
||||||
// get best quality source
|
// get best quality source
|
||||||
const source = qualities.reduce((p: any, c: any) =>
|
const source = data.qualities.reduce((p: any, c: any) =>
|
||||||
c.quality > p.quality ? c : p
|
c.quality > p.quality ? c : p
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const mappedCaptions = data.subtitles.map((sub: Record<string, any>) => ({
|
||||||
|
needsProxy: false,
|
||||||
|
url: sub.url.replace("https://convert-srt-to-vtt.vercel.app/?url=", ""),
|
||||||
|
type: MWCaptionType.SRT,
|
||||||
|
langIso: sub.language,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
embeds: [],
|
embeds: [],
|
||||||
stream: {
|
stream: {
|
||||||
streamUrl: source.url,
|
streamUrl: source.url
|
||||||
|
.replace("akm-cdn", "aws-cdn")
|
||||||
|
.replace("gg-cdn", "aws-cdn"),
|
||||||
quality: qualityMap[source.quality as QualityInMap],
|
quality: qualityMap[source.quality as QualityInMap],
|
||||||
type: MWStreamType.HLS,
|
type: MWStreamType.HLS,
|
||||||
captions: [],
|
captions: mappedCaptions,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -109,20 +121,29 @@ registerProvider({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { qualities } = episodeStream.data;
|
const data = episodeStream.data;
|
||||||
|
|
||||||
// get best quality source
|
// get best quality source
|
||||||
const source = qualities.reduce((p: any, c: any) =>
|
const source = data.qualities.reduce((p: any, c: any) =>
|
||||||
c.quality > p.quality ? c : p
|
c.quality > p.quality ? c : p
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const mappedCaptions = data.subtitles.map((sub: Record<string, any>) => ({
|
||||||
|
needsProxy: false,
|
||||||
|
url: sub.url.replace("https://convert-srt-to-vtt.vercel.app/?url=", ""),
|
||||||
|
type: MWCaptionType.SRT,
|
||||||
|
langIso: sub.language,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
embeds: [],
|
embeds: [],
|
||||||
stream: {
|
stream: {
|
||||||
streamUrl: source.url,
|
streamUrl: source.url
|
||||||
|
.replace("akm-cdn", "aws-cdn")
|
||||||
|
.replace("gg-cdn", "aws-cdn"),
|
||||||
quality: qualityMap[source.quality as QualityInMap],
|
quality: qualityMap[source.quality as QualityInMap],
|
||||||
type: MWStreamType.HLS,
|
type: MWStreamType.HLS,
|
||||||
captions: [],
|
captions: mappedCaptions,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -71,6 +71,8 @@
|
|||||||
"episode": "E{{index}} - {{title}}",
|
"episode": "E{{index}} - {{title}}",
|
||||||
"noCaptions": "No captions",
|
"noCaptions": "No captions",
|
||||||
"linkedCaptions": "Linked captions",
|
"linkedCaptions": "Linked captions",
|
||||||
|
"customCaption": "Custom caption",
|
||||||
|
"uploadCustomCaption": "Upload caption (SRT, VTT)",
|
||||||
"noEmbeds": "No embeds were found for this source",
|
"noEmbeds": "No embeds were found for this source",
|
||||||
"errors": {
|
"errors": {
|
||||||
"loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}",
|
"loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}",
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { getCaptionUrl } from "@/backend/helpers/captions";
|
import {
|
||||||
|
getCaptionUrl,
|
||||||
|
convertCustomCaptionFileToWebVTT,
|
||||||
|
CUSTOM_CAPTION_ID,
|
||||||
|
} from "@/backend/helpers/captions";
|
||||||
import { MWCaption } from "@/backend/helpers/streams";
|
import { MWCaption } from "@/backend/helpers/streams";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
@ -6,7 +10,7 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
|||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useSource } from "@/video/state/logic/source";
|
import { useSource } from "@/video/state/logic/source";
|
||||||
import { useMemo, useRef } from "react";
|
import { ChangeEvent, useMemo, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
||||||
|
|
||||||
@ -37,6 +41,29 @@ export function CaptionSelectionPopout() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const currentCaption = source.source?.caption?.id;
|
const currentCaption = source.source?.caption?.id;
|
||||||
|
const customCaptionUploadElement = useRef<HTMLInputElement>(null);
|
||||||
|
const [setCustomCaption, loadingCustomCaption, errorCustomCaption] =
|
||||||
|
useLoading(async (captionFile: File) => {
|
||||||
|
if (
|
||||||
|
!captionFile.name.endsWith(".srt") &&
|
||||||
|
!captionFile.name.endsWith(".vtt")
|
||||||
|
) {
|
||||||
|
throw new Error("Only SRT or VTT files are allowed");
|
||||||
|
}
|
||||||
|
controls.setCaption(
|
||||||
|
CUSTOM_CAPTION_ID,
|
||||||
|
await convertCustomCaptionFileToWebVTT(captionFile)
|
||||||
|
);
|
||||||
|
controls.closePopout();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleUploadCaption(e: ChangeEvent<HTMLInputElement>) {
|
||||||
|
if (!e.target.files) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const captionFile = e.target.files[0];
|
||||||
|
setCustomCaption(captionFile);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -54,6 +81,26 @@ export function CaptionSelectionPopout() {
|
|||||||
>
|
>
|
||||||
{t("videoPlayer.popouts.noCaptions")}
|
{t("videoPlayer.popouts.noCaptions")}
|
||||||
</PopoutListEntry>
|
</PopoutListEntry>
|
||||||
|
<PopoutListEntry
|
||||||
|
key={CUSTOM_CAPTION_ID}
|
||||||
|
active={currentCaption === CUSTOM_CAPTION_ID}
|
||||||
|
loading={loadingCustomCaption}
|
||||||
|
errored={!!errorCustomCaption}
|
||||||
|
onClick={() => {
|
||||||
|
customCaptionUploadElement.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentCaption === CUSTOM_CAPTION_ID
|
||||||
|
? t("videoPlayer.popouts.customCaption")
|
||||||
|
: t("videoPlayer.popouts.uploadCustomCaption")}
|
||||||
|
<input
|
||||||
|
ref={customCaptionUploadElement}
|
||||||
|
type="file"
|
||||||
|
onChange={handleUploadCaption}
|
||||||
|
className="hidden"
|
||||||
|
accept=".vtt, .srt"
|
||||||
|
/>
|
||||||
|
</PopoutListEntry>
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
|
|
||||||
<p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-200 px-5 py-3 text-sm font-bold uppercase">
|
<p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-200 px-5 py-3 text-sm font-bold uppercase">
|
||||||
|
@ -96,7 +96,7 @@ export function PopoutListEntry(props: PopoutListEntryTypes) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
"group -mx-2 flex cursor-pointer items-center justify-between space-x-1 rounded p-2 font-semibold transition-[background-color,color] duration-150",
|
"group my-2 -mx-2 flex cursor-pointer items-center justify-between space-x-1 rounded p-2 font-semibold transition-[background-color,color] duration-150",
|
||||||
hover,
|
hover,
|
||||||
props.active
|
props.active
|
||||||
? `${bg} active text-white outline-denim-700`
|
? `${bg} active text-white outline-denim-700`
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
} from "@/video/components/hooks/volumeStore";
|
} from "@/video/components/hooks/volumeStore";
|
||||||
import { resetStateForSource } from "@/video/state/providers/helpers";
|
import { resetStateForSource } from "@/video/state/providers/helpers";
|
||||||
import { updateInterface } from "@/video/state/logic/interface";
|
import { updateInterface } from "@/video/state/logic/interface";
|
||||||
|
import { revokeCaptionBlob } from "@/backend/helpers/captions";
|
||||||
import { getPlayerState } from "../cache";
|
import { getPlayerState } from "../cache";
|
||||||
import { updateMediaPlaying } from "../logic/mediaplaying";
|
import { updateMediaPlaying } from "../logic/mediaplaying";
|
||||||
import { VideoPlayerStateProvider } from "./providerTypes";
|
import { VideoPlayerStateProvider } from "./providerTypes";
|
||||||
@ -138,6 +139,7 @@ export function createCastingStateProvider(
|
|||||||
},
|
},
|
||||||
setCaption(id, url) {
|
setCaption(id, url) {
|
||||||
if (state.source) {
|
if (state.source) {
|
||||||
|
revokeCaptionBlob(state.source.caption?.url);
|
||||||
state.source.caption = {
|
state.source.caption = {
|
||||||
id,
|
id,
|
||||||
url,
|
url,
|
||||||
@ -147,6 +149,7 @@ export function createCastingStateProvider(
|
|||||||
},
|
},
|
||||||
clearCaption() {
|
clearCaption() {
|
||||||
if (state.source) {
|
if (state.source) {
|
||||||
|
revokeCaptionBlob(state.source.caption?.url);
|
||||||
state.source.caption = null;
|
state.source.caption = null;
|
||||||
updateSource(descriptor, state);
|
updateSource(descriptor, state);
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
import { updateError } from "@/video/state/logic/error";
|
import { updateError } from "@/video/state/logic/error";
|
||||||
import { updateMisc } from "@/video/state/logic/misc";
|
import { updateMisc } from "@/video/state/logic/misc";
|
||||||
import { resetStateForSource } from "@/video/state/providers/helpers";
|
import { resetStateForSource } from "@/video/state/providers/helpers";
|
||||||
|
import { revokeCaptionBlob } from "@/backend/helpers/captions";
|
||||||
import { getPlayerState } from "../cache";
|
import { getPlayerState } from "../cache";
|
||||||
import { updateMediaPlaying } from "../logic/mediaplaying";
|
import { updateMediaPlaying } from "../logic/mediaplaying";
|
||||||
import { VideoPlayerStateProvider } from "./providerTypes";
|
import { VideoPlayerStateProvider } from "./providerTypes";
|
||||||
@ -193,6 +194,7 @@ export function createVideoStateProvider(
|
|||||||
},
|
},
|
||||||
setCaption(id, url) {
|
setCaption(id, url) {
|
||||||
if (state.source) {
|
if (state.source) {
|
||||||
|
revokeCaptionBlob(state.source.caption?.url);
|
||||||
state.source.caption = {
|
state.source.caption = {
|
||||||
id,
|
id,
|
||||||
url,
|
url,
|
||||||
@ -202,6 +204,7 @@ export function createVideoStateProvider(
|
|||||||
},
|
},
|
||||||
clearCaption() {
|
clearCaption() {
|
||||||
if (state.source) {
|
if (state.source) {
|
||||||
|
revokeCaptionBlob(state.source.caption?.url);
|
||||||
state.source.caption = null;
|
state.source.caption = null;
|
||||||
updateSource(descriptor, state);
|
updateSource(descriptor, state);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user