mirror of
https://github.com/movie-web/movie-web.git
synced 2025-09-13 18:13:24 +00:00
Compare commits
97 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1fd458fa27 | ||
|
e4c15c624b | ||
|
b12649bd2e | ||
|
37e10fb40e | ||
|
61b75da402 | ||
|
73b2f57fdc | ||
|
0b8c6439d7 | ||
|
4ad0d53683 | ||
|
3958df8e29 | ||
|
fa36493c50 | ||
|
efd87ab96e | ||
|
f80d79070e | ||
|
be7b875666 | ||
|
bb869fd7e3 | ||
|
2b30bb0e2b | ||
|
b9448b5231 | ||
|
7a6af6c072 | ||
|
2657d1f856 | ||
|
21cc8c16d6 | ||
|
b04209d9b3 | ||
|
55bfa2be9d | ||
|
dd8b6c3f9e | ||
|
835e818ca0 | ||
942725d04c | |||
|
010f1d3987 | ||
|
7bad6eaff9 | ||
|
bcff5a8972 | ||
|
caba492ca2 | ||
|
f03145ee6d | ||
|
c0aebca4d9 | ||
|
c7651950ce | ||
|
cd3bd22a2c | ||
|
9773fcc7b5 | ||
|
c937acfb09 | ||
|
d1f3a7ad24 | ||
|
cd0e4522c9 | ||
f4be26d92d | |||
|
22f8d8a581 | ||
|
6cfd1235bc | ||
|
bdeaca3062 | ||
|
15e95923be | ||
|
571df9e0ad | ||
|
cce47fab5d | ||
|
6eb25fb49c | ||
|
e61937b5c4 | ||
|
2338b0d652 | ||
|
37463afc8d | ||
|
9c8e89a274 | ||
|
bf135a2bdf | ||
|
4dc6658e67 | ||
|
fffc119e88 | ||
|
5468a4677b | ||
|
85cfba1a7a | ||
|
fd6895c326 | ||
|
dfc3d9e50f | ||
|
fcdf45d3f5 | ||
|
592837e2a6 | ||
|
9b3c1ffa28 | ||
|
7cb9ccaf14 | ||
|
aa91bae418 | ||
|
7737bd1866 | ||
|
4c0c61b0b9 | ||
|
4880d46dc4 | ||
|
ef39d87b4b | ||
|
e2a4caa8aa | ||
|
b6a60cf5f8 | ||
|
f784f5f4b2 | ||
|
01348f2f9a | ||
|
8200079af7 | ||
|
dcb5d2f068 | ||
|
99e47f16ea | ||
|
6fb76908ae | ||
|
a718abdcdd | ||
|
0e77d63caf | ||
|
106290070a | ||
|
433d618096 | ||
|
af954af36c | ||
|
16841b8e69 | ||
|
41979712c3 | ||
|
9b62b55fbb | ||
|
6ef41bdf1c | ||
|
33ebd34808 | ||
|
52598599e7 | ||
|
cccc84624a | ||
|
d54921900b | ||
|
2a4bc7349c | ||
|
7b641c61cd | ||
|
3a7b05264d | ||
|
a1e3d98538 | ||
|
68e5742c25 | ||
|
283b9cc996 | ||
|
75ef831ddc | ||
|
e2d1842946 | ||
|
f12f53d32c | ||
|
a910c1c18c | ||
|
42dee51570 | ||
|
9c13be37e8 |
53
.eslintrc.js
53
.eslintrc.js
@@ -8,27 +8,28 @@ const a11yOff = Object.keys(require("eslint-plugin-jsx-a11y").rules).reduce(
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
env: {
|
||||||
browser: true
|
browser: true,
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
"airbnb",
|
"airbnb",
|
||||||
"airbnb/hooks",
|
"airbnb/hooks",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"prettier",
|
"plugin:prettier/recommended",
|
||||||
"plugin:prettier/recommended"
|
|
||||||
],
|
],
|
||||||
ignorePatterns: ["public/*", "dist/*", "/*.js", "/*.ts"],
|
ignorePatterns: ["public/*", "dist/*", "/*.js", "/*.ts"],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: "./tsconfig.json",
|
project: "./tsconfig.json",
|
||||||
tsconfigRootDir: "./"
|
tsconfigRootDir: "./",
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
"import/resolver": {
|
"import/resolver": {
|
||||||
typescript: {}
|
typescript: {
|
||||||
}
|
project: "./tsconfig.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: ["@typescript-eslint", "import"],
|
plugins: ["@typescript-eslint", "import", "prettier"],
|
||||||
rules: {
|
rules: {
|
||||||
"react/jsx-uses-react": "off",
|
"react/jsx-uses-react": "off",
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
@@ -54,16 +55,44 @@ module.exports = {
|
|||||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
||||||
"react/jsx-filename-extension": [
|
"react/jsx-filename-extension": [
|
||||||
"error",
|
"error",
|
||||||
{ extensions: [".js", ".tsx", ".jsx"] }
|
{ extensions: [".js", ".tsx", ".jsx"] },
|
||||||
],
|
],
|
||||||
"import/extensions": [
|
"import/extensions": [
|
||||||
"error",
|
"error",
|
||||||
"ignorePackages",
|
"ignorePackages",
|
||||||
{
|
{
|
||||||
ts: "never",
|
ts: "never",
|
||||||
tsx: "never"
|
tsx: "never",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
...a11yOff
|
"import/order": [
|
||||||
}
|
"error",
|
||||||
|
{
|
||||||
|
groups: [
|
||||||
|
"builtin",
|
||||||
|
"external",
|
||||||
|
"internal",
|
||||||
|
["sibling", "parent"],
|
||||||
|
"index",
|
||||||
|
"unknown",
|
||||||
|
],
|
||||||
|
"newlines-between": "always",
|
||||||
|
alphabetize: {
|
||||||
|
order: "asc",
|
||||||
|
caseInsensitive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"sort-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
ignoreCase: false,
|
||||||
|
ignoreDeclarationSort: true,
|
||||||
|
ignoreMemberSort: false,
|
||||||
|
memberSyntaxSortOrder: ["none", "all", "multiple", "single"],
|
||||||
|
allowSeparatedGroups: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...a11yOff,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<a href="https://discord.gg/vXsRvye8BS"><img src="https://discordapp.com/api/guilds/871713465100816424/widget.png?style=banner2" alt="Discord Server"></a>
|
<a href="https://discord.gg/vXsRvye8BS"><img src="https://discordapp.com/api/guilds/871713465100816424/widget.png?style=banner2" alt="Discord Server"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
movie-web is a web app for watching movies easily. Check it out at **[movie.squeezebox.dev](https://movie.squeezebox.dev)**.
|
movie-web is a web app for watching movies easily. Check it out at **[movie-web.app](https://movie-web.app)**.
|
||||||
|
|
||||||
This service works by displaying video files from third-party providers inside an intuitive and aesthetic user interface.
|
This service works by displaying video files from third-party providers inside an intuitive and aesthetic user interface.
|
||||||
|
|
||||||
|
@@ -29,6 +29,9 @@
|
|||||||
<!-- prevent darkreader extension from messing with our already dark site -->
|
<!-- prevent darkreader extension from messing with our already dark site -->
|
||||||
<meta name="darkreader-lock" />
|
<meta name="darkreader-lock" />
|
||||||
|
|
||||||
|
<!-- disabling referrer can fix some provider problems -->
|
||||||
|
<meta name="referrer" content="no-referrer" />
|
||||||
|
|
||||||
<title>movie-web</title>
|
<title>movie-web</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "movie-web",
|
"name": "movie-web",
|
||||||
"version": "3.0.12",
|
"version": "3.0.15",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://movie-web.app",
|
"homepage": "https://movie-web.app",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"react-stickynode": "^4.1.0",
|
"react-stickynode": "^4.1.0",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"subsrt-ts": "^2.1.0",
|
"subsrt-ts": "^2.1.1",
|
||||||
"unpacker": "^1.0.1"
|
"unpacker": "^1.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
"eslint-config-airbnb": "19.0.4",
|
"eslint-config-airbnb": "19.0.4",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"eslint-import-resolver-typescript": "^2.5.0",
|
"eslint-import-resolver-typescript": "^2.5.0",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "7.29.4",
|
"eslint-plugin-react": "7.29.4",
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { describe, it } from "vitest";
|
import { describe, it } from "vitest";
|
||||||
|
|
||||||
import "@/backend";
|
import "@/backend";
|
||||||
import { getProviders } from "@/backend/helpers/register";
|
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
|
||||||
import { runProvider } from "@/backend/helpers/run";
|
|
||||||
import { testData } from "@/__tests__/providers/testdata";
|
import { testData } from "@/__tests__/providers/testdata";
|
||||||
|
import { getProviders } from "@/backend/helpers/register";
|
||||||
|
import { runProvider } from "@/backend/helpers/run";
|
||||||
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
|
|
||||||
describe("providers", () => {
|
describe("providers", () => {
|
||||||
const providers = getProviders();
|
const providers = getProviders();
|
||||||
|
152
src/__tests__/subtitles/subtitles.test.ts
Normal file
152
src/__tests__/subtitles/subtitles.test.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { describe, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getMWCaptionTypeFromUrl,
|
||||||
|
isSupportedSubtitle,
|
||||||
|
parseSubtitles,
|
||||||
|
} from "@/backend/helpers/captions";
|
||||||
|
import { MWCaptionType } from "@/backend/helpers/streams";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ass,
|
||||||
|
multilineSubtitlesTestVtt,
|
||||||
|
srt,
|
||||||
|
visibleSubtitlesTestVtt,
|
||||||
|
vtt,
|
||||||
|
} from "./testdata";
|
||||||
|
|
||||||
|
describe("subtitles", () => {
|
||||||
|
it("should return true if given url ends with a known subtitle type", ({
|
||||||
|
expect,
|
||||||
|
}) => {
|
||||||
|
expect(isSupportedSubtitle("https://example.com/test.srt")).toBe(true);
|
||||||
|
expect(isSupportedSubtitle("https://example.com/test.vtt")).toBe(true);
|
||||||
|
expect(isSupportedSubtitle("https://example.com/test.txt")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return corresponding MWCaptionType", ({ expect }) => {
|
||||||
|
expect(getMWCaptionTypeFromUrl("https://example.com/test.srt")).toBe(
|
||||||
|
MWCaptionType.SRT
|
||||||
|
);
|
||||||
|
expect(getMWCaptionTypeFromUrl("https://example.com/test.vtt")).toBe(
|
||||||
|
MWCaptionType.VTT
|
||||||
|
);
|
||||||
|
expect(getMWCaptionTypeFromUrl("https://example.com/test.txt")).toBe(
|
||||||
|
MWCaptionType.UNKNOWN
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw when empty text is given", ({ expect }) => {
|
||||||
|
expect(() => parseSubtitles("")).toThrow("Given text is empty");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse srt", ({ expect }) => {
|
||||||
|
const parsed = parseSubtitles(srt);
|
||||||
|
const parsedSrt = [
|
||||||
|
{
|
||||||
|
type: "caption",
|
||||||
|
index: 1,
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
duration: 0,
|
||||||
|
content: "Test",
|
||||||
|
text: "Test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "caption",
|
||||||
|
index: 2,
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
duration: 0,
|
||||||
|
content: "Test",
|
||||||
|
text: "Test",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(parsed).toHaveLength(2);
|
||||||
|
expect(parsed).toEqual(parsedSrt);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse vtt", ({ expect }) => {
|
||||||
|
const parsed = parseSubtitles(vtt);
|
||||||
|
const parsedVtt = [
|
||||||
|
{
|
||||||
|
type: "caption",
|
||||||
|
index: 1,
|
||||||
|
start: 0,
|
||||||
|
end: 4000,
|
||||||
|
duration: 4000,
|
||||||
|
content: "Where did he go?",
|
||||||
|
text: "Where did he go?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "caption",
|
||||||
|
index: 2,
|
||||||
|
start: 3000,
|
||||||
|
end: 6500,
|
||||||
|
duration: 3500,
|
||||||
|
content: "I think he went down this lane.",
|
||||||
|
text: "I think he went down this lane.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "caption",
|
||||||
|
index: 3,
|
||||||
|
start: 4000,
|
||||||
|
end: 6500,
|
||||||
|
duration: 2500,
|
||||||
|
content: "What are you waiting for?",
|
||||||
|
text: "What are you waiting for?",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(parsed).toHaveLength(3);
|
||||||
|
expect(parsed).toEqual(parsedVtt);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse ass", ({ expect }) => {
|
||||||
|
const parsed = parseSubtitles(ass);
|
||||||
|
expect(parsed).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should delay subtitles when given a delay", ({ expect }) => {
|
||||||
|
const videoTime = 11;
|
||||||
|
let delayedSeconds = 0;
|
||||||
|
const parsed = parseSubtitles(visibleSubtitlesTestVtt);
|
||||||
|
const isVisible = (start: number, end: number, delay: number): boolean => {
|
||||||
|
const delayedStart = start / 1000 + delay;
|
||||||
|
const delayedEnd = end / 1000 + delay;
|
||||||
|
return (
|
||||||
|
Math.max(0, delayedStart) <= videoTime &&
|
||||||
|
Math.max(0, delayedEnd) >= videoTime
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const visibleSubtitles = parsed.filter((c) =>
|
||||||
|
isVisible(c.start, c.end, delayedSeconds)
|
||||||
|
);
|
||||||
|
expect(visibleSubtitles).toHaveLength(1);
|
||||||
|
|
||||||
|
delayedSeconds = 10;
|
||||||
|
const delayedVisibleSubtitles = parsed.filter((c) =>
|
||||||
|
isVisible(c.start, c.end, delayedSeconds)
|
||||||
|
);
|
||||||
|
expect(delayedVisibleSubtitles).toHaveLength(1);
|
||||||
|
|
||||||
|
delayedSeconds = -10;
|
||||||
|
const delayedVisibleSubtitles2 = parsed.filter((c) =>
|
||||||
|
isVisible(c.start, c.end, delayedSeconds)
|
||||||
|
);
|
||||||
|
expect(delayedVisibleSubtitles2).toHaveLength(1);
|
||||||
|
|
||||||
|
delayedSeconds = -20;
|
||||||
|
const delayedVisibleSubtitles3 = parsed.filter((c) =>
|
||||||
|
isVisible(c.start, c.end, delayedSeconds)
|
||||||
|
);
|
||||||
|
expect(delayedVisibleSubtitles3).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse multiline captions", ({ expect }) => {
|
||||||
|
const parsed = parseSubtitles(multilineSubtitlesTestVtt);
|
||||||
|
|
||||||
|
expect(parsed[0].text).toBe(`- Test 1\n- Test 2\n- Test 3`);
|
||||||
|
expect(parsed[1].text).toBe(`- Test 4`);
|
||||||
|
expect(parsed[2].text).toBe(`- Test 6`);
|
||||||
|
});
|
||||||
|
});
|
68
src/__tests__/subtitles/testdata.ts
Normal file
68
src/__tests__/subtitles/testdata.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
const srt = `
|
||||||
|
1
|
||||||
|
00:00:00,000 --> 00:00:00,000
|
||||||
|
Test
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:00,000 --> 00:00:00,000
|
||||||
|
Test
|
||||||
|
`;
|
||||||
|
const vtt = `
|
||||||
|
WEBVTT
|
||||||
|
|
||||||
|
00:00:00.000 --> 00:00:04.000 position:10%,line-left align:left size:35%
|
||||||
|
Where did he go?
|
||||||
|
|
||||||
|
00:00:03.000 --> 00:00:06.500 position:90% align:right size:35%
|
||||||
|
I think he went down this lane.
|
||||||
|
|
||||||
|
00:00:04.000 --> 00:00:06.500 position:45%,line-right align:center size:35%
|
||||||
|
What are you waiting for?
|
||||||
|
`;
|
||||||
|
const ass = `[Script Info]
|
||||||
|
; Generated by Ebby.co
|
||||||
|
Title:
|
||||||
|
Original Script:
|
||||||
|
ScriptType: v4.00+
|
||||||
|
Collisions: Normal
|
||||||
|
PlayResX: 384
|
||||||
|
PlayResY: 288
|
||||||
|
PlayDepth: 0
|
||||||
|
Timer: 100.0
|
||||||
|
WrapStyle: 0
|
||||||
|
|
||||||
|
[v4+ Styles]
|
||||||
|
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||||
|
Style: Default, Arial, 16, &H00FFFFFF, &H00000000, &H00000000, &H00000000, 0, 0, 0, 0, 100, 100, 0, 0, 1, 1, 0, 2, 15, 15, 15, 0
|
||||||
|
|
||||||
|
[Events]
|
||||||
|
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||||
|
Dialogue: 0,0:00:10.00,0:00:20.00,Default,,0000,0000,0000,,This is the first subtitle.
|
||||||
|
Dialogue: 0,0:00:30.00,0:00:34.00,Default,,0000,0000,0000,,This is the second.
|
||||||
|
Dialogue: 0,0:00:34.00,0:00:35.00,Default,,0000,0000,0000,,Third`;
|
||||||
|
|
||||||
|
const visibleSubtitlesTestVtt = `WEBVTT
|
||||||
|
|
||||||
|
00:00:00.000 --> 00:00:10.000 position:10%,line-left align:left size:35%
|
||||||
|
Test 1
|
||||||
|
|
||||||
|
00:00:10.000 --> 00:00:20.000 position:90% align:right size:35%
|
||||||
|
Test 2
|
||||||
|
|
||||||
|
00:00:20.000 --> 00:00:31.000 position:45%,line-right align:center size:35%
|
||||||
|
Test 3
|
||||||
|
`;
|
||||||
|
|
||||||
|
const multilineSubtitlesTestVtt = `WEBVTT
|
||||||
|
|
||||||
|
00:00:00.000 --> 00:00:10.000
|
||||||
|
- Test 1\n- Test 2\n- Test 3
|
||||||
|
|
||||||
|
00:00:10.000 --> 00:00:20.000
|
||||||
|
- Test 4
|
||||||
|
|
||||||
|
00:00:20.000 --> 00:00:31.000
|
||||||
|
- Test 6
|
||||||
|
`;
|
||||||
|
|
||||||
|
export { vtt, srt, ass, visibleSubtitlesTestVtt, multilineSubtitlesTestVtt };
|
@@ -1,11 +1,11 @@
|
|||||||
import { MWEmbedType } from "@/backend/helpers/embed";
|
import { MWEmbedType } from "@/backend/helpers/embed";
|
||||||
|
import { proxiedFetch } from "@/backend/helpers/fetch";
|
||||||
import { registerEmbedScraper } from "@/backend/helpers/register";
|
import { registerEmbedScraper } from "@/backend/helpers/register";
|
||||||
import {
|
import {
|
||||||
|
MWEmbedStream,
|
||||||
MWStreamQuality,
|
MWStreamQuality,
|
||||||
MWStreamType,
|
MWStreamType,
|
||||||
MWEmbedStream,
|
|
||||||
} from "@/backend/helpers/streams";
|
} from "@/backend/helpers/streams";
|
||||||
import { proxiedFetch } from "@/backend/helpers/fetch";
|
|
||||||
|
|
||||||
const HOST = "streamm4u.club";
|
const HOST = "streamm4u.club";
|
||||||
const URL_BASE = `https://${HOST}`;
|
const URL_BASE = `https://${HOST}`;
|
||||||
|
@@ -1,19 +1,33 @@
|
|||||||
import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
|
|
||||||
import { MWCaption } from "@/backend/helpers/streams";
|
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { parse, detect, list } from "subsrt-ts";
|
import { convert, detect, list, parse } from "subsrt-ts";
|
||||||
import { ContentCaption } from "subsrt-ts/dist/types/handler";
|
import { ContentCaption } from "subsrt-ts/dist/types/handler";
|
||||||
|
|
||||||
|
import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
|
||||||
|
import { MWCaption, MWCaptionType } from "@/backend/helpers/streams";
|
||||||
|
|
||||||
export const customCaption = "external-custom";
|
export const customCaption = "external-custom";
|
||||||
export function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
|
export function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
|
||||||
return isLinked ? `linked-${caption.langIso}` : `external-${caption.langIso}`;
|
return isLinked ? `linked-${caption.langIso}` : `external-${caption.langIso}`;
|
||||||
}
|
}
|
||||||
export const subtitleTypeList = list().map((type) => `.${type}`);
|
export const subtitleTypeList = list().map((type) => `.${type}`);
|
||||||
|
export function isSupportedSubtitle(url: string): boolean {
|
||||||
|
return subtitleTypeList.some((type) => url.endsWith(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMWCaptionTypeFromUrl(url: string): MWCaptionType {
|
||||||
|
if (!isSupportedSubtitle(url)) return MWCaptionType.UNKNOWN;
|
||||||
|
const type = subtitleTypeList.find((t) => url.endsWith(t));
|
||||||
|
if (!type) return MWCaptionType.UNKNOWN;
|
||||||
|
return type.slice(1) as MWCaptionType;
|
||||||
|
}
|
||||||
|
|
||||||
export const sanitize = DOMPurify.sanitize;
|
export const sanitize = DOMPurify.sanitize;
|
||||||
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
||||||
if (caption.url.startsWith("blob:")) return caption.url;
|
|
||||||
let captionBlob: Blob;
|
let captionBlob: Blob;
|
||||||
if (caption.needsProxy) {
|
if (caption.url.startsWith("blob:")) {
|
||||||
|
// custom subtitle
|
||||||
|
captionBlob = await (await fetch(caption.url)).blob();
|
||||||
|
} else if (caption.needsProxy) {
|
||||||
captionBlob = await proxiedFetch<Blob>(caption.url, {
|
captionBlob = await proxiedFetch<Blob>(caption.url, {
|
||||||
responseType: "blob" as any,
|
responseType: "blob" as any,
|
||||||
});
|
});
|
||||||
@@ -22,7 +36,10 @@ export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
|||||||
responseType: "blob" as any,
|
responseType: "blob" as any,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return URL.createObjectURL(captionBlob);
|
// convert to vtt for track element source which will be used in PiP mode
|
||||||
|
const text = await captionBlob.text();
|
||||||
|
const vtt = convert(text, "vtt");
|
||||||
|
return URL.createObjectURL(new Blob([vtt], { type: "text/vtt" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function revokeCaptionBlob(url: string | undefined) {
|
export function revokeCaptionBlob(url: string | undefined) {
|
||||||
@@ -32,10 +49,14 @@ export function revokeCaptionBlob(url: string | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseSubtitles(text: string): ContentCaption[] {
|
export function parseSubtitles(text: string): ContentCaption[] {
|
||||||
if (detect(text) === "") {
|
const textTrimmed = text.trim();
|
||||||
|
if (textTrimmed === "") {
|
||||||
|
throw new Error("Given text is empty");
|
||||||
|
}
|
||||||
|
if (detect(textTrimmed) === "") {
|
||||||
throw new Error("Invalid subtitle format");
|
throw new Error("Invalid subtitle format");
|
||||||
}
|
}
|
||||||
return parse(text).filter(
|
return parse(textTrimmed).filter(
|
||||||
(cue) => cue.type === "caption"
|
(cue) => cue.type === "caption"
|
||||||
) as ContentCaption[];
|
) as ContentCaption[];
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
|
import { FetchOptions, FetchResponse, ofetch } from "ofetch";
|
||||||
|
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { ofetch } from "ofetch";
|
|
||||||
|
|
||||||
let proxyUrlIndex = Math.floor(Math.random() * conf().PROXY_URLS.length);
|
let proxyUrlIndex = Math.floor(Math.random() * conf().PROXY_URLS.length);
|
||||||
|
|
||||||
@@ -58,3 +59,36 @@ export function proxiedFetch<T>(url: string, ops: P<T>[1] = {}): R<T> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function rawProxiedFetch<T>(
|
||||||
|
url: string,
|
||||||
|
ops: FetchOptions = {}
|
||||||
|
): Promise<FetchResponse<T>> {
|
||||||
|
let combinedUrl = ops?.baseURL ?? "";
|
||||||
|
if (
|
||||||
|
combinedUrl.length > 0 &&
|
||||||
|
combinedUrl.endsWith("/") &&
|
||||||
|
url.startsWith("/")
|
||||||
|
)
|
||||||
|
combinedUrl += url.slice(1);
|
||||||
|
else if (
|
||||||
|
combinedUrl.length > 0 &&
|
||||||
|
!combinedUrl.endsWith("/") &&
|
||||||
|
!url.startsWith("/")
|
||||||
|
)
|
||||||
|
combinedUrl += `/${url}`;
|
||||||
|
else combinedUrl += url;
|
||||||
|
|
||||||
|
const parsedUrl = new URL(combinedUrl);
|
||||||
|
Object.entries(ops?.params ?? {}).forEach(([k, v]) => {
|
||||||
|
parsedUrl.searchParams.set(k, v);
|
||||||
|
});
|
||||||
|
|
||||||
|
return baseFetch.raw(getProxyUrl(), {
|
||||||
|
...ops,
|
||||||
|
baseURL: undefined,
|
||||||
|
params: {
|
||||||
|
destination: parsedUrl.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { DetailedMeta } from "../metadata/getmeta";
|
|
||||||
import { MWMediaType } from "../metadata/types";
|
|
||||||
import { MWEmbed } from "./embed";
|
import { MWEmbed } from "./embed";
|
||||||
import { MWStream } from "./streams";
|
import { MWStream } from "./streams";
|
||||||
|
import { DetailedMeta } from "../metadata/getmeta";
|
||||||
|
import { MWMediaType } from "../metadata/types";
|
||||||
|
|
||||||
export type MWProviderScrapeResult = {
|
export type MWProviderScrapeResult = {
|
||||||
stream?: MWStream;
|
stream?: MWStream;
|
||||||
|
@@ -3,9 +3,16 @@ export enum MWStreamType {
|
|||||||
HLS = "hls",
|
HLS = "hls",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// subsrt-ts supported types
|
||||||
export enum MWCaptionType {
|
export enum MWCaptionType {
|
||||||
VTT = "vtt",
|
VTT = "vtt",
|
||||||
SRT = "srt",
|
SRT = "srt",
|
||||||
|
LRC = "lrc",
|
||||||
|
SBV = "sbv",
|
||||||
|
SUB = "sub",
|
||||||
|
SSA = "ssa",
|
||||||
|
ASS = "ass",
|
||||||
|
JSON = "json",
|
||||||
UNKNOWN = "unknown",
|
UNKNOWN = "unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@ import "./providers/superstream";
|
|||||||
import "./providers/netfilm";
|
import "./providers/netfilm";
|
||||||
import "./providers/m4ufree";
|
import "./providers/m4ufree";
|
||||||
import "./providers/hdwatched";
|
import "./providers/hdwatched";
|
||||||
|
import "./providers/2embed";
|
||||||
|
|
||||||
// embeds
|
// embeds
|
||||||
import "./embeds/streamm4u";
|
import "./embeds/streamm4u";
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import { FetchError } from "ofetch";
|
import { FetchError } from "ofetch";
|
||||||
import { makeUrl, proxiedFetch } from "../helpers/fetch";
|
|
||||||
import {
|
import {
|
||||||
formatJWMeta,
|
|
||||||
JWMediaResult,
|
JWMediaResult,
|
||||||
JWSeasonMetaResult,
|
JWSeasonMetaResult,
|
||||||
JW_API_BASE,
|
JW_API_BASE,
|
||||||
|
formatJWMeta,
|
||||||
mediaTypeToJW,
|
mediaTypeToJW,
|
||||||
} from "./justwatch";
|
} from "./justwatch";
|
||||||
import { MWMediaMeta, MWMediaType } from "./types";
|
import { MWMediaMeta, MWMediaType } from "./types";
|
||||||
|
import { makeUrl, proxiedFetch } from "../helpers/fetch";
|
||||||
|
|
||||||
type JWExternalIdType =
|
type JWExternalIdType =
|
||||||
| "eidr"
|
| "eidr"
|
||||||
@@ -28,8 +29,8 @@ interface JWDetailedMeta extends JWMediaResult {
|
|||||||
|
|
||||||
export interface DetailedMeta {
|
export interface DetailedMeta {
|
||||||
meta: MWMediaMeta;
|
meta: MWMediaMeta;
|
||||||
tmdbId: string;
|
imdbId?: string;
|
||||||
imdbId: string;
|
tmdbId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMetaFromId(
|
export async function getMetaFromId(
|
||||||
@@ -66,8 +67,6 @@ export async function getMetaFromId(
|
|||||||
if (!tmdbId)
|
if (!tmdbId)
|
||||||
tmdbId = data.external_ids.find((v) => v.provider === "tmdb")?.external_id;
|
tmdbId = data.external_ids.find((v) => v.provider === "tmdb")?.external_id;
|
||||||
|
|
||||||
if (!imdbId || !tmdbId) throw new Error("not enough info");
|
|
||||||
|
|
||||||
let seasonData: JWSeasonMetaResult | undefined;
|
let seasonData: JWSeasonMetaResult | undefined;
|
||||||
if (data.object_type === "show") {
|
if (data.object_type === "show") {
|
||||||
const seasonToScrape = seasonId ?? data.seasons?.[0].id.toString() ?? "";
|
const seasonToScrape = seasonId ?? data.seasons?.[0].id.toString() ?? "";
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import { SimpleCache } from "@/utils/cache";
|
import { SimpleCache } from "@/utils/cache";
|
||||||
import { proxiedFetch } from "../helpers/fetch";
|
|
||||||
import {
|
import {
|
||||||
formatJWMeta,
|
|
||||||
JWContentTypes,
|
JWContentTypes,
|
||||||
JWMediaResult,
|
JWMediaResult,
|
||||||
JW_API_BASE,
|
JW_API_BASE,
|
||||||
|
formatJWMeta,
|
||||||
mediaTypeToJW,
|
mediaTypeToJW,
|
||||||
} from "./justwatch";
|
} from "./justwatch";
|
||||||
import { MWMediaMeta, MWQuery } from "./types";
|
import { MWMediaMeta, MWQuery } from "./types";
|
||||||
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
|
|
||||||
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
|
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
|
||||||
cache.setCompare((a, b) => {
|
cache.setCompare((a, b) => {
|
||||||
|
251
src/backend/providers/2embed.ts
Normal file
251
src/backend/providers/2embed.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import Base64 from "crypto-js/enc-base64";
|
||||||
|
import Utf8 from "crypto-js/enc-utf8";
|
||||||
|
|
||||||
|
import { proxiedFetch, rawProxiedFetch } from "../helpers/fetch";
|
||||||
|
import { registerProvider } from "../helpers/register";
|
||||||
|
import {
|
||||||
|
MWCaptionType,
|
||||||
|
MWStreamQuality,
|
||||||
|
MWStreamType,
|
||||||
|
} from "../helpers/streams";
|
||||||
|
import { MWMediaType } from "../metadata/types";
|
||||||
|
|
||||||
|
const twoEmbedBase = "https://www.2embed.to";
|
||||||
|
|
||||||
|
async function fetchCaptchaToken(recaptchaKey: string) {
|
||||||
|
const domainHash = Base64.stringify(Utf8.parse(twoEmbedBase)).replace(
|
||||||
|
/=/g,
|
||||||
|
"."
|
||||||
|
);
|
||||||
|
|
||||||
|
const recaptchaRender = await proxiedFetch<any>(
|
||||||
|
`https://www.google.com/recaptcha/api.js?render=${recaptchaKey}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const vToken = recaptchaRender.substring(
|
||||||
|
recaptchaRender.indexOf("/releases/") + 10,
|
||||||
|
recaptchaRender.indexOf("/recaptcha__en.js")
|
||||||
|
);
|
||||||
|
|
||||||
|
const recaptchaAnchor = await proxiedFetch<any>(
|
||||||
|
`https://www.google.com/recaptcha/api2/anchor?ar=1&hl=en&size=invisible&cb=flicklax&k=${recaptchaKey}&co=${domainHash}&v=${vToken}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const cToken = new DOMParser()
|
||||||
|
.parseFromString(recaptchaAnchor, "text/html")
|
||||||
|
.getElementById("recaptcha-token")
|
||||||
|
?.getAttribute("value");
|
||||||
|
|
||||||
|
if (!cToken) throw new Error("Unable to find cToken");
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
v: vToken,
|
||||||
|
reason: "q",
|
||||||
|
k: recaptchaKey,
|
||||||
|
c: cToken,
|
||||||
|
sa: "",
|
||||||
|
co: twoEmbedBase,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenData = await proxiedFetch<string>(
|
||||||
|
`https://www.google.com/recaptcha/api2/reload?${new URLSearchParams(
|
||||||
|
payload
|
||||||
|
).toString()}`,
|
||||||
|
{
|
||||||
|
headers: { referer: "https://www.google.com/recaptcha/api2/" },
|
||||||
|
method: "POST",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const token = tokenData.match('rresp","(.+?)"');
|
||||||
|
return token ? token[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IEmbedRes {
|
||||||
|
link: string;
|
||||||
|
sources: [];
|
||||||
|
tracks: [];
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IStreamData {
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
type: string;
|
||||||
|
token: string;
|
||||||
|
result:
|
||||||
|
| {
|
||||||
|
Original: {
|
||||||
|
label: string;
|
||||||
|
file: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISubtitles {
|
||||||
|
url: string;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStream(sourceId: string, captchaToken: string) {
|
||||||
|
const embedRes = await proxiedFetch<IEmbedRes>(
|
||||||
|
`${twoEmbedBase}/ajax/embed/play?id=${sourceId}&_token=${captchaToken}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Referer: twoEmbedBase,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Link format: https://rabbitstream.net/embed-4/{data-id}?z=
|
||||||
|
const rabbitStreamUrl = new URL(embedRes.link);
|
||||||
|
|
||||||
|
const dataPath = rabbitStreamUrl.pathname.split("/");
|
||||||
|
const dataId = dataPath[dataPath.length - 1];
|
||||||
|
|
||||||
|
// https://rabbitstream.net/embed/m-download/{data-id}
|
||||||
|
const download = await proxiedFetch<any>(
|
||||||
|
`${rabbitStreamUrl.origin}/embed/m-download/${dataId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
referer: twoEmbedBase,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadPage = new DOMParser().parseFromString(download, "text/html");
|
||||||
|
|
||||||
|
const streamlareEl = Array.from(
|
||||||
|
downloadPage.querySelectorAll(".dls-brand")
|
||||||
|
).find((el) => el.textContent?.trim() === "Streamlare");
|
||||||
|
if (!streamlareEl) throw new Error("Unable to find streamlare element");
|
||||||
|
|
||||||
|
const streamlareUrl =
|
||||||
|
streamlareEl.nextElementSibling?.querySelector("a")?.href;
|
||||||
|
if (!streamlareUrl) throw new Error("Unable to parse streamlare url");
|
||||||
|
|
||||||
|
const subtitles: ISubtitles[] = [];
|
||||||
|
const subtitlesDropdown = downloadPage.querySelectorAll(
|
||||||
|
"#user_menu .dropdown-item"
|
||||||
|
);
|
||||||
|
subtitlesDropdown.forEach((item) => {
|
||||||
|
const url = item.getAttribute("href");
|
||||||
|
const lang = item.textContent?.trim().replace("Download", "").trim();
|
||||||
|
if (url && lang) subtitles.push({ url, lang });
|
||||||
|
});
|
||||||
|
|
||||||
|
const streamlare = await proxiedFetch<any>(streamlareUrl);
|
||||||
|
|
||||||
|
const streamlarePage = new DOMParser().parseFromString(
|
||||||
|
streamlare,
|
||||||
|
"text/html"
|
||||||
|
);
|
||||||
|
|
||||||
|
const csrfToken = streamlarePage
|
||||||
|
.querySelector("head > meta:nth-child(3)")
|
||||||
|
?.getAttribute("content");
|
||||||
|
|
||||||
|
if (!csrfToken) throw new Error("Unable to find CSRF token");
|
||||||
|
|
||||||
|
const videoId = streamlareUrl.match("/[ve]/([^?#&/]+)")?.[1];
|
||||||
|
if (!videoId) throw new Error("Unable to get streamlare video id");
|
||||||
|
|
||||||
|
const streamRes = await proxiedFetch<IStreamData>(
|
||||||
|
`${new URL(streamlareUrl).origin}/api/video/download/get`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: videoId,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"X-CSRF-Token": csrfToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (streamRes.message !== "OK") throw new Error("Unable to fetch stream");
|
||||||
|
|
||||||
|
const streamData = Array.isArray(streamRes.result)
|
||||||
|
? streamRes.result[0]
|
||||||
|
: streamRes.result.Original;
|
||||||
|
if (!streamData) throw new Error("Unable to get stream data");
|
||||||
|
|
||||||
|
const followStream = await rawProxiedFetch(streamData.url, {
|
||||||
|
method: "HEAD",
|
||||||
|
referrer: new URL(streamlareUrl).origin,
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalStreamUrl = followStream.headers.get("X-Final-Destination");
|
||||||
|
return { url: finalStreamUrl, subtitles };
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProvider({
|
||||||
|
id: "2embed",
|
||||||
|
displayName: "2Embed",
|
||||||
|
rank: 125,
|
||||||
|
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
||||||
|
async scrape({ media, episode, progress }) {
|
||||||
|
let embedUrl = `${twoEmbedBase}/embed/tmdb/movie?id=${media.tmdbId}`;
|
||||||
|
|
||||||
|
if (media.meta.type === MWMediaType.SERIES) {
|
||||||
|
const seasonNumber = media.meta.seasonData.number;
|
||||||
|
const episodeNumber = media.meta.seasonData.episodes.find(
|
||||||
|
(e) => e.id === episode
|
||||||
|
)?.number;
|
||||||
|
|
||||||
|
embedUrl = `${twoEmbedBase}/embed/tmdb/tv?id=${media.tmdbId}&s=${seasonNumber}&e=${episodeNumber}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = await proxiedFetch<any>(embedUrl);
|
||||||
|
progress(20);
|
||||||
|
|
||||||
|
const embedPage = new DOMParser().parseFromString(embed, "text/html");
|
||||||
|
|
||||||
|
const pageServerItems = Array.from(
|
||||||
|
embedPage.querySelectorAll(".item-server")
|
||||||
|
);
|
||||||
|
const pageStreamItem = pageServerItems.find((item) =>
|
||||||
|
item.textContent?.includes("Vidcloud")
|
||||||
|
);
|
||||||
|
|
||||||
|
const sourceId = pageStreamItem
|
||||||
|
? pageStreamItem.getAttribute("data-id")
|
||||||
|
: null;
|
||||||
|
if (!sourceId) throw new Error("Unable to get source id");
|
||||||
|
|
||||||
|
const siteKey = embedPage
|
||||||
|
.querySelector("body")
|
||||||
|
?.getAttribute("data-recaptcha-key");
|
||||||
|
if (!siteKey) throw new Error("Unable to get site key");
|
||||||
|
|
||||||
|
const captchaToken = await fetchCaptchaToken(siteKey);
|
||||||
|
if (!captchaToken) throw new Error("Unable to fetch captcha token");
|
||||||
|
progress(35);
|
||||||
|
|
||||||
|
const stream = await fetchStream(sourceId, captchaToken);
|
||||||
|
if (!stream.url) throw new Error("Unable to find stream url");
|
||||||
|
|
||||||
|
return {
|
||||||
|
embeds: [],
|
||||||
|
stream: {
|
||||||
|
streamUrl: stream.url,
|
||||||
|
quality: MWStreamQuality.QUNKNOWN,
|
||||||
|
type: MWStreamType.MP4,
|
||||||
|
captions: stream.subtitles.map((sub) => {
|
||||||
|
return {
|
||||||
|
langIso: sub.lang,
|
||||||
|
url: `https://cc.2cdns.com${new URL(sub.url).pathname}`,
|
||||||
|
type: MWCaptionType.VTT,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
@@ -1,14 +1,15 @@
|
|||||||
import { compareTitle } from "@/utils/titleMatch";
|
import { compareTitle } from "@/utils/titleMatch";
|
||||||
import { proxiedFetch } from "../helpers/fetch";
|
|
||||||
import { registerProvider } from "../helpers/register";
|
|
||||||
import {
|
import {
|
||||||
MWCaptionType,
|
getMWCaptionTypeFromUrl,
|
||||||
MWStreamQuality,
|
isSupportedSubtitle,
|
||||||
MWStreamType,
|
} from "../helpers/captions";
|
||||||
} from "../helpers/streams";
|
import { mwFetch } from "../helpers/fetch";
|
||||||
|
import { registerProvider } from "../helpers/register";
|
||||||
|
import { MWCaption, MWStreamQuality, MWStreamType } from "../helpers/streams";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types";
|
||||||
|
|
||||||
const flixHqBase = "https://api.consumet.org/meta/tmdb";
|
const flixHqBase = "https://consumet-api-clone.vercel.app/meta/tmdb"; // instance stolen from streaminal :)
|
||||||
|
|
||||||
type FlixHQMediaType = "Movie" | "TV Series";
|
type FlixHQMediaType = "Movie" | "TV Series";
|
||||||
interface FLIXMediaBase {
|
interface FLIXMediaBase {
|
||||||
@@ -19,15 +20,19 @@ interface FLIXMediaBase {
|
|||||||
type: FlixHQMediaType;
|
type: FlixHQMediaType;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
}
|
}
|
||||||
|
interface FLIXSubType {
|
||||||
function castSubtitles({ url, lang }: { url: string; lang: string }) {
|
url: string;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
function convertSubtitles({ url, lang }: FLIXSubType): MWCaption | null {
|
||||||
|
if (lang.includes("(maybe)")) return null;
|
||||||
|
const supported = isSupportedSubtitle(url);
|
||||||
|
if (!supported) return null;
|
||||||
|
const type = getMWCaptionTypeFromUrl(url);
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
langIso: lang,
|
langIso: lang,
|
||||||
type:
|
type,
|
||||||
url.substring(url.length - 3) === "vtt"
|
|
||||||
? MWCaptionType.VTT
|
|
||||||
: MWCaptionType.SRT,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +59,7 @@ registerProvider({
|
|||||||
throw new Error("Unsupported type");
|
throw new Error("Unsupported type");
|
||||||
}
|
}
|
||||||
// search for relevant item
|
// search for relevant item
|
||||||
const searchResults = await proxiedFetch<any>(
|
const searchResults = await mwFetch<any>(
|
||||||
`/${encodeURIComponent(media.meta.title)}`,
|
`/${encodeURIComponent(media.meta.title)}`,
|
||||||
{
|
{
|
||||||
baseURL: flixHqBase,
|
baseURL: flixHqBase,
|
||||||
@@ -74,7 +79,7 @@ registerProvider({
|
|||||||
|
|
||||||
// get media info
|
// get media info
|
||||||
progress(25);
|
progress(25);
|
||||||
const mediaInfo = await proxiedFetch<any>(`/info/${foundItem.id}`, {
|
const mediaInfo = await mwFetch<any>(`/info/${foundItem.id}`, {
|
||||||
baseURL: flixHqBase,
|
baseURL: flixHqBase,
|
||||||
params: {
|
params: {
|
||||||
type: flixTypeToMWType(foundItem.type),
|
type: flixTypeToMWType(foundItem.type),
|
||||||
@@ -98,7 +103,7 @@ registerProvider({
|
|||||||
}
|
}
|
||||||
if (!episodeId) throw new Error("No watchable item found");
|
if (!episodeId) throw new Error("No watchable item found");
|
||||||
progress(75);
|
progress(75);
|
||||||
const watchInfo = await proxiedFetch<any>(`/watch/${episodeId}`, {
|
const watchInfo = await mwFetch<any>(`/watch/${episodeId}`, {
|
||||||
baseURL: flixHqBase,
|
baseURL: flixHqBase,
|
||||||
params: {
|
params: {
|
||||||
id: mediaInfo.id,
|
id: mediaInfo.id,
|
||||||
@@ -116,11 +121,7 @@ registerProvider({
|
|||||||
streamUrl: source.url,
|
streamUrl: source.url,
|
||||||
quality: qualityMap[source.quality],
|
quality: qualityMap[source.quality],
|
||||||
type: source.isM3U8 ? MWStreamType.HLS : MWStreamType.MP4,
|
type: source.isM3U8 ? MWStreamType.HLS : MWStreamType.MP4,
|
||||||
captions: watchInfo.subtitles
|
captions: watchInfo.subtitles.map(convertSubtitles).filter(Boolean),
|
||||||
.filter(
|
|
||||||
(x: { url: string; lang: string }) => !x.lang.includes("(maybe)")
|
|
||||||
)
|
|
||||||
.map(castSubtitles),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { unpack } from "unpacker";
|
|
||||||
import CryptoJS from "crypto-js";
|
import CryptoJS from "crypto-js";
|
||||||
|
import { unpack } from "unpacker";
|
||||||
|
|
||||||
import { registerProvider } from "@/backend/helpers/register";
|
import { registerProvider } from "@/backend/helpers/register";
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
|
||||||
import { MWStreamQuality } from "@/backend/helpers/streams";
|
import { MWStreamQuality } from "@/backend/helpers/streams";
|
||||||
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
|
|
||||||
import { proxiedFetch } from "../helpers/fetch";
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
|
|
||||||
const format = {
|
const format = {
|
||||||
@@ -40,6 +41,7 @@ registerProvider({
|
|||||||
type: [MWMediaType.MOVIE],
|
type: [MWMediaType.MOVIE],
|
||||||
|
|
||||||
async scrape({ progress, media: { imdbId } }) {
|
async scrape({ progress, media: { imdbId } }) {
|
||||||
|
if (!imdbId) throw new Error("not enough info");
|
||||||
progress(10);
|
progress(10);
|
||||||
const streamRes = await proxiedFetch<string>(
|
const streamRes = await proxiedFetch<string>(
|
||||||
"https://database.gdriveplayer.us/player.php",
|
"https://database.gdriveplayer.us/player.php",
|
||||||
|
@@ -123,6 +123,7 @@ registerProvider({
|
|||||||
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
||||||
async scrape(options) {
|
async scrape(options) {
|
||||||
const { media, progress } = options;
|
const { media, progress } = options;
|
||||||
|
if (!media.imdbId) throw new Error("not enough info");
|
||||||
if (!this.type.includes(media.meta.type)) {
|
if (!this.type.includes(media.meta.type)) {
|
||||||
throw new Error("Unsupported type");
|
throw new Error("Unsupported type");
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed";
|
import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed";
|
||||||
|
|
||||||
import { proxiedFetch } from "../helpers/fetch";
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
import { registerProvider } from "../helpers/register";
|
import { registerProvider } from "../helpers/register";
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types";
|
||||||
|
@@ -1,15 +1,19 @@
|
|||||||
import { registerProvider } from "@/backend/helpers/register";
|
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
|
||||||
|
|
||||||
import { customAlphabet } from "nanoid";
|
|
||||||
import CryptoJS from "crypto-js";
|
import CryptoJS from "crypto-js";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getMWCaptionTypeFromUrl,
|
||||||
|
isSupportedSubtitle,
|
||||||
|
} from "@/backend/helpers/captions";
|
||||||
import { proxiedFetch } from "@/backend/helpers/fetch";
|
import { proxiedFetch } from "@/backend/helpers/fetch";
|
||||||
|
import { registerProvider } from "@/backend/helpers/register";
|
||||||
import {
|
import {
|
||||||
MWCaption,
|
MWCaption,
|
||||||
MWCaptionType,
|
MWCaptionType,
|
||||||
MWStreamQuality,
|
MWStreamQuality,
|
||||||
MWStreamType,
|
MWStreamType,
|
||||||
} from "@/backend/helpers/streams";
|
} from "@/backend/helpers/streams";
|
||||||
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
import { compareTitle } from "@/utils/titleMatch";
|
import { compareTitle } from "@/utils/titleMatch";
|
||||||
|
|
||||||
const nanoid = customAlphabet("0123456789abcdef", 32);
|
const nanoid = customAlphabet("0123456789abcdef", 32);
|
||||||
@@ -111,6 +115,30 @@ const getBestQuality = (list: any[]) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const convertSubtitles = (subtitleGroup: any): MWCaption | null => {
|
||||||
|
let subtitles = subtitleGroup.subtitles;
|
||||||
|
subtitles = subtitles
|
||||||
|
.map((subFile: any) => {
|
||||||
|
const supported = isSupportedSubtitle(subFile.file_path);
|
||||||
|
if (!supported) return null;
|
||||||
|
const type = getMWCaptionTypeFromUrl(subFile.file_path);
|
||||||
|
return {
|
||||||
|
...subFile,
|
||||||
|
type: type as MWCaptionType,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (subtitles.length === 0) return null;
|
||||||
|
const subFile = subtitles[0];
|
||||||
|
return {
|
||||||
|
needsProxy: true,
|
||||||
|
langIso: subtitleGroup.language,
|
||||||
|
url: subFile.file_path,
|
||||||
|
type: subFile.type,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
registerProvider({
|
registerProvider({
|
||||||
id: "superstream",
|
id: "superstream",
|
||||||
displayName: "Superstream",
|
displayName: "Superstream",
|
||||||
@@ -164,16 +192,9 @@ registerProvider({
|
|||||||
|
|
||||||
const subtitleRes = (await get(subtitleApiQuery)).data;
|
const subtitleRes = (await get(subtitleApiQuery)).data;
|
||||||
|
|
||||||
const mappedCaptions = subtitleRes.list.map(
|
const mappedCaptions = subtitleRes.list
|
||||||
(subtitle: any): MWCaption => {
|
.map(convertSubtitles)
|
||||||
return {
|
.filter(Boolean);
|
||||||
needsProxy: true,
|
|
||||||
langIso: subtitle.language,
|
|
||||||
url: subtitle.subtitles[0].file_path,
|
|
||||||
type: MWCaptionType.SRT,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
embeds: [],
|
embeds: [],
|
||||||
@@ -224,22 +245,9 @@ registerProvider({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const subtitleRes = (await get(subtitleApiQuery)).data;
|
const subtitleRes = (await get(subtitleApiQuery)).data;
|
||||||
|
const mappedCaptions = subtitleRes.list
|
||||||
const mappedCaptions = subtitleRes.list.map(
|
.map(convertSubtitles)
|
||||||
(subtitle: any): MWCaption | null => {
|
.filter(Boolean);
|
||||||
const sub = subtitle;
|
|
||||||
sub.subtitles = subtitle.subtitles.filter((subFile: any) => {
|
|
||||||
const extension = subFile.file_path.slice(-3);
|
|
||||||
return [MWCaptionType.SRT, MWCaptionType.VTT].includes(extension);
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
needsProxy: true,
|
|
||||||
langIso: subtitle.language,
|
|
||||||
url: sub.subtitles[0].file_path,
|
|
||||||
type: MWCaptionType.SRT,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
embeds: [],
|
embeds: [],
|
||||||
stream: {
|
stream: {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Icon, Icons } from "@/components/Icon";
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
icon?: Icons;
|
icon?: Icons;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { useSettings } from "@/state/settings";
|
import { useSettings } from "@/state/settings";
|
||||||
|
|
||||||
import { Icon, Icons } from "./Icon";
|
import { Icon, Icons } from "./Icon";
|
||||||
|
|
||||||
export const colors = ["#ffffff", "#00ffff", "#ffff00"];
|
export const colors = ["#ffffff", "#00ffff", "#ffff00"];
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
|
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
export interface OptionItem {
|
export interface OptionItem {
|
||||||
@@ -37,7 +37,7 @@ export function Dropdown(props: DropdownProps) {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute top-10 left-0 right-0 z-10 mt-1 max-h-60 overflow-auto rounded-md bg-denim-500 py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 focus:outline-none sm:top-10 sm:text-sm">
|
<Listbox.Options className="absolute left-0 right-0 top-10 z-10 mt-1 max-h-60 overflow-auto rounded-md bg-denim-500 py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 focus:outline-none sm:top-10 sm:text-sm">
|
||||||
{props.options.map((opt) => (
|
{props.options.map((opt) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Transition } from "@/components/Transition";
|
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
|
import { Transition } from "@/components/Transition";
|
||||||
|
|
||||||
export function Overlay(props: { children: React.ReactNode }) {
|
export function Overlay(props: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { MWMediaType, MWQuery } from "@/backend/metadata/types";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { MWMediaType, MWQuery } from "@/backend/metadata/types";
|
||||||
|
|
||||||
import { DropdownButton } from "./buttons/DropdownButton";
|
import { DropdownButton } from "./buttons/DropdownButton";
|
||||||
import { Icon, Icons } from "./Icon";
|
import { Icon, Icons } from "./Icon";
|
||||||
import { TextInputControl } from "./text-inputs/TextInputControl";
|
import { TextInputControl } from "./text-inputs/TextInputControl";
|
||||||
@@ -38,7 +40,7 @@ export function SearchBarInput(props: SearchBarProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col rounded-[28px] bg-denim-400 transition-colors focus-within:bg-denim-400 hover:bg-denim-500 sm:flex-row sm:items-center">
|
<div className="relative flex flex-col rounded-[28px] bg-denim-400 transition-colors focus-within:bg-denim-400 hover:bg-denim-500 sm:flex-row sm:items-center">
|
||||||
<div className="pointer-events-none absolute left-5 top-0 bottom-0 flex max-h-14 items-center">
|
<div className="pointer-events-none absolute bottom-0 left-5 top-0 flex max-h-14 items-center">
|
||||||
<Icon icon={Icons.SEARCH} />
|
<Icon icon={Icons.SEARCH} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ export function SearchBarInput(props: SearchBarProps) {
|
|||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="px-4 py-4 pt-0 sm:py-2 sm:px-2">
|
<div className="px-4 py-4 pt-0 sm:px-2 sm:py-2">
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
icon={Icons.SEARCH}
|
icon={Icons.SEARCH}
|
||||||
open={dropdownOpen}
|
open={dropdownOpen}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { Fragment, ReactNode } from "react";
|
|
||||||
import {
|
import {
|
||||||
Transition as HeadlessTransition,
|
Transition as HeadlessTransition,
|
||||||
TransitionClasses,
|
TransitionClasses,
|
||||||
} from "@headlessui/react";
|
} from "@headlessui/react";
|
||||||
|
import { Fragment, ReactNode } from "react";
|
||||||
|
|
||||||
type TransitionAnimations =
|
type TransitionAnimations =
|
||||||
| "slide-down"
|
| "slide-down"
|
||||||
|
@@ -4,10 +4,11 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
|
||||||
|
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { BackdropContainer, useBackdrop } from "@/components/layout/Backdrop";
|
import { BackdropContainer, useBackdrop } from "@/components/layout/Backdrop";
|
||||||
import { ButtonControlProps, ButtonControl } from "./ButtonControl";
|
|
||||||
|
import { ButtonControl, ButtonControlProps } from "./ButtonControl";
|
||||||
|
|
||||||
export interface OptionItem {
|
export interface OptionItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import { Icon, Icons } from "@/components/Icon";
|
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
import { ButtonControl } from "./ButtonControl";
|
import { ButtonControl } from "./ButtonControl";
|
||||||
|
|
||||||
export interface EditButtonProps {
|
export interface EditButtonProps {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { ButtonControlProps, ButtonControl } from "./ButtonControl";
|
|
||||||
|
import { ButtonControl, ButtonControlProps } from "./ButtonControl";
|
||||||
|
|
||||||
export interface IconButtonProps extends ButtonControlProps {
|
export interface IconButtonProps extends ButtonControlProps {
|
||||||
icon: Icons;
|
icon: Icons;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React, { createRef, useEffect, useState } from "react";
|
import React, { createRef, useEffect, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
import { useFade } from "@/hooks/useFade";
|
import { useFade } from "@/hooks/useFade";
|
||||||
|
|
||||||
interface BackdropProps {
|
interface BackdropProps {
|
||||||
@@ -99,7 +100,7 @@ export function BackdropContainer(
|
|||||||
return (
|
return (
|
||||||
<div ref={root}>
|
<div ref={root}>
|
||||||
{createPortal(
|
{createPortal(
|
||||||
<div className="pointer-events-none fixed top-0 left-0 z-[999]">
|
<div className="pointer-events-none fixed left-0 top-0 z-[999]">
|
||||||
<Backdrop active={props.active} {...props} />
|
<Backdrop active={props.active} {...props} />
|
||||||
<div ref={copy} className="pointer-events-auto absolute">
|
<div ref={copy} className="pointer-events-auto absolute">
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
export function BrandPill(props: {
|
export function BrandPill(props: {
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { Component } from "react";
|
import { Component } from "react";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { Link } from "@/components/text/Link";
|
import { Link } from "@/components/text/Link";
|
||||||
import { Title } from "@/components/text/Title";
|
import { Title } from "@/components/text/Title";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
interface ErrorShowcaseProps {
|
interface ErrorShowcaseProps {
|
||||||
error: {
|
error: {
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import { Overlay } from "@/components/Overlay";
|
|
||||||
import { Transition } from "@/components/Transition";
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
|
import { Overlay } from "@/components/Overlay";
|
||||||
|
import { Transition } from "@/components/Transition";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
import { ReactNode, useState } from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { conf } from "@/setup/config";
|
|
||||||
import { useBannerSize } from "@/hooks/useBanner";
|
import { useBannerSize } from "@/hooks/useBanner";
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
import SettingsModal from "@/views/SettingsModal";
|
import SettingsModal from "@/views/SettingsModal";
|
||||||
|
|
||||||
import { BrandPill } from "./BrandPill";
|
import { BrandPill } from "./BrandPill";
|
||||||
|
|
||||||
export interface NavigationProps {
|
export interface NavigationProps {
|
||||||
@@ -22,7 +24,7 @@ export function Navigation(props: NavigationProps) {
|
|||||||
top: `${bannerHeight}px`,
|
top: `${bannerHeight}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="fixed left-0 right-0 flex items-center justify-between py-5 px-7">
|
<div className="fixed left-0 right-0 flex items-center justify-between px-7 py-5">
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
props.bg ? "opacity-100" : "opacity-0"
|
props.bg ? "opacity-100" : "opacity-0"
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
interface SectionHeadingProps {
|
interface SectionHeadingProps {
|
||||||
|
@@ -9,12 +9,12 @@ export function Episode(props: EpisodeProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
className={`transition-[background-color, transform, box-shadow] relative mr-3 mb-3 inline-flex h-10 w-10 cursor-pointer select-none items-center justify-center overflow-hidden rounded bg-denim-500 font-bold text-white hover:bg-denim-400 active:scale-110 ${
|
className={`transition-[background-color, transform, box-shadow] relative mb-3 mr-3 inline-flex h-10 w-10 cursor-pointer select-none items-center justify-center overflow-hidden rounded bg-denim-500 font-bold text-white hover:bg-denim-400 active:scale-110 ${
|
||||||
props.active ? "shadow-[inset_0_0_0_2px] shadow-bink-500" : ""
|
props.active ? "shadow-[inset_0_0_0_2px] shadow-bink-500" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-0 top-0 left-0 bg-bink-500 bg-opacity-50"
|
className="absolute bottom-0 left-0 top-0 bg-bink-500 bg-opacity-50"
|
||||||
style={{
|
style={{
|
||||||
width: `${props.progress || 0}%`,
|
width: `${props.progress || 0}%`,
|
||||||
}}
|
}}
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
import { Link } from "react-router-dom";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DotList } from "@/components/text/DotList";
|
import { Link } from "react-router-dom";
|
||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
|
||||||
import { JWMediaToId } from "@/backend/metadata/justwatch";
|
import { JWMediaToId } from "@/backend/metadata/justwatch";
|
||||||
import { Icons } from "../Icon";
|
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
|
import { DotList } from "@/components/text/DotList";
|
||||||
|
|
||||||
import { IconPatch } from "../buttons/IconPatch";
|
import { IconPatch } from "../buttons/IconPatch";
|
||||||
|
import { Icons } from "../Icon";
|
||||||
|
|
||||||
export interface MediaCardProps {
|
export interface MediaCardProps {
|
||||||
media: MWMediaMeta;
|
media: MWMediaMeta;
|
||||||
@@ -59,7 +61,7 @@ function MediaCardContent({
|
|||||||
{series ? (
|
{series ? (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
"absolute right-2 top-2 rounded-md bg-denim-200 py-1 px-2 transition-colors",
|
"absolute right-2 top-2 rounded-md bg-denim-200 px-2 py-1 transition-colors",
|
||||||
closable ? "" : "group-hover:bg-denim-500",
|
closable ? "" : "group-hover:bg-denim-500",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
import { useWatchedContext } from "@/state/watched";
|
import { useWatchedContext } from "@/state/watched";
|
||||||
import { useMemo } from "react";
|
|
||||||
import { MediaCard } from "./MediaCard";
|
import { MediaCard } from "./MediaCard";
|
||||||
|
|
||||||
export interface WatchedMediaCardProps {
|
export interface WatchedMediaCardProps {
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
|
import { animated, easings, useSpringValue } from "@react-spring/web";
|
||||||
|
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { FloatingCardAnchorPosition } from "@/components/popout/positions/FloatingCardAnchorPosition";
|
import { FloatingCardAnchorPosition } from "@/components/popout/positions/FloatingCardAnchorPosition";
|
||||||
import { FloatingCardMobilePosition } from "@/components/popout/positions/FloatingCardMobilePosition";
|
import { FloatingCardMobilePosition } from "@/components/popout/positions/FloatingCardMobilePosition";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { PopoutSection } from "@/video/components/popouts/PopoutUtils";
|
import { PopoutSection } from "@/video/components/popouts/PopoutUtils";
|
||||||
import { useSpringValue, animated, easings } from "@react-spring/web";
|
|
||||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { Icon, Icons } from "../Icon";
|
|
||||||
import { FloatingDragHandle, MobilePopoutSpacer } from "./FloatingDragHandle";
|
import { FloatingDragHandle, MobilePopoutSpacer } from "./FloatingDragHandle";
|
||||||
|
import { Icon, Icons } from "../Icon";
|
||||||
|
|
||||||
interface FloatingCardProps {
|
interface FloatingCardProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@@ -165,7 +167,7 @@ export const FloatingCardView = {
|
|||||||
<div>{props.action ?? null}</div>
|
<div>{props.action ?? null}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="mt-8 mb-2 text-3xl font-bold text-white">
|
<h2 className="mb-2 mt-8 text-3xl font-bold text-white">
|
||||||
{props.title}
|
{props.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p>{props.description}</p>
|
<p>{props.description}</p>
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import { Transition } from "@/components/Transition";
|
|
||||||
import React, {
|
import React, {
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -8,6 +7,8 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
|
import { Transition } from "@/components/Transition";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { createFloatingAnchorEvent } from "@/components/popout/FloatingAnchor";
|
|
||||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { createFloatingAnchorEvent } from "@/components/popout/FloatingAnchor";
|
||||||
|
|
||||||
interface AnchorPositionProps {
|
interface AnchorPositionProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
id: string;
|
id: string;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useSpring, animated, config } from "@react-spring/web";
|
import { animated, config, useSpring } from "@react-spring/web";
|
||||||
import { useDrag } from "@use-gesture/react";
|
import { useDrag } from "@use-gesture/react";
|
||||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Link as LinkRouter } from "react-router-dom";
|
import { Link as LinkRouter } from "react-router-dom";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
interface IArrowLinkPropsBase {
|
interface IArrowLinkPropsBase {
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
ReactNode,
|
|
||||||
createContext,
|
|
||||||
useState,
|
|
||||||
useMemo,
|
|
||||||
Dispatch,
|
Dispatch,
|
||||||
|
ReactNode,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
useEffect,
|
createContext,
|
||||||
useContext,
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useMeasure } from "react-use";
|
import { useMeasure } from "react-use";
|
||||||
|
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
/// <reference types="chromecast-caf-sender"/>
|
/// <reference types="chromecast-caf-sender"/>
|
||||||
|
|
||||||
import { isChromecastAvailable } from "@/setup/chromecast";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { isChromecastAvailable } from "@/setup/chromecast";
|
||||||
|
|
||||||
export function useChromecastAvailable() {
|
export function useChromecastAvailable() {
|
||||||
const [available, setAvailable] = useState<boolean | null>(null);
|
const [available, setAvailable] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { findBestStream } from "@/backend/helpers/scrape";
|
import { findBestStream } from "@/backend/helpers/scrape";
|
||||||
import { MWStream } from "@/backend/helpers/streams";
|
import { MWStream } from "@/backend/helpers/streams";
|
||||||
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export interface ScrapeEventLog {
|
export interface ScrapeEventLog {
|
||||||
type: "provider" | "embed";
|
type: "provider" | "embed";
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { MWMediaType, MWQuery } from "@/backend/metadata/types";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
|
import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
|
||||||
|
|
||||||
|
import { MWMediaType, MWQuery } from "@/backend/metadata/types";
|
||||||
|
|
||||||
function getInitialValue(params: { type: string; query: string }) {
|
function getInitialValue(params: { type: string; query: string }) {
|
||||||
const type =
|
const type =
|
||||||
Object.values(MWMediaType).find((v) => params.type === v) ||
|
Object.values(MWMediaType).find((v) => params.type === v) ||
|
||||||
|
@@ -1,18 +1,19 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export function useVolumeControl(descriptor: string) {
|
export function useVolumeControl(descriptor: string) {
|
||||||
const [storedVolume, setStoredVolume] = useState(1);
|
const [storedVolume, setStoredVolume] = useState(1);
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
const mediaPlaying = useMediaPlaying(descriptor);
|
const mediaPlaying = useMediaPlaying(descriptor);
|
||||||
|
|
||||||
const toggleVolume = () => {
|
const toggleVolume = (isKeyboardEvent = false) => {
|
||||||
if (mediaPlaying.volume > 0) {
|
if (mediaPlaying.volume > 0) {
|
||||||
setStoredVolume(mediaPlaying.volume);
|
setStoredVolume(mediaPlaying.volume);
|
||||||
controls.setVolume(0);
|
controls.setVolume(0, isKeyboardEvent);
|
||||||
} else {
|
} else {
|
||||||
controls.setVolume(storedVolume > 0 ? storedVolume : 1);
|
controls.setVolume(storedVolume > 0 ? storedVolume : 1, isKeyboardEvent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -3,11 +3,12 @@ import React, { Suspense } from "react";
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { BrowserRouter, HashRouter } from "react-router-dom";
|
import { BrowserRouter, HashRouter } from "react-router-dom";
|
||||||
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
|
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
|
||||||
import { ErrorBoundary } from "@/components/layout/ErrorBoundary";
|
|
||||||
import { conf } from "@/setup/config";
|
|
||||||
import { registerSW } from "virtual:pwa-register";
|
import { registerSW } from "virtual:pwa-register";
|
||||||
|
|
||||||
|
import { ErrorBoundary } from "@/components/layout/ErrorBoundary";
|
||||||
import App from "@/setup/App";
|
import App from "@/setup/App";
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
import "@/setup/ga";
|
import "@/setup/ga";
|
||||||
import "@/setup/sentry";
|
import "@/setup/sentry";
|
||||||
import "@/setup/i18n";
|
import "@/setup/i18n";
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
import { Redirect, Route, Switch } from "react-router-dom";
|
import { Redirect, Route, Switch } from "react-router-dom";
|
||||||
import { BookmarkContextProvider } from "@/state/bookmark";
|
|
||||||
import { WatchedContextProvider } from "@/state/watched";
|
|
||||||
import { SettingsProvider } from "@/state/settings";
|
|
||||||
|
|
||||||
import { NotFoundPage } from "@/views/notfound/NotFoundView";
|
|
||||||
import { MediaView } from "@/views/media/MediaView";
|
|
||||||
import { SearchView } from "@/views/search/SearchView";
|
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
import { V2MigrationView } from "@/views/other/v2Migration";
|
|
||||||
import { BannerContextProvider } from "@/hooks/useBanner";
|
import { BannerContextProvider } from "@/hooks/useBanner";
|
||||||
import { Layout } from "@/setup/Layout";
|
import { Layout } from "@/setup/Layout";
|
||||||
|
import { BookmarkContextProvider } from "@/state/bookmark";
|
||||||
|
import { SettingsProvider } from "@/state/settings";
|
||||||
|
import { WatchedContextProvider } from "@/state/watched";
|
||||||
|
import { MediaView } from "@/views/media/MediaView";
|
||||||
|
import { NotFoundPage } from "@/views/notfound/NotFoundView";
|
||||||
|
import { V2MigrationView } from "@/views/other/v2Migration";
|
||||||
|
import { SearchView } from "@/views/search/SearchView";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -40,15 +40,23 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* other */}
|
{/* other */}
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/dev"
|
||||||
|
component={lazy(
|
||||||
|
() => import("@/views/developer/DeveloperView")
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/dev/video"
|
||||||
|
component={lazy(
|
||||||
|
() => import("@/views/developer/VideoTesterView")
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* developer routes that can abuse workers are disabled in production */}
|
||||||
{process.env.NODE_ENV === "development" ? (
|
{process.env.NODE_ENV === "development" ? (
|
||||||
<>
|
<>
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/dev"
|
|
||||||
component={lazy(
|
|
||||||
() => import("@/views/developer/DeveloperView")
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/dev/test"
|
path="/dev/test"
|
||||||
@@ -56,13 +64,7 @@ function App() {
|
|||||||
() => import("@/views/developer/TestView")
|
() => import("@/views/developer/TestView")
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/dev/video"
|
|
||||||
component={lazy(
|
|
||||||
() => import("@/views/developer/VideoTesterView")
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/dev/providers"
|
path="/dev/providers"
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Banner } from "@/components/Banner";
|
import { Banner } from "@/components/Banner";
|
||||||
import { useBannerSize } from "@/hooks/useBanner";
|
import { useBannerSize } from "@/hooks/useBanner";
|
||||||
import { useIsOnline } from "@/hooks/usePing";
|
import { useIsOnline } from "@/hooks/usePing";
|
||||||
import { ReactNode } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export function Layout(props: { children: ReactNode }) {
|
export function Layout(props: { children: ReactNode }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { APP_VERSION, GITHUB_LINK, DISCORD_LINK } from "./constants";
|
import { APP_VERSION, DISCORD_LINK, GITHUB_LINK } from "./constants";
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
APP_VERSION: string;
|
APP_VERSION: string;
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import ReactGA from "react-ga4";
|
import ReactGA from "react-ga4";
|
||||||
|
|
||||||
import { GA_ID } from "@/setup/constants";
|
import { GA_ID } from "@/setup/constants";
|
||||||
|
|
||||||
ReactGA.initialize([
|
ReactGA.initialize([
|
||||||
|
@@ -1,11 +1,15 @@
|
|||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
|
||||||
|
|
||||||
// Languages
|
// Languages
|
||||||
import en from "./locales/en/translation.json";
|
|
||||||
import nl from "./locales/nl/translation.json";
|
|
||||||
import { captionLanguages } from "./iso6391";
|
import { captionLanguages } from "./iso6391";
|
||||||
|
import cs from "./locales/cs/translation.json";
|
||||||
|
import de from "./locales/de/translation.json";
|
||||||
|
import en from "./locales/en/translation.json";
|
||||||
|
import fr from "./locales/fr/translation.json";
|
||||||
|
import nl from "./locales/nl/translation.json";
|
||||||
|
import tr from "./locales/tr/translation.json";
|
||||||
|
import zh from "./locales/zh/translation.json";
|
||||||
|
|
||||||
const locales = {
|
const locales = {
|
||||||
en: {
|
en: {
|
||||||
@@ -14,11 +18,23 @@ const locales = {
|
|||||||
nl: {
|
nl: {
|
||||||
translation: nl,
|
translation: nl,
|
||||||
},
|
},
|
||||||
|
tr: {
|
||||||
|
translation: tr,
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
translation: fr,
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
translation: de,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
translation: zh,
|
||||||
|
},
|
||||||
|
cs: {
|
||||||
|
translation: cs,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
i18n
|
i18n
|
||||||
// detect user language
|
|
||||||
// learn more: https://github.com/i18next/i18next-browser-languageDetector
|
|
||||||
.use(LanguageDetector)
|
|
||||||
// pass the i18n instance to react-i18next.
|
// pass the i18n instance to react-i18next.
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
// init i18next
|
// init i18next
|
||||||
|
128
src/setup/locales/cs/translation.json
Normal file
128
src/setup/locales/cs/translation.json
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"name": "movie-web"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"loading_series": "Načítání Vašich oblíbených seriálů...",
|
||||||
|
"loading_movie": "Načítání Vašich oblíbených filmů...",
|
||||||
|
"loading": "Načítání...",
|
||||||
|
"allResults": "To je vše co máme!",
|
||||||
|
"noResults": "Nemohli jsme nic najít!",
|
||||||
|
"allFailed": "Nepodařilo se najít média, zkuste to znovu!",
|
||||||
|
"headingTitle": "Výsledky vyhledávání",
|
||||||
|
"bookmarks": "Záložky",
|
||||||
|
"continueWatching": "Pokračujte ve sledování",
|
||||||
|
"title": "Co si přejete sledovat?",
|
||||||
|
"placeholder": "Co si přejete sledovat?"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"movie": "Filmy",
|
||||||
|
"series": "Seriály",
|
||||||
|
"stopEditing": "Zastavit upravování",
|
||||||
|
"errors": {
|
||||||
|
"genericTitle": "Jejda, rozbilo se to!",
|
||||||
|
"failedMeta": "Nepovedlo se načíst meta",
|
||||||
|
"mediaFailed": "Nepodařilo se nám požádat o Vaše média, zkontrolujte své internetové připojení a zkuste to znovu.",
|
||||||
|
"videoFailed": "Při přehrávání požadovaného videa došlo k chybě. Pokud se tohle opakuje prosím nahlašte nám to na <0>Discord serveru</0> nebo na <1>GitHubu</1>."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seasons": {
|
||||||
|
"seasonAndEpisode": "S{{season}} E{{episode}}"
|
||||||
|
},
|
||||||
|
"notFound": {
|
||||||
|
"genericTitle": "Nenalezeno",
|
||||||
|
"backArrow": "Zpátky domů",
|
||||||
|
"media": {
|
||||||
|
"title": "Nemohli jsme najít Vaše média.",
|
||||||
|
"description": "Nemohli jsme najít média o které jste požádali. Buďto jsme ho nemohli najít, nebo jste manipulovali s URL."
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"title": "Tento poskytovatel byl zakázán",
|
||||||
|
"description": "Měli jsme s tímto poskytovatelem problémy, nebo byl moc nestabilní na používání, a tak jsme ho museli zakázat."
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"title": "Tuto stránku se nepodařilo najít",
|
||||||
|
"description": "Dívali jsme se všude: pod koši, ve skříni, za proxy, ale nakonec jsme nemohli najít stránku, kterou hledáte."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"searchBar": {
|
||||||
|
"movie": "Film",
|
||||||
|
"series": "Seriál",
|
||||||
|
"Search": "Hledání"
|
||||||
|
},
|
||||||
|
"videoPlayer": {
|
||||||
|
"findingBestVideo": "Hledáme pro Vás nejlepší video",
|
||||||
|
"noVideos": "Jejda, nemohli jsme žádné video najít",
|
||||||
|
"loading": "Načítání...",
|
||||||
|
"backToHome": "Zpátky domů",
|
||||||
|
"backToHomeShort": "Zpět",
|
||||||
|
"seasonAndEpisode": "S{{season}} E{{episode}}",
|
||||||
|
"timeLeft": "Zbývá {{timeLeft}}",
|
||||||
|
"finishAt": "Končí ve {{timeFinished, datetime}}",
|
||||||
|
"buttons": {
|
||||||
|
"episodes": "Epizody",
|
||||||
|
"source": "Zdroj",
|
||||||
|
"captions": "Titulky",
|
||||||
|
"download": "Stáhnout",
|
||||||
|
"settings": "Nastavení",
|
||||||
|
"pictureInPicture": "Obraz v obraze",
|
||||||
|
"playbackSpeed": "Rychlost přehrávání"
|
||||||
|
},
|
||||||
|
"popouts": {
|
||||||
|
"back": "Zpět",
|
||||||
|
"sources": "Zdroje",
|
||||||
|
"seasons": "Sezóny",
|
||||||
|
"captions": "Titulky",
|
||||||
|
"playbackSpeed": "Rychlost přehrávání",
|
||||||
|
"customPlaybackSpeed": "Vlastní rychlost přehrávání",
|
||||||
|
"captionPreferences": {
|
||||||
|
"title": "Upravit",
|
||||||
|
"delay": "Zpoždení",
|
||||||
|
"fontSize": "Velikost",
|
||||||
|
"opacity": "Průhlednost",
|
||||||
|
"color": "Barva"
|
||||||
|
},
|
||||||
|
"episode": "E{{index}} - {{title}}",
|
||||||
|
"noCaptions": "Žádné titulky",
|
||||||
|
"linkedCaptions": "Propojené titulky",
|
||||||
|
"customCaption": "Vlastní titulky",
|
||||||
|
"uploadCustomCaption": "Nahrát titulky",
|
||||||
|
"noEmbeds": "Nebyla nalezena žádná vložení pro tento zdroj",
|
||||||
|
|
||||||
|
"errors": {
|
||||||
|
"loadingWentWong": "Něco se nepovedlo při načítání epizod pro {{seasonTitle}}",
|
||||||
|
"embedsError": "Něco se povedlo při načítání vložení pro tuhle věc, kterou máte tak rádi"
|
||||||
|
},
|
||||||
|
"descriptions": {
|
||||||
|
"sources": "Jakého poskytovatele chcete použít?",
|
||||||
|
"embeds": "Vyberte video, které chcete sledovat",
|
||||||
|
"seasons": "Vyberte sérii, kterou chcete sledovat",
|
||||||
|
"episode": "Vyberte epizodu",
|
||||||
|
"captions": "Vyberte jazyk titulků",
|
||||||
|
"captionPreferences": "Upravte titulky tak, jak se Vám budou líbit",
|
||||||
|
"playbackSpeed": "Změňtě rychlost přehrávání"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"fatalError": "Došlo k závažné chybě v přehrávači videa, prosím nahlašte ji na <0>Discord serveru</0> nebo na <1>GitHubu</1>."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Nastavení",
|
||||||
|
"language": "Jazyk",
|
||||||
|
"captionLanguage": "Jazyk titulků"
|
||||||
|
},
|
||||||
|
"v3": {
|
||||||
|
"newSiteTitle": "Je dostupná nová verze!",
|
||||||
|
"newDomain": "https://movie-web.app",
|
||||||
|
"newDomainText": "movie-web se brzy přesune na novou doménu: <0>https://movie-web.app</0>. Nezapomeňte si aktualizovat záložky, protože <1>stará stránka přestane fungovat {{date}}.</1>",
|
||||||
|
"tireless": "Pracovali jsme neúnavně na této nové aktualizaci, a tak doufáme, že se Vám bude líbit co jsme v posledních měsících kuchtili.",
|
||||||
|
"leaveAnnouncement": "Vezměte mě tam!"
|
||||||
|
},
|
||||||
|
"casting": {
|
||||||
|
"casting": "Přehrávání do zařízení..."
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"offline": "Zkontrolujte své internetové připojení"
|
||||||
|
}
|
||||||
|
}
|
127
src/setup/locales/de/translation.json
Normal file
127
src/setup/locales/de/translation.json
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"name": "movie-web"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"loading_series": "Auf der Suche nach Ihrer Lieblingsserie...",
|
||||||
|
"loading_movie": "Auf der Suche nach Ihren Lieblingsfilmen...",
|
||||||
|
"loading": "Wird geladen...",
|
||||||
|
"allResults": "Das ist alles, was wir haben!",
|
||||||
|
"noResults": "Wir haben nichts gefunden!",
|
||||||
|
"allFailed": "Das Medium wurde nicht gefunden, bitte versuchen Sie es erneut!",
|
||||||
|
"headingTitle": "Suchergebnisse",
|
||||||
|
"bookmarks": "Favoriten",
|
||||||
|
"continueWatching": "Weiter ansehen",
|
||||||
|
"title": "Was willst du sehen?",
|
||||||
|
"placeholder": "Was willst du sehen?"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"movie": "Filme",
|
||||||
|
"series": "Serie",
|
||||||
|
"stopEditing": "Beenden Sie die Bearbeitung",
|
||||||
|
"errors": {
|
||||||
|
"genericTitle": "Hoppla, etwas ist falsch gegangen!",
|
||||||
|
"failedMeta": "Metadaten konnten nicht geladen werden",
|
||||||
|
"mediaFailed": "Wir konnten die angeforderten Medien nicht abrufen.",
|
||||||
|
"videoFailed": "Beim Abspielen des angeforderten Videos ist ein Fehler aufgetreten. <0>Discord</0> Oder weiter <1>GitHub</1>."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seasons": {
|
||||||
|
"seasonAndEpisode": "S{{season}} E{{episode}}"
|
||||||
|
},
|
||||||
|
"notFound": {
|
||||||
|
"genericTitle": "Nicht gefunden",
|
||||||
|
"backArrow": "Zurück zur Startseite",
|
||||||
|
"media": {
|
||||||
|
"title": "Das Medium konnte nicht gefunden werden",
|
||||||
|
"description": "Wir konnten die angeforderten Medien nicht finden."
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"title": "Dieser Anbieter wurde deaktiviert",
|
||||||
|
"description": "Wir hatten Probleme mit dem Anbieter oder er war zu instabil, sodass wir ihn deaktivieren mussten."
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"title": "Diese Seite kann nicht gefunden werden",
|
||||||
|
"description": "Wir haben überall gesucht, aber am Ende konnten wir die gesuchte Seite nicht finden."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"searchBar": {
|
||||||
|
"movie": "Film",
|
||||||
|
"series": "Serie",
|
||||||
|
"Search": "Forschen"
|
||||||
|
},
|
||||||
|
"videoPlayer": {
|
||||||
|
"findingBestVideo": "Auf der Suche nach dem besten Video für Sie",
|
||||||
|
"noVideos": "Entschuldigung, wir konnten keine Videos für Sie finden",
|
||||||
|
"loading": "Wird geladen...",
|
||||||
|
"backToHome": "Zurück zur Startseite",
|
||||||
|
"backToHomeShort": "Rückmeldung",
|
||||||
|
"seasonAndEpisode": "S{{season}} E{{episode}}",
|
||||||
|
"timeLeft": "{{timeLeft}} bleibt",
|
||||||
|
"finishAt": "Ende um {{timeFinished, datetime}}",
|
||||||
|
"buttons": {
|
||||||
|
"episodes": "Folgen",
|
||||||
|
"source": "Quelle",
|
||||||
|
"captions": "Untertitel",
|
||||||
|
"download": "Herunterladen",
|
||||||
|
"settings": "Einstellungen",
|
||||||
|
"pictureInPicture": "Bild-im-Bild",
|
||||||
|
"playbackSpeed": "Wiedergabegeschwindigkeit"
|
||||||
|
},
|
||||||
|
"popouts": {
|
||||||
|
"back": "Zurück",
|
||||||
|
"sources": "Quellen",
|
||||||
|
"seasons": "Saison",
|
||||||
|
"captions": "Untertitel",
|
||||||
|
"playbackSpeed": "Lesegeschwindigkeit",
|
||||||
|
"customPlaybackSpeed": "Benutzerdefinierte Wiedergabegeschwindigkeit",
|
||||||
|
"captionPreferences": {
|
||||||
|
"title": "Personifizieren",
|
||||||
|
"delay": "Zeitlimit",
|
||||||
|
"fontSize": "Größe",
|
||||||
|
"opacity": "Opazität",
|
||||||
|
"color": "Farbe"
|
||||||
|
},
|
||||||
|
"episode": "E{{index}} - {{title}}",
|
||||||
|
"noCaptions": "Keine Untertitel",
|
||||||
|
"linkedCaptions": "Verbundene Untertitel",
|
||||||
|
"customCaption": "Benutzerdefinierte Untertitel",
|
||||||
|
"uploadCustomCaption": "Untertitel hochladen",
|
||||||
|
"noEmbeds": "Für diese Quelle wurde kein eingebetteter Inhalt gefunden",
|
||||||
|
"errors": {
|
||||||
|
"loadingWentWong": "Beim Laden der Folgen für {{seasonTitle}} ist ein Problem aufgetreten ",
|
||||||
|
"embedsError": "Beim Laden der eingebetteter Medien ist ein Problem aufgetreten"
|
||||||
|
},
|
||||||
|
"descriptions": {
|
||||||
|
"sources": "Welchen Anbieter möchten Sie nutzen?",
|
||||||
|
"embeds": "Wählen Sie das Video aus, das Sie ansehen möchten",
|
||||||
|
"seasons": "Wählen Sie die Staffel aus, die Sie sehen möchten",
|
||||||
|
"episode": "Wählen Sie eine Folge aus",
|
||||||
|
"captions": "Wählen Sie eine Untertitelsprache",
|
||||||
|
"captionPreferences": "Passen Sie das Erscheinungsbild von Untertiteln an",
|
||||||
|
"playbackSpeed": "Wiedergabegeschwindigkeit ändern"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"fatalError": "Der Videoplayer hat einen Fehler festgestellt, bitte melden Sie ihn dem Server <0>Discord</0> Oder weiter <1>GitHub</1>."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Einstellungen",
|
||||||
|
"language": "Sprache",
|
||||||
|
"captionLanguage": "Untertitelsprache"
|
||||||
|
},
|
||||||
|
"v3": {
|
||||||
|
"newSiteTitle": "Neue Version verfügbar!",
|
||||||
|
"newDomain": "https://movie-web.app",
|
||||||
|
"newDomainText": "movie-web zieht in Kürze auf eine neue Domain um: <0>https://movie-web.app</0>. <1>Die alte Website funktioniert nicht mehr {{date}}.</1>",
|
||||||
|
"tireless": "Wir haben unermüdlich an diesem neuen Update gearbeitet und hoffen, dass Ihnen das gefällt, was wir in den letzten Monaten vorbereitet haben.",
|
||||||
|
"leaveAnnouncement": "Bring mich dahin!"
|
||||||
|
},
|
||||||
|
"casting": {
|
||||||
|
"casting": "An Gerät übertragen..."
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"offline": "Ihre Internetverbindung ist instabil"
|
||||||
|
}
|
||||||
|
}
|
@@ -58,7 +58,7 @@
|
|||||||
"backToHomeShort": "Back",
|
"backToHomeShort": "Back",
|
||||||
"seasonAndEpisode": "S{{season}} E{{episode}}",
|
"seasonAndEpisode": "S{{season}} E{{episode}}",
|
||||||
"timeLeft": "{{timeLeft}} left",
|
"timeLeft": "{{timeLeft}} left",
|
||||||
"finishAt": "Finish at {{timeFinished}}",
|
"finishAt": "Finish at {{timeFinished, datetime}}",
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"episodes": "Episodes",
|
"episodes": "Episodes",
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
|
@@ -16,16 +16,34 @@
|
|||||||
"placeholder": "Que voulez-vous voir?"
|
"placeholder": "Que voulez-vous voir?"
|
||||||
},
|
},
|
||||||
"media": {
|
"media": {
|
||||||
"title": "Impossible de trouver ce média",
|
"movie": "Films",
|
||||||
"description": "Nous n'avons pas pu trouver le média que vous avez demandé. Soit il a été supprimé, soit vous avez altéré l'URL."
|
"series": "Séries",
|
||||||
|
"stopEditing": "Arrêter l'édition",
|
||||||
|
"errors": {
|
||||||
|
"genericTitle": "Oups, c'est coupé !",
|
||||||
|
"failedMeta": "Impossible de charger les métadonnées",
|
||||||
|
"mediaFailed": "Nous n'avons pas réussi à récupérer le média que vous avez demandé. Veuillez vérifier votre connexion Internet et réessayer.",
|
||||||
|
"videoFailed": "Nous avons rencontré une erreur lors de la lecture de la vidéo que vous avez demandée. Si cela se reproduit, veuillez signaler le problème au serveur <0>Discord</0> ou sur <1>GitHub</1>."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"provider": {
|
"seasons": {
|
||||||
"title": "Ce fournisseur a été désactivé",
|
"seasonAndEpisode": "S{{season}} E{{episode}}"
|
||||||
"description": "Nous avons eu des problèmes avec le fournisseur ou bien il était trop instable pour être utilisé, donc nous avons dû le désactiver."
|
|
||||||
},
|
},
|
||||||
"page": {
|
"notFound": {
|
||||||
"title": "Impossible de trouver cette page",
|
"genericTitle": "Introuvable",
|
||||||
"description": "Nous avons cherché partout : sous les poubelles, dans le placard, derrière le proxy, mais nous n'avons finalement pas pu trouver la page que vous recherchez."
|
"backArrow": "Retour à l'accueil",
|
||||||
|
"media": {
|
||||||
|
"title": "Impossible de trouver ce média",
|
||||||
|
"description": "Nous n'avons pas trouvé le média que vous avez demandé. Soit il a été supprimé, soit vous avez modifié l'URL."
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"title": "Ce fournisseur a été désactivé",
|
||||||
|
"description": "Nous avons eu des problèmes avec le fournisseur ou il était trop instable pour être utilisé, nous avons donc dû le désactiver."
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"title": "Impossible de trouver cette page",
|
||||||
|
"description": "Nous avons cherché partout : sous les poubelles, dans le placard, derrière le proxy, mais nous n'avons finalement pas trouvé la page que vous cherchez."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"searchBar": {
|
"searchBar": {
|
||||||
"movie": "Film",
|
"movie": "Film",
|
||||||
@@ -40,7 +58,7 @@
|
|||||||
"backToHomeShort": "Retour",
|
"backToHomeShort": "Retour",
|
||||||
"seasonAndEpisode": "S{{season}} E{{episode}}",
|
"seasonAndEpisode": "S{{season}} E{{episode}}",
|
||||||
"timeLeft": "{{timeLeft}} restant",
|
"timeLeft": "{{timeLeft}} restant",
|
||||||
"finishAt": "Terminer à {{timeFinished}}",
|
"finishAt": "Terminer à {{timeFinished, datetime}}",
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"episodes": "Épisodes",
|
"episodes": "Épisodes",
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
@@ -51,9 +69,12 @@
|
|||||||
"playbackSpeed": "Vitesse"
|
"playbackSpeed": "Vitesse"
|
||||||
},
|
},
|
||||||
"popouts": {
|
"popouts": {
|
||||||
|
"back": "Retourner",
|
||||||
"sources": "Sources",
|
"sources": "Sources",
|
||||||
"seasons": "Saisons",
|
"seasons": "Saisons",
|
||||||
"captions": "Sous-titres",
|
"captions": "Sous-titres",
|
||||||
|
"playbackSpeed": "Vitesse de lecture",
|
||||||
|
"customPlaybackSpeed": "Vitesse de lecture personnalisée",
|
||||||
"captionPreferences": {
|
"captionPreferences": {
|
||||||
"title": "Personnaliser",
|
"title": "Personnaliser",
|
||||||
"delay": "Délai",
|
"delay": "Délai",
|
||||||
@@ -77,13 +98,19 @@
|
|||||||
"seasons": "Choisissez la saison que vous voulez regarder",
|
"seasons": "Choisissez la saison que vous voulez regarder",
|
||||||
"episode": "Sélectionnez un épisode",
|
"episode": "Sélectionnez un épisode",
|
||||||
"captions": "Choisissez une langue de sous-titres",
|
"captions": "Choisissez une langue de sous-titres",
|
||||||
"captionPreferences": "Personnalisez l'apparence des sous-titres"
|
"captionPreferences": "Personnalisez l'apparence des sous-titres",
|
||||||
|
"playbackSpeed": "Changer la vitesse de lecture"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"fatalError": "Le lecteur vidéo a rencontré une erreur fatale, veuillez la signaler au serveur <0>Discord</0> ou sur <1>GitHub</1>."
|
"fatalError": "Le lecteur vidéo a rencontré une erreur fatale, veuillez la signaler au serveur <0>Discord</0> ou sur <1>GitHub</1>."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Paramètres",
|
||||||
|
"language": "Language",
|
||||||
|
"captionLanguage": "Langue des sous-titres"
|
||||||
|
},
|
||||||
"v3": {
|
"v3": {
|
||||||
"newSiteTitle": "Nouvelle version disponible!",
|
"newSiteTitle": "Nouvelle version disponible!",
|
||||||
"newDomain": "https://movie-web.app",
|
"newDomain": "https://movie-web.app",
|
||||||
|
@@ -58,7 +58,7 @@
|
|||||||
"backToHomeShort": "Terug",
|
"backToHomeShort": "Terug",
|
||||||
"seasonAndEpisode": "S{{season}} A{{episode}}",
|
"seasonAndEpisode": "S{{season}} A{{episode}}",
|
||||||
"timeLeft": "Nog {{timeLeft}}",
|
"timeLeft": "Nog {{timeLeft}}",
|
||||||
"finishAt": "Afgelopen om {{timeFinished}}",
|
"finishAt": "Afgelopen om {{timeFinished, datetime}}",
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"episodes": "Afleveringen",
|
"episodes": "Afleveringen",
|
||||||
"source": "Bron",
|
"source": "Bron",
|
||||||
|
128
src/setup/locales/tr/translation.json
Normal file
128
src/setup/locales/tr/translation.json
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"name": "movie-web"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"loading_series": "Favori dizileriniz aranıyor...",
|
||||||
|
"loading_movie": "Favori filmleriniz aranıyor...",
|
||||||
|
"loading": "Yükleniyor...",
|
||||||
|
"allResults": "Bu kadarını bulabildik!",
|
||||||
|
"noResults": "Hiçbir şey bulamadık!",
|
||||||
|
"allFailed": "Medya bulunamadı, tekrar deneyin!",
|
||||||
|
"headingTitle": "Arama sonuçları",
|
||||||
|
"bookmarks": "Yerimleri",
|
||||||
|
"continueWatching": "İzlemeye devam edin",
|
||||||
|
"title": "Ne izlemek istersiniz?",
|
||||||
|
"placeholder": "Ne izlemek istersiniz?"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"movie": "Film",
|
||||||
|
"series": "Dizi",
|
||||||
|
"stopEditing": "Düzenlemeyi durdur",
|
||||||
|
"errors": {
|
||||||
|
"genericTitle": "Hay aksi, bozuldu!",
|
||||||
|
"failedMeta": "Önbilgi yüklenemedi",
|
||||||
|
"mediaFailed": "İstediğiniz medyaya istek atarken hata oluştu, internet bağlantınızı kontrol edin ve tekrar deneyin.",
|
||||||
|
"videoFailed": "İstediğiniz videoyu oynatırken bir sorunla karşılaştık. Bu durum devam ederse lütfen bunu <0>Discord sunucumuza</0> veya <1>GitHub</1> üzerinden bildiriniz."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seasons": {
|
||||||
|
"seasonAndEpisode": "S{{season}} B{{episode}}"
|
||||||
|
},
|
||||||
|
"notFound": {
|
||||||
|
"genericTitle": "Bulunamadı",
|
||||||
|
"backArrow": "Geri",
|
||||||
|
"media": {
|
||||||
|
"title": "Medya bulunamadı",
|
||||||
|
"description": "İstediğiniz medyayı bulamadık. URL'i yanlış girdiniz ya da medya kaldırıldı."
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"title": "Bu sağlayıcı devre dışı bırakıldı",
|
||||||
|
"description": "Sağlayıcı ile ilgili bir sorun oluştu ya da kullanılacak kadar stabil değildi bu yüzden devre dışı bırakmak zorunda kaldık."
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"title": "Sayfa bulunamadı",
|
||||||
|
"description": "Her yere baktık: bazanın altına, dolabın içine hatta ara sunucuya ama maalesef aradığınız sayfayı bulamadık."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"searchBar": {
|
||||||
|
"movie": "Film",
|
||||||
|
"series": "Dizi",
|
||||||
|
"Search": "Ara"
|
||||||
|
},
|
||||||
|
"videoPlayer": {
|
||||||
|
"findingBestVideo": "Sizin için en iyi videoyu buluyoruz...",
|
||||||
|
"noVideos": "Hay aksi, hiçbir video bulamadık",
|
||||||
|
"loading": "Yükleniyor...",
|
||||||
|
"backToHome": "Ana sayfaya dön",
|
||||||
|
"backToHomeShort": "Geri",
|
||||||
|
"seasonAndEpisode": "S{{season}} B{{episode}}",
|
||||||
|
"timeLeft": "{{timeLeft}} kaldı",
|
||||||
|
"finishAt": "{{timeFinished, datetime}}'de/da bitiyor",
|
||||||
|
"buttons": {
|
||||||
|
"episodes": "Bölümler",
|
||||||
|
"source": "Kaynak",
|
||||||
|
"captions": "Altyazılar",
|
||||||
|
"download": "İndir",
|
||||||
|
"settings": "Ayarlar",
|
||||||
|
"pictureInPicture": "Resim içinde Resim",
|
||||||
|
"playbackSpeed": "Oynatma Hızı"
|
||||||
|
},
|
||||||
|
"popouts": {
|
||||||
|
"back": "Geri git",
|
||||||
|
"sources": "Kaynaklar",
|
||||||
|
"seasons": "Sezonlar",
|
||||||
|
"captions": "Altyazılar",
|
||||||
|
"playbackSpeed": "Oynatma hızı",
|
||||||
|
"customPlaybackSpeed": "Özel oynatma hızı",
|
||||||
|
"captionPreferences": {
|
||||||
|
"title": "Kişiselleştirme",
|
||||||
|
"delay": "Gecikme",
|
||||||
|
"fontSize": "Boyut",
|
||||||
|
"opacity": "Opaklık",
|
||||||
|
"color": "Renk"
|
||||||
|
},
|
||||||
|
"episode": "B{{index}} - {{title}}",
|
||||||
|
"noCaptions": "Altyazı yok",
|
||||||
|
"linkedCaptions": "Kaynak Altyazıları",
|
||||||
|
"customCaption": "Özel altyazı",
|
||||||
|
"uploadCustomCaption": "Altyazı yükle",
|
||||||
|
"noEmbeds": "Bu kaynak için gömülü video bulunamadı",
|
||||||
|
|
||||||
|
"errors": {
|
||||||
|
"loadingWentWong": "{{seasonTitle}} için bölümler yüklenirken bir hata oluştu",
|
||||||
|
"embedsError": "İstediğiniz şey için gömülü video bulunurken bir hata oluştu"
|
||||||
|
},
|
||||||
|
"descriptions": {
|
||||||
|
"sources": "Hangi sağlayıcıyı kullanmak istersiniz?",
|
||||||
|
"embeds": "Görüntülemek istediğiniz videoyu seçiniz",
|
||||||
|
"seasons": "İzlemek istediğiniz sezonu seçiniz",
|
||||||
|
"episode": "Bir bölüm seçiniz",
|
||||||
|
"captions": "Altyazı dili seçiniz",
|
||||||
|
"captionPreferences": "Altyazıları istediğiniz gibi ayarlayın",
|
||||||
|
"playbackSpeed": "Oynatma hızınızı değiştirin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"fatalError": "Video oynatıcıda bir hata oluştu, lütfen bunu <0>Discord sunucumuzda</0> ya da <1>GitHub</1> üzeriden bildiriniz."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Ayarlar",
|
||||||
|
"language": "Dil",
|
||||||
|
"captionLanguage": "Altyazı Dili"
|
||||||
|
},
|
||||||
|
"v3": {
|
||||||
|
"newSiteTitle": "Yeni sürüm yayınlandı!",
|
||||||
|
"newDomain": "https://movie-web.app",
|
||||||
|
"newDomainText": "movie-web yakında yeni bir alan adına taşınacak: <0>https://movie-web.app</0>. <1>{{date}} tarihinde eski site çalışmayacağı için</1> yerimlerinizi güncellemeyi unutmayın.",
|
||||||
|
"tireless": "Bu yeni güncelleme için gece gündüz çalıştık, umarız aylardan beri hazırladığımız bu güncellemeyi beğenirsiniz.",
|
||||||
|
"leaveAnnouncement": "Götür beni!"
|
||||||
|
},
|
||||||
|
"casting": {
|
||||||
|
"casting": "Cihaza aktarılıyor..."
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"offline": "İnternet bağlantınızı kontrol ediniz"
|
||||||
|
}
|
||||||
|
}
|
127
src/setup/locales/zh/translation.json
Normal file
127
src/setup/locales/zh/translation.json
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"name": "movie-web"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"loading_series": "正在获取您最喜欢的连续剧……",
|
||||||
|
"loading_movie": "正在获取您最喜欢的影片……",
|
||||||
|
"loading": "载入中……",
|
||||||
|
"allResults": "以上是我们能找到的所有结果!",
|
||||||
|
"noResults": "我们找不到任何结果!",
|
||||||
|
"allFailed": "查找媒体失败,请重试!",
|
||||||
|
"headingTitle": "搜索结果",
|
||||||
|
"bookmarks": "书签",
|
||||||
|
"continueWatching": "继续观看",
|
||||||
|
"title": "您想看些什么?",
|
||||||
|
"placeholder": "您想看些什么?"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"movie": "电影",
|
||||||
|
"series": "连续剧",
|
||||||
|
"stopEditing": "退出编辑",
|
||||||
|
"errors": {
|
||||||
|
"genericTitle": "哎呀,出问题了!",
|
||||||
|
"failedMeta": "加载元数据失败",
|
||||||
|
"mediaFailed": "我们未能请求到您要求的媒体,检查互联网连接并重试。",
|
||||||
|
"videoFailed": "我们在播放您要求的视频时遇到了错误。如果错误持续发生,请向 <0>Discord 服务器</0>或 <1>GitHub</1> 提交问题报告。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seasons": {
|
||||||
|
"seasonAndEpisode": "第{{season}}季 第{{episode}}集"
|
||||||
|
},
|
||||||
|
"notFound": {
|
||||||
|
"genericTitle": "未找到",
|
||||||
|
"backArrow": "返回首页",
|
||||||
|
"media": {
|
||||||
|
"title": "无法找到媒体",
|
||||||
|
"description": "我们无法找到您请求的媒体。它可能已被删除,或您篡改了 URL"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"title": "该内容提供者已被停用",
|
||||||
|
"description": "我们的提供者出现问题,或是太不稳定,导致无法使用,所以我们不得不将其停用。"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"title": "无法找到页面",
|
||||||
|
"description": "我们已经到处找过了:不管是垃圾桶下、橱柜里或是代理之后。但最终并没有发现您查找的页面。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"searchBar": {
|
||||||
|
"movie": "电影",
|
||||||
|
"series": "连续剧",
|
||||||
|
"Search": "搜索"
|
||||||
|
},
|
||||||
|
"videoPlayer": {
|
||||||
|
"findingBestVideo": "正在为您探测最佳视频",
|
||||||
|
"noVideos": "哎呀,无法为您找到任何视频",
|
||||||
|
"loading": "载入中……",
|
||||||
|
"backToHome": "返回首页",
|
||||||
|
"backToHomeShort": "返回",
|
||||||
|
"seasonAndEpisode": "第{{season}}季 第{{episode}}集",
|
||||||
|
"timeLeft": "还剩余 {{timeLeft}}",
|
||||||
|
"finishAt": "在 {{timeFinished, datetime}} 结束",
|
||||||
|
"buttons": {
|
||||||
|
"episodes": "分集",
|
||||||
|
"source": "视频源",
|
||||||
|
"captions": "字幕",
|
||||||
|
"download": "下载",
|
||||||
|
"settings": "设置",
|
||||||
|
"pictureInPicture": "画中画",
|
||||||
|
"playbackSpeed": "播放速度"
|
||||||
|
},
|
||||||
|
"popouts": {
|
||||||
|
"back": "返回",
|
||||||
|
"sources": "视频源",
|
||||||
|
"seasons": "分季",
|
||||||
|
"captions": "字幕",
|
||||||
|
"playbackSpeed": "播放速度",
|
||||||
|
"customPlaybackSpeed": "自定义播放速度",
|
||||||
|
"captionPreferences": {
|
||||||
|
"title": "自定义",
|
||||||
|
"delay": "延迟",
|
||||||
|
"fontSize": "尺寸",
|
||||||
|
"opacity": "透明度",
|
||||||
|
"color": "颜色"
|
||||||
|
},
|
||||||
|
"episode": "第{{index}}集 - {{title}}",
|
||||||
|
"noCaptions": "没有字幕",
|
||||||
|
"linkedCaptions": "已链接字幕",
|
||||||
|
"customCaption": "自定义字幕",
|
||||||
|
"uploadCustomCaption": "上传字幕",
|
||||||
|
"noEmbeds": "未发现该视频源的嵌入内容",
|
||||||
|
"errors": {
|
||||||
|
"loadingWentWong": "加载 {{seasonTitle}} 的分集时出现了一些问题",
|
||||||
|
"embedsError": "为您喜欢的这一东西加载嵌入内容时出现了一些问题"
|
||||||
|
},
|
||||||
|
"descriptions": {
|
||||||
|
"sources": "您想使用哪个内容提供者?",
|
||||||
|
"embeds": "选择要观看的视频",
|
||||||
|
"seasons": "选择您要观看的季",
|
||||||
|
"episode": "选择一个分集",
|
||||||
|
"captions": "选择字幕语言",
|
||||||
|
"captionPreferences": "让字幕看起来如您所想",
|
||||||
|
"playbackSpeed": "改变播放速度"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"fatalError": "视频播放器遇到致命错误,请向 <0>Discord 服务器</0>或 <1>GitHub</1> 报告。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "设置",
|
||||||
|
"language": "语言",
|
||||||
|
"captionLanguage": "字幕语言"
|
||||||
|
},
|
||||||
|
"v3": {
|
||||||
|
"newSiteTitle": "新的版本现已发布!",
|
||||||
|
"newDomain": "https://movie-web.app",
|
||||||
|
"newDomainText": "movie-web 将很快转移到新的域名:<0>https://movie-web.app</0>。请确保已经更新全部书签链接,<1>旧网站将于 {{date}} 停止工作。</1>",
|
||||||
|
"tireless": "为了这一新版本,我们不懈努力,希望您会喜欢我们在过去几个月中所做的一切。",
|
||||||
|
"leaveAnnouncement": "请带我去!"
|
||||||
|
},
|
||||||
|
"casting": {
|
||||||
|
"casting": "正在投射到设备……"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"offline": "检查您的互联网连接"
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,8 @@
|
|||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import { CaptureConsole, HttpClient } from "@sentry/integrations";
|
import { CaptureConsole, HttpClient } from "@sentry/integrations";
|
||||||
import { SENTRY_DSN } from "@/setup/constants";
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
import { SENTRY_DSN } from "@/setup/constants";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: SENTRY_DSN,
|
dsn: SENTRY_DSN,
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
|
import { ReactNode, createContext, useContext, useMemo } from "react";
|
||||||
|
|
||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
import { useStore } from "@/utils/storage";
|
import { useStore } from "@/utils/storage";
|
||||||
import { createContext, ReactNode, useContext, useMemo } from "react";
|
|
||||||
import { BookmarkStore } from "./store";
|
import { BookmarkStore } from "./store";
|
||||||
import { BookmarkStoreData } from "./types";
|
import { BookmarkStoreData } from "./types";
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { createVersionedStore } from "@/utils/storage";
|
import { createVersionedStore } from "@/utils/storage";
|
||||||
import { migrateV1Bookmarks, OldBookmarks } from "../watched/migrations/v2";
|
|
||||||
import { BookmarkStoreData } from "./types";
|
import { BookmarkStoreData } from "./types";
|
||||||
|
import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2";
|
||||||
|
|
||||||
export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
|
export const BookmarkStore = createVersionedStore<BookmarkStoreData>()
|
||||||
.setKey("mw-bookmarks")
|
.setKey("mw-bookmarks")
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { useStore } from "@/utils/storage";
|
import { ReactNode, createContext, useContext, useMemo } from "react";
|
||||||
import { createContext, ReactNode, useContext, useMemo } from "react";
|
|
||||||
import { LangCode } from "@/setup/iso6391";
|
import { LangCode } from "@/setup/iso6391";
|
||||||
|
import { useStore } from "@/utils/storage";
|
||||||
|
|
||||||
import { SettingsStore } from "./store";
|
import { SettingsStore } from "./store";
|
||||||
import { MWSettingsData } from "./types";
|
import { MWSettingsData } from "./types";
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { createVersionedStore } from "@/utils/storage";
|
import { createVersionedStore } from "@/utils/storage";
|
||||||
|
|
||||||
import { MWSettingsData, MWSettingsDataV1 } from "./types";
|
import { MWSettingsData, MWSettingsDataV1 } from "./types";
|
||||||
|
|
||||||
export const SettingsStore = createVersionedStore<MWSettingsData>()
|
export const SettingsStore = createVersionedStore<MWSettingsData>()
|
||||||
|
@@ -1,16 +1,18 @@
|
|||||||
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
|
||||||
import { useStore } from "@/utils/storage";
|
|
||||||
import {
|
import {
|
||||||
createContext,
|
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
|
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
||||||
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
|
import { useStore } from "@/utils/storage";
|
||||||
|
|
||||||
import { VideoProgressStore } from "./store";
|
import { VideoProgressStore } from "./store";
|
||||||
import { StoreMediaItem, WatchedStoreItem, WatchedStoreData } from "./types";
|
import { StoreMediaItem, WatchedStoreData, WatchedStoreItem } from "./types";
|
||||||
|
|
||||||
const FIVETEEN_MINUTES = 15 * 60;
|
const FIVETEEN_MINUTES = 15 * 60;
|
||||||
const FIVE_MINUTES = 5 * 60;
|
const FIVE_MINUTES = 5 * 60;
|
||||||
|
@@ -2,6 +2,7 @@ import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
|||||||
import { searchForMedia } from "@/backend/metadata/search";
|
import { searchForMedia } from "@/backend/metadata/search";
|
||||||
import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types";
|
||||||
import { compareTitle } from "@/utils/titleMatch";
|
import { compareTitle } from "@/utils/titleMatch";
|
||||||
|
|
||||||
import { WatchedStoreData, WatchedStoreItem } from "../types";
|
import { WatchedStoreData, WatchedStoreItem } from "../types";
|
||||||
|
|
||||||
interface OldMediaBase {
|
interface OldMediaBase {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { createVersionedStore } from "@/utils/storage";
|
import { createVersionedStore } from "@/utils/storage";
|
||||||
import { migrateV2Videos, OldData } from "./migrations/v2";
|
|
||||||
|
import { OldData, migrateV2Videos } from "./migrations/v2";
|
||||||
import { WatchedStoreData } from "./types";
|
import { WatchedStoreData } from "./types";
|
||||||
|
|
||||||
export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
|
export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
|
||||||
|
@@ -1,36 +1,38 @@
|
|||||||
|
import { ReactNode, useCallback, useState } from "react";
|
||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { AirplayAction } from "@/video/components/actions/AirplayAction";
|
import { AirplayAction } from "@/video/components/actions/AirplayAction";
|
||||||
import { BackdropAction } from "@/video/components/actions/BackdropAction";
|
import { BackdropAction } from "@/video/components/actions/BackdropAction";
|
||||||
|
import { CastingTextAction } from "@/video/components/actions/CastingTextAction";
|
||||||
|
import { ChromecastAction } from "@/video/components/actions/ChromecastAction";
|
||||||
import { FullscreenAction } from "@/video/components/actions/FullscreenAction";
|
import { FullscreenAction } from "@/video/components/actions/FullscreenAction";
|
||||||
import { HeaderAction } from "@/video/components/actions/HeaderAction";
|
import { HeaderAction } from "@/video/components/actions/HeaderAction";
|
||||||
|
import { KeyboardShortcutsAction } from "@/video/components/actions/KeyboardShortcutsAction";
|
||||||
import { LoadingAction } from "@/video/components/actions/LoadingAction";
|
import { LoadingAction } from "@/video/components/actions/LoadingAction";
|
||||||
import { MiddlePauseAction } from "@/video/components/actions/MiddlePauseAction";
|
import { MiddlePauseAction } from "@/video/components/actions/MiddlePauseAction";
|
||||||
import { MobileCenterAction } from "@/video/components/actions/MobileCenterAction";
|
import { MobileCenterAction } from "@/video/components/actions/MobileCenterAction";
|
||||||
import { PageTitleAction } from "@/video/components/actions/PageTitleAction";
|
import { PageTitleAction } from "@/video/components/actions/PageTitleAction";
|
||||||
import { PauseAction } from "@/video/components/actions/PauseAction";
|
import { PauseAction } from "@/video/components/actions/PauseAction";
|
||||||
|
import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction";
|
||||||
import { ProgressAction } from "@/video/components/actions/ProgressAction";
|
import { ProgressAction } from "@/video/components/actions/ProgressAction";
|
||||||
import { SeriesSelectionAction } from "@/video/components/actions/SeriesSelectionAction";
|
import { SeriesSelectionAction } from "@/video/components/actions/SeriesSelectionAction";
|
||||||
import { ShowTitleAction } from "@/video/components/actions/ShowTitleAction";
|
import { ShowTitleAction } from "@/video/components/actions/ShowTitleAction";
|
||||||
import { KeyboardShortcutsAction } from "@/video/components/actions/KeyboardShortcutsAction";
|
|
||||||
import { SkipTimeAction } from "@/video/components/actions/SkipTimeAction";
|
import { SkipTimeAction } from "@/video/components/actions/SkipTimeAction";
|
||||||
import { TimeAction } from "@/video/components/actions/TimeAction";
|
import { TimeAction } from "@/video/components/actions/TimeAction";
|
||||||
import { VolumeAction } from "@/video/components/actions/VolumeAction";
|
import { VolumeAction } from "@/video/components/actions/VolumeAction";
|
||||||
import { VideoPlayerError } from "@/video/components/parts/VideoPlayerError";
|
import { VideoPlayerError } from "@/video/components/parts/VideoPlayerError";
|
||||||
|
import { PopoutProviderAction } from "@/video/components/popouts/PopoutProviderAction";
|
||||||
import {
|
import {
|
||||||
VideoPlayerBase,
|
VideoPlayerBase,
|
||||||
VideoPlayerBaseProps,
|
VideoPlayerBaseProps,
|
||||||
} from "@/video/components/VideoPlayerBase";
|
} from "@/video/components/VideoPlayerBase";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { ReactNode, useCallback, useState } from "react";
|
|
||||||
import { PopoutProviderAction } from "@/video/components/popouts/PopoutProviderAction";
|
|
||||||
import { ChromecastAction } from "@/video/components/actions/ChromecastAction";
|
|
||||||
import { CastingTextAction } from "@/video/components/actions/CastingTextAction";
|
|
||||||
import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction";
|
|
||||||
import { CaptionRendererAction } from "./actions/CaptionRendererAction";
|
import { CaptionRendererAction } from "./actions/CaptionRendererAction";
|
||||||
import { SettingsAction } from "./actions/SettingsAction";
|
|
||||||
import { DividerAction } from "./actions/DividerAction";
|
import { DividerAction } from "./actions/DividerAction";
|
||||||
|
import { SettingsAction } from "./actions/SettingsAction";
|
||||||
import { VolumeAdjustedAction } from "./actions/VolumeAdjustedAction";
|
import { VolumeAdjustedAction } from "./actions/VolumeAdjustedAction";
|
||||||
|
|
||||||
type Props = VideoPlayerBaseProps;
|
type Props = VideoPlayerBaseProps;
|
||||||
@@ -118,7 +120,7 @@ export function VideoPlayer(props: Props) {
|
|||||||
<Transition
|
<Transition
|
||||||
animation="slide-down"
|
animation="slide-down"
|
||||||
show={show}
|
show={show}
|
||||||
className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2"
|
className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col px-8 py-6 pb-2"
|
||||||
>
|
>
|
||||||
<HeaderAction
|
<HeaderAction
|
||||||
showControls={isMobile}
|
showControls={isMobile}
|
||||||
|
@@ -1,15 +1,17 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
import { CastingInternal } from "@/video/components/internal/CastingInternal";
|
import { CastingInternal } from "@/video/components/internal/CastingInternal";
|
||||||
import { WrapperRegisterInternal } from "@/video/components/internal/WrapperRegisterInternal";
|
import { WrapperRegisterInternal } from "@/video/components/internal/WrapperRegisterInternal";
|
||||||
import { VideoErrorBoundary } from "@/video/components/parts/VideoErrorBoundary";
|
import { VideoErrorBoundary } from "@/video/components/parts/VideoErrorBoundary";
|
||||||
import { useInterface } from "@/video/state/logic/interface";
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useRef } from "react";
|
|
||||||
import {
|
|
||||||
useVideoPlayerDescriptor,
|
|
||||||
VideoPlayerContextProvider,
|
|
||||||
} from "../state/hooks";
|
|
||||||
import { MetaAction } from "./actions/MetaAction";
|
import { MetaAction } from "./actions/MetaAction";
|
||||||
import { VideoElementInternal } from "./internal/VideoElementInternal";
|
import { VideoElementInternal } from "./internal/VideoElementInternal";
|
||||||
|
import {
|
||||||
|
VideoPlayerContextProvider,
|
||||||
|
useVideoPlayerDescriptor,
|
||||||
|
} from "../state/hooks";
|
||||||
|
|
||||||
export interface VideoPlayerBaseProps {
|
export interface VideoPlayerBaseProps {
|
||||||
children?:
|
children?:
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useMisc } from "@/video/state/logic/misc";
|
import { useMisc } from "@/video/state/logic/misc";
|
||||||
import { useCallback } from "react";
|
|
||||||
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useInterface } from "@/video/state/logic/interface";
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
interface BackdropActionProps {
|
interface BackdropActionProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import { useAsync } from "react-use";
|
||||||
|
import { ContentCaption } from "subsrt-ts/dist/types/handler";
|
||||||
|
|
||||||
|
import { parseSubtitles, sanitize } from "@/backend/helpers/captions";
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
import { useSettings } from "@/state/settings";
|
import { useSettings } from "@/state/settings";
|
||||||
import { sanitize, parseSubtitles } from "@/backend/helpers/captions";
|
|
||||||
import { ContentCaption } from "subsrt-ts/dist/types/handler";
|
|
||||||
import { useRef } from "react";
|
|
||||||
import { useAsync } from "react-use";
|
|
||||||
import { useVideoPlayerDescriptor } from "../../state/hooks";
|
import { useVideoPlayerDescriptor } from "../../state/hooks";
|
||||||
import { useProgress } from "../../state/logic/progress";
|
import { useProgress } from "../../state/logic/progress";
|
||||||
import { useSource } from "../../state/logic/source";
|
import { useSource } from "../../state/logic/source";
|
||||||
@@ -48,9 +50,14 @@ export function CaptionRendererAction({
|
|||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const source = useSource(descriptor).source;
|
const source = useSource(descriptor).source;
|
||||||
const videoTime = useProgress(descriptor).time;
|
const videoTime = useProgress(descriptor).time;
|
||||||
const { captionSettings } = useSettings();
|
const { captionSettings, setCaptionDelay } = useSettings();
|
||||||
const captions = useRef<ContentCaption[]>([]);
|
const captions = useRef<ContentCaption[]>([]);
|
||||||
|
|
||||||
|
const captionSetRef = useRef<(delay: number) => void>(setCaptionDelay);
|
||||||
|
useEffect(() => {
|
||||||
|
captionSetRef.current = setCaptionDelay;
|
||||||
|
}, [setCaptionDelay]);
|
||||||
|
|
||||||
useAsync(async () => {
|
useAsync(async () => {
|
||||||
const blobUrl = source?.caption?.url;
|
const blobUrl = source?.caption?.url;
|
||||||
if (blobUrl) {
|
if (blobUrl) {
|
||||||
@@ -61,20 +68,38 @@ export function CaptionRendererAction({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
captions.current = [];
|
captions.current = [];
|
||||||
}
|
}
|
||||||
|
// reset delay on every subtitle change
|
||||||
|
setCaptionDelay(0);
|
||||||
} else {
|
} else {
|
||||||
captions.current = [];
|
captions.current = [];
|
||||||
}
|
}
|
||||||
}, [source?.caption?.url]);
|
}, [source?.caption?.url]);
|
||||||
|
|
||||||
|
// reset delay when loading new source url
|
||||||
|
useEffect(() => {
|
||||||
|
captionSetRef.current(0);
|
||||||
|
}, [source?.caption?.url]);
|
||||||
|
|
||||||
|
const isVisible = useCallback(
|
||||||
|
(
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
delay: number,
|
||||||
|
currentTime: number
|
||||||
|
): boolean => {
|
||||||
|
const delayedStart = start / 1000 + delay;
|
||||||
|
const delayedEnd = end / 1000 + delay;
|
||||||
|
return (
|
||||||
|
Math.max(0, delayedStart) <= currentTime &&
|
||||||
|
Math.max(0, delayedEnd) >= currentTime
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
if (!captions.current.length) return null;
|
if (!captions.current.length) return null;
|
||||||
const isVisible = (start: number, end: number): boolean => {
|
const visibileCaptions = captions.current.filter(({ start, end }) =>
|
||||||
const delayedStart = start / 1000 + captionSettings.delay;
|
isVisible(start, end, captionSettings.delay, videoTime)
|
||||||
const delayedEnd = end / 1000 + captionSettings.delay;
|
);
|
||||||
return (
|
|
||||||
Math.max(0, delayedStart) <= videoTime &&
|
|
||||||
Math.max(0, delayedEnd) >= videoTime
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Transition
|
<Transition
|
||||||
className={[
|
className={[
|
||||||
@@ -84,12 +109,9 @@ export function CaptionRendererAction({
|
|||||||
animation="slide-up"
|
animation="slide-up"
|
||||||
show
|
show
|
||||||
>
|
>
|
||||||
{captions.current.map(
|
{visibileCaptions.map(({ start, end, content }) => (
|
||||||
({ start, end, content }) =>
|
<CaptionCue key={`${start}-${end}`} text={content} />
|
||||||
isVisible(start, end) && (
|
))}
|
||||||
<CaptionCue key={`${start}-${end}`} text={content} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Transition>
|
</Transition>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useMisc } from "@/video/state/logic/misc";
|
import { useMisc } from "@/video/state/logic/misc";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export function CastingTextAction() {
|
export function CastingTextAction() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";
|
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useMisc } from "@/video/state/logic/misc";
|
import { useMisc } from "@/video/state/logic/misc";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
|
||||||
|
|
||||||
export function DividerAction() {
|
export function DividerAction() {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { canFullscreen } from "@/utils/detectFeatures";
|
import { canFullscreen } from "@/utils/detectFeatures";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useInterface } from "@/video/state/logic/interface";
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
import { useCallback } from "react";
|
|
||||||
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { useVolumeControl } from "@/hooks/useVolumeToggle";
|
||||||
|
import { getPlayerState } from "@/video/state/cache";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useInterface } from "@/video/state/logic/interface";
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
import { getPlayerState } from "@/video/state/cache";
|
|
||||||
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
import { useProgress } from "@/video/state/logic/progress";
|
import { useProgress } from "@/video/state/logic/progress";
|
||||||
import { useVolumeControl } from "@/hooks/useVolumeToggle";
|
|
||||||
|
|
||||||
export function KeyboardShortcutsAction() {
|
export function KeyboardShortcutsAction() {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
@@ -60,7 +61,7 @@ export function KeyboardShortcutsAction() {
|
|||||||
|
|
||||||
// Mute
|
// Mute
|
||||||
case "m":
|
case "m":
|
||||||
toggleVolume();
|
toggleVolume(true);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Decrease volume
|
// Decrease volume
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { MWCaption } from "@/backend/helpers/streams";
|
import { MWCaption } from "@/backend/helpers/streams";
|
||||||
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useProgress } from "@/video/state/logic/progress";
|
import { useProgress } from "@/video/state/logic/progress";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export type WindowMeta = {
|
export type WindowMeta = {
|
||||||
meta: DetailedMeta;
|
meta: DetailedMeta;
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
import { useCallback } from "react";
|
|
||||||
|
|
||||||
export function MiddlePauseAction() {
|
export function MiddlePauseAction() {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
|
||||||
import { useCurrentSeriesEpisodeInfo } from "../hooks/useCurrentSeriesEpisodeInfo";
|
import { useCurrentSeriesEpisodeInfo } from "../hooks/useCurrentSeriesEpisodeInfo";
|
||||||
|
|
||||||
export function PageTitleAction() {
|
export function PageTitleAction() {
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
import { useCallback } from "react";
|
|
||||||
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import {
|
import {
|
||||||
canPictureInPicture,
|
canPictureInPicture,
|
||||||
canWebkitPictureInPicture,
|
canWebkitPictureInPicture,
|
||||||
} from "@/utils/detectFeatures";
|
} from "@/utils/detectFeatures";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
|
||||||
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
makePercentage,
|
makePercentage,
|
||||||
makePercentageString,
|
makePercentageString,
|
||||||
@@ -7,7 +9,6 @@ import { getPlayerState } from "@/video/state/cache";
|
|||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useProgress } from "@/video/state/logic/progress";
|
import { useProgress } from "@/video/state/logic/progress";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
export function ProgressAction() {
|
export function ProgressAction() {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
import { Icons } from "@/components/Icon";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { Icons } from "@/components/Icon";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
|
||||||
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";
|
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useInterface } from "@/video/state/logic/interface";
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
|
||||||
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";
|
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useInterface } from "@/video/state/logic/interface";
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { FloatingAnchor } from "@/components/popout/FloatingAnchor";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
|
||||||
import { useCurrentSeriesEpisodeInfo } from "../hooks/useCurrentSeriesEpisodeInfo";
|
import { useCurrentSeriesEpisodeInfo } from "../hooks/useCurrentSeriesEpisodeInfo";
|
||||||
|
|
||||||
export function ShowTitleAction() {
|
export function ShowTitleAction() {
|
||||||
|
@@ -2,6 +2,7 @@ import { Icons } from "@/components/Icon";
|
|||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useProgress } from "@/video/state/logic/progress";
|
import { useProgress } from "@/video/state/logic/progress";
|
||||||
|
|
||||||
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
import { useProgress } from "@/video/state/logic/progress";
|
import { useProgress } from "@/video/state/logic/progress";
|
||||||
import { useInterface } from "@/video/state/logic/interface";
|
|
||||||
import { VideoPlayerTimeFormat } from "@/video/state/types";
|
import { VideoPlayerTimeFormat } from "@/video/state/types";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
|
||||||
|
|
||||||
function durationExceedsHour(secs: number): boolean {
|
function durationExceedsHour(secs: number): boolean {
|
||||||
return secs > 60 * 60;
|
return secs > 60 * 60;
|
||||||
@@ -54,20 +55,20 @@ export function TimeAction(props: Props) {
|
|||||||
hasHours
|
hasHours
|
||||||
);
|
);
|
||||||
const duration = formatSeconds(videoTime.duration, hasHours);
|
const duration = formatSeconds(videoTime.duration, hasHours);
|
||||||
const timeLeft = formatSeconds(
|
const remaining = formatSeconds(
|
||||||
(videoTime.duration - videoTime.time) / mediaPlaying.playbackSpeed,
|
(videoTime.duration - videoTime.time) / mediaPlaying.playbackSpeed,
|
||||||
hasHours
|
hasHours
|
||||||
);
|
);
|
||||||
const timeFinished = new Date(
|
const timeFinished = new Date(
|
||||||
new Date().getTime() +
|
new Date().getTime() +
|
||||||
(videoTime.duration * 1000) / mediaPlaying.playbackSpeed
|
((videoTime.duration - videoTime.time) * 1000) /
|
||||||
).toLocaleTimeString("en-US", {
|
mediaPlaying.playbackSpeed
|
||||||
hour: "numeric",
|
);
|
||||||
minute: "numeric",
|
|
||||||
hour12: true,
|
|
||||||
});
|
|
||||||
const formattedTimeFinished = ` - ${t("videoPlayer.finishAt", {
|
const formattedTimeFinished = ` - ${t("videoPlayer.finishAt", {
|
||||||
timeFinished,
|
timeFinished,
|
||||||
|
formatParams: {
|
||||||
|
timeFinished: { hour: "numeric", minute: "numeric" },
|
||||||
|
},
|
||||||
})}`;
|
})}`;
|
||||||
|
|
||||||
let formattedTime: string;
|
let formattedTime: string;
|
||||||
@@ -76,10 +77,10 @@ export function TimeAction(props: Props) {
|
|||||||
formattedTime = `${currentTime} ${props.noDuration ? "" : `/ ${duration}`}`;
|
formattedTime = `${currentTime} ${props.noDuration ? "" : `/ ${duration}`}`;
|
||||||
} else if (timeFormat === VideoPlayerTimeFormat.REMAINING && !isMobile) {
|
} else if (timeFormat === VideoPlayerTimeFormat.REMAINING && !isMobile) {
|
||||||
formattedTime = `${t("videoPlayer.timeLeft", {
|
formattedTime = `${t("videoPlayer.timeLeft", {
|
||||||
timeLeft,
|
timeLeft: remaining,
|
||||||
})}${videoTime.time === videoTime.duration ? "" : formattedTimeFinished} `;
|
})}${videoTime.time === videoTime.duration ? "" : formattedTimeFinished} `;
|
||||||
} else if (timeFormat === VideoPlayerTimeFormat.REMAINING && isMobile) {
|
} else if (timeFormat === VideoPlayerTimeFormat.REMAINING && isMobile) {
|
||||||
formattedTime = `-${timeLeft}`;
|
formattedTime = `-${remaining}`;
|
||||||
} else {
|
} else {
|
||||||
formattedTime = "";
|
formattedTime = "";
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import {
|
import {
|
||||||
makePercentage,
|
makePercentage,
|
||||||
@@ -10,7 +12,6 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
|||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useInterface } from "@/video/state/logic/interface";
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@@ -14,7 +14,7 @@ export function VolumeAdjustedAction() {
|
|||||||
videoInterface.volumeChangedWithKeybind
|
videoInterface.volumeChangedWithKeybind
|
||||||
? "mt-10 scale-100 opacity-100"
|
? "mt-10 scale-100 opacity-100"
|
||||||
: "mt-5 scale-75 opacity-0",
|
: "mt-5 scale-75 opacity-0",
|
||||||
"absolute left-1/2 z-[100] flex -translate-x-1/2 items-center space-x-4 rounded-full bg-bink-300 bg-opacity-50 py-2 px-5 transition-all duration-100",
|
"absolute left-1/2 z-[100] flex -translate-x-1/2 items-center space-x-4 rounded-full bg-bink-300 bg-opacity-50 px-5 py-2 transition-all duration-100",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { Icons } from "@/components/Icon";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
|
||||||
import { PopoutListAction } from "../../popouts/PopoutUtils";
|
import { PopoutListAction } from "../../popouts/PopoutUtils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
import { Icons } from "@/components/Icon";
|
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
|
||||||
import { useSource } from "@/video/state/logic/source";
|
|
||||||
import { MWStreamType } from "@/backend/helpers/streams";
|
|
||||||
import { normalizeTitle } from "@/utils/normalizeTitle";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { MWStreamType } from "@/backend/helpers/streams";
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { normalizeTitle } from "@/utils/normalizeTitle";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
|
import { useSource } from "@/video/state/logic/source";
|
||||||
|
|
||||||
import { PopoutListAction } from "../../popouts/PopoutUtils";
|
import { PopoutListAction } from "../../popouts/PopoutUtils";
|
||||||
|
|
||||||
export function DownloadAction() {
|
export function DownloadAction() {
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { Icons } from "@/components/Icon";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
|
||||||
import { PopoutListAction } from "../../popouts/PopoutUtils";
|
import { PopoutListAction } from "../../popouts/PopoutUtils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@@ -8,7 +8,7 @@ export function QualityDisplayAction() {
|
|||||||
if (!source.source) return null;
|
if (!source.source) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md bg-denim-300 py-1 px-2 transition-colors">
|
<div className="rounded-md bg-denim-300 px-2 py-1 transition-colors">
|
||||||
<p className="text-center text-xs font-bold text-slate-300 transition-colors">
|
<p className="text-center text-xs font-bold text-slate-300 transition-colors">
|
||||||
{source.source.quality}
|
{source.source.quality}
|
||||||
</p>
|
</p>
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import { Icons } from "@/components/Icon";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PopoutListAction } from "../../popouts/PopoutUtils";
|
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
|
||||||
import { QualityDisplayAction } from "./QualityDisplayAction";
|
import { QualityDisplayAction } from "./QualityDisplayAction";
|
||||||
|
import { PopoutListAction } from "../../popouts/PopoutUtils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClick?: () => any;
|
onClick?: () => any;
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { MWCaption } from "@/backend/helpers/streams";
|
import { MWCaption } from "@/backend/helpers/streams";
|
||||||
import { MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
|
import { MWSeasonWithEpisodeMeta } from "@/backend/metadata/types";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { VideoPlayerMeta } from "@/video/state/types";
|
import { VideoPlayerMeta } from "@/video/state/types";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
interface MetaControllerProps {
|
interface MetaControllerProps {
|
||||||
data?: VideoPlayerMeta;
|
data?: VideoPlayerMeta;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user