diff --git a/src/lib/components/ExternalRatings.svelte b/src/lib/components/ExternalRatings.svelte index 81c3508..7d02df1 100644 --- a/src/lib/components/ExternalRatings.svelte +++ b/src/lib/components/ExternalRatings.svelte @@ -1,126 +1,114 @@ -{#if (imdbId && (imdbScore || compactImdbVotes)) || rottenTomatoesRating || (streaming && streaming.providers.length > 0)} -
- {#if (imdbId && (imdbScore || compactImdbVotes)) || rottenTomatoesRating} -
- {#if imdbId && (imdbScore || compactImdbVotes)} - - - IMDb - - {#if imdbScore} - {imdbScore} - {/if} - {#if compactImdbVotes} - ({compactImdbVotes}) - {/if} - +{#if popfeedScore !== null || (imdbId && (imdbScore || compactImdbVotes)) || rottenTomatoesRating} +
+ {#if popfeedScore !== null} +
+ + {popfeedScore.toFixed(1)} + {#if compactPopfeedRatings} + + ({compactPopfeedRatings}) + {/if} +
+ {/if} - {#if rottenTomatoesRating} -
- - - {rottenTomatoesRating.value} -
+ {#if imdbId && (imdbScore || compactImdbVotes)} + + + IMDb + + {#if imdbScore} + {imdbScore} {/if} -
+ {#if compactImdbVotes} + + ({compactImdbVotes}) + + {/if} + {/if} - {#if streaming && streaming.providers.length > 0} + {#if rottenTomatoesRating}
- stream on - {#each streaming.providers as provider (provider.id)} - {#if streaming.link} - - - - {:else} - {provider.name} - {/if} - {/each} + + + {rottenTomatoesRating.value} +
{/if}
diff --git a/src/lib/components/ItemPage.svelte b/src/lib/components/ItemPage.svelte index 9b7a6f7..09c29d7 100644 --- a/src/lib/components/ItemPage.svelte +++ b/src/lib/components/ItemPage.svelte @@ -1,14 +1,11 @@ +{#snippet reviewButton()} + +{/snippet} + {data.item.title} | atmo.watch @@ -84,129 +112,243 @@ -{#if data.item.backdrop} - -{/if} -
- - -
- {#if data.item.poster} +
+ {#if data.item.backdrop} +
Poster for {data.item.title} - {/if} - -
-

- {data.item.title} -

- -
- - {#if data.trailer_url} - - {/if} -
+
-
- -
-
-

overview

-

{data.item.overview}

-
-
- - {#if detailTabs.length > 0} -
-

More about {data.item.title}

- - - {#if selectedSection === 'reviews' && data.reviews.length > 0} -
-
- - {(averageReviewRating / 2).toFixed(1)} - - average from {data.reviews.length} - {data.reviews.length === 1 ? 'review' : 'reviews'} - -
-
- {/if} - + + {#if data.recommendations.length > 0} +
+

Similar

+
+ +
+ + + {#if !showAllRecommendations && data.recommendations.length > 10} + + {:else if !showAllRecommendations && data.recommendations.length > 6} + + {/if} +
+ {/if} + + {#if data.cast.length > 0} +
+

Cast

+
+ {#each data.cast as castMember (castMember.id)} + + + {castMember.name} + {castMember.character} + + {/each} +
+
+ {/if} +
+
+ diff --git a/src/lib/components/Review.svelte b/src/lib/components/Review.svelte index ba9e47a..a2ad4d3 100644 --- a/src/lib/components/Review.svelte +++ b/src/lib/components/Review.svelte @@ -5,6 +5,7 @@ import { posterUrl } from '$lib/images'; import type { ReviewCardModel } from '$lib/types'; import { cn, slugify, toMediaRouteKind } from '$lib/utils'; + import Avatar from './Avatar.svelte'; import Rating from './Rating.svelte'; let { @@ -40,7 +41,7 @@ let spoilerHidden = $derived(hasSpoilerText && !spoilerRevealed); -
+
diff --git a/src/lib/components/TrailerDialog.svelte b/src/lib/components/TrailerDialog.svelte index dd231b0..e4d0609 100644 --- a/src/lib/components/TrailerDialog.svelte +++ b/src/lib/components/TrailerDialog.svelte @@ -1,14 +1,33 @@ +{#snippet playIcon(className: string)} + +{/snippet} + {#if embedUrl} - - (useFallbackThumbnail = true)} + /> + {/if} + + + + Watch trailer + + + {:else} + - - - trailer - + {@render playIcon('size-4')} + trailer + + {/if} diff --git a/src/lib/contrail/index.ts b/src/lib/contrail/index.ts index 8f00605..acc4c69 100644 --- a/src/lib/contrail/index.ts +++ b/src/lib/contrail/index.ts @@ -6,7 +6,7 @@ export const contrail = createPublicServiceClient({ endpoint: "https://api.atmo.watch", contractDigest: "sha256:331d01ac6beadd756e777004daf2a4e278bb3c864e91450bf7f974c4547dedd9", serviceDid: "did:web:api.atmo.watch#contrail", - scope: "rpc?lxm=*&aud=did:web:api.atmo.watch%23contrail", + scope: "rpc?lxm=watch.atmo.notifyOfUpdate&aud=did:web:api.atmo.watch%23contrail", serviceMethods: [ "watch.atmo.comment.getRecord", "watch.atmo.comment.listRecords", diff --git a/src/lib/icons/popfeed.svg b/src/lib/icons/popfeed.svg new file mode 100644 index 0000000..e87af8a --- /dev/null +++ b/src/lib/icons/popfeed.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/reviews.server.ts b/src/lib/reviews.server.ts index 21ad153..b1fdab2 100644 --- a/src/lib/reviews.server.ts +++ b/src/lib/reviews.server.ts @@ -65,6 +65,25 @@ export function toReview( }; } +function getReviewAuthor(did: string, profiles: ReviewListRecords.ProfileEntry[]): ActorSummary { + const bskyProfile = profiles.find( + (profile) => profile.did === did && profile.collection === 'app.bsky.actor.profile' + ); + const popfeedProfile = profiles.find( + (profile) => profile.did === did && profile.collection === 'social.popfeed.actor.profile' + ); + const profile = bskyProfile ?? popfeedProfile; + const avatar = + bskyProfile?.value && 'avatar' in bskyProfile.value ? bskyProfile.value.avatar : undefined; + + return { + did, + handle: profile?.handle ?? did, + displayName: popfeedProfile?.value?.displayName ?? bskyProfile?.value?.displayName, + avatarUrl: avatar ? getAtprotoCdnImageUrl({ did, blob: avatar, preset: 'avatar' }) : undefined + }; +} + function getCommentAuthor( did: string, profiles: Map @@ -171,14 +190,12 @@ export async function getMediaReviews( throw new Error(`Could not load reviews from Contrail (${response.status})`); } - const handles = new Map( - (response.data.profiles ?? []).map((profile) => [profile.did, profile.handle ?? profile.did]) - ); - + const profiles = response.data.profiles ?? []; const reviews = response.data.records.flatMap((record) => { - const review = toReview(record, handles.get(record.did)); + const author = getReviewAuthor(record.did, profiles); + const review = toReview(record, author.handle); return review?.media.tmdbId === tmdbId && review.media.creativeWorkType === creativeWorkType - ? [review] + ? [{ ...review, author }] : []; }); diff --git a/src/lib/tmdb.server.ts b/src/lib/tmdb.server.ts index d73b39b..feec1f8 100644 --- a/src/lib/tmdb.server.ts +++ b/src/lib/tmdb.server.ts @@ -131,10 +131,25 @@ function toMediaDetails( source: MediaDetailsSource, creativeWorkType: SupportedCreativeWorkType ): MediaDetails { + const metadata = + 'release_date' in source + ? { + releaseDate: source.release_date || null, + runtime: source.runtime ?? null, + numberOfSeasons: null + } + : { + releaseDate: source.first_air_date || null, + runtime: source.episode_run_time[0] ?? source.last_episode_to_air?.runtime ?? null, + numberOfSeasons: source.number_of_seasons || null + }; + return { ...toMediaSummary(source, creativeWorkType), backdrop: toTmdbImage(source.backdrop_path), - overview: source.overview ?? '' + overview: source.overview ?? '', + genres: source.genres.map((genre) => genre.name), + ...metadata }; } diff --git a/src/lib/types.ts b/src/lib/types.ts index 57a9ac8..82f02af 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -15,6 +15,10 @@ export type MediaSummary = MediaIdentity & { export type MediaDetails = MediaSummary & { backdrop: MediaImage | null; overview: string; + releaseDate: string | null; + runtime: number | null; + genres: string[]; + numberOfSeasons: number | null; }; export type MediaCredit = MediaSummary & { diff --git a/src/routes/profile/[actor]/+page.server.ts b/src/routes/profile/[actor]/+page.server.ts index 04f74fb..62443e9 100644 --- a/src/routes/profile/[actor]/+page.server.ts +++ b/src/routes/profile/[actor]/+page.server.ts @@ -14,7 +14,18 @@ export const load: PageServerLoad = async ({ parent }) => { const reviews = response.data.records.flatMap((record) => { const review = toReview(record, profile.handle); - return review ? [review] : []; + return review + ? [ + { + ...review, + author: { + ...review.author, + displayName: profile.displayName, + avatarUrl: profile.avatarUrl + } + } + ] + : []; }); const lists = await getProfileMediaLists(profile).catch((cause) => {