diff --git a/docs/design/playing_media.md b/docs/design/playing_media.md new file mode 100644 index 0000000..8c09652 --- /dev/null +++ b/docs/design/playing_media.md @@ -0,0 +1,85 @@ +# Playing media + +This document will describe the application UX flow. + +## Types of media & Expected behavior + +- movie + - actual movie -> show the video player and start/continue playing the movie + - collection -> Show a picker for which movie to play (also allow play-all) +- audio/music + - artist -> Show top songs, albums, and a play-all button + - album -> Show songs in album and a play-all button + - song -> Show audio player and start playing the song +- show/tv + - series -> Show a picker for the seasons and include a play button that will start/continue the first non-completed episode + - season -> Show a picker for the episodes and include a play button that will start/continue the first non-completed episode + - episode -> Show the video player and start/continue the episode (include the skip to previous&next episode buttons) +- playlist -> play the selected entry according to the above listed definitions + +## UX flow + +```txt +WHEN type of media IS { + audio -> { + WHEN entry does not exist in lidarr { + add entry to lidarr + } + + WHEN queue record status IS { + paused -> ??? + + downloading -> { + display estimated time remaining + wait for download to complete + } + + _ -> ??? + } + + play audio + } + + show/tv -> { + WHEN entry is not an episode { + redirect to earliest non-completed episode + } + + WHEN entry does not exist in sonarr { + add entry to sonarr + } + + WHEN queue record status IS { + paused -> ??? + + downloading -> { + display estimated time remaining + wait for download to complete + } + + _ -> ??? + } + + play episode + } + + movie -> { + WHEN entry does not exist in radarr { + add entry to radarr + } + + WHEN queue record status IS { + paused -> ??? + + downloading -> { + display estimated time remaining + wait for download to complete + } + + _ -> ??? + } + + play movie + } +} +``` diff --git a/src/components/hero/hero.tsx b/src/components/hero/hero.tsx index ec6a51b..f68307d 100644 --- a/src/components/hero/hero.tsx +++ b/src/components/hero/hero.tsx @@ -25,7 +25,7 @@ const Page: Component<{ entry: Entry }> = (props) => { >

{props.entry.title}

