feat(tracking): add a paginated watch history and Last watched library sort

Adds tracking.history (cursor-paginated, movies and episodes merged), a
last_watched library sort, and a /history web page.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Sonnet 5.5 committed 2026-10-05 14:36:44 -04:00
1 parent 6cd99a39a9
commit b292fe37ab
30 files changed
+1711 -6

No files matched your search

@@ -7,7 +7,14 @@ import { MenuTrigger } from "@/components/ui/menu-trigger";
import { ScaledIcon } from "@/components/ui/scaled-icon";
import * as Haptics from "@/utils/haptics";
type SortBy = "added_at" | "title" | "release_date" | "user_rating" | "vote_average" | "popularity";
type SortBy =
| "added_at"
| "title"
| "release_date"
| "user_rating"
| "vote_average"
| "popularity"
| "last_watched";
type SortDirection = "asc" | "desc";
interface SortOption {
@@ -33,6 +40,7 @@ export function SortMenu({ sortBy, sortDirection, onSortChange }: SortMenuProps)
{ sortBy: "release_date", sortDirection: "desc", label: t`Release Date` },
{ sortBy: "user_rating", sortDirection: "desc", label: t`User Rating` },
{ sortBy: "vote_average", sortDirection: "desc", label: t`TMDB Rating` },
{ sortBy: "last_watched", sortDirection: "desc", label: t`Last watched` },
{ sortBy: "popularity", sortDirection: "desc", label: t`Popularity` },
];
+2 -1
View File
@@ -6,7 +6,8 @@ export type SortBy =
| "release_date"
| "user_rating"
| "vote_average"
| "popularity";
| "popularity"
| "last_watched";
export const libraryActiveFilterCountAtom = atom(0);
export const librarySortByAtom = atom<SortBy>("added_at");
@@ -7,6 +7,7 @@ import { getOrFetchTitleByTmdbId } from "@sofa/core/metadata";
import {
getDisplayStatusesByTitleIds,
getUserTitleInfo,
listWatchHistory,
logEpisodeWatch,
logEpisodeWatchBatch,
logMovieWatch,
@@ -21,6 +22,7 @@ import {
watchSeason,
} from "@sofa/core/tracking";
import { createLogger } from "@sofa/logger";
import { tmdbImageUrl } from "@sofa/tmdb/image";
import { os } from "../context";
import { authed } from "../middleware";
@@ -122,3 +124,21 @@ export const stats = os.tracking.stats.use(authed).handler(({ input, context })
const history = getWatchHistory(context.user.id, coreType, input.period);
return { count, history };
});
export const history = os.tracking.history.use(authed).handler(({ input, context }) => {
const result = listWatchHistory(context.user.id, input);
return {
items: result.items.map((item) => ({
watchId: item.watchId,
kind: item.kind,
watchedAt: item.watchedAt.toISOString(),
source: item.source,
title: {
...item.title,
posterPath: tmdbImageUrl(item.title.posterPath, "posters"),
},
episode: item.episode,
})),
nextCursor: result.nextCursor,
};
});
+1
View File
@@ -21,6 +21,7 @@ export const implementedRouter = {
rate: tracking.rate,
userInfo: tracking.userInfo,
stats: tracking.stats,
history: tracking.history,
},
library: {
list: library.list,
@@ -0,0 +1,78 @@
import { useLingui } from "@lingui/react/macro";
import { IconMovie } from "@tabler/icons-react";
import { Link } from "@tanstack/react-router";
import type { z } from "zod";
import { thumbHashToUrl } from "@/lib/thumbhash";
import type { WatchHistoryItemSchema } from "@sofa/api/schemas";
import { formatDate } from "@sofa/i18n/format";
type WatchHistoryItem = z.infer<typeof WatchHistoryItemSchema>;
export function HistoryRow({ item }: { item: WatchHistoryItem }) {
const { t } = useLingui();
const sourceLabels = {
manual: t`Manual`,
import: t`Import`,
plex: t`Plex`,
jellyfin: t`Jellyfin`,
emby: t`Emby`,
} as const;
let subtitle: string | null = null;
if (item.episode) {
const season = item.episode.seasonNumber;
const episode = item.episode.episodeNumber;
const name = item.episode.name;
subtitle = name ? t`S${season} E${episode} · ${name}` : t`S${season} E${episode}`;
}
const time = formatDate(item.watchedAt, {
year: undefined,
month: undefined,
day: undefined,
hour: "numeric",
minute: "2-digit",
});
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>
);
}
@@ -51,6 +51,7 @@ export function LibraryToolbar({
{ label: t`Release Date`, sortBy: "release_date", direction: "desc" },
{ label: t`User Rating`, sortBy: "user_rating", direction: "desc" },
{ label: t`TMDB Rating`, sortBy: "vote_average", direction: "desc" },
{ label: t`Last watched`, sortBy: "last_watched", direction: "desc" },
{ label: t`Popularity`, sortBy: "popularity", direction: "desc" },
],
[t],
+3
View File
@@ -3,6 +3,7 @@ import {
IconBooks,
IconCalendarEvent,
IconCompass,
IconHistory,
IconHome,
IconLogout,
IconSearch,
@@ -125,6 +126,7 @@ export function NavBar({
{ href: "/library", label: t`Library` },
{ href: "/explore", label: t`Explore` },
{ href: "/upcoming", label: t`Upcoming` },
{ href: "/history", label: t`History` },
] as const;
const initial = userName?.charAt(0).toUpperCase() ?? "?";
@@ -289,6 +291,7 @@ export function MobileTabBar() {
{ href: "/library", label: t`Library`, icon: IconBooks },
{ href: "/explore", label: t`Explore`, icon: IconCompass },
{ href: "/upcoming", label: t`Upcoming`, icon: IconCalendarEvent },
{ href: "/history", label: t`History`, icon: IconHistory },
{ href: "/settings", label: t`Settings`, icon: IconSettings },
] as const;
+21
View File
@@ -15,6 +15,7 @@ import { Route as AuthRouteImport } from './routes/_auth'
import { Route as SetupRouteImport } from './routes/setup'
import { Route as AppDashboardRouteImport } from './routes/_app/dashboard'
import { Route as AppExploreRouteImport } from './routes/_app/explore'
import { Route as AppHistoryRouteImport } from './routes/_app/history'
import { Route as AppLibraryRouteImport } from './routes/_app/library'
import { Route as AppOnboardingRouteImport } from './routes/_app/onboarding'
import { Route as AppSettingsRouteImport } from './routes/_app/settings'
@@ -52,6 +53,11 @@ const AppExploreRoute = AppExploreRouteImport.update({
path: '/explore',
getParentRoute: () => AppRoute,
} as any)
const AppHistoryRoute = AppHistoryRouteImport.update({
id: '/history',
path: '/history',
getParentRoute: () => AppRoute,
} as any)
const AppLibraryRoute = AppLibraryRouteImport.update({
id: '/library',
path: '/library',
@@ -98,6 +104,7 @@ export interface FileRoutesByFullPath {
'/setup': typeof SetupRoute
'/dashboard': typeof AppDashboardRoute
'/explore': typeof AppExploreRoute
'/history': typeof AppHistoryRoute
'/library': typeof AppLibraryRoute
'/onboarding': typeof AppOnboardingRoute
'/settings': typeof AppSettingsRoute
@@ -112,6 +119,7 @@ export interface FileRoutesByTo {
'/setup': typeof SetupRoute
'/dashboard': typeof AppDashboardRoute
'/explore': typeof AppExploreRoute
'/history': typeof AppHistoryRoute
'/library': typeof AppLibraryRoute
'/onboarding': typeof AppOnboardingRoute
'/settings': typeof AppSettingsRoute
@@ -129,6 +137,7 @@ export interface FileRoutesById {
'/setup': typeof SetupRoute
'/_app/dashboard': typeof AppDashboardRoute
'/_app/explore': typeof AppExploreRoute
'/_app/history': typeof AppHistoryRoute
'/_app/library': typeof AppLibraryRoute
'/_app/onboarding': typeof AppOnboardingRoute
'/_app/settings': typeof AppSettingsRoute
@@ -145,6 +154,7 @@ export interface FileRouteTypes {
| '/setup'
| '/dashboard'
| '/explore'
| '/history'
| '/library'
| '/onboarding'
| '/settings'
@@ -159,6 +169,7 @@ export interface FileRouteTypes {
| '/setup'
| '/dashboard'
| '/explore'
| '/history'
| '/library'
| '/onboarding'
| '/settings'
@@ -175,6 +186,7 @@ export interface FileRouteTypes {
| '/setup'
| '/_app/dashboard'
| '/_app/explore'
| '/_app/history'
| '/_app/library'
| '/_app/onboarding'
| '/_app/settings'
@@ -236,6 +248,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppExploreRouteImport
parentRoute: typeof AppRoute
}
'/_app/history': {
id: '/_app/history'
path: '/history'
fullPath: '/history'
preLoaderRoute: typeof AppHistoryRouteImport
parentRoute: typeof AppRoute
}
'/_app/library': {
id: '/_app/library'
path: '/library'
@@ -298,6 +317,7 @@ declare module '@tanstack/react-router' {
interface AppRouteChildren {
AppDashboardRoute: typeof AppDashboardRoute
AppExploreRoute: typeof AppExploreRoute
AppHistoryRoute: typeof AppHistoryRoute
AppLibraryRoute: typeof AppLibraryRoute
AppOnboardingRoute: typeof AppOnboardingRoute
AppSettingsRoute: typeof AppSettingsRoute
@@ -309,6 +329,7 @@ interface AppRouteChildren {
const AppRouteChildren: AppRouteChildren = {
AppDashboardRoute: AppDashboardRoute,
AppExploreRoute: AppExploreRoute,
AppHistoryRoute: AppHistoryRoute,
AppLibraryRoute: AppLibraryRoute,
AppOnboardingRoute: AppOnboardingRoute,
AppSettingsRoute: AppSettingsRoute,
+212
View File
@@ -0,0 +1,212 @@
import { Trans, useLingui } from "@lingui/react/macro";
import { IconHistory } from "@tabler/icons-react";
import { useInfiniteQuery } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { HistoryRow } from "@/components/history/history-row";
import { RouteError } from "@/components/route-error";
import { Skeleton } from "@/components/ui/skeleton";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
import { orpc } from "@/lib/orpc/client";
import { formatLocalDate } from "@sofa/i18n/date-buckets";
import { formatDate } from "@sofa/i18n/format";
const historySearchSchema = z.object({
type: z.enum(["all", "movie", "tv"]).optional().catch(undefined),
source: z
.enum(["all", "manual", "import", "plex", "jellyfin", "emby"])
.optional()
.catch(undefined),
});
type SourceFilter = "manual" | "import" | "plex" | "jellyfin" | "emby";
export const Route = createFileRoute("/_app/history")({
validateSearch: zodValidator(historySearchSchema),
staleTime: 30_000,
loader: async ({ context }) => {
await context.queryClient.ensureInfiniteQueryData(
orpc.tracking.history.infiniteOptions({
input: (pageParam: string | undefined) => ({
limit: 30,
cursor: pageParam,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
}),
);
},
head: () => ({ meta: [{ title: "History — Sofa" }] }),
pendingComponent: HistorySkeleton,
errorComponent: RouteError,
component: HistoryPage,
});
function HistorySkeleton() {
return (
<div className="mx-auto max-w-2xl space-y-6">
<div>
<Skeleton className="h-8 w-48" />
<Skeleton className="mt-2 h-4 w-64" />
</div>
{Array.from({ length: 5 }, (_, i) => (
<div key={i} className="flex items-center gap-3.5 py-2">
<Skeleton className="size-[52px] rounded-md" />
<div className="flex-1 space-y-1.5">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-3 w-28" />
</div>
</div>
))}
</div>
);
}
function HistoryPage() {
const { t } = useLingui();
const search = Route.useSearch();
const navigate = useNavigate({ from: Route.fullPath });
const typeFilter = search.type ?? "all";
const sourceFilter = search.source ?? "all";
const { data, isPending, fetchNextPage, hasNextPage, isFetchingNextPage, isFetchNextPageError } =
useInfiniteQuery(
orpc.tracking.history.infiniteOptions({
// Omit filter keys (rather than sending "all") so the default input
// matches the loader's prefetch key.
input: (pageParam: string | undefined) => ({
limit: 30,
cursor: pageParam,
...(typeFilter !== "all" ? { type: typeFilter } : {}),
...(sourceFilter !== "all" ? { source: sourceFilter } : {}),
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
}),
);
const sentinelRef = useInfiniteScroll({
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isFetchNextPageError,
});
function setTypeFilter(value: string) {
void navigate({
search: (prev) => ({
...prev,
type: value === "all" ? undefined : (value as "movie" | "tv"),
}),
replace: true,
});
}
function setSourceFilter(value: string) {
void navigate({
search: (prev) => ({
...prev,
source: value === "all" ? undefined : (value as SourceFilter),
}),
replace: true,
});
}
const header = (
<div>
<h1 className="font-display text-2xl tracking-tight">{t`History`}</h1>
<p className="text-muted-foreground mt-1 text-sm">
<Trans>Everything you've watched, newest first.</Trans>
</p>
</div>
);
const filterToggles = (
<div className="flex flex-wrap gap-3">
<ToggleGroup
value={[typeFilter]}
onValueChange={(values) => setTypeFilter(values.find((v) => v !== typeFilter) ?? "all")}
variant="outline"
size="sm"
>
<ToggleGroupItem value="all">{t`All`}</ToggleGroupItem>
<ToggleGroupItem value="movie">{t`Movies`}</ToggleGroupItem>
<ToggleGroupItem value="tv">{t`TV Shows`}</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup
value={[sourceFilter]}
onValueChange={(values) => setSourceFilter(values.find((v) => v !== sourceFilter) ?? "all")}
variant="outline"
size="sm"
>
<ToggleGroupItem value="all">{t`All`}</ToggleGroupItem>
<ToggleGroupItem value="manual">{t`Manual`}</ToggleGroupItem>
<ToggleGroupItem value="import">{t`Import`}</ToggleGroupItem>
<ToggleGroupItem value="plex">{t`Plex`}</ToggleGroupItem>
<ToggleGroupItem value="jellyfin">{t`Jellyfin`}</ToggleGroupItem>
<ToggleGroupItem value="emby">{t`Emby`}</ToggleGroupItem>
</ToggleGroup>
</div>
);
if (isPending) return <HistorySkeleton />;
const allItems = data?.pages.flatMap((p) => p.items) ?? [];
if (allItems.length === 0) {
return (
<div className="mx-auto max-w-2xl space-y-6">
{header}
{filterToggles}
<div className="flex flex-col items-center justify-center py-20 text-center">
<IconHistory className="text-muted-foreground/40 size-12" />
<p className="text-muted-foreground mt-4 text-sm">
<Trans>Nothing watched yet.</Trans>
</p>
</div>
</div>
);
}
// Group by local calendar day, preserving newest-first order.
const days: { key: string; label: string; items: typeof allItems }[] = [];
for (const item of allItems) {
const date = new Date(item.watchedAt);
const key = formatLocalDate(date);
const last = days[days.length - 1];
if (last && last.key === key) {
last.items.push(item);
} else {
days.push({
key,
label: formatDate(date, { weekday: "long" }),
items: [item],
});
}
}
return (
<div className="mx-auto max-w-2xl space-y-6">
{header}
{filterToggles}
{days.map((day) => (
<section key={day.key}>
<h2 className="font-display text-muted-foreground mb-2 text-sm font-medium tracking-wider uppercase">
{day.label}
</h2>
<div className="space-y-2">
{day.items.map((item) => (
<HistoryRow key={item.watchId} item={item} />
))}
</div>
</section>
))}
<div ref={sentinelRef} />
{isFetchingNextPage && <HistorySkeleton />}
</div>
);
}
@@ -0,0 +1,27 @@
---
title: List watch history
full: true
_openapi:
preload:
- ./public/openapi.json
method: GET
webhook: false
toc: []
structuredData:
headings: []
contents:
- content: List the user's watches (movies and episodes), newest first, with
cursor pagination.
---
{/* 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","method":"get"}]} showDescription />
</>
);
}
+241 -1
View File
@@ -2294,6 +2294,245 @@
]
}
},
"/tracking/history": {
"get": {
"operationId": "tracking.history",
"summary": "List watch history",
"description": "List the user's watches (movies and episodes), newest first, with cursor pagination.",
"tags": [
"Tracking"
],
"parameters": [
{
"name": "limit",
"in": "query",
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 50,
"default": 30,
"description": "Maximum items to return"
},
"allowEmptyValue": true,
"allowReserved": true
},
{
"name": "cursor",
"in": "query",
"schema": {
"type": "string",
"description": "Opaque cursor from a previous response's nextCursor"
},
"allowEmptyValue": true,
"allowReserved": true
},
{
"name": "type",
"in": "query",
"schema": {
"enum": [
"movie",
"tv"
],
"type": "string",
"description": "Only movies or only TV episodes"
},
"allowEmptyValue": true,
"allowReserved": true
},
{
"name": "source",
"in": "query",
"schema": {
"enum": [
"manual",
"import",
"plex",
"jellyfin",
"emby"
],
"type": "string",
"description": "Only watches logged by this source"
},
"allowEmptyValue": true,
"allowReserved": true
}
],
"responses": {
"200": {
"description": "A page of watch history",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"watchId": {
"type": "string",
"description": "Watch record ID"
},
"kind": {
"enum": [
"movie",
"episode"
],
"type": "string",
"description": "Whether this watch is a movie or an episode"
},
"watchedAt": {
"type": "string",
"description": "ISO 8601 timestamp of the watch"
},
"source": {
"enum": [
"manual",
"import",
"plex",
"jellyfin",
"emby"
],
"type": "string",
"description": "How the watch was logged"
},
"title": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Title ID"
},
"title": {
"type": "string",
"description": "Display title"
},
"type": {
"enum": [
"movie",
"tv"
],
"type": "string",
"description": "Title type"
},
"posterPath": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Poster image URL"
},
"posterThumbHash": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Poster ThumbHash placeholder"
}
},
"required": [
"id",
"title",
"type",
"posterPath",
"posterThumbHash"
],
"description": "The movie or show"
},
"episode": {
"anyOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Episode ID"
},
"seasonNumber": {
"type": "number",
"description": "Season number"
},
"episodeNumber": {
"type": "number",
"description": "Episode number"
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Episode name"
}
},
"required": [
"id",
"seasonNumber",
"episodeNumber",
"name"
]
},
{
"type": "null"
}
],
"description": "Episode details (null for movies)"
}
},
"required": [
"watchId",
"kind",
"watchedAt",
"source",
"title",
"episode"
],
"description": "A single watch event"
},
"description": "Watches, newest first"
},
"nextCursor": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Cursor for the next page, or null at the end"
}
},
"required": [
"items",
"nextCursor"
],
"description": "A page of watch history"
}
}
}
}
},
"security": [
{
"session": []
}
]
}
},
"/library": {
"get": {
"operationId": "library.list",
@@ -2439,7 +2678,8 @@
"release_date",
"popularity",
"user_rating",
"vote_average"
"vote_average",
"last_watched"
],
"type": "string",
"default": "added_at",
+14
View File
@@ -62,6 +62,8 @@ import {
UserInfoOutput,
UserPlatformsOutput,
WatchHistoryInput,
WatchHistoryListInput,
WatchHistoryListOutput,
WatchHistoryOutput,
WatchInput,
} from "./schemas";
@@ -183,6 +185,18 @@ export const contract = {
})
.input(WatchHistoryInput)
.output(WatchHistoryOutput),
history: oc
.route({
method: "GET",
path: "/tracking/history",
tags: ["Tracking"],
summary: "List watch history",
description:
"List the user's watches (movies and episodes), newest first, with cursor pagination.",
successDescription: "A page of watch history",
})
.input(WatchHistoryListInput)
.output(WatchHistoryListOutput),
},
// ─── Library ────────────────────────────────────────────────
+56 -1
View File
@@ -134,6 +134,53 @@ export const WatchHistoryInput = z
})
.meta({ description: "Filters for watch history chart data" });
// ─── Watch history list ────────────────────────────────────────
const watchSource = z.enum(["manual", "import", "plex", "jellyfin", "emby"]);
export const WatchHistoryListInput = z
.object({
limit: z.number().int().min(1).max(50).default(30).describe("Maximum items to return"),
cursor: z.string().optional().describe("Opaque cursor from a previous response's nextCursor"),
type: z.enum(["movie", "tv"]).optional().describe("Only movies or only TV episodes"),
source: watchSource.optional().describe("Only watches logged by this source"),
})
.meta({ description: "Filters and pagination for watch history" });
export const WatchHistoryItemSchema = z
.object({
watchId: z.string().describe("Watch record ID"),
kind: z.enum(["movie", "episode"]).describe("Whether this watch is a movie or an episode"),
watchedAt: z.string().describe("ISO 8601 timestamp of the watch"),
source: watchSource.describe("How the watch was logged"),
title: z
.object({
id: z.string().describe("Title ID"),
title: z.string().describe("Display title"),
type: z.enum(["movie", "tv"]).describe("Title type"),
posterPath: z.string().nullable().describe("Poster image URL"),
posterThumbHash: z.string().nullable().describe("Poster ThumbHash placeholder"),
})
.describe("The movie or show"),
episode: z
.object({
id: z.string().describe("Episode ID"),
seasonNumber: z.number().describe("Season number"),
episodeNumber: z.number().describe("Episode number"),
name: z.string().nullable().describe("Episode name"),
})
.nullable()
.describe("Episode details (null for movies)"),
})
.meta({ description: "A single watch event" });
export const WatchHistoryListOutput = z
.object({
items: z.array(WatchHistoryItemSchema).describe("Watches, newest first"),
nextCursor: z.string().nullable().describe("Cursor for the next page, or null at the end"),
})
.meta({ description: "A page of watch history" });
// ─── Integration inputs ────────────────────────────────────────
export const CreateIntegrationInput = z
@@ -603,7 +650,15 @@ export const LibraryListInput = z
.optional()
.describe("Only show titles available on the user's streaming services"),
sortBy: z
.enum(["title", "added_at", "release_date", "popularity", "user_rating", "vote_average"])
.enum([
"title",
"added_at",
"release_date",
"popularity",
"user_rating",
"vote_average",
"last_watched",
])
.default("added_at")
.describe("Sort field"),
sortDirection: z.enum(["asc", "desc"]).default("desc").describe("Sort direction"),
+132
View File
@@ -14,7 +14,9 @@ import {
getEpisodeProgressByTitleIds as getEpisodeProgressByTitleIdsQuery,
getEpisodeTitleId,
getEpisodeTitleIds,
getEpisodeWatchHistory,
getExistingEpisodeWatchIds,
getMovieWatchHistory,
getSeasonById,
getSeasonEpisodeIds,
getTitleStatus,
@@ -289,3 +291,133 @@ export function quickAddTitle(
export function watchSeason(userId: string, seasonId: string): void {
logEpisodeWatchBatch(userId, getSeasonEpisodeIds(seasonId));
}
// ─── Watch history timeline ─────────────────────────────────────────
type WatchSource = "manual" | "import" | "plex" | "jellyfin" | "emby";
interface WatchHistoryCursorKey {
/** watchedAt in epoch ms */
t: number;
/** watch row id */
i: string;
}
function encodeWatchHistoryCursor(key: WatchHistoryCursorKey): string {
return Buffer.from(JSON.stringify(key), "utf8").toString("base64url");
}
function decodeWatchHistoryCursor(cursor: string): WatchHistoryCursorKey | null {
try {
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
if (
typeof parsed?.t !== "number" ||
!Number.isFinite(parsed.t) ||
typeof parsed?.i !== "string"
) {
return null;
}
return { t: parsed.t, i: parsed.i };
} catch {
return null;
}
}
export interface WatchHistoryEntry {
watchId: string;
kind: "movie" | "episode";
watchedAt: Date;
source: WatchSource;
title: {
id: string;
title: string;
type: "movie" | "tv";
posterPath: string | null;
posterThumbHash: string | null;
};
episode: {
id: string;
seasonNumber: number;
episodeNumber: number;
name: string | null;
} | null;
}
/**
* One page of the user's watch history (movies + episodes merged), newest first.
* Keyset-paginated on (watchedAt desc, id desc). Image paths are raw DB paths.
*/
export function listWatchHistory(
userId: string,
input: { limit: number; cursor?: string; type?: "movie" | "tv"; source?: WatchSource },
): { items: WatchHistoryEntry[]; nextCursor: string | null } {
const { limit, type, source } = input;
const key = input.cursor ? decodeWatchHistoryCursor(input.cursor) : null;
const opts = {
limit: limit + 1,
source,
before: key ? { watchedAt: new Date(key.t), id: key.i } : undefined,
};
const entries: WatchHistoryEntry[] = [];
if (type !== "tv") {
for (const r of getMovieWatchHistory(userId, opts)) {
entries.push({
watchId: r.watchId,
kind: "movie",
watchedAt: r.watchedAt,
source: r.source,
title: {
id: r.titleId,
title: r.title,
type: r.type,
posterPath: r.posterPath,
posterThumbHash: r.posterThumbHash,
},
episode: null,
});
}
}
if (type !== "movie") {
for (const r of getEpisodeWatchHistory(userId, opts)) {
entries.push({
watchId: r.watchId,
kind: "episode",
watchedAt: r.watchedAt,
source: r.source,
title: {
id: r.titleId,
title: r.title,
type: r.type,
posterPath: r.posterPath,
posterThumbHash: r.posterThumbHash,
},
episode: {
id: r.episodeId,
seasonNumber: r.seasonNumber,
episodeNumber: r.episodeNumber,
name: r.episodeName,
},
});
}
}
entries.sort(
(a, b) =>
b.watchedAt.getTime() - a.watchedAt.getTime() ||
(a.watchId < b.watchId ? 1 : a.watchId > b.watchId ? -1 : 0),
);
const hasMore = entries.length > limit;
const items = entries.slice(0, limit);
const last = items[items.length - 1];
return {
items,
nextCursor:
hasMore && last
? encodeWatchHistoryCursor({ t: last.watchedAt.getTime(), i: last.watchId })
: null,
};
}
+132
View File
@@ -0,0 +1,132 @@
import { beforeEach, describe, expect, test } from "vitest";
import { userEpisodeWatches, userMovieWatches } from "@sofa/db/schema";
import { clearAllTables, insertTitle, insertTvShow, insertUser, testDb } from "@sofa/test/db";
import { listWatchHistory } from "../src/tracking";
type Source = "manual" | "import" | "plex" | "jellyfin" | "emby";
function movieWatch(
id: string,
userId: string,
titleId: string,
at: number,
source: Source = "manual",
) {
testDb
.insert(userMovieWatches)
.values({ id, userId, titleId, watchedAt: new Date(at * 1000), source })
.run();
}
function episodeWatch(
id: string,
userId: string,
episodeId: string,
at: number,
source: Source = "manual",
) {
testDb
.insert(userEpisodeWatches)
.values({ id, userId, episodeId, watchedAt: new Date(at * 1000), source })
.run();
}
beforeEach(() => {
clearAllTables();
insertUser("user-1");
insertUser("user-2");
insertTitle({ id: "m1", tmdbId: 1, title: "Movie One" });
insertTitle({ id: "m2", tmdbId: 2, title: "Movie Two" });
insertTvShow("tv-1", 100, 1, 3);
});
function ids(items: { watchId: string }[]) {
return items.map((i) => i.watchId);
}
describe("listWatchHistory", () => {
test("merges movies and episodes newest-first with episode details", () => {
movieWatch("w1", "user-1", "m1", 1000);
episodeWatch("w2", "user-1", "tv-1-s1e1", 3000);
movieWatch("w3", "user-1", "m2", 2000);
const { items, nextCursor } = listWatchHistory("user-1", { limit: 10 });
expect(ids(items)).toEqual(["w2", "w3", "w1"]);
expect(nextCursor).toBeNull();
expect(items[0].kind).toBe("episode");
expect(items[0].episode).toMatchObject({ seasonNumber: 1, episodeNumber: 1 });
expect(items[0].title.id).toBe("tv-1");
expect(items[1].kind).toBe("movie");
expect(items[1].episode).toBeNull();
});
test("respects limit and returns null cursor on the last page", () => {
for (let i = 1; i <= 5; i++) movieWatch(`w${i}`, "user-1", "m1", i * 100);
const p1 = listWatchHistory("user-1", { limit: 2 });
expect(ids(p1.items)).toEqual(["w5", "w4"]);
expect(p1.nextCursor).not.toBeNull();
const p2 = listWatchHistory("user-1", { limit: 2, cursor: p1.nextCursor! });
expect(ids(p2.items)).toEqual(["w3", "w2"]);
expect(p2.nextCursor).not.toBeNull();
const p3 = listWatchHistory("user-1", { limit: 2, cursor: p2.nextCursor! });
expect(ids(p3.items)).toEqual(["w1"]);
expect(p3.nextCursor).toBeNull();
});
test("paginates without duplicates or gaps when watches share a timestamp", () => {
// Same watchedAt across movies and episodes; ties broken by id desc.
movieWatch("a1", "user-1", "m1", 500);
movieWatch("a3", "user-1", "m2", 500);
episodeWatch("a2", "user-1", "tv-1-s1e1", 500);
episodeWatch("a4", "user-1", "tv-1-s1e2", 500);
movieWatch("a0", "user-1", "m1", 400);
episodeWatch("a5", "user-1", "tv-1-s1e3", 600);
const seen: string[] = [];
let cursor: string | undefined;
for (let guard = 0; guard < 10; guard++) {
const page = listWatchHistory("user-1", { limit: 2, cursor });
seen.push(...ids(page.items));
if (!page.nextCursor) break;
cursor = page.nextCursor;
}
expect(seen).toEqual(["a5", "a4", "a3", "a2", "a1", "a0"]);
});
test("ignores an invalid cursor", () => {
movieWatch("w1", "user-1", "m1", 100);
const { items } = listWatchHistory("user-1", { limit: 10, cursor: "not-a-cursor" });
expect(items).toHaveLength(1);
});
test("filters by type", () => {
movieWatch("w1", "user-1", "m1", 100);
episodeWatch("w2", "user-1", "tv-1-s1e1", 200);
expect(ids(listWatchHistory("user-1", { limit: 10, type: "movie" }).items)).toEqual(["w1"]);
expect(ids(listWatchHistory("user-1", { limit: 10, type: "tv" }).items)).toEqual(["w2"]);
});
test("filters by source", () => {
movieWatch("w1", "user-1", "m1", 100, "plex");
movieWatch("w2", "user-1", "m2", 200, "manual");
episodeWatch("w3", "user-1", "tv-1-s1e1", 300, "plex");
const { items } = listWatchHistory("user-1", { limit: 10, source: "plex" });
expect(ids(items)).toEqual(["w3", "w1"]);
});
test("never includes other users' watches", () => {
movieWatch("w1", "user-1", "m1", 100);
movieWatch("w2", "user-2", "m1", 200);
episodeWatch("w3", "user-2", "tv-1-s1e1", 300);
const { items } = listWatchHistory("user-1", { limit: 10 });
expect(ids(items)).toEqual(["w1"]);
});
});
+41 -1
View File
@@ -1,6 +1,14 @@
import { beforeAll, beforeEach, describe, expect, test } from "vitest";
import { clearAllTables, insertStatus, insertTitle, insertUser } from "@sofa/test/db";
import {
clearAllTables,
insertEpisodeWatch,
insertMovieWatch,
insertStatus,
insertTitle,
insertTvShow,
insertUser,
} from "@sofa/test/db";
import { getFilteredLibraryFeed, getLibraryGenresList } from "../src/library";
@@ -136,6 +144,38 @@ describe("getFilteredLibraryFeed", () => {
// ── getLibraryGenresList ──────────────────────────────────────────────
describe("getFilteredLibraryFeed last_watched sort", () => {
test("orders by most recent watch (movies and shows) with unwatched last", () => {
insertUser();
insertTitle({ id: "old-movie", tmdbId: 1 });
insertTitle({ id: "never", tmdbId: 2 });
insertTitle({ id: "recent-movie", tmdbId: 3 });
const { episodeIds } = insertTvShow("show", 4, 1, 2);
for (const id of ["old-movie", "never", "recent-movie", "show"]) {
insertStatus("user-1", id, "watchlist");
}
insertMovieWatch("user-1", "old-movie", new Date("2024-01-01T00:00:00Z"));
insertMovieWatch("user-1", "recent-movie", new Date("2024-03-01T00:00:00Z"));
insertEpisodeWatch("user-1", episodeIds[0], new Date("2024-02-01T00:00:00Z"));
insertEpisodeWatch("user-1", episodeIds[1], new Date("2024-02-10T00:00:00Z"));
const desc = getFilteredLibraryFeed("user-1", { ...defaultFilters, sortBy: "last_watched" });
expect(desc.items.map((i) => i.titleId)).toEqual([
"recent-movie",
"show",
"old-movie",
"never",
]);
const asc = getFilteredLibraryFeed("user-1", {
...defaultFilters,
sortBy: "last_watched",
sortDirection: "asc",
});
expect(asc.items.map((i) => i.titleId)).toEqual(["old-movie", "show", "recent-movie", "never"]);
});
});
describe("getLibraryGenresList", () => {
test("returns empty array when user has no titles", () => {
insertUser();
+19
View File
@@ -11,6 +11,7 @@ import {
titleGenres,
titles,
userEpisodeWatches,
userMovieWatches,
userPlatforms,
userRatings,
userTitleStatus,
@@ -220,6 +221,24 @@ export function getFilteredLibrary(userId: string, filters: LibraryFilters) {
case "vote_average":
sortExpressions.push(dirFn(titles.voteAverage));
break;
case "last_watched": {
// Latest watch per title (movie watch, or any episode watch for a show);
// never-watched titles sort last regardless of direction.
const lastWatched = sql<number | null>`(
SELECT MAX(w.watchedAt) FROM (
SELECT ${userMovieWatches.watchedAt} AS watchedAt FROM ${userMovieWatches}
WHERE ${userMovieWatches.titleId} = ${titles.id} AND ${userMovieWatches.userId} = ${userId}
UNION ALL
SELECT uew.watchedAt AS watchedAt FROM ${userEpisodeWatches} uew
JOIN ${episodes} e ON e.id = uew.episodeId
JOIN ${seasons} s ON s.id = e.seasonId
WHERE s.titleId = ${titles.id} AND uew.userId = ${userId}
) w
)`;
sortExpressions.push(asc(sql`CASE WHEN ${lastWatched} IS NULL THEN 1 ELSE 0 END`));
sortExpressions.push(dirFn(lastWatched));
break;
}
default:
sortExpressions.push(desc(userTitleStatus.addedAt));
}
+90 -1
View File
@@ -1,4 +1,4 @@
import { and, eq, inArray, sql } from "drizzle-orm";
import { and, desc, eq, inArray, lt, or, sql } from "drizzle-orm";
import { localDateString } from "@sofa/config";
@@ -349,3 +349,92 @@ export function deleteAllEpisodeWatchesForTitle(userId: string, titleId: string)
if (titleEpisodeIds.length === 0) return;
deleteEpisodeWatches(userId, titleEpisodeIds);
}
// ─── Watch history ──────────────────────────────────────────────────
type WatchSource = "manual" | "import" | "plex" | "jellyfin" | "emby";
export interface WatchHistoryCursor {
/** watchedAt in epoch ms */
t: number;
/** watch row id */
i: string;
}
export interface WatchHistoryQueryOptions {
limit: number;
source?: WatchSource;
before?: { watchedAt: Date; id: string };
}
export function getMovieWatchHistory(userId: string, opts: WatchHistoryQueryOptions) {
const conditions = [eq(userMovieWatches.userId, userId)];
if (opts.source) conditions.push(eq(userMovieWatches.source, opts.source));
if (opts.before) {
conditions.push(
or(
lt(userMovieWatches.watchedAt, opts.before.watchedAt),
and(
eq(userMovieWatches.watchedAt, opts.before.watchedAt),
lt(userMovieWatches.id, opts.before.id),
),
)!,
);
}
return db
.select({
watchId: userMovieWatches.id,
watchedAt: userMovieWatches.watchedAt,
source: userMovieWatches.source,
titleId: titles.id,
title: titles.title,
type: titles.type,
posterPath: titles.posterPath,
posterThumbHash: titles.posterThumbHash,
})
.from(userMovieWatches)
.innerJoin(titles, eq(userMovieWatches.titleId, titles.id))
.where(and(...conditions))
.orderBy(desc(userMovieWatches.watchedAt), desc(userMovieWatches.id))
.limit(opts.limit)
.all();
}
export function getEpisodeWatchHistory(userId: string, opts: WatchHistoryQueryOptions) {
const conditions = [eq(userEpisodeWatches.userId, userId)];
if (opts.source) conditions.push(eq(userEpisodeWatches.source, opts.source));
if (opts.before) {
conditions.push(
or(
lt(userEpisodeWatches.watchedAt, opts.before.watchedAt),
and(
eq(userEpisodeWatches.watchedAt, opts.before.watchedAt),
lt(userEpisodeWatches.id, opts.before.id),
),
)!,
);
}
return db
.select({
watchId: userEpisodeWatches.id,
watchedAt: userEpisodeWatches.watchedAt,
source: userEpisodeWatches.source,
titleId: titles.id,
title: titles.title,
type: titles.type,
posterPath: titles.posterPath,
posterThumbHash: titles.posterThumbHash,
episodeId: episodes.id,
seasonNumber: seasons.seasonNumber,
episodeNumber: episodes.episodeNumber,
episodeName: episodes.name,
})
.from(userEpisodeWatches)
.innerJoin(episodes, eq(userEpisodeWatches.episodeId, episodes.id))
.innerJoin(seasons, eq(episodes.seasonId, seasons.id))
.innerJoin(titles, eq(seasons.titleId, titles.id))
.where(and(...conditions))
.orderBy(desc(userEpisodeWatches.watchedAt), desc(userEpisodeWatches.id))
.limit(opts.limit)
.all();
}
+51
View File
@@ -378,6 +378,7 @@ msgstr "العمر {age}"
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr "الكل"
@@ -1096,6 +1097,11 @@ msgstr "البريد الإلكتروني"
msgid "Email is required"
msgstr "البريد الإلكتروني مطلوب"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr ""
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr "فعّل نوع الإشعار \"Playback Stop\""
@@ -1216,6 +1222,10 @@ msgstr "الأخطاء ({errorCount})"
msgid "Every {n}h"
msgstr "كل {n} ساعة"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "الأعلى تقييماً"
msgid "Hindi"
msgstr "الهندية"
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr "ذاكرة التخزين المؤقت للصور"
msgid "Image cache and backup disk usage"
msgstr "استخدام القرص لذاكرة التخزين المؤقت للصور والنسخ الاحتياطية"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr "الاستيراد"
@@ -1717,6 +1734,11 @@ msgstr "الإيطالية"
msgid "Japanese"
msgstr "اليابانية"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr "المهمة"
@@ -1799,6 +1821,11 @@ msgstr "فشل آخر تشغيل"
msgid "Last run succeeded"
msgstr "نجح آخر تشغيل"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr ""
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr "انقطع الاتصال بعملية الاستيراد. تحقق من
msgid "Manage your account and preferences"
msgstr "إدارة حسابك وتفضيلاتك"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr ""
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr "نسخة احتياطية يدوية"
@@ -2031,6 +2063,7 @@ 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
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr "أفلام"
@@ -2210,6 +2243,10 @@ msgstr ""
msgid "Nothing upcoming"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr ""
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "يرجى تأكيد كلمة المرور"
msgid "Please sign in again."
msgstr "يرجى تسجيل الدخول مرة أخرى."
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr "تشغيل الآن"
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr ""
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr "حلقات المسلسلات"
msgid "TV show"
msgstr "مسلسل تلفزيوني"
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "المسلسلات"
+51
View File
@@ -378,6 +378,7 @@ msgstr "Alter {age}"
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr "Alle"
@@ -1096,6 +1097,11 @@ msgstr "E-Mail"
msgid "Email is required"
msgstr "E-Mail ist erforderlich"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr ""
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr "Benachrichtigungstyp \"Playback Stop\" aktivieren"
@@ -1216,6 +1222,10 @@ msgstr "Fehler ({errorCount})"
msgid "Every {n}h"
msgstr "Alle {n}h"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "Bestbewertet"
msgid "Hindi"
msgstr ""
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr "Bild-Cache"
msgid "Image cache and backup disk usage"
msgstr "Speichernutzung für Bild-Cache und Backups"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr "Importieren"
@@ -1717,6 +1734,11 @@ msgstr "Italienisch"
msgid "Japanese"
msgstr "Japanisch"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr "Aufgabe"
@@ -1799,6 +1821,11 @@ msgstr "Letzter Lauf fehlgeschlagen"
msgid "Last run succeeded"
msgstr "Letzter Lauf erfolgreich"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr ""
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr "Verbindung zum Import verloren. Status in den Einstellungen prüfen."
msgid "Manage your account and preferences"
msgstr "Konto und Einstellungen verwalten"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr ""
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr "Manuelles Backup"
@@ -2031,6 +2063,7 @@ msgstr "Film"
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/components/settings/imports-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr "Filme"
@@ -2210,6 +2243,10 @@ msgstr ""
msgid "Nothing upcoming"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr ""
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "Bitte bestätige dein Passwort"
msgid "Please sign in again."
msgstr "Bitte melde dich erneut an."
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr "Jetzt ausführen"
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr ""
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr "TV-Episoden"
msgid "TV show"
msgstr "Serie"
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "Serien"
+51
View File
@@ -378,6 +378,7 @@ msgstr ""
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr ""
@@ -1096,6 +1097,11 @@ msgstr ""
msgid "Email is required"
msgstr "Email is required"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr "Emby"
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr ""
@@ -1216,6 +1222,10 @@ msgstr "Errors ({errorCount})"
msgid "Every {n}h"
msgstr "Every {n}h"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr "Everything you've watched, newest first."
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "Highest rated"
msgid "Hindi"
msgstr "Hindi"
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr "History"
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr ""
msgid "Image cache and backup disk usage"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr ""
@@ -1717,6 +1734,11 @@ msgstr "Italian"
msgid "Japanese"
msgstr "Japanese"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr "Jellyfin"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr ""
@@ -1799,6 +1821,11 @@ msgstr ""
msgid "Last run succeeded"
msgstr ""
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr "Last watched"
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr ""
msgid "Manage your account and preferences"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr "Manual"
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr ""
@@ -2031,6 +2063,7 @@ 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
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr ""
@@ -2210,6 +2243,10 @@ msgstr "Nothing to watch"
msgid "Nothing upcoming"
msgstr "Nothing upcoming"
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr "Nothing watched yet."
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "Please confirm your password"
msgid "Please sign in again."
msgstr "Please sign in again."
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr "Plex"
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr ""
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr "S{season} E{episode}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr "S{season} E{episode} · {name}"
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr ""
msgid "TV show"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "TV Shows"
+51
View File
@@ -378,6 +378,7 @@ msgstr "edad {age}"
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr "Todo"
@@ -1096,6 +1097,11 @@ msgstr "Correo electrónico"
msgid "Email is required"
msgstr "El correo electrónico es obligatorio"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr ""
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr "Activa el tipo de notificación \"Playback Stop\""
@@ -1216,6 +1222,10 @@ msgstr "Errores ({errorCount})"
msgid "Every {n}h"
msgstr "Cada {n}h"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "Mejor valorados"
msgid "Hindi"
msgstr ""
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr "Caché de imágenes"
msgid "Image cache and backup disk usage"
msgstr "Uso del disco de caché de imágenes y copias de seguridad"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr "Importar"
@@ -1717,6 +1734,11 @@ msgstr "Italiano"
msgid "Japanese"
msgstr "Japonés"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr "Tarea"
@@ -1799,6 +1821,11 @@ msgstr "Última ejecución fallida"
msgid "Last run succeeded"
msgstr "Última ejecución exitosa"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr ""
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr "Se perdió la conexión con la importación. Comprueba el estado en ajus
msgid "Manage your account and preferences"
msgstr "Gestiona tu cuenta y preferencias"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr ""
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr "Copia de seguridad manual"
@@ -2031,6 +2063,7 @@ msgstr "Película"
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/components/settings/imports-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr "Películas"
@@ -2210,6 +2243,10 @@ msgstr ""
msgid "Nothing upcoming"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr ""
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "Por favor, confirma tu contraseña"
msgid "Please sign in again."
msgstr "Por favor, inicia sesión de nuevo."
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr "Ejecutar ahora"
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr ""
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr "Episodios de series"
msgid "TV show"
msgstr "Serie"
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "Series de TV"
+51
View File
@@ -378,6 +378,7 @@ msgstr "âge {age}"
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr "Tout"
@@ -1096,6 +1097,11 @@ msgstr "E-mail"
msgid "Email is required"
msgstr "L'adresse e-mail est requise"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr ""
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr "Activez le type de notification « Playback Stop »"
@@ -1216,6 +1222,10 @@ msgstr "Erreurs ({errorCount})"
msgid "Every {n}h"
msgstr "Toutes les {n}h"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "Mieux notés"
msgid "Hindi"
msgstr ""
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr "Cache d'images"
msgid "Image cache and backup disk usage"
msgstr "Utilisation du disque : cache d'images et sauvegardes"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr "Importation"
@@ -1717,6 +1734,11 @@ msgstr "Italien"
msgid "Japanese"
msgstr "Japonais"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr "Tâche"
@@ -1799,6 +1821,11 @@ msgstr "Dernière exécution échouée"
msgid "Last run succeeded"
msgstr "Dernière exécution réussie"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr ""
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr "Connexion à l'importation perdue. Vérifiez l'état dans les paramètre
msgid "Manage your account and preferences"
msgstr "Gérez votre compte et vos préférences"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr ""
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr "Sauvegarde manuelle"
@@ -2031,6 +2063,7 @@ msgstr "Film"
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/components/settings/imports-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr "Films"
@@ -2210,6 +2243,10 @@ msgstr ""
msgid "Nothing upcoming"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr ""
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "Veuillez confirmer votre mot de passe"
msgid "Please sign in again."
msgstr "Veuillez vous reconnecter."
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr "Exécuter maintenant"
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr ""
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr "Épisodes de séries"
msgid "TV show"
msgstr "Série TV"
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "Séries TV"
+51
View File
@@ -378,6 +378,7 @@ msgstr "גיל {age}"
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr "הכל"
@@ -1096,6 +1097,11 @@ msgstr "דוא\"ל"
msgid "Email is required"
msgstr "כתובת דוא\"ל נדרשת"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr ""
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr "הפעל את סוג ההתראה \"Playback Stop\""
@@ -1216,6 +1222,10 @@ msgstr "שגיאות ({errorCount})"
msgid "Every {n}h"
msgstr "כל {n} שעות"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "בדירוג הגבוה ביותר"
msgid "Hindi"
msgstr "הינדי"
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr "מטמון תמונות"
msgid "Image cache and backup disk usage"
msgstr "שימוש בדיסק של מטמון תמונות וגיבויים"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr "ייבוא"
@@ -1717,6 +1734,11 @@ msgstr "איטלקית"
msgid "Japanese"
msgstr "יפנית"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr "משימה"
@@ -1799,6 +1821,11 @@ msgstr "ההרצה האחרונה נכשלה"
msgid "Last run succeeded"
msgstr "ההרצה האחרונה הצליחה"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr ""
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr "אבד החיבור לייבוא. בדוק את הסטטוס בהגדר
msgid "Manage your account and preferences"
msgstr "נהל את החשבון וההעדפות שלך"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr ""
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr "גיבוי ידני"
@@ -2031,6 +2063,7 @@ 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
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr "סרטים"
@@ -2210,6 +2243,10 @@ msgstr ""
msgid "Nothing upcoming"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr ""
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "אנא אשר את הסיסמה שלך"
msgid "Please sign in again."
msgstr "אנא התחבר שוב."
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr "הפעל עכשיו"
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr ""
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr "פרקי טלוויזיה"
msgid "TV show"
msgstr "סדרת טלוויזיה"
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "תוכניות טלוויזיה"
+51
View File
@@ -378,6 +378,7 @@ msgstr "età {age}"
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr "Tutti"
@@ -1096,6 +1097,11 @@ msgstr ""
msgid "Email is required"
msgstr "L'email è obbligatoria"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr ""
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr "Abilita il tipo di notifica \"Playback Stop\""
@@ -1216,6 +1222,10 @@ msgstr "Errori ({errorCount})"
msgid "Every {n}h"
msgstr "Ogni {n}h"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "Meglio valutati"
msgid "Hindi"
msgstr ""
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr "Cache immagini"
msgid "Image cache and backup disk usage"
msgstr "Utilizzo disco: cache immagini e backup"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr "Importazione"
@@ -1717,6 +1734,11 @@ msgstr "Italiano"
msgid "Japanese"
msgstr "Giapponese"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr "Attività"
@@ -1799,6 +1821,11 @@ msgstr "Ultima esecuzione fallita"
msgid "Last run succeeded"
msgstr "Ultima esecuzione riuscita"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr ""
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr "Connessione all'importazione persa. Controlla lo stato nelle impostazion
msgid "Manage your account and preferences"
msgstr "Gestisci il tuo account e le tue preferenze"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr ""
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr "Backup manuale"
@@ -2031,6 +2063,7 @@ msgstr "Film"
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/components/settings/imports-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr "Film"
@@ -2210,6 +2243,10 @@ msgstr ""
msgid "Nothing upcoming"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr ""
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "Conferma la tua password"
msgid "Please sign in again."
msgstr "Accedi di nuovo."
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr "Esegui ora"
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr ""
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr "Episodi TV"
msgid "TV show"
msgstr "Serie TV"
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "Serie TV"
+51
View File
@@ -378,6 +378,7 @@ msgstr "年齢 {age}"
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr "すべて"
@@ -1096,6 +1097,11 @@ msgstr "メールアドレス"
msgid "Email is required"
msgstr "メールアドレスは必須です"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr ""
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr "「Playback Stop」通知タイプを有効にしてください"
@@ -1216,6 +1222,10 @@ msgstr "エラー ({errorCount})"
msgid "Every {n}h"
msgstr "{n}時間ごと"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "評価が高い順"
msgid "Hindi"
msgstr "ヒンディー語"
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr "画像キャッシュ"
msgid "Image cache and backup disk usage"
msgstr "画像キャッシュとバックアップのディスク使用量"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr "インポート"
@@ -1717,6 +1734,11 @@ msgstr "イタリア語"
msgid "Japanese"
msgstr "日本語"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr "ジョブ"
@@ -1799,6 +1821,11 @@ msgstr "最終実行失敗"
msgid "Last run succeeded"
msgstr "最終実行成功"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr ""
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr "インポートへの接続が切断されました。設定でステー
msgid "Manage your account and preferences"
msgstr "アカウントと設定を管理する"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr ""
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr "手動バックアップ"
@@ -2031,6 +2063,7 @@ 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
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr "映画"
@@ -2210,6 +2243,10 @@ msgstr ""
msgid "Nothing upcoming"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr ""
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "パスワードを確認してください"
msgid "Please sign in again."
msgstr "再度サインインしてください。"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr "今すぐ実行"
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr ""
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr "TVエピソード"
msgid "TV show"
msgstr "TV番組"
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "TVシリーズ"
+51
View File
@@ -378,6 +378,7 @@ msgstr "나이 {age}"
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr "전체"
@@ -1096,6 +1097,11 @@ msgstr "이메일"
msgid "Email is required"
msgstr "이메일을 입력해주세요"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr ""
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr "\"Playback Stop\" 알림 유형 활성화"
@@ -1216,6 +1222,10 @@ msgstr "오류 ({errorCount})"
msgid "Every {n}h"
msgstr "매 {n}시간"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "평점 높은 순"
msgid "Hindi"
msgstr "힌디어"
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr "이미지 캐시"
msgid "Image cache and backup disk usage"
msgstr "이미지 캐시 및 백업 디스크 사용량"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr "가져오기"
@@ -1717,6 +1734,11 @@ msgstr "이탈리아어"
msgid "Japanese"
msgstr "일본어"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr "작업"
@@ -1799,6 +1821,11 @@ msgstr "마지막 실행 실패"
msgid "Last run succeeded"
msgstr "마지막 실행 성공"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr ""
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr "가져오기 연결이 끊어졌습니다. 설정에서 상태를 확인
msgid "Manage your account and preferences"
msgstr "계정 및 환경설정을 관리하세요"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr ""
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr "수동 백업"
@@ -2031,6 +2063,7 @@ 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
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr "영화"
@@ -2210,6 +2243,10 @@ msgstr ""
msgid "Nothing upcoming"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr ""
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "비밀번호를 확인해주세요"
msgid "Please sign in again."
msgstr "다시 로그인해주세요."
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr "지금 실행"
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr ""
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr "TV 에피소드"
msgid "TV show"
msgstr "TV 프로그램"
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "TV 프로그램"
+51
View File
@@ -378,6 +378,7 @@ msgstr "leeftijd {age}"
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr "Alles"
@@ -1096,6 +1097,11 @@ msgstr "E-mail"
msgid "Email is required"
msgstr "E-mail is vereist"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr ""
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr "Schakel het meldingstype \"Playback Stop\" in"
@@ -1216,6 +1222,10 @@ msgstr "Fouten ({errorCount})"
msgid "Every {n}h"
msgstr "Elke {n}u"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "Hoogst beoordeeld"
msgid "Hindi"
msgstr ""
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr "Afbeeldingscache"
msgid "Image cache and backup disk usage"
msgstr "Schijfgebruik voor afbeeldingscache en back-ups"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr "Importeren"
@@ -1717,6 +1734,11 @@ msgstr "Italiaans"
msgid "Japanese"
msgstr "Japans"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr "Taak"
@@ -1799,6 +1821,11 @@ msgstr "Laatste uitvoering mislukt"
msgid "Last run succeeded"
msgstr "Laatste uitvoering geslaagd"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr ""
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr "Verbinding met de import verbroken. Controleer de status in de instellin
msgid "Manage your account and preferences"
msgstr "Beheer je account en voorkeuren"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr ""
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr "Handmatige back-up"
@@ -2031,6 +2063,7 @@ msgstr "Film"
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/components/settings/imports-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr "Films"
@@ -2210,6 +2243,10 @@ msgstr ""
msgid "Nothing upcoming"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr ""
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "Bevestig je wachtwoord"
msgid "Please sign in again."
msgstr "Meld je opnieuw aan."
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr "Nu uitvoeren"
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr ""
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr "Tv-afleveringen"
msgid "TV show"
msgstr "Tv-serie"
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "Tv-series"
+51
View File
@@ -378,6 +378,7 @@ msgstr "idade {age}"
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr "Todos"
@@ -1096,6 +1097,11 @@ msgstr "E-mail"
msgid "Email is required"
msgstr "O email é obrigatório"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr ""
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr "Ative o tipo de notificação \"Playback Stop\""
@@ -1216,6 +1222,10 @@ msgstr "Erros ({errorCount})"
msgid "Every {n}h"
msgstr "A cada {n}h"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "Melhor avaliados"
msgid "Hindi"
msgstr ""
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr "Cache de imagens"
msgid "Image cache and backup disk usage"
msgstr "Uso de disco de cache de imagens e backups"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr "Importar"
@@ -1717,6 +1734,11 @@ msgstr "Italiano"
msgid "Japanese"
msgstr "Japonês"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr "Tarefa"
@@ -1799,6 +1821,11 @@ msgstr "Última execução falhou"
msgid "Last run succeeded"
msgstr "Última execução bem-sucedida"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr ""
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr "Conexão de importação perdida. Verifique o status nas configurações
msgid "Manage your account and preferences"
msgstr "Gerencie sua conta e preferências"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr ""
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr "Backup manual"
@@ -2031,6 +2063,7 @@ msgstr "Filme"
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/components/settings/imports-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr "Filmes"
@@ -2210,6 +2243,10 @@ msgstr ""
msgid "Nothing upcoming"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr ""
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "Por favor confirme a sua palavra-passe"
msgid "Please sign in again."
msgstr "Por favor inicie sessão novamente."
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr "Executar agora"
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr ""
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr "Episódios de séries"
msgid "TV show"
msgstr "Série"
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "Séries"
+51
View File
@@ -378,6 +378,7 @@ msgstr "{age} 岁"
#: apps/native/src/components/library/filter-sheet.tsx
#: apps/web/src/components/library/library-filters.tsx
#: apps/web/src/components/people/filmography-grid.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "All"
msgstr "全部"
@@ -1096,6 +1097,11 @@ msgstr "邮箱"
msgid "Email is required"
msgstr "邮箱不能为空"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Emby"
msgstr ""
#: apps/native/src/components/settings/integration-configs.ts
msgid "Enable the \"Playback Stop\" notification type"
msgstr "启用“Playback Stop”通知类型"
@@ -1216,6 +1222,10 @@ msgstr "错误 ({errorCount})"
msgid "Every {n}h"
msgstr "每 {n} 小时"
#: apps/web/src/routes/_app/history.tsx
msgid "Everything you've watched, newest first."
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/_layout.tsx
#: apps/native/src/app/(tabs)/(home)/index.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
@@ -1570,6 +1580,11 @@ msgstr "评分最高"
msgid "Hindi"
msgstr "印地语"
#: apps/web/src/components/nav-bar.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "History"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1590,7 +1605,9 @@ msgstr "图片缓存"
msgid "Image cache and backup disk usage"
msgstr "图片缓存与备份磁盘用量"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/components/settings/account-section.tsx
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr "导入"
@@ -1717,6 +1734,11 @@ msgstr "意大利语"
msgid "Japanese"
msgstr "日语"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Jellyfin"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Job"
msgstr "任务"
@@ -1799,6 +1821,11 @@ msgstr "上次运行失败"
msgid "Last run succeeded"
msgstr "上次运行成功"
#: apps/native/src/components/library/sort-menu.tsx
#: apps/web/src/components/library/library-toolbar.tsx
msgid "Last watched"
msgstr ""
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
#: apps/native/src/components/navigation/native-tab-bar.android.tsx
#: apps/native/src/components/navigation/native-tab-bar.tsx
@@ -1837,6 +1864,11 @@ msgstr "导入连接已断开,请在设置中查看状态。"
msgid "Manage your account and preferences"
msgstr "管理您的账户和偏好设置"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Manual"
msgstr ""
#: apps/web/src/components/settings/backup-section.tsx
msgid "Manual backup"
msgstr "手动备份"
@@ -2031,6 +2063,7 @@ 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
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Movies"
msgstr "电影"
@@ -2210,6 +2243,10 @@ msgstr ""
msgid "Nothing upcoming"
msgstr ""
#: apps/web/src/routes/_app/history.tsx
msgid "Nothing watched yet."
msgstr ""
#: apps/web/src/components/library/library-filters.tsx
#~ msgid "Older"
#~ msgstr "Older"
@@ -2399,6 +2436,11 @@ msgstr "请确认您的密码"
msgid "Please sign in again."
msgstr "请重新登录。"
#: apps/web/src/components/history/history-row.tsx
#: apps/web/src/routes/_app/history.tsx
msgid "Plex"
msgstr ""
#: apps/native/src/app/(tabs)/(explore)/index.tsx
#: apps/web/src/routes/_app/explore.tsx
msgid "Popular Movies"
@@ -2817,6 +2859,14 @@ msgstr "立即运行"
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode}"
msgstr ""
#: apps/web/src/components/history/history-row.tsx
msgid "S{season} E{episode} · {name}"
msgstr ""
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/lib/widgets.ts
#: apps/web/src/components/dashboard/upcoming-item.tsx
@@ -3486,6 +3536,7 @@ msgstr "电视剧集"
msgid "TV show"
msgstr "电视剧"
#: apps/web/src/routes/_app/history.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "TV Shows"
msgstr "电视剧"