diff --git a/src/components/details/details.module.css b/src/components/details/details.module.css index 49fb003..6166745 100644 --- a/src/components/details/details.module.css +++ b/src/components/details/details.module.css @@ -15,7 +15,7 @@ inset: 0; display: block; background: linear-gradient( - calc(atan(var(--ratio))), + atan(var(--ratio, .2)), var(--surface-2) 20em, transparent 90% ); diff --git a/src/components/details/details.tsx b/src/components/details/details.tsx index 4391ca7..3a0d797 100644 --- a/src/components/details/details.tsx +++ b/src/components/details/details.tsx @@ -11,13 +11,10 @@ export const Details: Component = (props) => { onMount(() => { const observer = new ResizeObserver(([entry]) => { - const _20em = 20 * 16; const { inlineSize, blockSize } = entry.contentBoxSize[0]; - console.log(blockSize / inlineSize); - (entry.target as HTMLElement).style.setProperty( "--ratio", - String(_20em / inlineSize), + String((blockSize * 0.2) / inlineSize) ); }); diff --git a/src/components/hero/hero.tsx b/src/components/hero/hero.tsx index 5e058da..ec6a51b 100644 --- a/src/components/hero/hero.tsx +++ b/src/components/hero/hero.tsx @@ -8,9 +8,6 @@ type HeroProps = { }; export function Hero(props: HeroProps) { - const entry = createMemo(() => props.entries.at(0)!); - const slug = createMemo(() => createSlug(entry())); - return (
{(entry) => } diff --git a/src/features/content/apis/jellyfin.ts b/src/features/content/apis/jellyfin.ts index bffca42..e7e1b74 100644 --- a/src/features/content/apis/jellyfin.ts +++ b/src/features/content/apis/jellyfin.ts @@ -1,4 +1,4 @@ -import type { paths } from "./jellyfin.generated"; // generated by openapi-typescript +import type { components, paths } from "./jellyfin.generated"; // generated by openapi-typescript import createClient from "openapi-fetch"; import { query } from "@solidjs/router"; import { Entry } from "../types"; @@ -39,7 +39,7 @@ const getClient = () => { export const getCurrentUser = query(async () => { "use server"; - const { data, error, response } = await getClient().GET("/Users/Public", { + const { data } = await getClient().GET("/Users/Public", { params: {}, }); @@ -83,7 +83,7 @@ export const listItemIds = query( return Object.fromEntries( data.Items?.map((item) => ([ - item.ProviderIds!["Tmdb"]!, + `${item.MediaType as any}-${item.ProviderIds!["Tmdb"]!}`, { jellyfin: item.Id! }, ])) ?? [] ); @@ -118,12 +118,7 @@ export const listItems = query( } return ( - data.Items?.map((item) => ({ - // id: item.Id!, - id: item.ProviderIds!["Tmdb"]!, - title: item.Name!, - thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'), - })) ?? [] + data.Items?.map((item) => mapToEntry(item)) ?? [] ); }, "jellyfin.listItems", @@ -164,13 +159,7 @@ export const getRandomItems = query( }); return ( - data?.Items?.map((item) => ({ - // id: item.Id!, - id: item.ProviderIds!["Tmdb"]!, - title: item.Name!, - thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'), - image: new URL(`/Items/${item.Id!}/Images/Backdrop`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'), - })) ?? [] + data?.Items?.map((item) => mapToEntry(item)) ?? [] ); }, "jellyfin.listRandomItems", @@ -205,18 +194,7 @@ export const getItem = query( return undefined; } - return { - // id: data.Id!, - id: data.ProviderIds!["Tmdb"]!, - title: data.Name!, - overview: data.Overview!, - thumbnail: new URL(`/Items/${itemId}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'), - image: new URL(`/Items/${itemId}/Images/Backdrop`, getBaseUrl()), - providers: { - jellyfin: data.Id - } - // ...data, - }; + return mapToEntry(data); }, "jellyfin.getItem", ); @@ -235,7 +213,7 @@ export const getItemStream = query( }, query: { static: true, - container: 'mkv', + // container: 'mkv', }, }, parseAs: 'stream', @@ -315,8 +293,6 @@ export const queryItems = query(async () => { }, }, }); - - console.log(data); }, "jellyfin.queryItems"); export const getContinueWatching = query( @@ -373,3 +349,22 @@ function assertNoErrors( }); } } + +const mapToEntry = (item: components['schemas']['BaseItemDto']): Entry => { + const type = { + Movie: 'movie', + Series: 'tv', + }[item.Type as string] as any; + + return { + type, + id: item.ProviderIds!["Tmdb"]!, + title: item.Name!, + overview: item.Overview!, + thumbnail: new URL(`/Items/${item.Id}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'), + image: new URL(`/Items/${item.Id}/Images/Backdrop`, getBaseUrl()), + providers: { + jellyfin: item.Id + } + }; +}; \ No newline at end of file diff --git a/src/features/content/apis/tmdb.ts b/src/features/content/apis/tmdb.ts index 3f0625c..b7cab87 100644 --- a/src/features/content/apis/tmdb.ts +++ b/src/features/content/apis/tmdb.ts @@ -28,16 +28,24 @@ const getClients = () => { }; export const getEntry = query( - async (id: string): Promise => { - "use server"; + async (type: Entry['type'], id: string): Promise => { + "use server"; const [ clientV3 ] = getClients(); - const { data } = await clientV3.GET("/movie/{movie_id}", { + const endpoint = ({ + movie: "/movie/{movie_id}", + tv: '/tv/{series_id}', + } as const)[type]; + + const params = ({ + movie: { movie_id: Number.parseInt(id) }, + tv: { series_id: Number.parseInt(id) }, + } as const)[type]; + + const { data } = await clientV3.GET(endpoint, { params: { - path: { - movie_id: Number.parseInt(id), - }, + path: params, }, }); @@ -46,6 +54,7 @@ export const getEntry = query( } return { + type, id: String(data.id ?? -1), title: data.title!, overview: data.overview, @@ -78,6 +87,7 @@ export const getRecommendations = query(async (): Promise => { return data?.results.map( ({ id, title, overview, poster_path, backdrop_path }) => ({ + type: 'movie', id: String(id ?? -1), title: title!, overview, @@ -102,7 +112,8 @@ export const getDiscovery = query(async (): Promise => { } const movieEntries = movies?.results?.slice(0, 10) - .map(({ id, title, overview, poster_path, backdrop_path }) => ({ + .map(({ id, title, overview, poster_path, backdrop_path }): Entry => ({ + type: 'movie', id: String(id ?? -1), title: title!, overview, @@ -111,7 +122,8 @@ export const getDiscovery = query(async (): Promise => { })) ?? [] const seriesEntries = series?.results?.slice(0, 10) - .map(({ id, name, overview, poster_path, backdrop_path }) => ({ + .map(({ id, name, overview, poster_path, backdrop_path }): Entry => ({ + type: 'tv', id: String(id ?? -1), title: name!, overview, @@ -137,7 +149,7 @@ export const searchMulti = query(async (query: string, page: number = 1): Promis query, page, include_adult: false, - language: 'en-US' + language: 'en-US', } } }); @@ -147,8 +159,13 @@ export const searchMulti = query(async (query: string, page: number = 1): Promis } console.log(`loaded page ${page}, found ${data.results?.length} results`); + console.log(data.results[0]); - return { count: data.total_results!, pages: data.total_pages!, results: data.results?.map(({ id, name, title, media_type, overview, backdrop_path, poster_path }) => ({ + return { count: data.total_results!, pages: data.total_pages!, results: data.results?.filter(({ media_type }) => media_type === 'movie' || media_type === 'tv').map(({ id, name, title, media_type, overview, backdrop_path, poster_path }): Entry => ({ + type: ({ + movie: 'movie', + tv: 'tv', + }[media_type ?? '']) as any, id: String(id), title: `${name ?? title ?? ''} (${media_type})`, overview, diff --git a/src/features/content/data.ts b/src/features/content/data.ts index 1d93b83..1250b37 100644 --- a/src/features/content/data.ts +++ b/src/features/content/data.ts @@ -1,33 +1,6 @@ import { toSlug } from "~/utilities"; import type { Entry } from "./types"; -export const entries = new Map([ - { id: '1', title: 'Realtime with Bill Maher', thumbnail: 'https://www.themoviedb.org/t/p/w342/pbpoLLp4kvnYVfnEGiEhagpJuVZ.jpg' }, - { id: '2', title: 'Binnelanders', thumbnail: 'https://www.themoviedb.org/t/p/w342/v9nGSRx5lFz6KEgfmgHJMSgaARC.jpg' }, - { id: '3', title: 'Family guy', thumbnail: 'https://www.themoviedb.org/t/p/w342/y0HUz4eUNUe3TeEd8fQWYazPaC7.jpg' }, - { id: '4', title: 'The Simpsons', thumbnail: 'https://www.themoviedb.org/t/p/w342/vHqeLzYl3dEAutojCO26g0LIkom.jpg' }, - { id: '5', title: 'Breaking Bad', thumbnail: 'https://www.themoviedb.org/t/p/w342/ztkUQFLlC19CCMYHW9o1zWhJRNq.jpg' }, - { id: '6', title: 'Loki', thumbnail: 'https://www.themoviedb.org/t/p/w342/oJdVHUYrjdS2IqiNztVIP4GPB1p.jpg' }, - { id: '7', title: 'Dark desire', thumbnail: 'https://www.themoviedb.org/t/p/w342/uxFNAo2A6ZRcgNASLk02hJUbybn.jpg' }, - { id: '8', title: 'Bridgerton', thumbnail: 'https://www.themoviedb.org/t/p/w342/luoKpgVwi1E5nQsi7W0UuKHu2Rq.jpg' }, - { id: '9', title: 'Naruto', thumbnail: 'https://www.themoviedb.org/t/p/w342/xppeysfvDKVx775MFuH8Z9BlpMk.jpg' }, - { id: '11', title: 'Teenwolf', thumbnail: 'https://www.themoviedb.org/t/p/w342/fmlMmxSBgPEunHS5gjokIej048g.jpg' }, - { id: '12', title: 'Record of Ragnarok', thumbnail: 'https://www.themoviedb.org/t/p/w342/kTs2WNZOukpWdNhoRlH94pSJ3xf.jpg' }, - { id: '13', title: 'The Mandalorian', thumbnail: 'https://www.themoviedb.org/t/p/w342/eU1i6eHXlzMOlEq0ku1Rzq7Y4wA.jpg' }, - { - id: '14', - title: 'Wednesday', - thumbnail: 'https://www.themoviedb.org/t/p/w342/9PFonBhy4cQy7Jz20NpMygczOkv.jpg', - image: 'https://www.themoviedb.org/t/p/original/iHSwvRVsRyxpX7FE7GbviaDvgGZ.jpg', - summary: 'Wednesday Addams is sent to Nevermore Academy, a bizarre boarding school where she attempts to master her psychic powers, stop a monstrous killing spree of the town citizens, and solve the supernatural mystery that affected her family', - releaseDate: '2022', - sources: [ - { label: 'TMDB', url: 'https://themoviedb.org/tv/119051', rating: { score: 8.5, max: 10 } } - ] - }, -].map((entry) => [entry.id, entry])); +export const emptyEntry = Object.freeze({ type: 'movie', id: '0', title: '' }); - -export const emptyEntry = Object.freeze({ id: '0', title: '' }); - -export const createSlug = (entry: Entry) => toSlug(`${entry.title}-${entry.id}`); +export const createSlug = (entry: Entry) => toSlug(`${entry.title}-${entry.type}-${entry.id}`); diff --git a/src/features/content/service.ts b/src/features/content/service.ts index 12d3af9..025358b 100644 --- a/src/features/content/service.ts +++ b/src/features/content/service.ts @@ -14,12 +14,11 @@ const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763"; const lookupTable = query(async () => listItemIds(), 'content.lookupTable'); -// export const getHighlights = () => getRandomItems(jellyfinUserId); export const getHighlights = () => getContinueWatching(jellyfinUserId); -export const getStream = query(async (id: string, range: string) => { +export const getStream = query(async (type: Entry['type'], id: string, range: string) => { const table = await lookupTable(); - return getItemStream(table[id].jellyfin, range); + return getItemStream(table[`${type}-${id}`].jellyfin, range); }, 'content.stream'); export const listCategories = query(async (): Promise => { @@ -34,9 +33,18 @@ export const listCategories = query(async (): Promise => { ]; }, "content.categories.list"); +export const getEntryFromSlug = query( + async (slug: string): Promise => { + const { type, id } = slug.match(/^.+-(?\w+)-(?\w+)$/)?.groups ?? {}; + + return getTmdbEntry(type as any, id); + }, + "content.getFromSlug", +); + export const getEntry = query( - async (id: Entry["id"]): Promise => { - return getTmdbEntry(id); + async (type: Entry['type'], id: Entry["id"]): Promise => { + return getTmdbEntry(type, id); }, "content.get", ); diff --git a/src/features/content/types.ts b/src/features/content/types.ts index 8e5be69..6e5a3d7 100644 --- a/src/features/content/types.ts +++ b/src/features/content/types.ts @@ -4,6 +4,7 @@ export interface Category { } export interface Entry { + type: 'movie'|'tv'; id: string; title: string; overview?: string; diff --git a/src/features/player/context.tsx b/src/features/player/context.tsx index 83a64bc..3a72912 100644 --- a/src/features/player/context.tsx +++ b/src/features/player/context.tsx @@ -169,6 +169,9 @@ export const [VideoProvider, useVideo] = createContextProvider< ); }, canplay() { + // setStore("loading", false); + }, + canplaythrough() { setStore("loading", false); }, waiting() { diff --git a/src/features/player/player.module.css b/src/features/player/player.module.css index cc6a070..4263e2b 100644 --- a/src/features/player/player.module.css +++ b/src/features/player/player.module.css @@ -34,8 +34,7 @@ max-inline-size: none; align-content: end; - gap: var(--size-2); - padding: var(--size-2); + background: linear-gradient(to bottom, black, transparent) top left / 100% 20% no-repeat; & > header { display: block grid; @@ -60,16 +59,30 @@ } & > footer { + position: relative; display: block grid; grid: auto auto / auto auto auto; place-content: space-between; - gap: var(--size-2); - & > :nth-child(1) { - grid-column: 1 / -1; + gap: var(--size-2); + padding: var(--size-2); + + &::before { + content: ''; + position: absolute; + inset: 0; + inset-block-start: -5rem; + backdrop-filter: blur(5px); + mask-image: linear-gradient(to bottom, transparent, black 5rem); + z-index: 0; } & > section { + z-index: 1; + &:nth-child(1) { + grid-column: 1 / -1; + } + & > button { font-size: var(--size-7); text-shadow: 0 0 .5rem #000; diff --git a/src/features/player/player.tsx b/src/features/player/player.tsx index d956d5d..1eb6b23 100644 --- a/src/features/player/player.tsx +++ b/src/features/player/player.tsx @@ -121,7 +121,9 @@ export const Player: Component = (props) => {