From f97852ae98000d3fbb0c03e02e90e0940e197a8b Mon Sep 17 00:00:00 2001 From: Gauvino Date: Thu, 11 Jun 2026 11:42:27 +0200 Subject: [PATCH] refactor: migrate JS/MJS sources and scripts to TypeScript Migrate all remaining migratable .js/.mjs files to .ts with strong typing: - constants/MediaTypes: as const + derived MediaType union - utils/profiles/{download,subtitles,trackplayer}: typed against @jellyfin/sdk DeviceProfile/SubtitleProfile/CodecProfile models; native.ts now validates its profile with `satisfies DeviceProfile` - trackplayer.d.ts removed (superseded by real TS implementation) - index.js -> index.ts (entry point, "main" is extension-less) - scripts/{typecheck,check-i18n-keys,detect-duplicate-issue} -> .ts, all run via bun (typecheck switched from node to bun) Remove scripts/symlink-native-dirs.js: dead since 446439c2 (2025-02-28) when its only reference (prebuild:tv-new) was dropped; superseded by `expo prebuild --clean` + cross-env EXPO_TV. Drop the matching .gitignore relics (/iostv, /iosmobile, /androidmobile, /androidtv). Document tooling-required .js exceptions (babel/metro/react-native/ tailwind configs) in CLAUDE.md and copilot-instructions.md so code review guidelines stop flagging them. --- .github/copilot-instructions.md | 2 +- .github/workflows/detect-duplicate.yml | 2 +- .gitignore | 4 - CLAUDE.md | 2 +- constants/{MediaTypes.js => MediaTypes.ts} | 8 +- index.js => index.ts | 0 package.json | 6 +- ...check-i18n-keys.mjs => check-i18n-keys.ts} | 79 +++++++++++-------- ...te-issue.mjs => detect-duplicate-issue.ts} | 24 +++--- scripts/symlink-native-dirs.js | 62 --------------- scripts/{typecheck.js => typecheck.ts} | 33 +++++--- utils/profiles/{download.js => download.ts} | 19 +++-- utils/profiles/native.ts | 3 +- utils/profiles/{subtitles.js => subtitles.ts} | 11 +-- utils/profiles/trackplayer.d.ts | 19 ----- .../{trackplayer.js => trackplayer.ts} | 34 ++++---- 16 files changed, 128 insertions(+), 180 deletions(-) rename constants/{MediaTypes.js => MediaTypes.ts} (67%) rename index.js => index.ts (100%) rename scripts/{check-i18n-keys.mjs => check-i18n-keys.ts} (80%) rename scripts/{detect-duplicate-issue.mjs => detect-duplicate-issue.ts} (93%) delete mode 100644 scripts/symlink-native-dirs.js rename scripts/{typecheck.js => typecheck.ts} (88%) rename utils/profiles/{download.js => download.ts} (86%) rename utils/profiles/{subtitles.js => subtitles.ts} (81%) delete mode 100644 utils/profiles/trackplayer.d.ts rename utils/profiles/{trackplayer.js => trackplayer.ts} (77%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2111dd2e6..d3b750c28 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -42,7 +42,7 @@ and provides seamless media streaming with offline capabilities and Chromecast s ## Coding Standards -- Use TypeScript for ALL files (no .js files) +- Use TypeScript for ALL files (no .js files). Tooling-required exceptions: `babel.config.js`, `metro.config.js`, `react-native.config.js`, `tailwind.config.js` (their loaders cannot parse TypeScript) - Use descriptive English names for variables, functions, and components - Prefer functional React components with hooks - Use Jotai atoms for global state management diff --git a/.github/workflows/detect-duplicate.yml b/.github/workflows/detect-duplicate.yml index 265f9efe9..adb4c6cd5 100644 --- a/.github/workflows/detect-duplicate.yml +++ b/.github/workflows/detect-duplicate.yml @@ -29,7 +29,7 @@ jobs: bun-version: latest - name: 🔍 Detect duplicate issues - run: bun scripts/detect-duplicate-issue.mjs + run: bun scripts/detect-duplicate-issue.ts env: GH_TOKEN: ${{ github.token }} GITHUB_REPOSITORY: ${{ github.repository }} diff --git a/.gitignore b/.gitignore index d46c8a6f8..92bdf316d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,10 +12,6 @@ web-build/ # Platform-specific Build Directories /ios /android -/iostv -/iosmobile -/androidmobile -/androidtv # Gradle caches (top-level + per-module native projects) **/.gradle/ diff --git a/CLAUDE.md b/CLAUDE.md index eb2ae87e5..6b3ef5e52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -152,7 +152,7 @@ import { apiAtom } from "@/providers/JellyfinProvider"; ## Coding Standards -- Use TypeScript for all files (no .js) +- Use TypeScript for all files (no .js). Tooling-required exceptions: `babel.config.js`, `metro.config.js`, `react-native.config.js`, `tailwind.config.js` (their loaders cannot parse TypeScript) - Use functional React components with hooks - Use Jotai atoms for global state, React Query for server state - Follow BiomeJS formatting rules (2-space indent, semicolons, LF line endings) diff --git a/constants/MediaTypes.js b/constants/MediaTypes.ts similarity index 67% rename from constants/MediaTypes.js rename to constants/MediaTypes.ts index 2cf275cc2..77d08834a 100644 --- a/constants/MediaTypes.js +++ b/constants/MediaTypes.ts @@ -3,9 +3,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -export default { +const MediaTypes = { Audio: "Audio", Video: "Video", Photo: "Photo", Book: "Book", -}; +} as const; + +export type MediaType = (typeof MediaTypes)[keyof typeof MediaTypes]; + +export default MediaTypes; diff --git a/index.js b/index.ts similarity index 100% rename from index.js rename to index.ts diff --git a/package.json b/package.json index 588ada9ae..ad21deb3d 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,13 @@ "ios:unsigned-build": "cross-env EXPO_TV=0 bun scripts/ios/build-ios.ts --production", "ios:unsigned-build:tv": "cross-env EXPO_TV=1 bun scripts/ios/build-ios.ts --production", "prepare": "husky", - "typecheck": "node scripts/typecheck.js", + "typecheck": "bun scripts/typecheck.ts", "check": "biome check . --max-diagnostics 1000", "lint": "biome check --write --unsafe --max-diagnostics 1000", "format": "biome format --write .", "doctor": "expo-doctor", - "i18n:check": "bun scripts/check-i18n-keys.mjs", - "i18n:fix-unused": "bun scripts/check-i18n-keys.mjs --fix-unused", + "i18n:check": "bun scripts/check-i18n-keys.ts", + "i18n:fix-unused": "bun scripts/check-i18n-keys.ts --fix-unused", "test": "bun run typecheck && bun run lint && bun run format && bun run i18n:check && bun run doctor", "postinstall": "patch-package" }, diff --git a/scripts/check-i18n-keys.mjs b/scripts/check-i18n-keys.ts similarity index 80% rename from scripts/check-i18n-keys.mjs rename to scripts/check-i18n-keys.ts index 0e20e462e..eb201b42e 100644 --- a/scripts/check-i18n-keys.mjs +++ b/scripts/check-i18n-keys.ts @@ -18,11 +18,11 @@ * - Edge cases the static scan cannot see can be allow-listed in the config file. * * Usage: - * bun scripts/check-i18n-keys.mjs # report + exit 1 on missing OR unused - * bun scripts/check-i18n-keys.mjs --unused=warn # exit 1 only on missing; unused = warning - * bun scripts/check-i18n-keys.mjs --unused=off # ignore unused entirely - * bun scripts/check-i18n-keys.mjs --json # machine-readable output - * bun scripts/check-i18n-keys.mjs --fix-unused # remove dead keys from en.json (Crowdin syncs the rest) + * bun scripts/check-i18n-keys.ts # report + exit 1 on missing OR unused + * bun scripts/check-i18n-keys.ts --unused=warn # exit 1 only on missing; unused = warning + * bun scripts/check-i18n-keys.ts --unused=off # ignore unused entirely + * bun scripts/check-i18n-keys.ts --json # machine-readable output + * bun scripts/check-i18n-keys.ts --fix-unused # remove dead keys from en.json (Crowdin syncs the rest) */ import { @@ -34,9 +34,20 @@ import { } from "node:fs"; import { extname, join, relative } from "node:path"; +type LocaleTree = { [key: string]: LocaleTree | string }; + +interface I18nConfig { + localesDir: string; + sourceLocale: string; + srcDirs: string[]; + srcExtensions: string[]; + excludeDirs: string[]; + ignoreUnused: string[]; +} + const ROOT = process.cwd(); const args = process.argv.slice(2); -const flag = (name, def) => { +const flag = (name: string, def: string | boolean): string | boolean => { const a = args.find((x) => x === `--${name}` || x.startsWith(`--${name}=`)); if (!a) return def; const [, v] = a.split("="); @@ -48,7 +59,7 @@ const FIX_UNUSED = !!flag("fix-unused", false); // ---- config ---- const CONFIG_PATH = join(ROOT, "scripts", "i18n-keys.config.json"); -const DEFAULT_CONFIG = { +const DEFAULT_CONFIG: I18nConfig = { localesDir: "translations", sourceLocale: "en", // Scan the whole repo by default so keys referenced outside the obvious dirs @@ -69,29 +80,36 @@ const DEFAULT_CONFIG = { // Keys (or glob-ish prefixes ending with .* or *) known to be used dynamically / externally. ignoreUnused: [], }; -const config = existsSync(CONFIG_PATH) - ? { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(CONFIG_PATH, "utf8")) } +const config: I18nConfig = existsSync(CONFIG_PATH) + ? { + ...DEFAULT_CONFIG, + ...(JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial), + } : DEFAULT_CONFIG; // ---- helpers ---- -const flatten = (obj, prefix = "", out = {}) => { +const flatten = ( + obj: LocaleTree, + prefix = "", + out: Record = {}, +): Record => { for (const [k, v] of Object.entries(obj)) { const key = prefix ? `${prefix}.${k}` : k; if (v && typeof v === "object" && !Array.isArray(v)) flatten(v, key, out); - else out[key] = v; + else out[key] = v as string; } return out; }; -const globMatch = (key, pattern) => { +const globMatch = (key: string, pattern: string): boolean => { if (pattern.endsWith(".*")) return key === pattern.slice(0, -2) || key.startsWith(pattern.slice(0, -1)); if (pattern.endsWith("*")) return key.startsWith(pattern.slice(0, -1)); return key === pattern; }; -const walk = (dir, files = []) => { - let entries; +const walk = (dir: string, files: string[] = []): string[] => { + let entries: string[]; try { entries = readdirSync(dir); } catch { @@ -99,7 +117,7 @@ const walk = (dir, files = []) => { } for (const name of entries) { const full = join(dir, name); - let st; + let st: ReturnType; try { st = statSync(full); } catch { @@ -118,7 +136,7 @@ const walk = (dir, files = []) => { // ---- load source keys ---- const sourcePath = join(ROOT, config.localesDir, `${config.sourceLocale}.json`); const sourceKeys = Object.keys( - flatten(JSON.parse(readFileSync(sourcePath, "utf8"))), + flatten(JSON.parse(readFileSync(sourcePath, "utf8")) as LocaleTree), ); const sourceKeySet = new Set(sourceKeys); @@ -129,16 +147,16 @@ const TPL_DYN_RE = /\bt\(\s*`([^`$]*)\$\{/g; // t(`a.b.${x}`) -> prefix "a.b." const I18NKEY_RE = /\bi18nKey\s*=\s*(?:\{\s*)?(['"])((?:\\.|(?!\1).)+?)\1/g; // const KEY_SHAPE = /^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)+$/; // dotted key, e.g. home.x.y -const usedStatic = new Set(); // keys passed to t(...) / i18nKey — used for MISSING detection -const dynamicPrefixes = new Set(); -const fullyDynamic = []; // { file, line } +const usedStatic = new Set(); // keys passed to t(...) / i18nKey — used for MISSING detection +const dynamicPrefixes = new Set(); +const fullyDynamic: Array<{ file: string; line: number }> = []; let codeBlob = ""; // all (comment-stripped) source text — searched for delimited key literals // Strip comments so keys mentioned in comments (e.g. `// t("old.key")`) are not counted as // usage. Block comments and JSX {/* */} are blanked (preserving newlines for line numbers); // line comments are only stripped when `//` follows start/whitespace/punctuation, which keeps // `://` inside string URLs intact. -const stripComments = (src) => +const stripComments = (src: string): string => src .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")) .replace(/(^|[\s;{}()[\],=>])\/\/[^\n]*/g, (_m, p) => p); @@ -168,11 +186,11 @@ const prefixList = [...dynamicPrefixes]; // the code (covers t("k"), , and keys stored as bare string constants in // arrays/config then resolved via t(variable)), or it is reached via a dynamic prefix, or // explicitly allow-listed. Delimited search avoids substring false-matches (e.g. a.b vs a.b_c). -const literalUsed = (key) => +const literalUsed = (key: string): boolean => codeBlob.includes(`"${key}"`) || codeBlob.includes(`'${key}'`) || codeBlob.includes(`\`${key}\``); -const isUsed = (key) => +const isUsed = (key: string): boolean => literalUsed(key) || prefixList.some((p) => key.startsWith(p)) || config.ignoreUnused.some((g) => globMatch(key, g)); @@ -191,25 +209,22 @@ const missing = [...usedStatic] // keys are static literals in practice; revisit if dynamic key constants become common. // ---- optional fix: strip dead keys from the source locale (en.json) ---- -const removeKey = (obj, parts) => { +const removeKey = (obj: LocaleTree, parts: string[]): void => { const [head, ...rest] = parts; if (!(head in obj)) return; if (rest.length === 0) { delete obj[head]; return; } - removeKey(obj[head], rest); - if ( - obj[head] && - typeof obj[head] === "object" && - Object.keys(obj[head]).length === 0 - ) - delete obj[head]; + const child = obj[head]; + if (!child || typeof child !== "object") return; + removeKey(child, rest); + if (Object.keys(child).length === 0) delete obj[head]; }; if (FIX_UNUSED && unused.length) { // Only edit the SOURCE locale (en.json). Crowdin owns the target locales and removes // the keys from them automatically on the next sync once they disappear from the source. - const data = JSON.parse(readFileSync(sourcePath, "utf8")); + const data = JSON.parse(readFileSync(sourcePath, "utf8")) as LocaleTree; for (const key of unused) removeKey(data, key.split(".")); writeFileSync(sourcePath, `${JSON.stringify(data, null, 2)}\n`); console.log( @@ -259,7 +274,7 @@ if (JSON_OUT) { ); for (const k of unused) console.log(` - ${k}`); console.log( - `\n → remove with: bun scripts/check-i18n-keys.mjs --fix-unused`, + `\n → remove with: bun scripts/check-i18n-keys.ts --fix-unused`, ); console.log( ` → or allow-list a dynamic key in scripts/i18n-keys.config.json ("ignoreUnused").`, diff --git a/scripts/detect-duplicate-issue.mjs b/scripts/detect-duplicate-issue.ts similarity index 93% rename from scripts/detect-duplicate-issue.mjs rename to scripts/detect-duplicate-issue.ts index 26886b265..f4ea467c3 100644 --- a/scripts/detect-duplicate-issue.mjs +++ b/scripts/detect-duplicate-issue.ts @@ -21,8 +21,14 @@ import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; +interface Issue { + number: number; + title: string; + body: string | null; +} + // Parse a numeric env var, falling back to `def` only when unset/empty/NaN so an explicit 0 is honoured. -const numEnv = (name, def) => { +const numEnv = (name: string, def: number): number => { const raw = process.env[name]; if (raw === undefined || raw === "") return def; const n = Number(raw); @@ -51,9 +57,9 @@ const STOP = new Set( ).split(/\s+/), ); -const stem = (w) => w.replace(/(ing|ed|es|s)$/, ""); +const stem = (w: string): string => w.replace(/(ing|ed|es|s)$/, ""); -const tokens = (s) => +const tokens = (s: string | null): string[] => (s || "") .toLowerCase() .replace(/```[\s\S]*?```/g, " ") // drop code blocks @@ -65,7 +71,7 @@ const tokens = (s) => .map(stem) .filter((w) => w.length > 2); -const jaccard = (a, b) => { +const jaccard = (a: string[], b: string[]): number => { const A = new Set(a); const B = new Set(b); if (!A.size || !B.size) return 0; @@ -76,14 +82,14 @@ const jaccard = (a, b) => { const newTitle = tokens(TITLE); const newBody = tokens(BODY); -const score = (o) => +const score = (o: Issue): number => 0.6 * jaccard(newTitle, tokens(o.title)) + 0.4 * jaccard(newBody, tokens(o.body)); // fetch open issues (excluding PRs and the new issue itself) -let issues; +let issues: Issue[]; if (process.env.DUP_FIXTURE) { - issues = JSON.parse(readFileSync(process.env.DUP_FIXTURE, "utf8")); + issues = JSON.parse(readFileSync(process.env.DUP_FIXTURE, "utf8")) as Issue[]; } else { const raw = execFileSync( "gh", @@ -105,7 +111,7 @@ if (process.env.DUP_FIXTURE) { issues = raw .split("\n") .filter(Boolean) - .map((l) => JSON.parse(l)); + .map((l) => JSON.parse(l) as Issue); } const matches = issues @@ -123,7 +129,7 @@ if (!matches.length) { // Neutralise other issues' titles before echoing them back: break @mentions and // strip markdown/HTML control chars so a maliciously-named issue can't ping people // or inject formatting into our comment. GitHub linkifies "#123" on its own. -const safeTitle = (t) => +const safeTitle = (t: string): string => (t || "") .replace(/@/g, "@​") .replace(/[`<>|*_~[\]]/g, " ") diff --git a/scripts/symlink-native-dirs.js b/scripts/symlink-native-dirs.js deleted file mode 100644 index dd014c996..000000000 --- a/scripts/symlink-native-dirs.js +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env node - -const _fs = require("node:fs"); -const path = require("node:path"); -const process = require("node:process"); -const { execSync } = require("node:child_process"); - -const root = process.cwd(); -// const tvosPath = path.join(root, 'iostv'); -// const iosPath = path.join(root, 'iosmobile'); -// const androidPath = path.join(root, 'androidmobile'); -// const androidTVPath = path.join(root, 'androidtv'); -// const device = process.argv[2]; -// const platform = process.argv[2]; -const isTV = process.env.EXPO_TV || false; - -const paths = new Map([ - ["tvos", path.join(root, "iostv")], - ["ios", path.join(root, "iosmobile")], - ["android", path.join(root, "androidmobile")], - ["androidtv", path.join(root, "androidtv")], -]); - -// const platformPath = paths.get(platform); - -if (isTV) { - stdout = execSync( - `mkdir -p ${paths.get("tvos")}; ln -nsf ${paths.get("tvos")} ios`, - ); - console.log(stdout.toString()); - stdout = execSync( - `mkdir -p ${paths.get("androidtv")}; ln -nsf ${paths.get( - "androidtv", - )} android`, - ); - console.log(stdout.toString()); -} else { - stdout = execSync( - `mkdir -p ${paths.get("ios")}; ln -nsf ${paths.get("ios")} ios`, - ); - console.log(stdout.toString()); - stdout = execSync( - `mkdir -p ${paths.get("android")}; ln -nsf ${paths.get("android")} android`, - ); - console.log(stdout.toString()); -} - -// target = ""; -// switch (platform) { -// case "tvos": -// target = "ios"; -// break; -// case "ios": -// target = "ios"; -// break; -// case "android": -// target = "android"; -// break; -// case "androidtv": -// target = "android"; -// break; -// } diff --git a/scripts/typecheck.js b/scripts/typecheck.ts similarity index 88% rename from scripts/typecheck.js rename to scripts/typecheck.ts index ea1f4bead..e8fa495b8 100644 --- a/scripts/typecheck.js +++ b/scripts/typecheck.ts @@ -1,5 +1,8 @@ -const { execFileSync } = require("node:child_process"); -const process = require("node:process"); +import { execFileSync } from "node:child_process"; +import { createRequire } from "node:module"; +import process from "node:process"; + +const require = createRequire(import.meta.url); // Enhanced ANSI color codes and styles const colors = { @@ -32,7 +35,7 @@ const centeredTitle = " ".repeat(titlePadding) + title; const useColor = process.stdout.isTTY && !process.env.NO_COLOR; -function log(message, color = "") { +function log(message: string, color = "") { if (useColor && color) { console.log(`${color}${message}${colors.reset}`); } else { @@ -40,7 +43,7 @@ function log(message, color = "") { } } -function formatError(errorLine) { +function formatError(errorLine: string): string { if (!useColor) return errorLine; // Color file paths in cyan @@ -70,12 +73,15 @@ function formatError(errorLine) { return formatted; } -function parseErrorsAndCreateSummary(errorOutput) { +function parseErrorsAndCreateSummary(errorOutput: string): { + formattedErrors: string[]; + errorsByFile: Map; +} { const lines = errorOutput.split("\n").filter((line) => line.trim()); - const errorsByFile = new Map(); - const formattedErrors = []; + const errorsByFile = new Map(); + const formattedErrors: string[] = []; - let currentError = []; + let currentError: string[] = []; for (const line of lines) { const trimmedLine = line.trim(); @@ -96,7 +102,7 @@ function parseErrorsAndCreateSummary(errorOutput) { if (!errorsByFile.has(filePath)) { errorsByFile.set(filePath, 0); } - errorsByFile.set(filePath, errorsByFile.get(filePath) + 1); + errorsByFile.set(filePath, (errorsByFile.get(filePath) ?? 0) + 1); // Start new error currentError.push(formatError(line)); @@ -119,7 +125,7 @@ function parseErrorsAndCreateSummary(errorOutput) { return { formattedErrors, errorsByFile }; } -function createErrorSummaryTable(errorsByFile) { +function createErrorSummaryTable(errorsByFile: Map): string { if (errorsByFile.size === 0) return ""; const sortedFiles = Array.from(errorsByFile.entries()).sort( @@ -136,12 +142,12 @@ function createErrorSummaryTable(errorsByFile) { return table; } -function runTypeCheck() { +function runTypeCheck(): { ok: boolean } { const extraArgs = process.argv.slice(2); // Prefer local TypeScript binary when available const runnerArgs = ["-p", "tsconfig.json", "--noEmit", ...extraArgs]; - let execArgs = null; + let execArgs: { cmd: string; args: string[] }; try { const tscBin = require.resolve("typescript/bin/tsc"); execArgs = { cmd: process.execPath, args: [tscBin, ...runnerArgs] }; @@ -174,7 +180,8 @@ function runTypeCheck() { ); return { ok: true }; } catch (error) { - const errorOutput = (error && (error.stderr || error.stdout)) || ""; + const execError = error as { stderr?: string; stdout?: string }; + const errorOutput = execError.stderr || execError.stdout || ""; // Filter out jellyseerr utils errors - this is a third-party git submodule // that generates a large volume of known type errors diff --git a/utils/profiles/download.js b/utils/profiles/download.ts similarity index 86% rename from utils/profiles/download.js rename to utils/profiles/download.ts index 18101b2c0..c75d0f580 100644 --- a/utils/profiles/download.js +++ b/utils/profiles/download.ts @@ -3,17 +3,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { generateDeviceProfile } from "./native"; - -/** - * @typedef {"auto" | "stereo" | "5.1" | "passthrough"} AudioTranscodeModeType - */ +import type { + DeviceProfile, + SubtitleProfile, +} from "@jellyfin/sdk/lib/generated-client/models"; +import { type AudioTranscodeModeType, generateDeviceProfile } from "./native"; /** * Download-specific subtitle profiles. * These are more permissive than streaming profiles since we can embed subtitles. */ -const downloadSubtitleProfiles = [ +const downloadSubtitleProfiles: SubtitleProfile[] = [ // Official formats { Format: "vtt", Method: "Encode" }, { Format: "webvtt", Method: "Encode" }, @@ -46,11 +46,10 @@ const downloadSubtitleProfiles = [ /** * Generates a device profile optimized for downloads. * Uses the same audio codec logic as streaming but with download-specific bitrate limits. - * - * @param {AudioTranscodeModeType} [audioMode="auto"] - Audio transcoding mode - * @returns {Object} Jellyfin device profile for downloads */ -export const generateDownloadProfile = (audioMode = "auto") => { +export const generateDownloadProfile = ( + audioMode: AudioTranscodeModeType = "auto", +): DeviceProfile => { // Get the base profile with proper audio codec configuration const baseProfile = generateDeviceProfile({ audioMode }); diff --git a/utils/profiles/native.ts b/utils/profiles/native.ts index 9d7224ff9..3a9336cd7 100644 --- a/utils/profiles/native.ts +++ b/utils/profiles/native.ts @@ -3,6 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import type { DeviceProfile } from "@jellyfin/sdk/lib/generated-client/models"; import { Platform } from "react-native"; import MediaTypes from "../../constants/MediaTypes"; import { getSubtitleProfiles } from "./subtitles"; @@ -193,7 +194,7 @@ export const generateDeviceProfile = (options: ProfileOptions = {}) => { }, ], SubtitleProfiles: getSubtitleProfiles(), - }; + } satisfies DeviceProfile; return profile; }; diff --git a/utils/profiles/subtitles.js b/utils/profiles/subtitles.ts similarity index 81% rename from utils/profiles/subtitles.js rename to utils/profiles/subtitles.ts index 99a9048a0..f1d8af44e 100644 --- a/utils/profiles/subtitles.js +++ b/utils/profiles/subtitles.ts @@ -3,6 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import type { SubtitleProfile } from "@jellyfin/sdk/lib/generated-client/models"; // Image-based formats - these need to be burned in by Jellyfin (Encode method) // because MPV cannot load them externally over HTTP @@ -13,7 +14,7 @@ const IMAGE_BASED_FORMATS = [ "pgssub", "teletext", "vobsub", -]; +] as const; // Text-based formats - these can be loaded externally by MPV const TEXT_BASED_FORMATS = [ @@ -37,10 +38,10 @@ const TEXT_BASED_FORMATS = [ "text", "vplayer", "xsub", -]; +] as const; -export const getSubtitleProfiles = () => { - const profiles = []; +export const getSubtitleProfiles = (): SubtitleProfile[] => { + const profiles: SubtitleProfile[] = []; // Image-based formats: Embed or Encode (burn-in), NOT External for (const format of IMAGE_BASED_FORMATS) { @@ -58,4 +59,4 @@ export const getSubtitleProfiles = () => { }; // Export for use in player filtering -export const IMAGE_SUBTITLE_CODECS = IMAGE_BASED_FORMATS; +export const IMAGE_SUBTITLE_CODECS: readonly string[] = IMAGE_BASED_FORMATS; diff --git a/utils/profiles/trackplayer.d.ts b/utils/profiles/trackplayer.d.ts deleted file mode 100644 index aa2f985fc..000000000 --- a/utils/profiles/trackplayer.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -export type PlatformType = "ios" | "android"; - -export interface TrackPlayerProfileOptions { - /** Target platform */ - platform?: PlatformType; -} - -export function generateTrackPlayerProfile( - options?: TrackPlayerProfileOptions, -): any; - -declare const _default: any; -export default _default; diff --git a/utils/profiles/trackplayer.js b/utils/profiles/trackplayer.ts similarity index 77% rename from utils/profiles/trackplayer.js rename to utils/profiles/trackplayer.ts index 2cbfdf934..4848747b3 100644 --- a/utils/profiles/trackplayer.js +++ b/utils/profiles/trackplayer.ts @@ -3,23 +3,26 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import type { + CodecProfile, + DeviceProfile, + DirectPlayProfile, +} from "@jellyfin/sdk/lib/generated-client/models"; import { Platform } from "react-native"; import MediaTypes from "../../constants/MediaTypes"; -/** - * @typedef {"ios" | "android"} PlatformType - * - * @typedef {Object} TrackPlayerProfileOptions - * @property {PlatformType} [platform] - Target platform - */ +export type PlatformType = "ios" | "android"; + +export interface TrackPlayerProfileOptions { + /** Target platform */ + platform?: PlatformType; +} /** * Audio direct play profiles for react-native-track-player. * iOS uses AVPlayer, Android uses ExoPlayer - each has different codec support. - * - * @param {PlatformType} platform */ -const getDirectPlayProfile = (platform) => { +const getDirectPlayProfile = (platform: PlatformType): DirectPlayProfile => { if (platform === "ios") { // iOS AVPlayer supported formats return { @@ -39,10 +42,8 @@ const getDirectPlayProfile = (platform) => { /** * Audio codec profiles for react-native-track-player. - * - * @param {PlatformType} platform */ -const getCodecProfile = (platform) => { +const getCodecProfile = (platform: PlatformType): CodecProfile => { if (platform === "ios") { // iOS AVPlayer codec constraints return { @@ -64,12 +65,11 @@ const getCodecProfile = (platform) => { * This profile is specifically for standalone audio playback using: * - AVPlayer on iOS * - ExoPlayer on Android - * - * @param {TrackPlayerProfileOptions} [options] - Profile configuration options - * @returns {Object} Jellyfin device profile for track player */ -export const generateTrackPlayerProfile = (options = {}) => { - const platform = options.platform || Platform.OS; +export const generateTrackPlayerProfile = ( + options: TrackPlayerProfileOptions = {}, +): DeviceProfile => { + const platform = (options.platform || Platform.OS) as PlatformType; return { Name: "Track Player",