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 = ({