Files
sofa/apps/server/src/orpc/procedures/library.ts
T
dce333e128 refactor: organize API around operation domains (#24)
* 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>

* fix: address review feedback on API reorganization

- Fix updateRating invalidation in native app — was only invalidating
  title queries, now calls invalidateTitleQueries() to also refresh
  tracking.userInfo (drives the rating UI)
- Make unwatchMovie status revert consistent with unwatchSeries — revert
  any non-watchlist status, not just "completed"
- Add sync invariant comment on handleWatch/handleUnwatch loops
- Remove unused queryClient import in native use-title-actions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: updateStatus NOT_FOUND error + rate() invalidation in native

- updateStatus now throws NOT_FOUND when quickAddTitle returns null
  (title doesn't exist), instead of silently succeeding
- Add NOT_FOUND error to updateStatus contract definition
- Fix rate() in native title-actions.ts to call invalidateTitleQueries()
  instead of only invalidating orpc.titles.key() — mirrors the fix
  already applied to the hook-based path in d2ddc0f

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: expand mobile app docs and add Play Store badge

- Add the Google Play badge to the README alongside the App Store badge
- Expand the mobile app docs with the Android/Play Store release details
- Refresh docs site dependencies and related package versions for the updated docs stack

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:44:05 -04:00

102 lines
3.3 KiB
TypeScript

import { getContinueWatchingFeed, getUserStats, getUpcomingFeed } from "@sofa/core/discovery";
import { getFilteredLibraryFeed, getLibraryGenresList } from "@sofa/core/library";
import { tmdbImageUrl } from "@sofa/tmdb/image";
import { os } from "../context";
import { authed } from "../middleware";
export const list = os.library.list.use(authed).handler(({ input, context }) => {
const result = getFilteredLibraryFeed(context.user.id, {
search: input.search,
statuses: input.statuses,
type: input.type,
genreId: input.genreId,
ratingMin: input.ratingMin,
ratingMax: input.ratingMax,
yearMin: input.yearMin,
yearMax: input.yearMax,
contentRating: input.contentRating,
onMyServices: input.onMyServices,
sortBy: input.sortBy,
sortDirection: input.sortDirection,
page: input.page,
limit: input.limit,
});
return {
items: result.items.map((item) => ({
id: item.titleId,
tmdbId: item.tmdbId,
type: item.type,
title: item.title,
posterPath: tmdbImageUrl(item.posterPath, "posters"),
posterThumbHash: item.posterThumbHash ?? null,
releaseDate: item.releaseDate ?? null,
firstAirDate: item.firstAirDate ?? null,
voteAverage: item.voteAverage,
userStatus: item.userStatus,
userRating: item.userRating,
})),
page: result.page,
totalPages: result.totalPages,
totalResults: result.totalResults,
};
});
export const genres = os.library.genres.use(authed).handler(({ context }) => {
return { genres: getLibraryGenresList(context.user.id) };
});
export const stats = os.library.stats.use(authed).handler(({ context }) => {
const userStats = getUserStats(context.user.id);
return { size: userStats.librarySize, completed: userStats.completed };
});
export const continueWatching = os.library.continueWatching.use(authed).handler(({ context }) => {
const feed = getContinueWatchingFeed(context.user.id);
const items = feed.map((item) => ({
title: {
id: item.title.id,
title: item.title.title,
backdropPath: tmdbImageUrl(item.title.backdropPath, "backdrops"),
backdropThumbHash: item.title.backdropThumbHash,
},
nextEpisode: item.nextEpisode
? {
seasonNumber: item.nextEpisode.seasonNumber,
episodeNumber: item.nextEpisode.episodeNumber,
name: item.nextEpisode.name,
stillPath: tmdbImageUrl(item.nextEpisode.stillPath, "stills"),
stillThumbHash: item.nextEpisode.stillThumbHash,
}
: null,
totalEpisodes: item.totalEpisodes,
watchedEpisodes: item.watchedEpisodes,
}));
return { items };
});
export const upcoming = os.library.upcoming.use(authed).handler(({ input, context }) => {
const result = getUpcomingFeed(context.user.id, {
days: input.days,
limit: input.limit,
cursor: input.cursor,
mediaType: input.mediaType,
statusFilter: input.statusFilter,
});
return {
items: result.items.map((item) => ({
...item,
posterPath: tmdbImageUrl(item.posterPath, "posters"),
backdropPath: tmdbImageUrl(item.backdropPath, "backdrops"),
streamingProvider: item.streamingProvider
? {
...item.streamingProvider,
logoPath: tmdbImageUrl(item.streamingProvider.logoPath, "logos"),
}
: null,
})),
nextCursor: result.nextCursor,
};
});