mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
* 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>
102 lines
3.3 KiB
TypeScript
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,
|
|
};
|
|
});
|