mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
chore: migrate oxlint config to e18e plugin and fix lint violations
- Replace root `eslint-plugin-lingui` jsPlugin with `@e18e/eslint-plugin`; move lingui rules into per-app `.oxlintrc.json` overrides for native and web
- Add `jsx-a11y`, `react-hooks-js` (native), and expanded lingui rule sets to per-app configs
- Add `oxc/no-barrel-file` warning at threshold 0 to root config
- Fix `e18e/prefer-url-canparse`: replace `try { new URL() } catch` with `URL.canParse()` in server-url screen and server lib
- Fix `e18e/prefer-timer-args`: pass callback args directly to `setTimeout` instead of wrapping in arrow functions (use-debounce, integration-card)
- Fix `e18e/prefer-static-regex`: hoist `/\/+$/` to module-level constant in server-url screen
- Fix `react-hooks-js/refs` and `react-hooks-js/set-state-in-effect`: convert `useRef` tracking patterns to `useState` + render-time derived updates in `use-server-connection`, `expandable-text`, and settings screen
- Fix `react-hooks-js/immutability`: extract stable palette sub-values before `useMemo` deps in title detail screen
- Apply `e18e` and other rule fixes across core, tmdb, web components, and i18n packages
This commit is contained in:
@@ -338,7 +338,11 @@ export function getRecommendationsFeed(userId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = [...recs.values()].sort((a, b) => b.score - a.score).slice(0, 20);
|
||||
const sorted = recs
|
||||
.values()
|
||||
.toArray()
|
||||
.toSorted((a, b) => b.score - a.score)
|
||||
.slice(0, 20);
|
||||
|
||||
if (sorted.length === 0) return [];
|
||||
|
||||
@@ -495,7 +499,7 @@ export function getUpcomingFeed(
|
||||
// Compute cursor from the last item on this page
|
||||
let nextCursor: string | null = null;
|
||||
if (hasMore && pageItems.length > 0) {
|
||||
const last = pageItems[pageItems.length - 1]!;
|
||||
const last = pageItems.at(-1)!;
|
||||
nextCursor = btoa(JSON.stringify({ d: last.date, n: last.titleName, i: last.titleId }));
|
||||
}
|
||||
|
||||
@@ -579,7 +583,7 @@ export function getRecommendationsForTitle(titleId: string) {
|
||||
tmdb_recommendations: 0,
|
||||
tmdb_similar: 1,
|
||||
} as const;
|
||||
const orderedRecs = [...recs].sort(
|
||||
const orderedRecs = recs.toSorted(
|
||||
(a, b) => a.rank - b.rank || sourcePriority[a.source] - sourcePriority[b.source],
|
||||
);
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ export async function cacheProviderLogos(titleId: string) {
|
||||
}
|
||||
}
|
||||
const checks = await Promise.all(
|
||||
[...uniqueLogos.entries()].map(async ([basename, logoPath]) => ({
|
||||
Array.from(uniqueLogos.entries(), async ([basename, logoPath]) => ({
|
||||
logoPath,
|
||||
cached: await isImageCached("logos", basename),
|
||||
})),
|
||||
@@ -248,7 +248,7 @@ export async function cacheProfilePhotos(titleId: string) {
|
||||
}
|
||||
}
|
||||
const checks = await Promise.all(
|
||||
[...uniqueProfiles.entries()].map(async ([basename, profilePath]) => ({
|
||||
Array.from(uniqueProfiles.entries(), async ([basename, profilePath]) => ({
|
||||
profilePath,
|
||||
cached: await isImageCached("profiles", basename),
|
||||
})),
|
||||
|
||||
@@ -6,6 +6,7 @@ const APP_VERSION = process.env.APP_VERSION || "0.0.0";
|
||||
|
||||
const log = createLogger("update-check");
|
||||
|
||||
const VERSION_PREFIX_RE = /^v/;
|
||||
const PUBLIC_API_URL = process.env.PUBLIC_API_URL || "https://public-api.sofa.watch";
|
||||
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
||||
|
||||
@@ -25,7 +26,7 @@ export function isUpdateCheckEnabled(): boolean {
|
||||
|
||||
/** @internal Returns true if `latest` is strictly newer than `current` using semver comparison. */
|
||||
export function isNewerVersion(latest: string, current: string): boolean {
|
||||
const parse = (v: string) => v.replace(/^v/, "").split(".").map(Number);
|
||||
const parse = (v: string) => v.replace(VERSION_PREFIX_RE, "").split(".").map(Number);
|
||||
const [lMajor = 0, lMinor = 0, lPatch = 0] = parse(latest);
|
||||
const [cMajor = 0, cMinor = 0, cPatch = 0] = parse(current);
|
||||
if (lMajor !== cMajor) return lMajor > cMajor;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import {
|
||||
clearAllTables,
|
||||
@@ -24,6 +24,17 @@ import {
|
||||
getWatchHistory,
|
||||
} from "../src/discovery";
|
||||
|
||||
const TEST_NOW = new Date("2026-03-01T12:00:00Z");
|
||||
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(TEST_NOW);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
clearAllTables();
|
||||
});
|
||||
@@ -56,7 +67,7 @@ describe("getWatchCount", () => {
|
||||
insertUser();
|
||||
insertTitle({ id: "m1", tmdbId: 1 });
|
||||
// Watch from 2 years ago
|
||||
const oldDate = new Date();
|
||||
const oldDate = new Date(TEST_NOW);
|
||||
oldDate.setFullYear(oldDate.getFullYear() - 2);
|
||||
insertMovieWatch("user-1", "m1", oldDate);
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
import { clearAllTables, insertUser } from "@sofa/test/db";
|
||||
|
||||
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T/;
|
||||
|
||||
import {
|
||||
createOrUpdateIntegration,
|
||||
deleteIntegration,
|
||||
@@ -82,7 +84,7 @@ describe("listUserIntegrations", () => {
|
||||
test("serializes dates as ISO strings", () => {
|
||||
createOrUpdateIntegration("user-1", "plex");
|
||||
const result = listUserIntegrations("user-1");
|
||||
expect(result.integrations[0].createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
||||
expect(result.integrations[0].createdAt).toMatch(ISO_DATE_RE);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { clearAllTables, insertUser } from "@sofa/test/db";
|
||||
|
||||
import { setSetting } from "../src/settings";
|
||||
import { isTelemetryEnabled, performTelemetryReport } from "../src/telemetry";
|
||||
|
||||
const TEST_NOW = new Date("2026-03-01T12:00:00Z");
|
||||
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(TEST_NOW);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
clearAllTables();
|
||||
insertUser("user-1");
|
||||
@@ -60,7 +71,7 @@ describe("performTelemetryReport", () => {
|
||||
|
||||
test("respects 24-hour report interval", async () => {
|
||||
setSetting("telemetryEnabled", "true");
|
||||
setSetting("telemetryLastReportedAt", new Date().toISOString());
|
||||
setSetting("telemetryLastReportedAt", TEST_NOW.toISOString());
|
||||
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
||||
await performTelemetryReport();
|
||||
@@ -69,7 +80,7 @@ describe("performTelemetryReport", () => {
|
||||
|
||||
test("reports again after interval expires", async () => {
|
||||
setSetting("telemetryEnabled", "true");
|
||||
const oldDate = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
|
||||
const oldDate = new Date(TEST_NOW.getTime() - 25 * 60 * 60 * 1000).toISOString();
|
||||
setSetting("telemetryLastReportedAt", oldDate);
|
||||
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import {
|
||||
clearAllTables,
|
||||
@@ -12,8 +12,19 @@ import {
|
||||
|
||||
import { getUpcomingFeed } from "../src/discovery";
|
||||
|
||||
const TEST_NOW = new Date("2026-03-01T12:00:00Z");
|
||||
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(TEST_NOW);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function daysFromNow(offset: number): string {
|
||||
const d = new Date();
|
||||
const d = new Date(TEST_NOW);
|
||||
d.setDate(d.getDate() + offset);
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const SIMPLE_PLACEHOLDER_RE = /^[a-zA-Z0-9_]+$/;
|
||||
const ICU_ARG_NAME_RE = /^([a-zA-Z0-9_]+)\s*,/;
|
||||
const NUMBERED_TAG_RE = /<\/?\d+\/?>/g;
|
||||
const LEADING_WS_RE = /^\s*/;
|
||||
const TRAILING_WS_RE = /\s*$/;
|
||||
const EDGE_WS_RE = /^\s|\s$/;
|
||||
|
||||
import type { CatalogType } from "@lingui/conf";
|
||||
import { formatter } from "@lingui/format-po";
|
||||
|
||||
@@ -198,11 +205,11 @@ function extractSimplePlaceholders(text: string): string[] {
|
||||
if (depth === 0 && argStart >= 0) {
|
||||
const inner = text.slice(argStart + 1, i);
|
||||
// Top-level simple placeholder: just an identifier, no comma (not plural/select)
|
||||
if (/^[a-zA-Z0-9_]+$/.test(inner)) {
|
||||
if (SIMPLE_PLACEHOLDER_RE.test(inner)) {
|
||||
results.push(`{${inner}}`);
|
||||
} else {
|
||||
// Complex ICU argument — extract just the argument name before the comma
|
||||
const argName = inner.match(/^([a-zA-Z0-9_]+)\s*,/)?.[1];
|
||||
const argName = inner.match(ICU_ARG_NAME_RE)?.[1];
|
||||
if (argName) results.push(`{${argName}}`);
|
||||
}
|
||||
argStart = -1;
|
||||
@@ -215,15 +222,15 @@ function extractSimplePlaceholders(text: string): string[] {
|
||||
|
||||
function extractNumberedTags(text: string): string[] {
|
||||
// Matches <0>, </0>, <0/>, <12>, </12>, <12/>
|
||||
return [...text.matchAll(/<\/?\d+\/?>/g)].map((m) => m[0]).sort();
|
||||
return Array.from(text.matchAll(NUMBERED_TAG_RE), (m) => m[0]).sort();
|
||||
}
|
||||
|
||||
function leadingWhitespace(text: string): string {
|
||||
return text.match(/^\s*/)?.[0] ?? "";
|
||||
return text.match(LEADING_WS_RE)?.[0] ?? "";
|
||||
}
|
||||
|
||||
function trailingWhitespace(text: string): string {
|
||||
return text.match(/\s*$/)?.[0] ?? "";
|
||||
return text.match(TRAILING_WS_RE)?.[0] ?? "";
|
||||
}
|
||||
|
||||
function validateTranslation(source: string, translated: string): string[] {
|
||||
@@ -246,7 +253,7 @@ function validateTranslation(source: string, translated: string): string[] {
|
||||
}
|
||||
|
||||
// Only enforce whitespace when the source has meaningful edge whitespace
|
||||
if (/^\s|\s$/.test(source)) {
|
||||
if (EDGE_WS_RE.test(source)) {
|
||||
if (leadingWhitespace(source) !== leadingWhitespace(translated)) {
|
||||
issues.push("leading whitespace mismatch");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { i18n } from "./index";
|
||||
|
||||
const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
/** Whether a string looks like a date-only value (no time component). */
|
||||
function isDateOnly(value: string): boolean {
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(value);
|
||||
return DATE_ONLY_RE.test(value);
|
||||
}
|
||||
|
||||
function toDate(date: Date | string): Date {
|
||||
|
||||
@@ -22,6 +22,7 @@ msgstr ""
|
||||
#~ msgid "...and {0} more"
|
||||
#~ msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "...and {remainingErrors} more"
|
||||
msgstr "...and {remainingErrors} more"
|
||||
@@ -145,10 +146,12 @@ msgstr ""
|
||||
msgid "{label} URL regenerated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "{movieCount} movies, {episodeCount} episodes"
|
||||
msgstr "{movieCount} movies, {episodeCount} episodes"
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "{ratingCount} ratings"
|
||||
msgstr "{ratingCount} ratings"
|
||||
@@ -185,6 +188,7 @@ msgstr "{watchedCount}/{episodeCount, plural, one {# episode} other {# episodes}
|
||||
msgid "{watchedEpisodes}/{totalEpisodes, plural, one {# episode} other {# episodes}}"
|
||||
msgstr "{watchedEpisodes}/{totalEpisodes, plural, one {# episode} other {# episodes}}"
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "{watchlistCount} items"
|
||||
msgstr "{watchlistCount} items"
|
||||
@@ -385,6 +389,7 @@ msgstr ""
|
||||
#: apps/native/src/components/settings/integration-card.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
#: apps/web/src/components/settings/backup-section.tsx
|
||||
#: apps/web/src/components/settings/danger-section.tsx
|
||||
@@ -611,6 +616,10 @@ msgstr ""
|
||||
msgid "Could not load person details"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "Could not load title details"
|
||||
msgstr "Could not load title details"
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Create account"
|
||||
msgstr ""
|
||||
@@ -726,6 +735,7 @@ msgstr ""
|
||||
msgid "Don't have an account?"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Done"
|
||||
msgstr ""
|
||||
@@ -747,6 +757,10 @@ msgstr ""
|
||||
msgid "Download backup"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
msgid "Download your library, watch history, and ratings as JSON"
|
||||
msgstr "Download your library, watch history, and ratings as JSON"
|
||||
|
||||
#: apps/native/src/app/person/[id].tsx
|
||||
#: apps/native/src/components/search/recently-viewed-row-content.tsx
|
||||
#: apps/web/src/components/people/person-hero.tsx
|
||||
@@ -819,6 +833,7 @@ msgstr ""
|
||||
msgid "Episode watched"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
#: apps/web/src/components/titles/title-seasons.tsx
|
||||
msgid "Episodes"
|
||||
@@ -857,6 +872,7 @@ msgstr ""
|
||||
#~ msgid "Errors ({0})"
|
||||
#~ msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Errors ({errorCount})"
|
||||
msgstr "Errors ({errorCount})"
|
||||
@@ -874,6 +890,11 @@ msgstr ""
|
||||
msgid "Explore titles"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
msgid "Export"
|
||||
msgstr "Export"
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Failed"
|
||||
msgstr ""
|
||||
@@ -949,6 +970,7 @@ msgstr ""
|
||||
msgid "Failed to mark some episodes"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Failed to parse file"
|
||||
msgstr ""
|
||||
@@ -1063,6 +1085,10 @@ msgstr ""
|
||||
msgid "Finished importing from {source}."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
msgid "Finished importing your Sofa export."
|
||||
msgstr "Finished importing your Sofa export."
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
msgid "Free"
|
||||
msgstr ""
|
||||
@@ -1086,6 +1112,7 @@ msgstr ""
|
||||
#: apps/native/src/app/person/[id].tsx
|
||||
#: apps/native/src/app/person/[id].tsx
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "Go back"
|
||||
msgstr ""
|
||||
|
||||
@@ -1137,18 +1164,22 @@ msgstr ""
|
||||
msgid "Image cache and backup disk usage"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Import {totalItems} items"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Import complete"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Import failed"
|
||||
msgstr ""
|
||||
@@ -1165,15 +1196,23 @@ msgstr ""
|
||||
msgid "Import from {sourceLabel}"
|
||||
msgstr "Import from {sourceLabel}"
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Import is still running in the background. Check back later."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Import options"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
msgid "Import Sofa data"
|
||||
msgstr "Import Sofa data"
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Imported"
|
||||
msgstr ""
|
||||
@@ -1186,6 +1225,14 @@ msgstr ""
|
||||
msgid "Imported {importedCount} items from {sourceLabel}"
|
||||
msgstr "Imported {importedCount} items from {sourceLabel}"
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
msgid "Imported {importedCount} items from Sofa export"
|
||||
msgstr "Imported {importedCount} items from Sofa export"
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
msgid "Importing data"
|
||||
msgstr "Importing data"
|
||||
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Importing from {source}"
|
||||
msgstr ""
|
||||
@@ -1292,14 +1339,23 @@ msgstr ""
|
||||
msgid "Last run succeeded"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
msgid "Library"
|
||||
msgstr "Library"
|
||||
|
||||
#: apps/web/src/components/settings/system-health-section.tsx
|
||||
msgid "Library refresh"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
msgid "Library statuses"
|
||||
msgstr "Library statuses"
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Lost connection to import. Check status in settings."
|
||||
msgstr ""
|
||||
@@ -1404,6 +1460,7 @@ msgid "Movie"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/people/filmography-grid.tsx
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Movies"
|
||||
msgstr ""
|
||||
@@ -1602,6 +1659,10 @@ msgstr ""
|
||||
msgid "Page Not Found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
msgid "Parsing file..."
|
||||
msgstr "Parsing file..."
|
||||
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Parsing..."
|
||||
msgstr ""
|
||||
@@ -1770,6 +1831,8 @@ msgstr ""
|
||||
msgid "Rating removed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Ratings"
|
||||
@@ -1938,6 +2001,10 @@ msgstr ""
|
||||
msgid "Restore failed"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
msgid "Restore from a Sofa export file"
|
||||
msgstr "Restore from a Sofa export file"
|
||||
|
||||
#: apps/web/src/components/settings/backup-restore-section.tsx
|
||||
msgid "Restoring…"
|
||||
msgstr ""
|
||||
@@ -1961,6 +2028,7 @@ msgstr ""
|
||||
msgid "Return home"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Review what was found and choose what to import."
|
||||
msgstr ""
|
||||
@@ -2190,6 +2258,7 @@ msgstr ""
|
||||
msgid "Sign out of other sessions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Skipped"
|
||||
msgstr ""
|
||||
@@ -2217,6 +2286,7 @@ msgstr ""
|
||||
|
||||
#: apps/native/src/app/change-password.tsx
|
||||
#: apps/native/src/app/person/[id].tsx
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/routes/__root.tsx
|
||||
msgid "Something went wrong"
|
||||
@@ -2251,6 +2321,7 @@ msgstr ""
|
||||
msgid "Starting at"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Starting import..."
|
||||
msgstr ""
|
||||
@@ -2291,6 +2362,7 @@ msgstr ""
|
||||
msgid "The title you're looking for doesn't exist or may have been removed from the database."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "This may take a few minutes for large libraries. Please don't close this tab."
|
||||
msgstr ""
|
||||
@@ -2440,6 +2512,7 @@ msgstr ""
|
||||
msgid "Trigger job"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
#: apps/web/src/routes/__root.tsx
|
||||
msgid "Try again"
|
||||
msgstr ""
|
||||
@@ -2591,6 +2664,7 @@ msgstr ""
|
||||
msgid "Waiting for authorization..."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Warnings"
|
||||
msgstr ""
|
||||
@@ -2599,6 +2673,7 @@ msgstr ""
|
||||
#~ msgid "Warnings ({0})"
|
||||
#~ msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Warnings ({warningCount})"
|
||||
msgstr "Warnings ({warningCount})"
|
||||
@@ -2607,10 +2682,12 @@ msgstr "Warnings ({warningCount})"
|
||||
msgid "Watch all"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "Watch history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
msgid "Watch on {name}"
|
||||
msgstr ""
|
||||
|
||||
@@ -111,6 +111,7 @@ function getApiKey() {
|
||||
const DEFAULT_TMDB_BASE = "https://api.themoviedb.org/3";
|
||||
const configuredBase = process.env.TMDB_API_BASE_URL || DEFAULT_TMDB_BASE;
|
||||
const isCustomBase = configuredBase !== DEFAULT_TMDB_BASE;
|
||||
const TMDB_VERSION_PREFIX_RE = /^\/3\//;
|
||||
const baseUrl = configuredBase.replace(/\/3\/?$/, "");
|
||||
|
||||
const baseUrlRewriteMiddleware: Middleware | null = isCustomBase
|
||||
@@ -119,7 +120,7 @@ const baseUrlRewriteMiddleware: Middleware | null = isCustomBase
|
||||
// Custom proxy: strip the /3 prefix from schema paths so requests
|
||||
// go to e.g. https://tmdb.internal/search/multi instead of /3/search/multi
|
||||
const url = new URL(request.url);
|
||||
url.pathname = url.pathname.replace(/^\/3\//, "/");
|
||||
url.pathname = url.pathname.replace(TMDB_VERSION_PREFIX_RE, "/");
|
||||
return new Request(url.toString(), request);
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user