feat(tracking): remove a single watch and log a watch on a date

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Sonnet 5.5 committed 2026-10-05 21:50:56 -04:00
1 parent 4669d3b76d
commit cb5e0c19e7
30 files changed
+1715 -94

No files matched your search

@@ -8,7 +8,11 @@ import type { z } from "zod";
import { Image } from "@/components/ui/image";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text";
import { getErrorMessage } from "@/lib/error-messages";
import { formatLocalTimeOfDay } from "@/lib/format-time";
import { client } from "@/lib/orpc";
import { invalidateTitleQueries } from "@/lib/title-actions";
import { toast } from "@/lib/toast";
import type { WatchHistoryItemSchema } from "@sofa/api/schemas";
type WatchHistoryItem = z.infer<typeof WatchHistoryItemSchema>;
@@ -36,46 +40,66 @@ export function HistoryRow({ item }: { item: WatchHistoryItem }) {
const time = formatLocalTimeOfDay(new Date(item.watchedAt));
const accessibilityLabel = [item.title.title, subtitle, time].filter(Boolean).join(", ");
async function removeFromHistory() {
try {
await client.tracking.deleteWatch({ kind: item.kind, watchId: item.watchId });
toast.success(t`Removed from history`);
await invalidateTitleQueries();
} catch (err) {
toast.error(getErrorMessage(err, t`Failed to remove from history`));
}
}
return (
<Link href={`/title/${item.title.id}`} asChild>
<Pressable
accessibilityRole="link"
accessibilityLabel={accessibilityLabel}
className="bg-card/40 flex-row items-center gap-3 rounded-xl border border-white/[0.06] px-3 py-3"
>
<View className="overflow-hidden rounded-lg" style={{ width: 44, height: 66 }}>
{item.title.posterPath ? (
<Image
source={{ uri: item.title.posterPath }}
thumbHash={item.title.posterThumbHash}
className="size-full"
contentFit="cover"
/>
) : (
<View className="bg-muted size-full items-center justify-center">
<ScaledIcon icon={IconMovie} size={18} color={mutedColor} />
</View>
)}
</View>
<View className="min-w-0 flex-1">
<Text className="text-foreground text-sm font-medium" numberOfLines={1}>
{item.title.title}
</Text>
{subtitle && (
<Text className="text-muted-foreground mt-1 text-xs" numberOfLines={1}>
{subtitle}
</Text>
)}
</View>
<View className="items-end gap-1">
<Text className="text-muted-foreground text-xs">{time}</Text>
<View className="bg-muted rounded px-1.5 py-0.5">
<Text className="text-muted-foreground text-[10px] font-medium tracking-wider uppercase">
{sourceLabels[item.source]}
</Text>
<Link.Trigger>
<Pressable
accessibilityRole="link"
accessibilityLabel={accessibilityLabel}
className="bg-card/40 flex-row items-center gap-3 rounded-xl border border-white/[0.06] px-3 py-3"
>
<View className="overflow-hidden rounded-lg" style={{ width: 44, height: 66 }}>
{item.title.posterPath ? (
<Image
source={{ uri: item.title.posterPath }}
thumbHash={item.title.posterThumbHash}
className="size-full"
contentFit="cover"
/>
) : (
<View className="bg-muted size-full items-center justify-center">
<ScaledIcon icon={IconMovie} size={18} color={mutedColor} />
</View>
)}
</View>
</View>
</Pressable>
<View className="min-w-0 flex-1">
<Text className="text-foreground text-sm font-medium" numberOfLines={1}>
{item.title.title}
</Text>
{subtitle && (
<Text className="text-muted-foreground mt-1 text-xs" numberOfLines={1}>
{subtitle}
</Text>
)}
</View>
<View className="items-end gap-1">
<Text className="text-muted-foreground text-xs">{time}</Text>
<View className="bg-muted rounded px-1.5 py-0.5">
<Text className="text-muted-foreground text-[10px] font-medium tracking-wider uppercase">
{sourceLabels[item.source]}
</Text>
</View>
</View>
</Pressable>
</Link.Trigger>
<Link.Menu>
<Link.MenuAction
title={t`Remove from history`}
icon="trash"
destructive
onPress={removeFromHistory}
/>
</Link.Menu>
</Link>
);
}
@@ -6,11 +6,13 @@ import { getWatchCount, getWatchHistory } from "@sofa/core/discovery";
import { getOrFetchTitleByTmdbId } from "@sofa/core/metadata";
import {
getDisplayStatusesByTitleIds,
deleteWatch as deleteWatchRecord,
getUserTitleInfo,
listWatchHistory,
logEpisodeWatch,
logEpisodeWatchBatch,
logMovieWatch,
logWatchAt,
markAllEpisodesWatched,
quickAddTitle,
rateTitleStars,
@@ -142,3 +144,24 @@ export const history = os.tracking.history.use(authed).handler(({ input, context
nextCursor: result.nextCursor,
};
});
export const deleteWatch = os.tracking.deleteWatch.use(authed).handler(({ input, context }) => {
if (!deleteWatchRecord(context.user.id, input.kind, input.watchId)) {
throw new ORPCError("NOT_FOUND", {
message: "Watch not found",
data: { code: AppErrorCode.WATCH_NOT_FOUND },
});
}
});
export const logWatch = os.tracking.logWatch.use(authed).handler(({ input, context }) => {
const result = logWatchAt(context.user.id, input.kind, input.id, new Date(input.watchedAt));
if (result === "not_found") {
const code =
input.kind === "movie" ? AppErrorCode.TITLE_NOT_FOUND : AppErrorCode.EPISODE_NOT_FOUND;
throw new ORPCError("NOT_FOUND", {
message: code === AppErrorCode.TITLE_NOT_FOUND ? "Title not found" : "Episode not found",
data: { code },
});
}
});
+2
View File
@@ -22,6 +22,8 @@ export const implementedRouter = {
userInfo: tracking.userInfo,
stats: tracking.stats,
history: tracking.history,
deleteWatch: tracking.deleteWatch,
logWatch: tracking.logWatch,
},
library: {
list: library.list,
+77
View File
@@ -0,0 +1,77 @@
import { call } from "@orpc/server";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { auth } from "@sofa/auth/server";
import { userMovieWatches } from "@sofa/db/schema";
import { clearAllTables, insertTitle, insertUser, testDb } from "@sofa/test/db";
import { implementedRouter } from "../src/orpc/router";
vi.mock("@sofa/auth/server", () => ({
auth: {
api: { getSession: vi.fn<() => void>() },
handler: vi.fn<() => void>(),
},
}));
const getSession = vi.mocked(auth.api.getSession);
const ctx = { context: { headers: new Headers() } };
function sessionFor(userId: string) {
return {
session: { id: `session-${userId}`, userId },
user: { id: userId, role: "user", name: "U", email: `${userId}@example.com` },
} as never;
}
beforeEach(() => {
clearAllTables();
insertUser("user-1");
insertUser("user-2");
insertTitle({ id: "m1", tmdbId: 1, title: "Movie One" });
getSession.mockReset();
getSession.mockResolvedValue(sessionFor("user-1"));
});
describe("tracking.deleteWatch", () => {
test("another user's watch returns NOT_FOUND with WATCH_NOT_FOUND", async () => {
testDb
.insert(userMovieWatches)
.values({
id: "w1",
userId: "user-2",
titleId: "m1",
watchedAt: new Date(),
source: "manual",
})
.run();
await expect(
call(implementedRouter.tracking.deleteWatch, { kind: "movie", watchId: "w1" }, ctx),
).rejects.toMatchObject({ code: "NOT_FOUND", data: { code: "WATCH_NOT_FOUND" } });
expect(testDb.select().from(userMovieWatches).all()).toHaveLength(1);
});
});
describe("tracking.logWatch", () => {
test("rejects a future watchedAt", async () => {
const future = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
await expect(
call(
implementedRouter.tracking.logWatch,
{ kind: "movie", id: "m1", watchedAt: future },
ctx,
),
).rejects.toMatchObject({ code: "BAD_REQUEST" });
expect(testDb.select().from(userMovieWatches).all()).toHaveLength(0);
});
test("logs a past watch", async () => {
await call(
implementedRouter.tracking.logWatch,
{ kind: "movie", id: "m1", watchedAt: "2024-03-01T12:00:00.000Z" },
ctx,
);
expect(testDb.select().from(userMovieWatches).all()).toHaveLength(1);
});
});
+77 -38
View File
@@ -1,16 +1,26 @@
import { useLingui } from "@lingui/react/macro";
import { IconMovie } from "@tabler/icons-react";
import { IconDots, IconMovie, IconTrash } from "@tabler/icons-react";
import { Link } from "@tanstack/react-router";
import type { z } from "zod";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { thumbHashToUrl } from "@/lib/thumbhash";
import type { WatchHistoryItemSchema } from "@sofa/api/schemas";
import { formatDate } from "@sofa/i18n/format";
import { useDeleteWatch } from "./use-delete-watch";
type WatchHistoryItem = z.infer<typeof WatchHistoryItemSchema>;
export function HistoryRow({ item }: { item: WatchHistoryItem }) {
const { t } = useLingui();
const { deleteWatch, pendingId } = useDeleteWatch();
const sourceLabels = {
manual: t`Manual`,
@@ -37,42 +47,71 @@ export function HistoryRow({ item }: { item: WatchHistoryItem }) {
});
return (
<Link
to="/titles/$id"
params={{ id: item.title.id }}
className="group bg-card/40 hover:bg-card/60 hover:ring-primary/25 flex items-center gap-4 rounded-xl p-3 ring-1 ring-white/[0.06] transition-[background,ring-color] duration-200"
>
<div className="relative h-[66px] w-11 shrink-0 overflow-hidden rounded-lg ring-1 ring-white/[0.06]">
{item.title.posterPath ? (
<img
src={item.title.posterPath}
alt=""
className="size-full object-cover"
loading="lazy"
{...(item.title.posterThumbHash
? {
style: {
background: `url(${thumbHashToUrl(item.title.posterThumbHash)}) center/cover`,
},
}
: {})}
/>
) : (
<div className="bg-muted flex size-full items-center justify-center">
<IconMovie className="text-muted-foreground size-5" />
</div>
)}
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">{item.title.title}</div>
{subtitle && <div className="text-muted-foreground mt-1 truncate text-xs">{subtitle}</div>}
</div>
<div className="flex shrink-0 flex-col items-end gap-1">
<span className="text-muted-foreground text-xs tabular-nums">{time}</span>
<span className="bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wider uppercase">
{sourceLabels[item.source]}
</span>
</div>
</Link>
<div className="relative">
<Link
to="/titles/$id"
params={{ id: item.title.id }}
className="group bg-card/40 hover:bg-card/60 hover:ring-primary/25 flex items-center gap-4 rounded-xl p-3 pe-12 ring-1 ring-white/[0.06] transition-[background,ring-color] duration-200"
>
<div className="relative h-[66px] w-11 shrink-0 overflow-hidden rounded-lg ring-1 ring-white/[0.06]">
{item.title.posterPath ? (
<img
src={item.title.posterPath}
alt=""
className="size-full object-cover"
loading="lazy"
{...(item.title.posterThumbHash
? {
style: {
background: `url(${thumbHashToUrl(item.title.posterThumbHash)}) center/cover`,
},
}
: {})}
/>
) : (
<div className="bg-muted flex size-full items-center justify-center">
<IconMovie className="text-muted-foreground size-5" />
</div>
)}
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">{item.title.title}</div>
{subtitle && (
<div className="text-muted-foreground mt-1 truncate text-xs">{subtitle}</div>
)}
</div>
<div className="flex shrink-0 flex-col items-end gap-1">
<span className="text-muted-foreground text-xs tabular-nums">{time}</span>
<span className="bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wider uppercase">
{sourceLabels[item.source]}
</span>
</div>
</Link>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="icon-sm"
aria-label={t`More actions`}
className="absolute end-2 top-1/2 -translate-y-1/2"
>
<IconDots aria-hidden={true} />
</Button>
}
/>
<DropdownMenuContent align="end">
<DropdownMenuItem
variant="destructive"
className="cursor-pointer"
disabled={pendingId !== null}
onClick={() => deleteWatch(item.kind, item.watchId)}
>
<IconTrash aria-hidden={true} />
{t`Remove from history`}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
@@ -0,0 +1,35 @@
import { useLingui } from "@lingui/react/macro";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { getErrorMessage } from "@/lib/error-messages";
import { orpc } from "@/lib/orpc/client";
import { invalidateTrackingQueries } from "@/lib/orpc/invalidate";
/** Remove a single watch from history, with toast + cache refresh. */
export function useDeleteWatch() {
const { t } = useLingui();
const queryClient = useQueryClient();
// Handlers live on the hook-level options so the mutation stays pending until
// the tracking queries have refetched, which blocks duplicate clicks.
const { mutate, variables, isPending } = useMutation(
orpc.tracking.deleteWatch.mutationOptions({
onSuccess: async () => {
toast.success(t`Removed from history`);
await invalidateTrackingQueries(queryClient);
},
onError: (err) => {
toast.error(getErrorMessage(err, t`Failed to remove from history`));
},
}),
);
function deleteWatch(kind: "movie" | "episode", watchId: string) {
if (isPending) return;
mutate({ kind, watchId });
}
const pendingId = isPending ? (variables?.watchId ?? null) : null;
return { deleteWatch, pendingId };
}
@@ -0,0 +1,118 @@
import { useLingui } from "@lingui/react/macro";
import { IconHistory } from "@tabler/icons-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { getErrorMessage } from "@/lib/error-messages";
import { orpc } from "@/lib/orpc/client";
import { invalidateTrackingQueries } from "@/lib/orpc/invalidate";
function todayLocal() {
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
return `${now.getFullYear()}-${month}-${day}`;
}
/** Log a watch of a movie on a chosen date (and optional time). */
export function LogWatchDialog({ titleId }: { titleId: string }) {
const { t } = useLingui();
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [date, setDate] = useState(todayLocal);
const [time, setTime] = useState("");
const { mutate, isPending } = useMutation(
orpc.tracking.logWatch.mutationOptions({
onSuccess: async () => {
toast.success(t`Watch logged`);
setOpen(false);
await invalidateTrackingQueries(queryClient);
},
onError: (err) => {
toast.error(getErrorMessage(err, t`Failed to log watch`));
},
}),
);
function handleOpenChange(next: boolean) {
if (next) {
setDate(todayLocal());
setTime("");
}
setOpen(next);
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!date || isPending) return;
// Date-only watches land at local noon, matching imported date-only watches.
const watchedAt = new Date(`${date}T${time || "12:00"}:00`);
if (Number.isNaN(watchedAt.getTime())) return;
mutate({ kind: "movie", id: titleId, watchedAt: watchedAt.toISOString() });
}
return (
<>
<Button
type="button"
variant="outline"
size="lg"
className="h-9 rounded-lg px-4 text-sm active:scale-[0.97]"
onClick={() => handleOpenChange(true)}
>
<IconHistory aria-hidden={true} className="size-3.5" />
{t`Log a watch…`}
</Button>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent>
<form onSubmit={handleSubmit} className="grid gap-4">
<DialogHeader>
<DialogTitle>{t`Log a watch`}</DialogTitle>
<DialogDescription>{t`Record when you watched this.`}</DialogDescription>
</DialogHeader>
<div className="grid gap-3">
<div className="grid gap-1.5">
<Label htmlFor="log-watch-date">{t`Date`}</Label>
<Input
id="log-watch-date"
type="date"
required
value={date}
max={todayLocal()}
onChange={(e) => setDate(e.target.value)}
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor="log-watch-time">{t`Time (optional)`}</Label>
<Input
id="log-watch-time"
type="time"
value={time}
onChange={(e) => setTime(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={isPending || !date}>
{t`Log watch`}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
);
}
@@ -4,13 +4,14 @@ import { IconCheck, IconX } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { LogWatchDialog } from "./log-watch-dialog";
import { StarRating } from "./star-rating";
import { StatusButton } from "./status-button";
import { useTitleContext, useTitleUserInfo } from "./title-context";
import { useTitleActions } from "./use-title-actions";
export function TitleActions() {
const { titleType } = useTitleContext();
const { titleType, titleId } = useTitleContext();
const { userStatus, userRating } = useTitleUserInfo();
const { handleStatusChange, handleRating, handleWatchMovie, handleUnwatchMovie } =
useTitleActions();
@@ -39,6 +40,7 @@ export function TitleActions() {
)}
</Button>
)}
{titleType === "movie" && <LogWatchDialog titleId={titleId} />}
<Separator orientation="vertical" className="bg-border/50 mx-0.5 my-auto h-6" />
<StarRating value={userRating ?? 0} onChange={handleRating} />
</div>
@@ -0,0 +1,27 @@
---
title: Remove one watch
full: true
_openapi:
preload:
- ./public/openapi.json
method: POST
webhook: false
toc: []
structuredData:
headings: []
contents:
- content: Remove a single watch record (one play of a movie or episode) belonging
to the current user.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
export default function Layout(props) {
const { OpenAPIPage } = props.components ?? {};
return (
<>
{props.children}
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/tracking/history/delete","method":"post"}]} showDescription />
</>
);
}
@@ -0,0 +1,28 @@
---
title: Log a watch at a date
full: true
_openapi:
preload:
- ./public/openapi.json
method: POST
webhook: false
toc: []
structuredData:
headings: []
contents:
- content: Log a watch of a movie (title ID) or an episode (episode ID) at a
specific date and time. Dates in the future or before 1900 are
rejected.
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
export default function Layout(props) {
const { OpenAPIPage } = props.components ?? {};
return (
<>
{props.children}
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/tracking/log","method":"post"}]} showDescription />
</>
);
}
+252
View File
@@ -2533,6 +2533,258 @@
]
}
},
"/tracking/history/delete": {
"post": {
"operationId": "tracking.deleteWatch",
"summary": "Remove one watch",
"description": "Remove a single watch record (one play of a movie or episode) belonging to the current user.",
"tags": [
"Tracking"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"kind": {
"enum": [
"movie",
"episode"
],
"type": "string",
"description": "Whether the watch is a movie or an episode"
},
"watchId": {
"type": "string",
"minLength": 1,
"description": "Watch record ID (from the history list)"
}
},
"required": [
"kind",
"watchId"
],
"description": "Identifies a single watch record to remove"
}
}
}
},
"responses": {
"200": {
"description": "OK"
},
"404": {
"description": "404",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"type": "object",
"properties": {
"defined": {
"const": true
},
"code": {
"const": "NOT_FOUND"
},
"status": {
"const": 404
},
"message": {
"type": "string",
"default": "Watch not found"
},
"data": {
"type": "object",
"properties": {
"code": {
"const": "WATCH_NOT_FOUND"
}
},
"required": [
"code"
]
}
},
"required": [
"defined",
"code",
"status",
"message",
"data"
]
},
{
"type": "object",
"properties": {
"defined": {
"const": false
},
"code": {
"type": "string"
},
"status": {
"type": "number"
},
"message": {
"type": "string"
},
"data": {}
},
"required": [
"defined",
"code",
"status",
"message"
]
}
]
}
}
}
}
},
"security": [
{
"session": []
}
]
}
},
"/tracking/log": {
"post": {
"operationId": "tracking.logWatch",
"summary": "Log a watch at a date",
"description": "Log a watch of a movie (title ID) or an episode (episode ID) at a specific date and time. Dates in the future or before 1900 are rejected.",
"tags": [
"Tracking"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"kind": {
"enum": [
"movie",
"episode"
],
"type": "string",
"description": "Whether id is a movie title ID or an episode ID"
},
"id": {
"type": "string",
"minLength": 1,
"description": "Movie title ID or episode ID"
},
"watchedAt": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of when it was watched"
}
},
"required": [
"kind",
"id",
"watchedAt"
],
"description": "A watch to log at a specific date and time"
}
}
}
},
"responses": {
"200": {
"description": "OK"
},
"404": {
"description": "404",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"type": "object",
"properties": {
"defined": {
"const": true
},
"code": {
"const": "NOT_FOUND"
},
"status": {
"const": 404
},
"message": {
"type": "string",
"default": "Title or episode not found"
},
"data": {
"type": "object",
"properties": {
"code": {
"enum": [
"TITLE_NOT_FOUND",
"EPISODE_NOT_FOUND"
],
"type": "string"
}
},
"required": [
"code"
]
}
},
"required": [
"defined",
"code",
"status",
"message",
"data"
]
},
{
"type": "object",
"properties": {
"defined": {
"const": false
},
"code": {
"type": "string"
},
"status": {
"type": "number"
},
"message": {
"type": "string"
},
"data": {}
},
"required": [
"defined",
"code",
"status",
"message"
]
}
]
}
}
}
}
},
"security": [
{
"session": []
}
]
}
},
"/library": {
"get": {
"operationId": "library.list",
+38
View File
@@ -11,6 +11,7 @@ import {
ContinueWatchingOutput,
CreateImportJobInput,
CreateIntegrationInput,
DeleteWatchInput,
DiscoverInput,
DiscoverOutput,
DiscoverRecommendationsOutput,
@@ -26,6 +27,7 @@ import {
LibraryListInput,
LibraryListOutput,
LibraryStatsOutput,
LogWatchInput,
MediaTypeParam,
PageParam,
PaginatedInput,
@@ -197,6 +199,42 @@ export const contract = {
})
.input(WatchHistoryListInput)
.output(WatchHistoryListOutput),
deleteWatch: oc
.route({
method: "POST",
path: "/tracking/history/delete",
tags: ["Tracking"],
summary: "Remove one watch",
description:
"Remove a single watch record (one play of a movie or episode) belonging to the current user.",
})
.input(DeleteWatchInput)
.output(z.void())
.errors({
NOT_FOUND: {
message: "Watch not found",
data: appErrorData(AppErrorCode.WATCH_NOT_FOUND),
},
}),
logWatch: oc
.route({
method: "POST",
path: "/tracking/log",
tags: ["Tracking"],
summary: "Log a watch at a date",
description:
"Log a watch of a movie (title ID) or an episode (episode ID) at a specific date and time. Dates in the future or before 1900 are rejected.",
})
.input(LogWatchInput)
.output(z.void())
.errors({
NOT_FOUND: {
message: "Title or episode not found",
data: z.object({
code: z.enum([AppErrorCode.TITLE_NOT_FOUND, AppErrorCode.EPISODE_NOT_FOUND]),
}),
},
}),
},
// ─── Library ────────────────────────────────────────────────
+2
View File
@@ -2,6 +2,8 @@ import { z } from "zod";
export const AppErrorCode = {
TITLE_NOT_FOUND: "TITLE_NOT_FOUND",
EPISODE_NOT_FOUND: "EPISODE_NOT_FOUND",
WATCH_NOT_FOUND: "WATCH_NOT_FOUND",
PERSON_NOT_FOUND: "PERSON_NOT_FOUND",
INTEGRATION_NOT_FOUND: "INTEGRATION_NOT_FOUND",
BACKUP_NOT_FOUND: "BACKUP_NOT_FOUND",
+25
View File
@@ -147,6 +147,31 @@ export const WatchHistoryListInput = z
})
.meta({ description: "Filters and pagination for watch history" });
const MIN_WATCHED_AT_MS = Date.UTC(1900, 0, 1);
const MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
export const DeleteWatchInput = z
.object({
kind: z.enum(["movie", "episode"]).describe("Whether the watch is a movie or an episode"),
watchId: z.string().min(1).describe("Watch record ID (from the history list)"),
})
.meta({ description: "Identifies a single watch record to remove" });
export const LogWatchInput = z
.object({
kind: z.enum(["movie", "episode"]).describe("Whether id is a movie title ID or an episode ID"),
id: z.string().min(1).describe("Movie title ID or episode ID"),
watchedAt: z
.string()
.datetime({ offset: true })
.refine((value) => {
const ms = Date.parse(value);
return ms >= MIN_WATCHED_AT_MS && ms <= Date.now() + MAX_FUTURE_SKEW_MS;
}, "watchedAt must be between 1900-01-01 and now")
.describe("ISO 8601 timestamp of when it was watched"),
})
.meta({ description: "A watch to log at a specific date and time" });
export const WatchHistoryItemSchema = z
.object({
watchId: z.string().describe("Watch record ID"),
+59 -18
View File
@@ -4,9 +4,12 @@ import {
batchInsertEpisodeWatchesTransaction,
batchInsertMissingEpisodeWatches,
countDistinctEpisodeWatches,
countMovieWatches,
deleteAllEpisodeWatchesForTitle,
deleteEpisodeWatch,
deleteEpisodeWatchById,
deleteEpisodeWatches,
deleteMovieWatchById,
deleteMovieWatches,
deleteRating,
deleteTitleStatus,
@@ -124,12 +127,8 @@ export function markAllEpisodesWatched(
setTitleStatus(userId, titleId, "in_progress", source);
}
export function unwatchEpisode(userId: string, episodeId: string) {
deleteEpisodeWatch(userId, episodeId);
const titleId = getEpisodeTitleId(episodeId);
if (!titleId) return;
/** After removing episode watches: an in-progress show with none left goes back to watchlist. */
function downgradeShowIfNoEpisodeWatches(userId: string, titleId: string) {
const existing = getTitleStatus(userId, titleId);
if (!existing || existing.status !== "in_progress") return;
@@ -141,6 +140,23 @@ export function unwatchEpisode(userId: string, episodeId: string) {
}
}
/** After removing all movie watches: a tracked movie falls back to watchlist. */
function resetMovieToWatchlist(userId: string, titleId: string) {
const existing = getTitleStatus(userId, titleId);
if (existing && existing.status !== "watchlist") {
setTitleStatus(userId, titleId, "watchlist");
}
}
export function unwatchEpisode(userId: string, episodeId: string) {
deleteEpisodeWatch(userId, episodeId);
const titleId = getEpisodeTitleId(episodeId);
if (!titleId) return;
downgradeShowIfNoEpisodeWatches(userId, titleId);
}
export function unwatchSeason(userId: string, seasonId: string) {
const epIds = getSeasonEpisodeIds(seasonId);
if (epIds.length > 0) {
@@ -150,24 +166,49 @@ export function unwatchSeason(userId: string, seasonId: string) {
const season = getSeasonById(seasonId);
if (!season) return;
const existing = getTitleStatus(userId, season.titleId);
if (!existing || existing.status !== "in_progress") return;
// If no episodes remain watched, downgrade to watchlist
const allEpIds = getAllEpisodeIdsForTitle(season.titleId);
const watchCount = countDistinctEpisodeWatches(userId, allEpIds);
if (watchCount === 0) {
setTitleStatus(userId, season.titleId, "watchlist");
}
downgradeShowIfNoEpisodeWatches(userId, season.titleId);
}
export function unwatchMovie(userId: string, titleId: string) {
deleteMovieWatches(userId, titleId);
resetMovieToWatchlist(userId, titleId);
}
const existing = getTitleStatus(userId, titleId);
if (existing && existing.status !== "watchlist") {
setTitleStatus(userId, titleId, "watchlist");
/** Remove a single watch owned by the user. Returns false if it doesn't exist or isn't theirs. */
export function deleteWatch(userId: string, kind: "movie" | "episode", watchId: string): boolean {
if (kind === "movie") {
const deleted = deleteMovieWatchById(userId, watchId);
if (!deleted) return false;
if (countMovieWatches(userId, deleted.titleId) === 0) {
resetMovieToWatchlist(userId, deleted.titleId);
}
return true;
}
const deleted = deleteEpisodeWatchById(userId, watchId);
if (!deleted) return false;
const titleId = getEpisodeTitleId(deleted.episodeId);
if (titleId) downgradeShowIfNoEpisodeWatches(userId, titleId);
return true;
}
/** Log a manual watch at a given time. `id` is a movie title id or an episode id. */
export function logWatchAt(
userId: string,
kind: "movie" | "episode",
id: string,
watchedAt: Date,
): "ok" | "not_found" {
if (kind === "movie") {
const title = getTitleById(id);
if (!title || title.type !== "movie") return "not_found";
logMovieWatch(userId, id, "manual", watchedAt);
return "ok";
}
if (!getEpisodeTitleId(id)) return "not_found";
logEpisodeWatch(userId, id, "manual", watchedAt);
return "ok";
}
export function unwatchSeries(userId: string, titleId: string) {
+148
View File
@@ -0,0 +1,148 @@
import { eq } from "drizzle-orm";
import { beforeEach, describe, expect, test } from "vitest";
import { userEpisodeWatches, userMovieWatches, userTitleStatus } from "@sofa/db/schema";
import {
clearAllTables,
insertStatus,
insertTitle,
insertTvShow,
insertUser,
testDb,
} from "@sofa/test/db";
import { deleteWatch, logWatchAt } from "../src/tracking";
function movieWatchRows(userId: string, titleId: string) {
return testDb
.select()
.from(userMovieWatches)
.where(eq(userMovieWatches.titleId, titleId))
.all()
.filter((r) => r.userId === userId);
}
function episodeWatchRows(userId: string) {
return testDb
.select()
.from(userEpisodeWatches)
.all()
.filter((r) => r.userId === userId);
}
function statusOf(userId: string, titleId: string) {
return testDb
.select()
.from(userTitleStatus)
.all()
.find((r) => r.userId === userId && r.titleId === titleId)?.status;
}
function addMovieWatch(id: string, userId: string, titleId: string) {
testDb
.insert(userMovieWatches)
.values({ id, userId, titleId, watchedAt: new Date(), source: "manual" })
.run();
}
function addEpisodeWatch(id: string, userId: string, episodeId: string) {
testDb
.insert(userEpisodeWatches)
.values({ id, userId, episodeId, watchedAt: new Date(), source: "manual" })
.run();
}
beforeEach(() => {
clearAllTables();
insertUser("user-1");
insertUser("user-2");
insertTitle({ id: "m1", tmdbId: 1, title: "Movie One" });
insertTvShow("tv-1", 100, 1, 3);
});
describe("deleteWatch", () => {
test("deleting one of two movie watches keeps completed", () => {
insertStatus("user-1", "m1", "completed");
addMovieWatch("w1", "user-1", "m1");
addMovieWatch("w2", "user-1", "m1");
expect(deleteWatch("user-1", "movie", "w1")).toBe(true);
expect(movieWatchRows("user-1", "m1")).toHaveLength(1);
expect(statusOf("user-1", "m1")).toBe("completed");
});
test("deleting the last movie watch sets watchlist", () => {
insertStatus("user-1", "m1", "completed");
addMovieWatch("w1", "user-1", "m1");
expect(deleteWatch("user-1", "movie", "w1")).toBe(true);
expect(movieWatchRows("user-1", "m1")).toHaveLength(0);
expect(statusOf("user-1", "m1")).toBe("watchlist");
});
test("deleting the only episode watch of an in-progress show sets watchlist", () => {
insertStatus("user-1", "tv-1", "in_progress");
addEpisodeWatch("e1", "user-1", "tv-1-s1e1");
expect(deleteWatch("user-1", "episode", "e1")).toBe(true);
expect(statusOf("user-1", "tv-1")).toBe("watchlist");
});
test("deleting one of two episode watches keeps in_progress", () => {
insertStatus("user-1", "tv-1", "in_progress");
addEpisodeWatch("e1", "user-1", "tv-1-s1e1");
addEpisodeWatch("e2", "user-1", "tv-1-s1e2");
expect(deleteWatch("user-1", "episode", "e1")).toBe(true);
expect(episodeWatchRows("user-1")).toHaveLength(1);
expect(statusOf("user-1", "tv-1")).toBe("in_progress");
});
test("another user's watch is not deleted", () => {
insertStatus("user-1", "m1", "completed");
addMovieWatch("w1", "user-1", "m1");
addEpisodeWatch("e1", "user-1", "tv-1-s1e1");
expect(deleteWatch("user-2", "movie", "w1")).toBe(false);
expect(deleteWatch("user-2", "episode", "e1")).toBe(false);
expect(movieWatchRows("user-1", "m1")).toHaveLength(1);
expect(episodeWatchRows("user-1")).toHaveLength(1);
expect(statusOf("user-1", "m1")).toBe("completed");
});
});
describe("logWatchAt", () => {
test("logs a movie on a past date and sets completed", () => {
const at = new Date("2024-03-01T12:00:00Z");
expect(logWatchAt("user-1", "movie", "m1", at)).toBe("ok");
const rows = movieWatchRows("user-1", "m1");
expect(rows).toHaveLength(1);
expect(rows[0]?.watchedAt.getTime()).toBe(at.getTime());
expect(statusOf("user-1", "m1")).toBe("completed");
});
test("allows a rewatch of an already watched movie", () => {
insertStatus("user-1", "m1", "completed");
addMovieWatch("w1", "user-1", "m1");
expect(logWatchAt("user-1", "movie", "m1", new Date("2024-03-01T12:00:00Z"))).toBe("ok");
expect(movieWatchRows("user-1", "m1")).toHaveLength(2);
});
test("logs an episode and moves watchlist to in_progress", () => {
insertStatus("user-1", "tv-1", "watchlist");
expect(logWatchAt("user-1", "episode", "tv-1-s1e1", new Date("2024-03-01T12:00:00Z"))).toBe(
"ok",
);
expect(episodeWatchRows("user-1")).toHaveLength(1);
expect(statusOf("user-1", "tv-1")).toBe("in_progress");
});
test("returns not_found for a TV title as movie, or unknown ids", () => {
const at = new Date("2024-03-01T12:00:00Z");
expect(logWatchAt("user-1", "movie", "tv-1", at)).toBe("not_found");
expect(logWatchAt("user-1", "movie", "nope", at)).toBe("not_found");
expect(logWatchAt("user-1", "episode", "nope", at)).toBe("not_found");
expect(movieWatchRows("user-1", "tv-1")).toHaveLength(0);
});
});
+30
View File
@@ -337,6 +337,36 @@ export function deleteMovieWatches(userId: string, titleId: string): void {
.run();
}
export function deleteMovieWatchById(userId: string, watchId: string): { titleId: string } | null {
const row = db
.delete(userMovieWatches)
.where(and(eq(userMovieWatches.id, watchId), eq(userMovieWatches.userId, userId)))
.returning({ titleId: userMovieWatches.titleId })
.get();
return row ?? null;
}
export function deleteEpisodeWatchById(
userId: string,
watchId: string,
): { episodeId: string } | null {
const row = db
.delete(userEpisodeWatches)
.where(and(eq(userEpisodeWatches.id, watchId), eq(userEpisodeWatches.userId, userId)))
.returning({ episodeId: userEpisodeWatches.episodeId })
.get();
return row ?? null;
}
export function countMovieWatches(userId: string, titleId: string): number {
const row = db
.select({ count: sql<number>`count(*)` })
.from(userMovieWatches)
.where(and(eq(userMovieWatches.userId, userId), eq(userMovieWatches.titleId, titleId)))
.get();
return row?.count ?? 0;
}
export function deleteAllEpisodeWatchesForTitle(userId: string, titleId: string): void {
const titleEpisodeIds = db
.select({ id: episodes.id })
+2
View File
@@ -14,6 +14,8 @@ export function getAppErrorCode(error: unknown): AppErrorCode | null {
const APP_ERROR_MESSAGES: Record<AppErrorCode, MessageDescriptor> = {
TITLE_NOT_FOUND: msg`Title not found`,
EPISODE_NOT_FOUND: msg`Episode not found`,
WATCH_NOT_FOUND: msg`Watch not found`,
PERSON_NOT_FOUND: msg`Person not found`,
INTEGRATION_NOT_FOUND: msg`Integration not found`,
BACKUP_NOT_FOUND: msg`Backup not found`,
+59
View File
@@ -962,6 +962,10 @@ msgstr "نسخ احتياطية لقاعدة البيانات"
msgid "Database restored. Reloading..."
msgstr "تم استعادة قاعدة البيانات. جارٍ إعادة التحميل..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "الحلقة {episodeNumber}"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "الحلقة {episodeNumber}، {episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "فشل تحميل إعدادات جدول النسخ الاحتياطي"
msgid "Failed to load title"
msgstr "فشل تحميل العنوان"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr "فشل مسح ذاكرة التخزين المؤقت للبيانات ا
msgid "Failed to regenerate {label} URL"
msgstr "فشل إعادة إنشاء رابط {label}"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr ""
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr "فشل الإزالة من المكتبة"
@@ -1863,6 +1880,18 @@ msgstr "جارٍ التحميل"
msgid "Loading…"
msgstr "جارٍ التحميل…"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "فشل تسجيل الدخول"
@@ -2038,6 +2067,10 @@ msgstr "الاثنين"
msgid "More"
msgstr "المزيد"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr ""
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr "المزيد من هذا القبيل"
@@ -2676,6 +2709,10 @@ msgstr "موصى به"
msgid "Recommended for You"
msgstr "موصى به لك"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr "جارٍ إعادة التوجيه…"
@@ -2739,6 +2776,11 @@ msgstr "تاريخ الإصدار"
msgid "Remove"
msgstr "إزالة"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr "إزالة الصورة"
msgid "Remove profile picture"
msgstr "إزالة صورة الملف الشخصي"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr "هذا العام"
msgid "Thursday"
msgstr "الخميس"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr ""
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr "الوقت:"
@@ -3748,6 +3799,14 @@ msgstr "مشاهدة الكل"
msgid "Watch history"
msgstr "سجل المشاهدة"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""
+59
View File
@@ -962,6 +962,10 @@ msgstr "Datenbank-Backups"
msgid "Database restored. Reloading..."
msgstr "Datenbank wiederhergestellt. Lade neu..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "Folge {episodeNumber}"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "Folge {episodeNumber}, {episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "Backup-Zeitplaneinstellungen konnten nicht geladen werden"
msgid "Failed to load title"
msgstr "Titel konnte nicht geladen werden"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr "Metadaten-Cache konnte nicht geleert werden"
msgid "Failed to regenerate {label} URL"
msgstr "{label}-URL konnte nicht neu generiert werden"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr ""
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr "Konnte nicht aus der Bibliothek entfernt werden"
@@ -1863,6 +1880,18 @@ msgstr "Wird geladen"
msgid "Loading…"
msgstr "Lade..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "Anmeldung fehlgeschlagen"
@@ -2038,6 +2067,10 @@ msgstr "Montag"
msgid "More"
msgstr "Mehr"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr ""
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr "Ähnliche Titel"
@@ -2676,6 +2709,10 @@ msgstr "Empfohlen"
msgid "Recommended for You"
msgstr "Für dich empfohlen"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr "Weiterleiten..."
@@ -2739,6 +2776,11 @@ msgstr "Erscheinungsdatum"
msgid "Remove"
msgstr "Entfernen"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr "Bild entfernen"
msgid "Remove profile picture"
msgstr "Profilbild entfernen"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr "Dieses Jahr"
msgid "Thursday"
msgstr "Donnerstag"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr ""
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr "Uhrzeit:"
@@ -3748,6 +3799,14 @@ msgstr "Alle als gesehen markieren"
msgid "Watch history"
msgstr "Verlauf"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""
+59
View File
@@ -962,6 +962,10 @@ msgstr ""
msgid "Database restored. Reloading..."
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr "Date"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "Episode {episodeNumber}"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "Episode {episodeNumber}, {episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr "Episode not found"
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "Failed to load backup schedule settings"
msgid "Failed to load title"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr "Failed to log watch"
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr ""
msgid "Failed to regenerate {label} URL"
msgstr ""
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr "Failed to remove from history"
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr ""
@@ -1863,6 +1880,18 @@ msgstr "Loading"
msgid "Loading…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr "Log a watch"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr "Log a watch…"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr "Log watch"
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "Login failed"
@@ -2038,6 +2067,10 @@ msgstr ""
msgid "More"
msgstr "More"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr "More actions"
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr ""
@@ -2676,6 +2709,10 @@ msgstr ""
msgid "Recommended for You"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr "Record when you watched this."
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr ""
@@ -2739,6 +2776,11 @@ msgstr "Release Date"
msgid "Remove"
msgstr ""
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr "Remove from history"
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr ""
msgid "Remove profile picture"
msgstr ""
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr "Removed from history"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr ""
msgid "Thursday"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr "Time (optional)"
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr ""
@@ -3748,6 +3799,14 @@ msgstr ""
msgid "Watch history"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr "Watch logged"
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr "Watch not found"
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""
+59
View File
@@ -962,6 +962,10 @@ msgstr "Copias de seguridad de la base de datos"
msgid "Database restored. Reloading..."
msgstr "Base de datos restaurada. Recargando..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "Episodio {episodeNumber}"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "Episodio {episodeNumber}, {episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "No se pudieron cargar los ajustes del programa de copias de seguridad"
msgid "Failed to load title"
msgstr "Error al cargar el título"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr "Error al purgar la caché de metadatos"
msgid "Failed to regenerate {label} URL"
msgstr "Error al regenerar la URL de {label}"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr ""
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr "Error al eliminar de la biblioteca"
@@ -1863,6 +1880,18 @@ msgstr "Cargando"
msgid "Loading…"
msgstr "Cargando…"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "Error al iniciar sesión"
@@ -2038,6 +2067,10 @@ msgstr "Lunes"
msgid "More"
msgstr "Más"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr ""
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr "Más como este"
@@ -2676,6 +2709,10 @@ msgstr "Recomendado"
msgid "Recommended for You"
msgstr "Recomendado para ti"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr "Redirigiendo…"
@@ -2739,6 +2776,11 @@ msgstr "Fecha de estreno"
msgid "Remove"
msgstr "Eliminar"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr "Eliminar foto"
msgid "Remove profile picture"
msgstr "Eliminar foto de perfil"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr "Este año"
msgid "Thursday"
msgstr "Jueves"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr ""
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr "Hora:"
@@ -3748,6 +3799,14 @@ msgstr "Marcar todo como visto"
msgid "Watch history"
msgstr "Historial de visionado"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""
+59
View File
@@ -962,6 +962,10 @@ msgstr "Sauvegardes de la base de données"
msgid "Database restored. Reloading..."
msgstr "Base de données restaurée. Rechargement..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "Épisode {episodeNumber}"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "Épisode {episodeNumber}, {episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "Impossible de charger les paramètres de planification des sauvegardes"
msgid "Failed to load title"
msgstr "Échec du chargement du titre"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr "Échec de la purge du cache de métadonnées"
msgid "Failed to regenerate {label} URL"
msgstr "Échec de la régénération de l'URL {label}"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr ""
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr "Échec de la suppression de la bibliothèque"
@@ -1863,6 +1880,18 @@ msgstr "Chargement"
msgid "Loading…"
msgstr "Chargement…"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "Connexion échouée"
@@ -2038,6 +2067,10 @@ msgstr "Lundi"
msgid "More"
msgstr "Plus"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr ""
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr "Dans le même genre"
@@ -2676,6 +2709,10 @@ msgstr "Recommandé"
msgid "Recommended for You"
msgstr "Recommandé pour vous"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr "Redirection…"
@@ -2739,6 +2776,11 @@ msgstr "Date de sortie"
msgid "Remove"
msgstr "Supprimer"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr "Supprimer la photo"
msgid "Remove profile picture"
msgstr "Supprimer la photo de profil"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr "Cette année"
msgid "Thursday"
msgstr "Jeudi"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr ""
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr "Heure :"
@@ -3748,6 +3799,14 @@ msgstr "Tout marquer comme visionné"
msgid "Watch history"
msgstr "Historique de visionnage"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""
+59
View File
@@ -962,6 +962,10 @@ msgstr "גיבויי מסד נתונים"
msgid "Database restored. Reloading..."
msgstr "מסד הנתונים שוחזר. טוען מחדש..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "פרק {episodeNumber}"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "פרק {episodeNumber}, {episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "טעינת הגדרות תזמון הגיבוי נכשלה"
msgid "Failed to load title"
msgstr "טעינת הכותרת נכשלה"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr "ניקוי מטמון המטאדטה נכשל"
msgid "Failed to regenerate {label} URL"
msgstr "יצירת כתובת ה-URL של {label} מחדש נכשלה"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr ""
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr "הסרה מהספרייה נכשלה"
@@ -1863,6 +1880,18 @@ msgstr "טוען"
msgid "Loading…"
msgstr "טוען…"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "הכניסה נכשלה"
@@ -2038,6 +2067,10 @@ msgstr "שני"
msgid "More"
msgstr "עוד"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr ""
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr "עוד כמו זה"
@@ -2676,6 +2709,10 @@ msgstr "מומלץ"
msgid "Recommended for You"
msgstr "מומלץ עבורך"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr "מפנה…"
@@ -2739,6 +2776,11 @@ msgstr "תאריך יציאה"
msgid "Remove"
msgstr "הסר"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr "הסר תמונה"
msgid "Remove profile picture"
msgstr "הסר תמונת פרופיל"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr "השנה"
msgid "Thursday"
msgstr "חמישי"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr ""
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr "שעה:"
@@ -3748,6 +3799,14 @@ msgstr "סמן הכל כנצפה"
msgid "Watch history"
msgstr "היסטוריית צפייה"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""
+59
View File
@@ -962,6 +962,10 @@ msgstr "Backup del database"
msgid "Database restored. Reloading..."
msgstr "Database ripristinato. Ricaricamento..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "Episodio {episodeNumber}"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "Episodio {episodeNumber}, {episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "Impossibile caricare le impostazioni di pianificazione del backup"
msgid "Failed to load title"
msgstr "Impossibile caricare il titolo"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr "Impossibile svuotare la cache metadati"
msgid "Failed to regenerate {label} URL"
msgstr "Impossibile rigenerare l'URL di {label}"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr ""
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr "Impossibile rimuovere dalla libreria"
@@ -1863,6 +1880,18 @@ msgstr "Caricamento"
msgid "Loading…"
msgstr "Caricamento…"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "Accesso non riuscito"
@@ -2038,6 +2067,10 @@ msgstr "Lunedì"
msgid "More"
msgstr "Altro"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr ""
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr "Simili a questo"
@@ -2676,6 +2709,10 @@ msgstr "Consigliati"
msgid "Recommended for You"
msgstr "Consigliati per te"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr "Reindirizzamento in corso…"
@@ -2739,6 +2776,11 @@ msgstr "Data di uscita"
msgid "Remove"
msgstr "Rimuovi"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr "Rimuovi immagine"
msgid "Remove profile picture"
msgstr "Rimuovi immagine del profilo"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr "Quest'anno"
msgid "Thursday"
msgstr "Giovedì"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr ""
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr "Ora:"
@@ -3748,6 +3799,14 @@ msgstr "Segna tutta la stagione come vista"
msgid "Watch history"
msgstr "Cronologia visioni"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""
+59
View File
@@ -962,6 +962,10 @@ msgstr "データベースバックアップ"
msgid "Database restored. Reloading..."
msgstr "データベースを復元しました。再読み込み中..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "エピソード{episodeNumber}"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "エピソード{episodeNumber}、{episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "バックアップスケジュール設定を読み込めませんでし
msgid "Failed to load title"
msgstr "作品の読み込みに失敗しました"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr "メタデータキャッシュのクリアに失敗しました"
msgid "Failed to regenerate {label} URL"
msgstr "{label} URLの再生成に失敗しました"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr ""
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr "ライブラリからの削除に失敗しました"
@@ -1863,6 +1880,18 @@ msgstr "読み込み中"
msgid "Loading…"
msgstr "読み込み中…"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "ログインに失敗しました"
@@ -2038,6 +2067,10 @@ msgstr "月曜日"
msgid "More"
msgstr "もっと見る"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr ""
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr "関連作品"
@@ -2676,6 +2709,10 @@ msgstr "おすすめ作品"
msgid "Recommended for You"
msgstr "あなたへのおすすめ"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr "リダイレクト中…"
@@ -2739,6 +2776,11 @@ msgstr "リリース日"
msgid "Remove"
msgstr "削除"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr "画像を削除"
msgid "Remove profile picture"
msgstr "プロフィール画像を削除する"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr "今年"
msgid "Thursday"
msgstr "木曜日"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr ""
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr "時刻:"
@@ -3748,6 +3799,14 @@ msgstr "すべて視聴済みにする"
msgid "Watch history"
msgstr "視聴履歴"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""
+59
View File
@@ -962,6 +962,10 @@ msgstr "데이터베이스 백업"
msgid "Database restored. Reloading..."
msgstr "데이터베이스가 복원되었습니다. 다시 로드 중..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "에피소드 {episodeNumber}"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "에피소드 {episodeNumber}, {episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "백업 일정 설정을 불러오지 못했습니다"
msgid "Failed to load title"
msgstr "작품 정보를 불러오지 못했습니다"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr "메타데이터 캐시 삭제 실패"
msgid "Failed to regenerate {label} URL"
msgstr "{label} URL 재생성 실패"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr ""
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr "라이브러리에서 제거 실패"
@@ -1863,6 +1880,18 @@ msgstr "로딩 중"
msgid "Loading…"
msgstr "로딩 중…"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "로그인 실패"
@@ -2038,6 +2067,10 @@ msgstr "월요일"
msgid "More"
msgstr "더 보기"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr ""
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr "비슷한 작품"
@@ -2676,6 +2709,10 @@ msgstr "추천 작품"
msgid "Recommended for You"
msgstr "맞춤 추천"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr "리디렉션 중…"
@@ -2739,6 +2776,11 @@ msgstr "출시일"
msgid "Remove"
msgstr "제거"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr "사진 제거"
msgid "Remove profile picture"
msgstr "프로필 사진 제거"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr "올해"
msgid "Thursday"
msgstr "목요일"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr ""
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr "시간:"
@@ -3748,6 +3799,14 @@ msgstr "모두 시청 완료"
msgid "Watch history"
msgstr "시청 기록"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""
+59
View File
@@ -962,6 +962,10 @@ msgstr "Database back-ups"
msgid "Database restored. Reloading..."
msgstr "Database hersteld. Opnieuw laden..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "Aflevering {episodeNumber}"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "Aflevering {episodeNumber}, {episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "Kon instellingen voor back-upplanning niet laden"
msgid "Failed to load title"
msgstr "Titel laden mislukt"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr "Metadatacache leegmaken mislukt"
msgid "Failed to regenerate {label} URL"
msgstr "{label} URL opnieuw genereren mislukt"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr ""
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr "Verwijderen uit bibliotheek mislukt"
@@ -1863,6 +1880,18 @@ msgstr "Laden"
msgid "Loading…"
msgstr "Laden…"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "Aanmelden mislukt"
@@ -2038,6 +2067,10 @@ msgstr "Maandag"
msgid "More"
msgstr "Meer"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr ""
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr "Meer zoals dit"
@@ -2676,6 +2709,10 @@ msgstr "Aanbevolen"
msgid "Recommended for You"
msgstr "Aanbevolen voor jou"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr "Doorsturen…"
@@ -2739,6 +2776,11 @@ msgstr "Releasedatum"
msgid "Remove"
msgstr "Verwijderen"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr "Afbeelding verwijderen"
msgid "Remove profile picture"
msgstr "Profielfoto verwijderen"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr "Dit jaar"
msgid "Thursday"
msgstr "Donderdag"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr ""
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr "Tijd:"
@@ -3748,6 +3799,14 @@ msgstr "Alles als bekeken markeren"
msgid "Watch history"
msgstr "Kijkgeschiedenis"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""
+59
View File
@@ -962,6 +962,10 @@ msgstr "Backups do banco de dados"
msgid "Database restored. Reloading..."
msgstr "Banco de dados restaurado. Recarregando..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "Episódio {episodeNumber}"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "Episódio {episodeNumber}, {episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "Falha ao carregar as definições de agendamento de backup"
msgid "Failed to load title"
msgstr "Falha ao carregar título"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr "Falha ao limpar cache de metadados"
msgid "Failed to regenerate {label} URL"
msgstr "Falha ao regenerar URL de {label}"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr ""
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr "Falha ao remover da biblioteca"
@@ -1863,6 +1880,18 @@ msgstr "A carregar"
msgid "Loading…"
msgstr "Carregando…"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "Falha no login"
@@ -2038,6 +2067,10 @@ msgstr "Segunda-feira"
msgid "More"
msgstr "Mais"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr ""
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr "Mais Como Este"
@@ -2676,6 +2709,10 @@ msgstr "Recomendado"
msgid "Recommended for You"
msgstr "Recomendado para Você"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr "Redirecionando…"
@@ -2739,6 +2776,11 @@ msgstr "Data de Lançamento"
msgid "Remove"
msgstr "Remover"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr "Remover foto"
msgid "Remove profile picture"
msgstr "Remover foto de perfil"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr "Este Ano"
msgid "Thursday"
msgstr "Quinta-feira"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr ""
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr "Hora:"
@@ -3748,6 +3799,14 @@ msgstr "Marcar todos"
msgid "Watch history"
msgstr "Histórico de exibição"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""
+59
View File
@@ -962,6 +962,10 @@ msgstr "数据库备份"
msgid "Database restored. Reloading..."
msgstr "数据库已还原。正在重新加载..."
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Date"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Date Added"
@@ -1170,6 +1174,10 @@ msgstr "第 {episodeNumber} 集"
msgid "Episode {episodeNumber}, {episodeLabel}"
msgstr "第 {episodeNumber} 集,{episodeLabel}"
#: packages/i18n/src/errors.ts
msgid "Episode not found"
msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
@@ -1309,6 +1317,10 @@ msgstr "加载备份计划设置失败"
msgid "Failed to load title"
msgstr "加载影视内容失败"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Failed to log watch"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark all episodes as watched"
@@ -1365,6 +1377,11 @@ msgstr "清除元数据缓存失败"
msgid "Failed to regenerate {label} URL"
msgstr "重新生成 {label} URL 失败"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Failed to remove from history"
msgstr ""
#: apps/native/src/lib/title-actions.ts
msgid "Failed to remove from library"
msgstr "从媒体库移除失败"
@@ -1863,6 +1880,18 @@ msgstr "加载中"
msgid "Loading…"
msgstr "加载中…"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log a watch…"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Log watch"
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Login failed"
msgstr "登录失败"
@@ -2038,6 +2067,10 @@ msgstr "星期一"
msgid "More"
msgstr "更多"
#: apps/web/src/components/history/history-row.tsx
msgid "More actions"
msgstr ""
#: apps/native/src/app/title/[id].tsx
msgid "More Like This"
msgstr "更多相似内容"
@@ -2676,6 +2709,10 @@ msgstr "推荐内容"
msgid "Recommended for You"
msgstr "为您推荐"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Record when you watched this."
msgstr ""
#: apps/web/src/components/auth-form.tsx
msgid "Redirecting…"
msgstr "正在跳转…"
@@ -2739,6 +2776,11 @@ msgstr "发行日期"
msgid "Remove"
msgstr "移除"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/history-row.tsx
msgid "Remove from history"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/titles/status-action-button.tsx
@@ -2774,6 +2816,11 @@ msgstr "删除图片"
msgid "Remove profile picture"
msgstr "删除头像"
#: apps/native/src/components/history/history-row.tsx
#: apps/web/src/components/history/use-delete-watch.ts
msgid "Removed from history"
msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
@@ -3407,6 +3454,10 @@ msgstr "今年"
msgid "Thursday"
msgstr "星期四"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Time (optional)"
msgstr ""
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Time:"
msgstr "时间:"
@@ -3748,6 +3799,14 @@ msgstr "标记全部为已看"
msgid "Watch history"
msgstr "观看历史"
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "Watch logged"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "Watch not found"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#~ msgid "Watch on {name}"
#~ msgstr ""