- + Continue diff --git a/src/features/content/apis/jellyfin.ts b/src/features/content/apis/jellyfin.ts index b20a469..74a031f 100644 --- a/src/features/content/apis/jellyfin.ts +++ b/src/features/content/apis/jellyfin.ts @@ -213,15 +213,13 @@ export const getItemStream = query( const userData = await getItemUserData(userId, itemId); - console.log(userData); - const { response } = await getClient().GET("/Videos/{itemId}/stream", { params: { path: { itemId, }, query: { - // startTimeTicks: userData?.playbackPositionTicks, + startTimeTicks: userData?.playbackPositionTicks, }, }, parseAs: 'stream', diff --git a/src/features/content/apis/radarr.ts b/src/features/content/apis/radarr.ts index aebcca4..dddb2da 100644 --- a/src/features/content/apis/radarr.ts +++ b/src/features/content/apis/radarr.ts @@ -26,10 +26,52 @@ export const getClient = () => { }); }; -export const get = query(async () => { +export const TEST = query(async (id: number) => { "use server"; - const { data, error } = await getClient().GET('/api/v3/movie'); + const { data, error } = await getClient().GET('/api/v3/queue/details', { + params: { + query: { + movieId: id, + // includeMovie: true, + }, + }, + }); + + const item = data?.[0]; + + if (!item?.status) { + return; + } + + switch(item.status) { + case 'paused': { + console.log('not sure what to do now. there\'s a reason it is paused after all. just report it to the client perhaps?'); + return; + } + + case 'downloading': { + console.log(`download is esitmated to complete at ${item.estimatedCompletionTime}`); + + return; + } + + default: { + return; + } + } +}, 'radarr.TEST'); + +export const get = query(async (id: number) => { + "use server"; + + const { data, error } = await getClient().GET('/api/v3/movie/{id}', { + params: { + path: { + id, + }, + }, + }); return data; }, 'radarr.get'); diff --git a/src/features/content/service.ts b/src/features/content/service.ts index 97b059b..bd8e929 100644 --- a/src/features/content/service.ts +++ b/src/features/content/service.ts @@ -10,7 +10,7 @@ import { searchMulti, } from "./apis/tmdb"; import { listIds as listSerieIds, addSeries } from "./apis/sonarr"; -import { listIds as listMovieIds, addMovie } from "./apis/radarr"; +import { listIds as listMovieIds, addMovie, TEST } from "./apis/radarr"; import { merge } from "~/utilities"; const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763"; @@ -34,8 +34,14 @@ export const getStream = query(async (id: string, range: string) => { return getItemStream(jellyfinUserId, ids.jellyfin, range); } - if (ids?.[manager]) { - console.log('id is known, but jellyfin does not (yet) have the file'); + if (ids?.radarr) { + console.log(`radarr has the entry '${ids.radarr}', but jellyfin does not (yet) have the file`); + console.log(await TEST(ids.radarr)) + return; + } + + if (ids?.sonarr) { + console.log('sonarr has the entry, but jellyfin does not (yet) have the file'); return; } diff --git a/src/features/overview/list-item.tsx b/src/features/overview/list-item.tsx index e48a7ed..d0beff8 100644 --- a/src/features/overview/list-item.tsx +++ b/src/features/overview/list-item.tsx @@ -8,13 +8,12 @@ export const ListItem: Component<{ entry: Entry }> = (props) => { return (
- {props.entry.title} - + {props.entry.title}
{props.entry.title} - Watch now + Watch now
); diff --git a/src/features/player/context.tsx b/src/features/player/context.tsx index d5e0258..deaee6e 100644 --- a/src/features/player/context.tsx +++ b/src/features/player/context.tsx @@ -3,7 +3,7 @@ import { ContextProviderProps, createContextProvider, } from "@solid-primitives/context"; -import { Accessor, createEffect, onMount, Setter } from "solid-js"; +import { Accessor, createEffect, on, onMount, Setter } from "solid-js"; import { createStore } from "solid-js/store"; import { createEventListenerMap } from "@solid-primitives/event-listener"; import { createFullscreen } from "@solid-primitives/fullscreen"; @@ -124,13 +124,20 @@ export const [VideoProvider, useVideo] = createContextProvider< }, }; + console.log(props.root, props.video); + if (isServer || video === undefined) { return api; } - createEffect(() => { - video[store.state === "playing" ? "play" : "pause"](); - }); + createEffect( + on( + () => store.state, + (state) => { + video[state === "playing" ? "play" : "pause"](); + } + ) + ); createEffect(() => { video.muted = store.volume.muted; @@ -174,9 +181,11 @@ export const [VideoProvider, useVideo] = createContextProvider< ); }, canplay() { + console.log("can play!"); // setStore("loading", false); }, canplaythrough() { + console.log("can play through!"); setStore("loading", false); }, waiting() { diff --git a/src/features/player/player.tsx b/src/features/player/player.tsx index 1a44289..aab9adf 100644 --- a/src/features/player/player.tsx +++ b/src/features/player/player.tsx @@ -33,15 +33,15 @@ const metadata = query(async (id: string) => { // 3. create sprite from images // 4. remove thumbs - const path = `${import.meta.dirname}/SampleVideo_1280x720_10mb`; + // const path = `${import.meta.dirname}/SampleVideo_1280x720_10mb`; - return json({ - captions: await Bun.file(`${path}.captions.vtt`).bytes(), - thumbnails: { - track: await Bun.file(`${path}.thumbnails.vtt`).text(), - image: await Bun.file(`${import.meta.dirname}/overview.jpg`).bytes(), - }, - }); + // return json({ + // captions: await Bun.file(`${path}.captions.vtt`).bytes(), + // thumbnails: { + // track: await Bun.file(`${path}.thumbnails.vtt`).text(), + // image: await Bun.file(`${import.meta.dirname}/overview.jpg`).bytes(), + // }, + // }); }, "player.metadata"); interface PlayerProps { @@ -56,32 +56,30 @@ export const Player: Component = (props) => { undefined as unknown as HTMLVideoElement ); - const data = createAsync(() => metadata(props.entry.id), { - deferStream: true, - initialValue: {} as any, - }); - const captionUrl = createMemo(() => { - const { captions } = data(); + // const data = createAsync(() => metadata(props.entry.id), { + // deferStream: true, + // initialValue: {} as any, + // }); + // const captionUrl = createMemo(() => { + // const { captions } = data(); - return captions !== undefined - ? URL.createObjectURL(new Blob([captions], { type: "text/vtt" })) - : ""; - }); - const thumbnails = createMemo(() => { - const { thumbnails } = data(); + // return captions !== undefined + // ? URL.createObjectURL(new Blob([captions], { type: "text/vtt" })) + // : ""; + // }); + // const thumbnails = createMemo(() => { + // const { thumbnails } = data(); - return thumbnails !== undefined - ? URL.createObjectURL(new Blob([thumbnails.track], { type: "text/vtt" })) - : ""; - }); + // return thumbnails !== undefined + // ? URL.createObjectURL(new Blob([thumbnails.track], { type: "text/vtt" })) + // : ""; + // }); - createEffect(on(thumbnails, (thumbnails) => {})); + // createEffect(on(thumbnails, (thumbnails) => {})); return ( <>
- {/*

{props.entry.title}

*/} -
diff --git a/src/routes/(shell)/watch/[slug].tsx b/src/routes/(shell)/play/[slug].tsx similarity index 77% rename from src/routes/(shell)/watch/[slug].tsx rename to src/routes/(shell)/play/[slug].tsx index a0de601..1900a29 100644 --- a/src/routes/(shell)/watch/[slug].tsx +++ b/src/routes/(shell)/play/[slug].tsx @@ -7,7 +7,7 @@ import { RouteDefinition, useParams, } from "@solidjs/router"; -import { createEffect, createMemo, Show } from "solid-js"; +import { Show } from "solid-js"; import { createSlug, getEntryFromSlug } from "~/features/content"; import { Player } from "~/features/player"; import { Title } from "@solidjs/meta"; @@ -27,8 +27,8 @@ const healUrl = query(async (slug: string) => { } // Not entirely sure a permanent redirect is what we want in this case - throw redirect(`/watch/${actualSlug}`, { status: 308 }); -}, "watch.heal"); + throw redirect(`/play/${actualSlug}`, { status: 308 }); +}, "play.heal"); interface ItemParams extends Params { slug: string; @@ -51,19 +51,16 @@ export const route = { export default function Item() { const { slug } = useParams(); const entry = createAsync(() => getEntryFromSlug(slug)); - const title = createMemo(() => entry()?.title); - - console.log(entry()); - - createEffect(() => { - console.log(entry()); - }); return (
- {title()} + {entry()?.title} - {(entry) => } + {(entry) => ( + <> + + + )}
); diff --git a/src/routes/(shell)/watch/slug.module.css b/src/routes/(shell)/play/slug.module.css similarity index 100% rename from src/routes/(shell)/watch/slug.module.css rename to src/routes/(shell)/play/slug.module.css