All files / src/lib retention.ts

100% Statements 25/25
95.45% Branches 21/22
100% Functions 7/7
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62          13x     13x                           13x 28x 19x 28x       27x           13x 21x 21x 21x                             13x 158x 158x 23x 19x 19x 15x    
// SPDX-License-Identifier: GPL-3.0-only
// Copyright (C) 2026 Max Boksem. See NOTICE for additional terms under GPLv3 section 7.
 
import type { DownloadRecord } from "./types";
 
const DAY_SECS = 24 * 60 * 60;
 
/** Choices for "Delete downloads after", in days. 0 keeps downloads until removed. */
export const RETENTION_OPTIONS = [
	{ value: 0, label: "Never" },
	{ value: 7, label: "7 days" },
	{ value: 14, label: "14 days" },
	{ value: 30, label: "30 days" },
	{ value: 90, label: "90 days" },
];
 
/**
 * When the retention setting deletes `rec`, in Unix seconds, or null if it
 * never will. Matches `delete_at` in `crates/core/src/downloads/retention.rs`,
 * which does the deleting: the count starts when the download finished and
 * starts over each time it's played.
 */
export const deleteAt = (rec: DownloadRecord, days: number | null): number | null => {
	if (!days || rec.keep || rec.status !== "completed") return null;
	const since = Math.max(rec.finishedAt ?? rec.createdAt, rec.lastPlayedAt ?? 0);
	return since + days * DAY_SECS;
};
 
/** Whole days from `now` until `at`, rounded up, both in Unix seconds. 0 or less once `at` has passed. */
export const daysUntil = (at: number, now: number): number => Math.ceil((at - now) / DAY_SECS);
 
/**
 * "Deletes in 3 days" for a deletion time `at`, both in Unix seconds. With
 * `many`, the text is about the first of a series' episodes to go.
 */
export const deleteLabel = (at: number, now: number, many = false): string => {
	const days = daysUntil(at, now);
	const when = days <= 0 ? "soon" : days === 1 ? "in 1 day" : `in ${days} days`;
	return many ? `Episodes start deleting ${when}` : `Deletes ${when}`;
};
 
/** `count` is how many finished downloads the summary covers. */
export type RetentionSummary =
	| { state: "none" }
	| { state: "kept"; count: number }
	| { state: "due"; at: number; count: number };
 
/**
 * What the retention setting will do to a set of downloads: a movie, an
 * episode, or every episode of a series. Downloads the user kept are left out,
 * so `due` gives the soonest deletion among the rest. `kept` means every
 * finished download in the set is kept.
 */
export const summarize = (records: DownloadRecord[], days: number | null): RetentionSummary => {
	const finished = records.filter((r) => r.status === "completed");
	if (!days || finished.length === 0) return { state: "none" };
	const dates = finished.map((r) => deleteAt(r, days)).filter((at): at is number => at !== null);
	const count = finished.length;
	if (dates.length === 0) return { state: "kept", count };
	return { state: "due", at: Math.min(...dates), count };
};