feat: reorganize API around operation domains

Restructure the oRPC contract from 14 resource-oriented routers to 8
domain-oriented routers for a cleaner public API surface.

- Consolidate 7 watch procedures into unified tracking.watch/unwatch
  with scope + ids input (movie, episode, season, series)
- Split dashboard across tracking (stats, history), library
  (continueWatching, upcoming), and discover (recommendations)
- Merge explore + search + discover into single discover router
- Absorb integrations into account.integrations
- Merge system.authConfig into system.publicInfo
- Collapse 6 admin setting endpoints into admin.settings.get/update
- Deduplicate platforms.list + explore.watchProviders into
  discover.platforms
- Add symmetric unwatchMovie/unwatchSeries core functions
- Rename titles.detail→get, titles.recommendations→similar,
  people.detail→get

BREAKING CHANGE: All client API paths have changed. REST paths now
mirror router structure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 12:36:49 -04:00
co-authored by Claude Opus 4.6
parent 3906f6021d
commit 73e5c96bd8
112 changed files with 3291 additions and 4015 deletions
+5 -5
View File
@@ -41,7 +41,7 @@ export default function LoginScreen() {
const [errorFields, setErrorFields] = useState<Set<string>>(new Set());
const [isSignedIn, setIsSignedIn] = useState(false);
const authConfig = useQuery(orpc.system.authConfig.queryOptions());
const publicInfo = useQuery(orpc.system.publicInfo.queryOptions());
const form = useForm({
defaultValues: { email: "", password: "" },
@@ -76,9 +76,9 @@ export default function LoginScreen() {
const statusCompletedColor = useCSSVariable("--color-status-completed") as string;
const serverHost = splitUrl(getServerUrl()).host;
const showPasswordLogin = !authConfig.data?.passwordLoginDisabled;
const showOidc = authConfig.data?.oidcEnabled;
const showRegister = authConfig.data?.registrationOpen;
const showPasswordLogin = !publicInfo.data?.passwordLoginDisabled;
const showOidc = publicInfo.data?.oidcEnabled;
const showRegister = publicInfo.data?.registrationOpen;
const clearFieldError = (name: string) => {
if (errorFields.has(name)) {
@@ -94,7 +94,7 @@ export default function LoginScreen() {
<AuthScreen title="Sofa" subtitle={t`Sign in to continue`}>
{showOidc &&
(() => {
const providerName = authConfig.data?.oidcProviderName ?? "SSO";
const providerName = publicInfo.data?.oidcProviderName ?? "SSO";
return (
<Animated.View entering={FadeInDown.duration(300).delay(100)} className="mb-4">
<Button
@@ -18,7 +18,7 @@ const exploreContentContainerStyle = {
export default function ExploreScreen() {
const { t } = useLingui();
const trending = useInfiniteQuery(
orpc.explore.trending.infiniteOptions({
orpc.discover.trending.infiniteOptions({
input: (pageParam: number) => ({ type: "all" as const, page: pageParam }),
initialPageParam: 1,
getNextPageParam: (lastPage) =>
@@ -26,16 +26,15 @@ export default function ExploreScreen() {
maxPages: 10,
}),
);
const popularMovies = useQuery(orpc.explore.popular.queryOptions({ input: { type: "movie" } }));
const popularTv = useQuery(orpc.explore.popular.queryOptions({ input: { type: "tv" } }));
const movieGenres = useQuery(orpc.explore.genres.queryOptions({ input: { type: "movie" } }));
const tvGenres = useQuery(orpc.explore.genres.queryOptions({ input: { type: "tv" } }));
const popularMovies = useQuery(orpc.discover.popular.queryOptions({ input: { type: "movie" } }));
const popularTv = useQuery(orpc.discover.popular.queryOptions({ input: { type: "tv" } }));
const movieGenres = useQuery(orpc.discover.genres.queryOptions({ input: { type: "movie" } }));
const tvGenres = useQuery(orpc.discover.genres.queryOptions({ input: { type: "tv" } }));
const isRefreshing =
trending.isRefetching || popularMovies.isRefetching || popularTv.isRefetching;
const onRefresh = useCallback(() => {
queryClient.invalidateQueries({ queryKey: orpc.explore.key() });
queryClient.invalidateQueries({ queryKey: orpc.discover.key() });
}, []);
+6 -6
View File
@@ -51,20 +51,20 @@ export default function DashboardScreen() {
"--color-status-completed",
]) as [string, string, string, string];
const stats = useQuery(orpc.dashboard.stats.queryOptions());
const stats = useQuery(orpc.tracking.stats.queryOptions());
const movieHistory = useQuery(
orpc.dashboard.watchHistory.queryOptions({
orpc.tracking.history.queryOptions({
input: { type: "movie", period: moviePeriod },
}),
);
const episodeHistory = useQuery(
orpc.dashboard.watchHistory.queryOptions({
orpc.tracking.history.queryOptions({
input: { type: "episode", period: episodePeriod },
}),
);
const continueWatching = useQuery(orpc.dashboard.continueWatching.queryOptions());
const continueWatching = useQuery(orpc.library.continueWatching.queryOptions());
const library = useQuery(orpc.library.list.queryOptions({ input: { page: 1, limit: 10 } }));
const recommendations = useQuery(orpc.dashboard.recommendations.queryOptions());
const recommendations = useQuery(orpc.discover.recommendations.queryOptions());
const isRefreshing =
stats.isRefetching ||
@@ -74,7 +74,7 @@ export default function DashboardScreen() {
episodeHistory.isRefetching;
const onRefresh = useCallback(() => {
queryClient.invalidateQueries({ queryKey: orpc.dashboard.key() });
queryClient.invalidateQueries({ queryKey: orpc.tracking.key() });
queryClient.invalidateQueries({ queryKey: orpc.library.key() });
}, []);
@@ -57,7 +57,7 @@ export default function UpcomingScreen() {
const { data, isPending, fetchNextPage, hasNextPage, isFetchingNextPage, isRefetching } =
useInfiniteQuery(
orpc.dashboard.upcoming.infiniteOptions({
orpc.library.upcoming.infiniteOptions({
input: (pageParam: string | undefined) => ({
days: 90,
limit: 20,
@@ -83,7 +83,7 @@ export default function UpcomingScreen() {
);
const onRefresh = useCallback(() => {
queryClient.invalidateQueries({ queryKey: orpc.dashboard.upcoming.key() });
queryClient.invalidateQueries({ queryKey: orpc.library.upcoming.key() });
}, []);
const onEndReached = useCallback(() => {
@@ -20,7 +20,7 @@ export default function SearchScreen() {
const debouncedQuery = useDebounce(query.trim(), 300);
const searchResults = useInfiniteQuery({
...orpc.search.infiniteOptions({
...orpc.discover.search.infiniteOptions({
input:
debouncedQuery.length > 0
? (pageParam: number) => ({ query: debouncedQuery, page: pageParam })
+40 -32
View File
@@ -90,7 +90,7 @@ export default function SettingsScreen() {
const isAdmin = session?.user?.role === "admin";
const serverUrl = getServerUrl();
const authConfig = useQuery(orpc.system.authConfig.queryOptions());
const publicInfo = useQuery(orpc.system.publicInfo.queryOptions());
const { data: accounts } = useQuery({
queryKey: ["auth", "listAccounts"],
queryFn: async () => {
@@ -100,7 +100,7 @@ export default function SettingsScreen() {
});
const hasPassword =
accounts?.some((a: { providerId: string }) => a.providerId === "credential") ?? false;
const showPasswordOption = hasPassword && !(authConfig.data?.passwordLoginDisabled ?? true);
const showPasswordOption = hasPassword && !(publicInfo.data?.passwordLoginDisabled ?? true);
const systemHealth = useQuery({
...orpc.admin.systemHealth.queryOptions(),
@@ -162,40 +162,48 @@ export default function SettingsScreen() {
const hasAvatarImage = !!session?.user?.image;
const registration = useQuery({
...orpc.admin.registration.queryOptions(),
const adminSettings = useQuery({
...orpc.admin.settings.get.queryOptions(),
enabled: isAdmin,
});
const toggleRegistration = useMutation(
orpc.admin.toggleRegistration.mutationOptions({
onSuccess: (_data, { open }) => {
toast.success(open ? t`Registration opened` : t`Registration closed`);
const updateAdminSettings = useMutation(
orpc.admin.settings.update.mutationOptions({
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: orpc.admin.registration.key(),
});
},
onError: () => toast.error(t`Failed to update registration setting`),
}),
);
const updateCheck = useQuery({
...orpc.admin.updateCheck.queryOptions(),
enabled: isAdmin,
});
const toggleUpdateCheck = useMutation(
orpc.admin.toggleUpdateCheck.mutationOptions({
onSuccess: (_data, { enabled }) => {
toast.success(enabled ? t`Update checks enabled` : t`Update checks disabled`);
queryClient.invalidateQueries({
queryKey: orpc.admin.updateCheck.key(),
queryKey: orpc.admin.settings.key(),
});
},
onError: () => toast.error(t`Failed to update setting`),
}),
);
const toggleRegistration = {
mutate: ({ open }: { open: boolean }) => {
updateAdminSettings.mutate(
{ registration: { open } },
{
onSuccess: () => {
toast.success(open ? t`Registration opened` : t`Registration closed`);
},
},
);
},
};
const toggleUpdateCheck = {
mutate: ({ enabled }: { enabled: boolean }) => {
updateAdminSettings.mutate(
{ updateCheck: { enabled } },
{
onSuccess: () => {
toast.success(enabled ? t`Update checks enabled` : t`Update checks disabled`);
},
},
);
},
};
const primaryFgColor = useCSSVariable("--color-primary-foreground") as string;
const mutedFgColor = useCSSVariable("--color-muted-foreground") as string;
@@ -484,7 +492,7 @@ export default function SettingsScreen() {
icon={IconUserPlus}
right={
<Switch
value={registration.data?.open ?? false}
value={adminSettings.data?.registration?.open ?? false}
accessibilityLabel={t`Open registration`}
onValueChange={(open) => toggleRegistration.mutate({ open })}
/>
@@ -495,15 +503,15 @@ export default function SettingsScreen() {
icon={IconCloud}
right={
<Switch
value={updateCheck.data?.enabled ?? false}
value={adminSettings.data?.updateCheck?.enabled ?? false}
accessibilityLabel={t`Check for updates`}
onValueChange={(enabled) => toggleUpdateCheck.mutate({ enabled })}
/>
}
/>
{(() => {
const latestVersion = updateCheck.data?.updateCheck?.latestVersion;
return updateCheck.data?.updateCheck?.updateAvailable ? (
const latestVersion = adminSettings.data?.updateCheck?.latestVersion;
return adminSettings.data?.updateCheck?.updateAvailable ? (
<View className="py-3.5">
<Text className="text-status-completed font-sans text-sm font-medium">
<Trans>Update available: {latestVersion}</Trans>
@@ -567,8 +575,8 @@ export default function SettingsScreen() {
Native
{Application.nativeApplicationVersion ? ` v${Application.nativeApplicationVersion}` : ""}
{Application.nativeBuildVersion ? ` (${Application.nativeBuildVersion})` : ""}
{updateCheck.data?.updateCheck?.currentVersion
? ` · Server v${updateCheck.data.updateCheck.currentVersion}`
{adminSettings.data?.updateCheck?.currentVersion
? ` · Server v${adminSettings.data.updateCheck.currentVersion}`
: ""}
</Text>
</Animated.View>
+1 -1
View File
@@ -71,7 +71,7 @@ export default function PersonDetailScreen() {
const { data, isPending, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(
orpc.people.detail.infiniteOptions({
orpc.people.get.infiniteOptions({
input: (pageParam: number) => ({ id, page: pageParam, limit: 20 }),
initialPageParam: 1,
getNextPageParam: (lastPage) =>
+4 -4
View File
@@ -104,9 +104,9 @@ export default function TitleDetailScreen() {
"--color-title-accent-foreground",
]) as [string, string, string];
const detail = useQuery(orpc.titles.detail.queryOptions({ input: { id } }));
const userInfo = useQuery(orpc.titles.userInfo.queryOptions({ input: { id } }));
const recommendations = useQuery(orpc.titles.recommendations.queryOptions({ input: { id } }));
const detail = useQuery(orpc.titles.get.queryOptions({ input: { id } }));
const userInfo = useQuery(orpc.tracking.userInfo.queryOptions({ input: { id } }));
const recommendations = useQuery(orpc.titles.similar.queryOptions({ input: { id } }));
const {
updateStatus,
@@ -425,7 +425,7 @@ export default function TitleDetailScreen() {
{title.type === "movie" && (
<Pressable
onPress={() => watchMovie.mutate({ id })}
onPress={() => watchMovie.mutate({ scope: "movie", ids: [id] })}
disabled={watchMovie.isPending}
className="bg-title-accent flex-row items-center gap-1.5 rounded-lg px-4 py-2"
>
@@ -13,7 +13,7 @@ export function UpcomingSection() {
const { t } = useLingui();
const { push } = useRouter();
const { data, isPending } = useQuery(
orpc.dashboard.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
orpc.library.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
);
if (isPending) return null;
@@ -48,7 +48,7 @@ export function FilterableTitleRow({
const [selectedGenre, setSelectedGenre] = useState<number | null>(null);
const discover = useInfiniteQuery({
...orpc.discover.infiniteOptions({
...orpc.discover.browse.infiniteOptions({
input:
selectedGenre != null
? (pageParam: number) => ({
@@ -81,30 +81,30 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
const toggleSetup = useCallback(() => setSetupOpen((v) => !v), []);
const connectMutation = useMutation(
orpc.integrations.create.mutationOptions({
orpc.account.integrations.create.mutationOptions({
onSuccess: () => {
toast.success(t`${label} connected`);
queryClient.invalidateQueries({ queryKey: orpc.integrations.key() });
queryClient.invalidateQueries({ queryKey: orpc.account.integrations.key() });
},
onError: () => toast.error(t`Failed to connect ${label}`),
}),
);
const { mutate: deleteIntegration } = useMutation(
orpc.integrations.delete.mutationOptions({
orpc.account.integrations.delete.mutationOptions({
onSuccess: () => {
toast.success(t`${label} disconnected`);
queryClient.invalidateQueries({ queryKey: orpc.integrations.key() });
queryClient.invalidateQueries({ queryKey: orpc.account.integrations.key() });
},
onError: () => toast.error(t`Failed to disconnect ${label}`),
}),
);
const { mutate: regenerateToken, isPending: isRegenerating } = useMutation(
orpc.integrations.regenerateToken.mutationOptions({
orpc.account.integrations.regenerateToken.mutationOptions({
onSuccess: () => {
toast.success(t`${label} URL regenerated`);
queryClient.invalidateQueries({ queryKey: orpc.integrations.key() });
queryClient.invalidateQueries({ queryKey: orpc.account.integrations.key() });
},
onError: () => toast.error(t`Failed to regenerate ${label} URL`),
}),
@@ -14,7 +14,7 @@ import { orpc } from "@/lib/orpc";
export function IntegrationsSection() {
const { t, i18n } = useLingui();
const configs = getIntegrationConfigs(i18n);
const integrations = useQuery(orpc.integrations.list.queryOptions());
const integrations = useQuery(orpc.account.integrations.list.queryOptions());
return (
<View className="mb-6">
@@ -76,13 +76,15 @@ export function SeasonAccordion({
const { watchEpisode, unwatchEpisode, watchSeason } = useTitleActions({
toasts: {
watchEpisode: ({ id: epId }) => {
watchEpisode: ({ ids }) => {
const epId = ids[0];
const ep = episodes.find((e) => e.id === epId);
const sNum = season.seasonNumber;
const eNum = ep?.episodeNumber;
return ep ? t`Watched S${sNum} E${eNum}` : t`Episode watched`;
},
unwatchEpisode: ({ id: epId }) => {
unwatchEpisode: ({ ids }) => {
const epId = ids[0];
const ep = episodes.find((e) => e.id === epId);
const sNum = season.seasonNumber;
const eNum = ep?.episodeNumber;
@@ -99,9 +101,9 @@ export function SeasonAccordion({
const handleEpisodeToggle = useCallback(
(episodeId: string) => {
if (watchedEpisodeIds.has(episodeId)) {
unwatchEpisode.mutate({ id: episodeId });
unwatchEpisode.mutate({ scope: "episode", ids: [episodeId] });
} else {
watchEpisode.mutate({ id: episodeId });
watchEpisode.mutate({ scope: "episode", ids: [episodeId] });
}
},
[watchedEpisodeIds, unwatchEpisode, watchEpisode],
@@ -156,7 +158,7 @@ export function SeasonAccordion({
<Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)}>
{watchedCount < episodes.length && (
<Pressable
onPress={() => watchSeason.mutate({ id: season.id })}
onPress={() => watchSeason.mutate({ scope: "season", ids: [season.id] })}
className="bg-secondary mx-4 mb-2 flex-row items-center justify-center rounded-lg py-2"
>
<Text className="text-title-accent font-sans text-xs font-medium">
+17 -12
View File
@@ -18,15 +18,20 @@ function resolveToast<TInput>(
return typeof override === "function" ? override(input) : override;
}
interface WatchInput {
scope: string;
ids: string[];
}
interface UseTitleActionsOptions {
toasts?: {
quickAdd?: ToastOverride<{ id: string }>;
updateStatus?: ToastOverride<{ id: string; status: string | null }>;
watchMovie?: ToastOverride<{ id: string }>;
watchMovie?: ToastOverride<WatchInput>;
updateRating?: ToastOverride<{ id: string; stars: number }>;
watchEpisode?: ToastOverride<{ id: string }>;
unwatchEpisode?: ToastOverride<{ id: string }>;
watchSeason?: ToastOverride<{ id: string }>;
watchEpisode?: ToastOverride<WatchInput>;
unwatchEpisode?: ToastOverride<WatchInput>;
watchSeason?: ToastOverride<WatchInput>;
};
}
@@ -39,7 +44,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
const toastOverrides = options?.toasts;
const quickAdd = useMutation(
orpc.titles.quickAdd.mutationOptions({
orpc.tracking.quickAdd.mutationOptions({
onSuccess: (_data, input) => {
toast.success(resolveToast(toastOverrides?.quickAdd, t`Added to watchlist`, input));
invalidateTitleQueries();
@@ -53,7 +58,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
);
const updateStatus = useMutation(
orpc.titles.updateStatus.mutationOptions({
orpc.tracking.updateStatus.mutationOptions({
onSuccess: (_data, input) => {
const statusMessages: Record<string, string> = {
watchlist: t`Added to watchlist`,
@@ -69,7 +74,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
);
const watchMovie = useMutation(
orpc.titles.watchMovie.mutationOptions({
orpc.tracking.watch.mutationOptions({
onSuccess: (_data, input) => {
toast.success(resolveToast(toastOverrides?.watchMovie, t`Marked as watched`, input));
invalidateTitleQueries();
@@ -79,7 +84,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
);
const updateRating = useMutation(
orpc.titles.updateRating.mutationOptions({
orpc.tracking.rate.mutationOptions({
onSuccess: (_data, input) => {
const stars = input.stars;
const defaultMsg =
@@ -87,7 +92,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
? t`Rated ${plural(stars, { one: "# star", other: "# stars" })}`
: t`Rating removed`;
toast.success(resolveToast(toastOverrides?.updateRating, defaultMsg, input));
// Rating only invalidates title queries, not dashboard
// Rating only invalidates title queries, not tracking
queryClient.invalidateQueries({ queryKey: orpc.titles.key() });
},
onError: () => toast.error(t`Failed to update rating`),
@@ -95,7 +100,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
);
const watchEpisode = useMutation(
orpc.episodes.watch.mutationOptions({
orpc.tracking.watch.mutationOptions({
onSuccess: (_data, input) => {
toast.success(resolveToast(toastOverrides?.watchEpisode, t`Episode watched`, input));
invalidateTitleQueries();
@@ -105,7 +110,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
);
const unwatchEpisode = useMutation(
orpc.episodes.unwatch.mutationOptions({
orpc.tracking.unwatch.mutationOptions({
onSuccess: (_data, input) => {
toast.success(resolveToast(toastOverrides?.unwatchEpisode, t`Episode unwatched`, input));
invalidateTitleQueries();
@@ -115,7 +120,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
);
const watchSeason = useMutation(
orpc.seasons.watch.mutationOptions({
orpc.tracking.watch.mutationOptions({
onSuccess: (_data, input) => {
toast.success(resolveToast(toastOverrides?.watchSeason, t`Season watched`, input));
invalidateTitleQueries();
+11 -10
View File
@@ -8,10 +8,11 @@ import { i18n } from "@sofa/i18n";
let widgetRefreshTimer: ReturnType<typeof setTimeout> | null = null;
/** Invalidate title + dashboard queries. Used by most title mutations. */
/** Invalidate title + tracking + library queries. Used by most title mutations. */
export function invalidateTitleQueries() {
queryClient.invalidateQueries({ queryKey: orpc.titles.key() });
queryClient.invalidateQueries({ queryKey: orpc.dashboard.key() });
queryClient.invalidateQueries({ queryKey: orpc.tracking.key() });
queryClient.invalidateQueries({ queryKey: orpc.library.key() });
// Debounce widget refresh to batch rapid mutations (e.g. watching multiple episodes)
if (widgetRefreshTimer) clearTimeout(widgetRefreshTimer);
@@ -28,7 +29,7 @@ export function invalidateTitleQueries() {
export const titleActions = {
async quickAdd(id: string, titleName?: string) {
try {
await client.titles.quickAdd({ id });
await client.tracking.quickAdd({ id });
toast.success(
titleName
? i18n._(msg`Added "${titleName}" to watchlist`)
@@ -44,7 +45,7 @@ export const titleActions = {
async markMovieWatched(id: string, titleName?: string) {
try {
await client.titles.watchMovie({ id });
await client.tracking.watch({ scope: "movie", ids: [id] });
toast.success(
titleName ? i18n._(msg`Marked "${titleName}" as watched`) : i18n._(msg`Marked as watched`),
);
@@ -56,7 +57,7 @@ export const titleActions = {
async removeFromLibrary(id: string) {
try {
await client.titles.updateStatus({ id, status: null });
await client.tracking.updateStatus({ id, status: null });
toast.success(i18n._(msg`Removed from library`));
invalidateTitleQueries();
} catch {
@@ -66,7 +67,7 @@ export const titleActions = {
async rate(id: string, stars: number) {
try {
await client.titles.updateRating({ id, stars });
await client.tracking.rate({ id, stars });
toast.success(
stars > 0
? i18n._(msg`Rated ${plural(stars, { one: "# star", other: "# stars" })}`)
@@ -80,7 +81,7 @@ export const titleActions = {
async watchEpisode(id: string) {
try {
await client.episodes.watch({ id });
await client.tracking.watch({ scope: "episode", ids: [id] });
toast.success(i18n._(msg`Episode watched`));
invalidateTitleQueries();
} catch {
@@ -90,7 +91,7 @@ export const titleActions = {
async unwatchEpisode(id: string) {
try {
await client.episodes.unwatch({ id });
await client.tracking.unwatch({ scope: "episode", ids: [id] });
toast.success(i18n._(msg`Episode unwatched`));
invalidateTitleQueries();
} catch {
@@ -100,7 +101,7 @@ export const titleActions = {
async markAllWatched(id: string, titleName?: string) {
try {
await client.titles.watchAll({ id });
await client.tracking.watch({ scope: "series", ids: [id] });
toast.success(
titleName
? i18n._(msg`Marked all episodes of "${titleName}" as watched`)
@@ -114,7 +115,7 @@ export const titleActions = {
async watchSeason(id: string, seasonLabel?: string) {
try {
await client.seasons.watch({ id });
await client.tracking.watch({ scope: "season", ids: [id] });
toast.success(
seasonLabel ? i18n._(msg`Watched all of ${seasonLabel}`) : i18n._(msg`Season watched`),
);
+1 -1
View File
@@ -33,7 +33,7 @@ vi.mock("@/lib/widget-assets", () => ({
vi.mock("@/lib/orpc", () => ({
client: {
dashboard: {
library: {
continueWatching,
upcoming,
},
+2 -2
View File
@@ -140,7 +140,7 @@ async function refreshContinueWatching(
iconFilePath: string,
): Promise<void> {
try {
const { items } = await client.dashboard.continueWatching();
const { items } = await client.library.continueWatching();
if (items.length === 0) {
widget.updateSnapshot(
@@ -200,7 +200,7 @@ async function refreshUpcoming(
iconFilePath: string,
): Promise<void> {
try {
const { items } = await client.dashboard.upcoming({
const { items } = await client.library.upcoming({
days: 30,
limit: 5,
});