diff --git a/DIAGRAMS.md b/DIAGRAMS.md
index 40f93ec6..8b834aa3 100644
--- a/DIAGRAMS.md
+++ b/DIAGRAMS.md
@@ -1,4 +1,5 @@
# Diagrams
+These diagrams are created with Mermaid and rendered locally. For the best experience, please use a browser.
# Project Structure
Kyoo is a monorepo that consists of several projects each in their own directory. Diagram below shows an outline of kyoo, projects, and artifacts.
@@ -99,9 +100,9 @@ C4Container
System_Boundary(internal, "Kyoo") {
Container(frontend, "front/")
Container(backend, "back/")
+ ContainerQueue(emb, "emb", "", "EnterpriseMessageBus")
Container(transcoder, "transcoder/")
Container(scanner, "scanner/")
- ContainerQueue(emb, "emb", "", "EnterpriseMessageBus")
Container(autosync, "autosync/")
}
System_Boundary(external, "") {
@@ -118,7 +119,6 @@ C4Container
Rel(user, backend, "")
Rel(frontend, backend, "")
Rel(backend, emb, "")
- Rel(backend, media, "")
Rel(backend, transcoder, "")
Rel_Back(autosync, emb, "")
Rel(autosync, tracker, "")
@@ -162,7 +162,7 @@ C4Component
### Back
```mermaid
C4Component
- UpdateLayoutConfig($c4ShapeInRow="4", $c4BoundaryInRow="3")
+ UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="2")
title Component Diagram for Back
@@ -172,41 +172,37 @@ C4Component
Component(frontend_c1, "kyoo_front", "typescript, node.js", "Static Content")
}
Container_Boundary(backend, "back") {
- Component(backend_c1, "kyoo_migrations", "C#, .NET 8.0", "Postgres Migration")
- ComponentDb(backend_db1, "backend", "Postgres", "user data and session state")
- Component(backend_c3, "BackendMetadata", "Volume", "Persistent. Distributed Metadata")
ComponentDb(backend_db2, "search", "Meilisearch", "search resource")
+ Component(backend_c3, "BackendMetadata", "Volume", "Persistent. Distributed Metadata")
+ ComponentDb(backend_db1, "backend", "Postgres", "user data and session state")
+ Component(backend_c1, "kyoo_migrations", "C#, .NET 8.0", "Postgres Migration")
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
}
- Container_Boundary(media, "MediaLibrary") {
- Component_Ext(media_c1, "MediaShare", "Volume", "Read Only")
- }
- Container_Boundary(transcoder, "transcoder") {
- Component(transcoder_c1, "kyoo_transcoder", "go, go", "Video Transcoder")
- }
Container_Boundary(emb, "emb") {
ComponentQueue(emb_e1, "events.watched", "RabbitMQ, Exchange", "")
- ComponentQueue(emb_q2, "scanner.rescan", "RabbitMQ, Queue", "")
ComponentQueue(emb_q1, "autosync", "RabbitMQ, Queue", "")
+ ComponentQueue(emb_q2, "scanner.rescan", "RabbitMQ, Queue", "")
ComponentQueue(emb_e2, "events.resource", "RabbitMQ, Exchange", "unused")
}
Container_Boundary(scanner, "scanner") {
- Component(scanner_c1, "kyoo_scanner", "python, python3.12", "scanner")
Component(scanner_c2, "kyoo_scanner", "python, python3.12", "matcher")
+ Component(scanner_c1, "kyoo_scanner", "python, python3.12", "scanner")
}
Container_Boundary(autosync, "autosync") {
Component(autosync_c1, "kyoo_autosync", "python, python3.12", "")
}
+ Container_Boundary(transcoder, "transcoder") {
+ Component(transcoder_c1, "kyoo_transcoder", "go, go", "Video Transcoder")
+ }
Rel(user, backend_c2, "")
Rel(backend_c1, backend_db1, "")
Rel(backend_c2, backend_db1, "")
Rel(backend_c2, backend_db2, "")
- Rel(backend_c2, media_c1, "")
Rel(backend_c2, transcoder_c1, "")
Rel(backend_c2, backend_c3, "")
Rel(backend_c2, emb_q2, "produces")
@@ -260,14 +256,14 @@ C4Component
Component(scanner_c1, "kyoo_scanner", "python, python3.12", "scanner")
}
- Container_Boundary(emb, "emb") {
- ComponentQueue(emb_q2, "scanner.rescan", "RabbitMQ, Queue", "")
- }
-
Container_Boundary(backend, "back") {
Component(backend_c2, "kyoo_back", "C#, .NET 8.0", "API Backend")
}
+ Container_Boundary(emb, "emb") {
+ ComponentQueue(emb_q2, "scanner.rescan", "RabbitMQ, Queue", "")
+ }
+
Rel(scanner_c1, scanner_q1, "produces")
Rel(scanner_c1, media_c1, "watches")
Rel(scanner_c1, backend_c2, "Fetch existing scans")
diff --git a/back/.config/dotnet-tools.json b/back/.config/dotnet-tools.json
index 3c5d19b5..7f93f7a0 100644
--- a/back/.config/dotnet-tools.json
+++ b/back/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.6",
+ "version": "8.0.7",
"commands": [
"dotnet-ef"
]
diff --git a/back/src/Kyoo.Authentication/Kyoo.Authentication.csproj b/back/src/Kyoo.Authentication/Kyoo.Authentication.csproj
index a36504b2..dd497675 100644
--- a/back/src/Kyoo.Authentication/Kyoo.Authentication.csproj
+++ b/back/src/Kyoo.Authentication/Kyoo.Authentication.csproj
@@ -1,9 +1,9 @@
-
+
-
+
diff --git a/back/src/Kyoo.Core/Kyoo.Core.csproj b/back/src/Kyoo.Core/Kyoo.Core.csproj
index 1a28ba89..ebaaccef 100644
--- a/back/src/Kyoo.Core/Kyoo.Core.csproj
+++ b/back/src/Kyoo.Core/Kyoo.Core.csproj
@@ -16,14 +16,14 @@
-
-
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/back/src/Kyoo.Core/Views/Content/VideoApi.cs b/back/src/Kyoo.Core/Views/Content/VideoApi.cs
index e3deb132..aa1b9b0c 100644
--- a/back/src/Kyoo.Core/Views/Content/VideoApi.cs
+++ b/back/src/Kyoo.Core/Views/Content/VideoApi.cs
@@ -69,6 +69,15 @@ public class VideoApi : Controller
await _Proxy($"{path}/direct");
}
+ [HttpGet("{path:base64}/direct/{identifier}")]
+ [PartialPermission(Kind.Play)]
+ [ProducesResponseType(StatusCodes.Status206PartialContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task GetDirectStream(string path, string identifier)
+ {
+ await _Proxy($"{path}/direct/{identifier}");
+ }
+
[HttpGet("{path:base64}/master.m3u8")]
[PartialPermission(Kind.Play)]
[ProducesResponseType(StatusCodes.Status206PartialContent)]
@@ -78,18 +87,18 @@ public class VideoApi : Controller
await _Proxy($"{path}/master.m3u8");
}
- [HttpGet("{path:base64}/{quality}/index.m3u8")]
+ [HttpGet("{path:base64}/{video:int}/{quality}/index.m3u8")]
[PartialPermission(Kind.Play)]
- public async Task GetVideoIndex(string path, string quality)
+ public async Task GetVideoIndex(string path, int video, string quality)
{
- await _Proxy($"{path}/{quality}/index.m3u8");
+ await _Proxy($"{path}/{video}/{quality}/index.m3u8");
}
- [HttpGet("{path:base64}/{quality}/{segment}")]
+ [HttpGet("{path:base64}/{video:int}/{quality}/{segment}")]
[PartialPermission(Kind.Play)]
- public async Task GetVideoSegment(string path, string quality, string segment)
+ public async Task GetVideoSegment(string path, int video, string quality, string segment)
{
- await _Proxy($"{path}/{quality}/{segment}");
+ await _Proxy($"{path}/{video}/{quality}/{segment}");
}
[HttpGet("{path:base64}/audio/{audio}/index.m3u8")]
diff --git a/back/src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj b/back/src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj
index b6ff7e12..1582a316 100644
--- a/back/src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj
+++ b/back/src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
index 753e66ce..5c193da2 100644
--- a/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
+++ b/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
@@ -9,11 +9,11 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/back/src/Kyoo.Swagger/Kyoo.Swagger.csproj b/back/src/Kyoo.Swagger/Kyoo.Swagger.csproj
index 4b85c3f7..64d9514e 100644
--- a/back/src/Kyoo.Swagger/Kyoo.Swagger.csproj
+++ b/back/src/Kyoo.Swagger/Kyoo.Swagger.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/biome.json b/biome.json
index 1bd45c09..d8b12b95 100644
--- a/biome.json
+++ b/biome.json
@@ -39,7 +39,7 @@
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
- "trailingComma": "all",
+ "trailingCommas": "all",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSpacing": true,
diff --git a/front/Dockerfile b/front/Dockerfile
index 99b0db2b..fb007417 100644
--- a/front/Dockerfile
+++ b/front/Dockerfile
@@ -11,8 +11,8 @@ COPY packages/models/package.json packages/models/package.json
RUN yarn --immutable
COPY . .
-ENV NEXT_TELEMETRY_DISABLED 1
-ENV NODE_ENV production
+ENV NEXT_TELEMETRY_DISABLED=1
+ENV NODE_ENV=production
RUN yarn build:web
@@ -25,8 +25,8 @@ COPY --from=builder /app/apps/web/.next/static ./.next/static/
COPY --from=builder /app/apps/web/public ./public
EXPOSE 8901
-ENV PORT 8901
+ENV PORT=8901
-ENV NEXT_TELEMETRY_DISABLED 1
-ENV NODE_ENV production
+ENV NEXT_TELEMETRY_DISABLED=1
+ENV NODE_ENV=production
CMD ["node", "server.js"]
diff --git a/front/Dockerfile.dev b/front/Dockerfile.dev
index 0bdb7cbd..e62d96a7 100644
--- a/front/Dockerfile.dev
+++ b/front/Dockerfile.dev
@@ -11,7 +11,7 @@ COPY packages/primitives/package.json packages/primitives/package.json
COPY packages/models/package.json packages/models/package.json
RUN yarn --immutable
-ENV NEXT_TELEMETRY_DISABLED 1
+ENV NEXT_TELEMETRY_DISABLED=1
EXPOSE 3000
EXPOSE 8081
ENTRYPOINT ["yarn", "dev"]
diff --git a/front/apps/mobile/app.config.ts b/front/apps/mobile/app.config.ts
index 85044c04..afb76113 100644
--- a/front/apps/mobile/app.config.ts
+++ b/front/apps/mobile/app.config.ts
@@ -56,10 +56,6 @@ const config: ExpoConfig = {
backgroundColor: "#eff1f5",
},
splash,
- permissions: [
- "android.permission.FOREGROUND_SERVICE",
- "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
- ],
},
updates: {
url: "https://u.expo.dev/55de6b52-c649-4a15-9a45-569ff5ed036c",
@@ -73,27 +69,16 @@ const config: ExpoConfig = {
projectId: "55de6b52-c649-4a15-9a45-569ff5ed036c",
},
},
- plugins: ["expo-build-properties", "expo-localization"],
-};
-
-const withForegroundService = (c: ExpoConfig): ExpoConfig => {
- return withAndroidManifest(c, async (config) => {
- const manifest = config.modResults.manifest;
- manifest.application![0].service ??= [];
- manifest.application![0].service.push({
- $: {
- "android:name": "com.brentvatne.exoplayer.VideoPlaybackService",
- "android:exported": "false",
- "android:foregroundServiceType": "mediaPlayback",
+ plugins: [
+ "expo-build-properties",
+ "expo-localization",
+ [
+ "react-native-video",
+ {
+ enableNotificationControls: true,
},
- "intent-filter": [
- {
- action: [{ $: { "android:name": "androidx.media3.session.MediaSessionService" } }],
- },
- ],
- });
- return config;
- });
+ ],
+ ],
};
-export default withForegroundService(config);
+export default config;
diff --git a/front/apps/mobile/app/(app)/_layout.tsx b/front/apps/mobile/app/(app)/_layout.tsx
index f34f1b38..7fbb707a 100644
--- a/front/apps/mobile/app/(app)/_layout.tsx
+++ b/front/apps/mobile/app/(app)/_layout.tsx
@@ -40,6 +40,8 @@ export default function SignGuard() {
,
headerRight: () => ,
contentStyle: {
diff --git a/front/apps/mobile/package.json b/front/apps/mobile/package.json
index dc3cb697..31ac9ded 100644
--- a/front/apps/mobile/package.json
+++ b/front/apps/mobile/package.json
@@ -18,54 +18,53 @@
"@formatjs/intl-displaynames": "^6.6.8",
"@formatjs/intl-locale": "^4.0.0",
"@gorhom/portal": "^1.0.14",
- "@kesha-antonov/react-native-background-downloader": "^3.1.3",
+ "@kesha-antonov/react-native-background-downloader": "^3.2.0",
"@kyoo/ui": "workspace:^",
- "@material-symbols/svg-400": "^0.18.0",
- "@react-native-community/netinfo": "11.3.1",
- "@shopify/flash-list": "1.6.4",
- "@tanstack/query-sync-storage-persister": "^5.38.0",
- "@tanstack/react-query": "^5.39.0",
- "@tanstack/react-query-persist-client": "^5.39.0",
+ "@material-symbols/svg-400": "^0.22.0",
+ "@react-native-community/netinfo": "11.3.2",
+ "@shopify/flash-list": "1.7.1",
+ "@tanstack/query-sync-storage-persister": "^5.51.21",
+ "@tanstack/react-query": "^5.51.23",
+ "@tanstack/react-query-persist-client": "^5.51.23",
"array-shuffle": "^3.0.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
- "expo": "^51.0.8",
- "expo-build-properties": "~0.12.1",
- "expo-constants": "~16.0.1",
- "expo-dev-client": "~4.0.14",
+ "expo": "^51.0.26",
+ "expo-build-properties": "~0.12.5",
+ "expo-constants": "~16.0.2",
+ "expo-dev-client": "~4.0.22",
"expo-file-system": "~17.0.1",
- "expo-font": "~12.0.5",
- "expo-image-picker": "~15.0.5",
+ "expo-font": "~12.0.9",
+ "expo-image-picker": "~15.0.7",
"expo-linear-gradient": "~13.0.2",
"expo-linking": "~6.3.1",
"expo-localization": "~15.0.3",
- "expo-navigation-bar": "~3.0.4",
- "expo-router": "3.5.14",
+ "expo-navigation-bar": "~3.0.7",
+ "expo-router": "3.5.21",
"expo-screen-orientation": "~7.0.5",
- "expo-secure-store": "~13.0.1",
+ "expo-secure-store": "~13.0.2",
"expo-status-bar": "~1.12.1",
- "expo-updates": "~0.25.14",
- "i18next": "^23.11.5",
+ "expo-updates": "~0.25.22",
+ "i18next": "^23.12.2",
"intl-pluralrules": "^2.0.1",
"moti": "^0.29.0",
- "react": "18.2.0",
- "react-dom": "18.2.0",
- "react-i18next": "^14.1.2",
- "react-native": "0.74.1",
+ "react": "18.3.1",
+ "react-i18next": "^15.0.1",
+ "react-native": "0.74.5",
"react-native-blurhash": "^2.0.3",
"react-native-fast-image": "^8.6.3",
"react-native-mmkv": "^2.12.2",
- "react-native-reanimated": "~3.10.1",
- "react-native-safe-area-context": "4.10.1",
- "react-native-screens": "~3.31.1",
+ "react-native-reanimated": "~3.15.0",
+ "react-native-safe-area-context": "4.10.8",
+ "react-native-screens": "3.34.0",
"react-native-svg": "15.2.0",
"react-native-uuid": "^2.0.2",
- "react-native-video": "^6.1.2",
+ "react-native-video": "^6.4.3",
"yoshiki": "1.2.14"
},
"devDependencies": {
- "@babel/core": "^7.24.6",
- "react-native-svg-transformer": "^1.4.0",
- "typescript": "~5.3.3"
+ "@babel/core": "^7.25.2",
+ "react-native-svg-transformer": "^1.5.0",
+ "typescript": "~5.5.4"
},
"installConfig": {
"hoistingLimits": "workspaces"
diff --git a/front/apps/web/next.config.js b/front/apps/web/next.config.js
index 1b94df99..12d4c490 100755
--- a/front/apps/web/next.config.js
+++ b/front/apps/web/next.config.js
@@ -51,8 +51,14 @@ const nextConfig = {
alias: {
...config.resolve.alias,
"react-native$": "react-native-web",
- "react-native/Libraries/Image/AssetRegistry$":
- "react-native-web/dist/modules/AssetRegistry",
+ // "react-native/Libraries/Image/AssetRegistry$":
+ // "react-native-web/dist/modules/AssetRegistry",
+ "react-native/Libraries/EventEmitter/RCTDeviceEventEmitter$":
+ "react-native-web/dist/vendor/react-native/NativeEventEmitter/RCTDeviceEventEmitter",
+ "react-native/Libraries/vendor/emitter/EventEmitter$":
+ "react-native-web/dist/vendor/react-native/emitter/EventEmitter",
+ "react-native/Libraries/EventEmitter/NativeEventEmitter$":
+ "react-native-web/dist/vendor/react-native/NativeEventEmitter",
},
extensions: [".web.ts", ".web.tsx", ".web.js", ".web.jsx", ...config.resolve.extensions],
};
@@ -94,6 +100,7 @@ const nextConfig = {
"@kyoo/ui",
"@kyoo/primitives",
"@kyoo/models",
+ "@react-native/assets-registry",
"solito",
"react-native",
"react-native-web",
diff --git a/front/apps/web/package.json b/front/apps/web/package.json
index 2442f40e..1c813a61 100644
--- a/front/apps/web/package.json
+++ b/front/apps/web/package.json
@@ -16,45 +16,45 @@
"@kyoo/models": "workspace:^",
"@kyoo/primitives": "workspace:^",
"@kyoo/ui": "workspace:^",
- "@material-symbols/svg-400": "^0.18.0",
- "@radix-ui/react-dropdown-menu": "^2.0.6",
- "@radix-ui/react-select": "^2.0.0",
- "@tanstack/react-query": "^5.39.0",
- "@tanstack/react-query-devtools": "^5.39.0",
+ "@material-symbols/svg-400": "^0.22.0",
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
+ "@radix-ui/react-select": "^2.1.1",
+ "@tanstack/react-query": "^5.51.23",
+ "@tanstack/react-query-devtools": "^5.51.23",
"array-shuffle": "^3.0.0",
- "expo-image-picker": "~15.0.5",
+ "expo-image-picker": "~15.0.7",
"expo-linear-gradient": "^13.0.2",
- "expo-modules-core": "^1.12.11",
- "hls.js": "^1.5.8",
- "i18next": "^23.11.5",
- "jassub": "^1.7.15",
- "jotai": "^2.8.1",
+ "expo-modules-core": "^1.12.20",
+ "hls.js": "^1.5.14",
+ "i18next": "^23.12.2",
+ "jassub": "1.7.15",
+ "jotai": "^2.9.2",
"moti": "^0.29.0",
- "next": "14.2.3",
+ "next": "14.2.5",
"next-translate": "^2.6.2",
"raf": "^3.4.1",
- "react": "18.2.0",
+ "react": "18.3.1",
"react-dom": "18.3.1",
- "react-i18next": "^14.1.2",
- "react-native-reanimated": "3.11.0",
- "react-native-svg": "15.3.0",
- "react-native-video": "^6.1.2",
+ "react-i18next": "^15.0.1",
+ "react-native-reanimated": "3.15.0",
+ "react-native-svg": "15.2.0",
+ "react-native-video": "^6.4.3",
"react-native-web": "0.19.12",
- "react-tooltip": "^5.26.4",
+ "react-tooltip": "^5.28.0",
"solito": "^4.2.2",
"srt-webvtt": "zoriya/srt-webvtt#build",
"superjson": "^2.2.1",
- "sweetalert2": "^11.11.0",
+ "sweetalert2": "^11.12.4",
"yoshiki": "1.2.14",
"zod": "^3.23.8"
},
"devDependencies": {
"@svgr/webpack": "^8.1.0",
- "@types/node": "20.12.12",
+ "@types/node": "22.2.0",
"@types/react-dom": "18.3.0",
"copy-webpack-plugin": "^12.0.2",
- "react-native": "0.74.1",
- "typescript": "^5.4.5",
- "webpack": "^5.91.0"
+ "react-native": "0.74.5",
+ "typescript": "^5.5.4",
+ "webpack": "^5.93.0"
}
}
diff --git a/front/apps/web/src/i18n.tsx b/front/apps/web/src/i18n.tsx
index 2a796455..af53fc3a 100644
--- a/front/apps/web/src/i18n.tsx
+++ b/front/apps/web/src/i18n.tsx
@@ -47,7 +47,7 @@ export const withTranslations = (
...commonOptions,
lng: props.pageProps.__lang,
});
- i18next.systemLanguage = props.pageProps.__sysLang;
+ i18next.systemLanguage = props.pageProps?.__sysLang;
return i18next;
});
@@ -66,6 +66,7 @@ export const withTranslations = (
lng,
});
i18n.systemLanguage = sysLng;
+ props.pageProps ??= {};
props.pageProps.__lang = lng;
props.pageProps.__sysLang = sysLng;
return props;
diff --git a/front/apps/web/src/pages/_app.tsx b/front/apps/web/src/pages/_app.tsx
index 28db2a8c..7a43fe2c 100755
--- a/front/apps/web/src/pages/_app.tsx
+++ b/front/apps/web/src/pages/_app.tsx
@@ -201,7 +201,7 @@ const App = ({ Component, pageProps }: AppProps) => {
{...props}
/>
-
+
@@ -264,12 +264,12 @@ App.getInitialProps = async (ctx: AppContext) => {
) {
ctx.ctx.res!.writeHead(307, { Location: `/setup?step=${info!.setupStatus}` });
ctx.ctx.res!.end();
- return {} as any;
+ return { pageProps: {} };
}
if (info!.setupStatus === SetupStep.Done && ctx.router.route === "/setup") {
ctx.ctx.res!.writeHead(307, { Location: "/" });
ctx.ctx.res!.end();
- return {} as any;
+ return { pageProps: {} };
}
} catch (e) {
console.error("SSR error, disabling it.");
diff --git a/front/package.json b/front/package.json
index 5c6a6185..7d64b2e8 100644
--- a/front/package.json
+++ b/front/package.json
@@ -17,8 +17,8 @@
},
"workspaces": ["apps/*", "packages/*"],
"devDependencies": {
- "@biomejs/biome": "1.7.3",
- "typescript": "5.4.5"
+ "@biomejs/biome": "1.8.3",
+ "typescript": "5.5.4"
},
"packageManager": "yarn@3.2.4"
}
diff --git a/front/packages/models/package.json b/front/packages/models/package.json
index c5ba651f..630960ac 100644
--- a/front/packages/models/package.json
+++ b/front/packages/models/package.json
@@ -6,7 +6,7 @@
"packageManager": "yarn@3.2.4",
"devDependencies": {
"react-native-mmkv": "^2.12.2",
- "typescript": "^5.4.5"
+ "typescript": "^5.5.4"
},
"peerDependencies": {
"@tanstack/react-query": "*",
diff --git a/front/packages/models/src/resources/watch-info.ts b/front/packages/models/src/resources/watch-info.ts
index a0a10d86..134da343 100644
--- a/front/packages/models/src/resources/watch-info.ts
+++ b/front/packages/models/src/resources/watch-info.ts
@@ -23,14 +23,42 @@ import { imageFn } from "../traits";
import { QualityP } from "./quality";
/**
- * A Video track
+ * A audio or subtitle track.
*/
-export const VideoP = z.object({
+export const TrackP = z.object({
/**
- * The Codec of the Video Track.
- * E.g. "AVC"
+ * The index of this track on the episode.
+ * NOTE: external subtitles can have a null index
+ */
+ index: z.number().nullable(),
+ /**
+ * The title of the stream.
+ */
+ title: z.string().nullable(),
+ /**
+ * The language of this stream (as a ISO-639-2 language code)
+ */
+ language: z.string().nullable(),
+ /**
+ * The codec of this stream.
*/
codec: z.string(),
+ /**
+ * Is this stream the default one of it's type?
+ */
+ isDefault: z.boolean(),
+ /**
+ * Is this stream tagged as forced?
+ * NOTE: not available for videos
+ */
+ isForced: z.boolean().optional(),
+});
+export type Track = z.infer;
+
+/**
+ * A Video track
+ */
+export const VideoP = TrackP.extend({
/**
* The Quality of the Video
* E.g. "1080p"
@@ -55,37 +83,6 @@ export const VideoP = z.object({
export type Video = z.infer;
-/**
- * A audio or subtitle track.
- */
-export const TrackP = z.object({
- /**
- * The index of this track on the episode.
- */
- index: z.number(),
- /**
- * The title of the stream.
- */
- title: z.string().nullable(),
- /**
- * The language of this stream (as a ISO-639-2 language code)
- */
- language: z.string().nullable(),
- /**
- * The codec of this stream.
- */
- codec: z.string(),
- /**
- * Is this stream the default one of it's type?
- */
- isDefault: z.boolean(),
- /**
- * Is this stream tagged as forced?
- */
- isForced: z.boolean(),
-});
-export type Track = z.infer;
-
export const AudioP = TrackP;
export type Audio = z.infer;
@@ -94,6 +91,10 @@ export const SubtitleP = TrackP.extend({
* The url of this track (only if this is a subtitle)..
*/
link: z.string().transform(imageFn).nullable(),
+ /*
+ * Is this an external subtitle (as in stored in a different file)
+ */
+ isExternal: z.boolean(),
});
export type Subtitle = z.infer;
@@ -152,7 +153,7 @@ export const WatchInfoP = z
/**
* The video track.
*/
- video: VideoP.nullable(),
+ videos: z.array(VideoP),
/**
* The list of audio tracks.
*/
diff --git a/front/packages/primitives/package.json b/front/packages/primitives/package.json
index 106165a1..96b33756 100644
--- a/front/packages/primitives/package.json
+++ b/front/packages/primitives/package.json
@@ -6,7 +6,7 @@
"packageManager": "yarn@3.2.4",
"devDependencies": {
"@gorhom/portal": "^1.0.14",
- "typescript": "^5.4.5"
+ "typescript": "^5.5.4"
},
"peerDependencies": {
"@gorhom/portal": "*",
@@ -53,14 +53,14 @@
},
"dependencies": {
"@expo/html-elements": "^0.10.1",
- "@tanstack/react-query": "^5.39.0",
+ "@tanstack/react-query": "^5.51.23",
"solito": "^4.2.2"
},
"optionalDependencies": {
- "@radix-ui/react-select": "^2.0.0",
+ "@radix-ui/react-select": "^2.1.1",
"blurhash": "^2.0.5",
"react-native-blurhash": "^2.0.3",
"react-native-fast-image": "^8.6.3",
- "react-native-safe-area-context": "4.10.1"
+ "react-native-safe-area-context": "4.10.8"
}
}
diff --git a/front/packages/primitives/src/menu.web.tsx b/front/packages/primitives/src/menu.web.tsx
index 304e17cd..b5d3c15e 100644
--- a/front/packages/primitives/src/menu.web.tsx
+++ b/front/packages/primitives/src/menu.web.tsx
@@ -157,6 +157,12 @@ const MenuItem = forwardRef<
- `${(b / 1000000).toFixed(2)} Mbps`;
+
const MediaInfoTable = ({
- mediaInfo: { path, video, container, audios, subtitles, duration, size },
+ mediaInfo: { path, videos, container, audios, subtitles, duration, size },
}: {
mediaInfo: Partial;
}) => {
@@ -42,22 +45,22 @@ const MediaInfoTable = ({
const { t } = useTranslation();
const { css } = useYoshiki();
- const formatBitrate = (b: number) => `${(b / 1000000).toFixed(2)} Mbps`;
const formatTrackTable = (trackTable: (Audio | Subtitle)[], type: "subtitles" | "audio") => {
if (trackTable.length === 0) {
return undefined;
}
const singleTrack = trackTable.length === 1;
return trackTable.reduce(
- (collected, audioTrack, index) => {
+ (collected, track, index) => {
// If there is only one track, we do not need to show an index
collected[singleTrack ? t(`mediainfo.${type}`) : `${t(`mediainfo.${type}`)} ${index + 1}`] =
[
- getDisplayName(audioTrack),
+ getDisplayName(track),
// Only show it if there is more than one track
- audioTrack.isDefault && !singleTrack ? t("mediainfo.default") : undefined,
- audioTrack.isForced ? t("mediainfo.forced") : undefined,
- audioTrack.codec,
+ track.isDefault && !singleTrack ? t("mediainfo.default") : undefined,
+ track.isForced ? t("mediainfo.forced") : undefined,
+ "isExternal" in track && track.isExternal ? t("mediainfo.external") : undefined,
+ track.codec,
]
.filter((x) => x !== undefined)
.join(" - ");
@@ -66,6 +69,29 @@ const MediaInfoTable = ({
{} as Record,
);
};
+ const formatVideoTable = (trackTable: Video[]) => {
+ if (trackTable.length === 0) {
+ return { [t("mediainfo.video")]: t("mediainfo.novideo") };
+ }
+ const singleTrack = trackTable.length === 1;
+ return trackTable.reduce(
+ (collected, video, index) => {
+ // If there is only one track, we do not need to show an index
+ collected[singleTrack ? t("mediainfo.video") : `${t("mediainfo.video")} ${index + 1}`] = [
+ getDisplayName(video),
+ `${video.width}x${video.height} (${video.quality})`,
+ formatBitrate(video.bitrate),
+ // Only show it if there is more than one track
+ video.isDefault && !singleTrack ? t("mediainfo.default") : undefined,
+ video.codec,
+ ]
+ .filter((x) => x !== undefined)
+ .join(" - ");
+ return collected;
+ },
+ {} as Record,
+ );
+ };
const table = (
[
{
@@ -74,15 +100,7 @@ const MediaInfoTable = ({
[t("mediainfo.duration")]: duration,
[t("mediainfo.size")]: size,
},
- {
- [t("mediainfo.video")]: video
- ? `${video.width}x${video.height} (${video.quality}) - ${formatBitrate(
- video.bitrate,
- )} - ${video.codec}`
- : video === null
- ? t("mediainfo.novideo")
- : undefined,
- },
+ videos === undefined ? { [t("mediainfo.video")]: undefined } : formatVideoTable(videos),
audios === undefined
? { [t("mediainfo.audio")]: undefined }
: formatTrackTable(audios, "audio"),
diff --git a/front/packages/ui/src/details/header.tsx b/front/packages/ui/src/details/header.tsx
index 22ab23be..0c258b98 100644
--- a/front/packages/ui/src/details/header.tsx
+++ b/front/packages/ui/src/details/header.tsx
@@ -329,7 +329,7 @@ export const TitleLine = ({
- {rating !== null && (
+ {rating !== null && rating !== 0 && (
<>
{
- const { css } = useYoshiki();
-
+ const { css, theme } = useYoshiki();
return (
<>
+ ) : undefined
+ }
/>
(function SearchBar(props, ref)
setQuery(q);
}}
placeholder={t("navbar.search")}
- placeholderTextColor={theme.colors.white}
- containerStyle={{ height: ts(4), flexShrink: 1, borderColor: (theme) => theme.colors.white }}
+ placeholderTextColor={theme.contrast}
+ containerStyle={{ height: ts(4), flexShrink: 1, borderColor: (theme) => theme.contrast }}
{...tooltip(t("navbar.search"))}
{...props}
/>
@@ -177,9 +177,14 @@ export const NavbarRight = () => {
export const Navbar = ({
left,
right,
+ background,
...props
-}: { left?: ReactElement | null; right?: ReactElement | null } & Stylable) => {
- const { css } = useYoshiki();
+}: {
+ left?: ReactElement | null;
+ right?: ReactElement | null;
+ background?: ReactElement;
+} & Stylable) => {
+ const { css, theme } = useYoshiki();
const { t } = useTranslation();
return (
@@ -205,6 +210,7 @@ export const Navbar = ({
props,
)}
>
+ {background}
{left !== undefined ? (
left
diff --git a/front/packages/ui/src/player/components/right-buttons.tsx b/front/packages/ui/src/player/components/right-buttons.tsx
index 32c6a191..b1113100 100644
--- a/front/packages/ui/src/player/components/right-buttons.tsx
+++ b/front/packages/ui/src/player/components/right-buttons.tsx
@@ -71,9 +71,9 @@ export const RightButtons = ({
selected={!selectedSubtitle}
onSelect={() => setSubtitle(null)}
/>
- {subtitles.map((x) => (
+ {subtitles.map((x, i) => (
{
+ if (item.type === "movie") {
+ return item.name;
+ }
+ return `${item.name} (${episodeDisplayNumber({
+ seasonNumber: item.seasonNumber,
+ episodeNumber: item.episodeNumber,
+ absoluteNumber: item.absoluteNumber,
+ })})`;
+};
+
export const Player = ({
slug,
type,
@@ -81,6 +92,7 @@ export const Player = ({
const [playbackError, setPlaybackError] = useState(undefined);
const { data, error } = useFetch(Player.query(type, slug));
const { data: info, error: infoError } = useFetch(Player.infoQuery(type, slug));
+ const image = data && data.type === "episode" ? data.show?.poster ?? data?.poster : data?.poster;
const previous =
data && data.type === "episode" && data.previousEpisode
? `/watch/${data.previousEpisode.slug}?t=0`
@@ -89,15 +101,8 @@ export const Player = ({
data && data.type === "episode" && data.nextEpisode
? `/watch/${data.nextEpisode.slug}?t=0`
: undefined;
- const title =
- data &&
- (data.type === "movie"
- ? data.name
- : `${data.show!.name} ${episodeDisplayNumber({
- seasonNumber: data.seasonNumber,
- episodeNumber: data.episodeNumber,
- absoluteNumber: data.absoluteNumber,
- })}`);
+ const title = data && formatTitleMetadata(data);
+ const subtitle = data && data.type === "episode" ? data.show?.name : undefined;
useVideoKeyboard(info?.subtitles, info?.fonts, previous, next);
@@ -141,8 +146,9 @@ export const Player = ({