Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 27 additions & 35 deletions preview/settings.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions scripts/render-previews-impl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ function makeStore(
omdbApiKey: "",
adultEnabled: false,
adultScreenshots: true,
adultHistoryVisible: false,
streamActive: false,
castStatus: null,
debridStatus: null,
Expand Down Expand Up @@ -447,6 +448,7 @@ save(
onToggleAdult={() => {}}
onToggleProxy={() => {}}
onToggleAdultScreenshots={() => {}}
onToggleAdultHistoryVisible={() => {}}
/>
</Box>
</Box>
Expand Down
27 changes: 27 additions & 0 deletions src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
resolveReccConfig,
resolveAdultContent,
resolveAdultScreenshots,
resolveAdultHistoryVisible,
defaultConfig,
resolveActiveDebrid,
resolveDebridTokenFor,
Expand Down Expand Up @@ -220,6 +221,32 @@ describe("sanitiseSettingsPatch — adultScreenshots", () => {
});
});

describe("resolveAdultHistoryVisible", () => {
it("defaults to OFF when absent and honours an explicit true", () => {
expect(resolveAdultHistoryVisible({ downloadDir: "/d", trackers: [] })).toBe(false);
expect(
resolveAdultHistoryVisible({ downloadDir: "/d", trackers: [], adultHistoryVisible: false }),
).toBe(false);
expect(
resolveAdultHistoryVisible({ downloadDir: "/d", trackers: [], adultHistoryVisible: true }),
).toBe(true);
});
});

describe("sanitiseSettingsPatch — adultHistoryVisible", () => {
it("coerces to a strict boolean", () => {
expect(sanitiseSettingsPatch({ adultHistoryVisible: false })).toEqual({
adultHistoryVisible: false,
});
expect(sanitiseSettingsPatch({ adultHistoryVisible: true })).toEqual({
adultHistoryVisible: true,
});
expect(sanitiseSettingsPatch({ adultHistoryVisible: "yes" })).toEqual({
adultHistoryVisible: false,
});
});
});

describe("config vpnInterface", () => {
it("round-trips the VPN kill-switch interface", async () => {
await saveConfig({ downloadDir: "/tmp/dl", trackers: [], vpnInterface: "tun0" });
Expand Down
19 changes: 19 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ export interface Config {
// Screenshots pulled from adult torrent descriptions in the preview. Absent =
// ON (adult content is already an explicit opt-in); false turns them off.
adultScreenshots?: boolean;
// Whether adult ("Porn") items appear in the Library and Continue Watching
// lists. Absent/false = OFF (hidden), independent of adultContent (which
// gates search-time results) — a user can search adult content without
// wanting it mixed into their watch history, and items saved before this
// setting existed had no way to be hidden at all.
adultHistoryVisible?: boolean;
// Remembered UI preferences, so torlink reopens the way you left it. Stored
// as opaque strings validated by the UI layer (parseSort/parseSection) so a
// hand-edited or stale value degrades gracefully to the default.
Expand Down Expand Up @@ -263,6 +269,7 @@ export interface RawSettingsPatch {
mediaPlayer?: unknown;
adultContent?: unknown;
adultScreenshots?: unknown;
adultHistoryVisible?: unknown;
proxyDebridStreams?: unknown;
downloadLimitKbps?: unknown;
uploadLimitKbps?: unknown;
Expand Down Expand Up @@ -302,6 +309,8 @@ export function sanitiseSettingsPatch(raw: RawSettingsPatch): Partial<Config> {
}
if (raw.adultContent !== undefined) out.adultContent = raw.adultContent === true;
if (raw.adultScreenshots !== undefined) out.adultScreenshots = raw.adultScreenshots === true;
if (raw.adultHistoryVisible !== undefined)
out.adultHistoryVisible = raw.adultHistoryVisible === true;
if (raw.proxyDebridStreams !== undefined) out.proxyDebridStreams = raw.proxyDebridStreams === true;
if (raw.downloadLimitKbps !== undefined) out.downloadLimitKbps = positiveInt(raw.downloadLimitKbps);
if (raw.uploadLimitKbps !== undefined) out.uploadLimitKbps = positiveInt(raw.uploadLimitKbps);
Expand Down Expand Up @@ -426,6 +435,16 @@ export function resolveAdultScreenshots(config: Config): boolean {
return config.adultScreenshots !== false;
}

// Whether adult items show in Library/Continue Watching. Default OFF: unlike
// adultScreenshots (which only ever appears once adultContent is already an
// explicit opt-in), history items can predate the user ever having touched
// adultContent, so this must not surprise anyone browsing history for the
// first time after this feature ships. No env override: a plain preference,
// not host config.
export function resolveAdultHistoryVisible(config: Config): boolean {
return config.adultHistoryVisible === true;
}

const RECC_URL_ENV = "TORLINK_RECC_URL";
const RECC_TOKEN_ENV = "TORLINK_RECC_TOKEN";

Expand Down
74 changes: 73 additions & 1 deletion src/sources/registry.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { describe, it, expect } from "vitest";
import { SOURCES, enabledSources, sourcesByGroup, toggleDisabledSource } from "./registry";
import {
SOURCES,
enabledSources,
sourcesByGroup,
toggleDisabledSource,
categoryForSource,
isAdultSource,
visibleWithAdultHistory,
nonEmptyCategories,
} from "./registry";

// Adult sources are hidden unless the adult flag is passed, so the default
// enabled set is the non-adult subset.
Expand Down Expand Up @@ -60,6 +69,69 @@ describe("toggleDisabledSource", () => {
});
});

describe("categoryForSource", () => {
it("returns the source's first group", () => {
expect(categoryForSource("yts")).toBe("Movies");
expect(categoryForSource("tpb-porn")).toBe("Porn");
// bittorrented has multiple groups; the first is the documented pick.
expect(categoryForSource("bittorrented")).toBe("Movies");
});

it("returns Unknown for an unrecognised or missing source id", () => {
expect(categoryForSource("not-a-source" as never)).toBe("Unknown");
expect(categoryForSource(undefined)).toBe("Unknown");
});
});

describe("isAdultSource", () => {
it("is true only for adult sources", () => {
expect(isAdultSource("tpb-porn")).toBe(true);
expect(isAdultSource("x1337-porn")).toBe(true);
expect(isAdultSource("yts")).toBe(false);
});

it("treats an unrecognised or missing source id as non-adult", () => {
expect(isAdultSource("not-a-source" as never)).toBe(false);
expect(isAdultSource(undefined)).toBe(false);
});
});

describe("visibleWithAdultHistory", () => {
const items = [{ source: "yts" as const }, { source: "tpb-porn" as const }, { source: undefined }];

it("drops adult items when the flag is off", () => {
expect(visibleWithAdultHistory(items, false)).toEqual([
{ source: "yts" },
{ source: undefined },
]);
});

it("keeps every item when the flag is on", () => {
expect(visibleWithAdultHistory(items, true)).toEqual(items);
});

it("does not mutate the input", () => {
const input = [...items];
visibleWithAdultHistory(input, false);
expect(input).toEqual(items);
});
});

describe("nonEmptyCategories", () => {
it("always keeps All first, even with no items", () => {
expect(nonEmptyCategories([])).toEqual(["All"]);
});

it("keeps only categories that are present, in fixed order", () => {
expect(nonEmptyCategories(["Porn", "TV", "TV", "Movies"])).toEqual([
"All",
"Movies",
"TV",
"Porn",
]);
});
});

describe("RuTracker sources", () => {
it("includes the six RuTracker sources", () => {
const ids = SOURCES.map((s) => s.id);
Expand Down
Loading