alright, got it to the point where it'll automatically download new entries. now to fix the UX of it all...
This commit is contained in:
parent
6a209f8698
commit
b5306d1d11
10 changed files with 195 additions and 61 deletions
85
docs/design/playing_media.md
Normal file
85
docs/design/playing_media.md
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
```
|
|
@ -25,7 +25,7 @@ const Page: Component<{ entry: Entry }> = (props) => {
|
|||
>
|
||||
<h2 class={css.title}>{props.entry.title}</h2>
|
||||
|
||||
<a class={css.cta} href={`/watch/${slug()}`}>
|
||||
<a class={css.cta} href={`/play/${slug()}`}>
|
||||
Continue
|
||||
</a>
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,13 +8,12 @@ export const ListItem: Component<{ entry: Entry }> = (props) => {
|
|||
|
||||
return (
|
||||
<figure class={css.listItem} data-id={props.entry.id}>
|
||||
<img src={props.entry.thumbnail ?? ''} alt={props.entry.title} />
|
||||
|
||||
<img src={props.entry.thumbnail ?? ""} alt={props.entry.title} />
|
||||
|
||||
<figcaption>
|
||||
<strong>{props.entry.title}</strong>
|
||||
|
||||
<a href={`/watch/${slug()}`}>Watch now</a>
|
||||
<a href={`/play/${slug()}`}>Watch now</a>
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<PlayerProps> = (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 (
|
||||
<>
|
||||
<figure ref={setPlayer} class={css.player}>
|
||||
{/* <h1>{props.entry.title}</h1> */}
|
||||
|
||||
<video
|
||||
ref={setVideo}
|
||||
src={`/api/content/${props.entry.id}/stream${
|
||||
|
@ -91,7 +89,7 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
lang="en"
|
||||
autoplay
|
||||
>
|
||||
<track
|
||||
{/* <track
|
||||
default
|
||||
kind="captions"
|
||||
label="English"
|
||||
|
@ -99,11 +97,11 @@ export const Player: Component<PlayerProps> = (props) => {
|
|||
src={captionUrl()}
|
||||
/>
|
||||
<track default kind="chapters" src={thumbnails()} id="thumbnails" />
|
||||
{/* <track kind="captions" />
|
||||
<track kind="chapters" />
|
||||
<track kind="descriptions" />
|
||||
<track kind="metadata" />
|
||||
<track kind="subtitles" /> */}
|
||||
<track kind="captions" />
|
||||
<track kind="chapters" />
|
||||
<track kind="descriptions" />
|
||||
<track kind="metadata" />
|
||||
<track kind="subtitles" /> */}
|
||||
</video>
|
||||
|
||||
<figcaption>
|
||||
|
|
|
@ -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<ItemParams>();
|
||||
const entry = createAsync(() => getEntryFromSlug(slug));
|
||||
const title = createMemo(() => entry()?.title);
|
||||
|
||||
console.log(entry());
|
||||
|
||||
createEffect(() => {
|
||||
console.log(entry());
|
||||
});
|
||||
|
||||
return (
|
||||
<div class={css.page}>
|
||||
<Title>{title()}</Title>
|
||||
<Title>{entry()?.title}</Title>
|
||||
<Show when={entry()} fallback="Some kind of pretty 404 page I guess">
|
||||
{(entry) => <Player entry={entry()} />}
|
||||
{(entry) => (
|
||||
<>
|
||||
<Player entry={entry()} />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue