@axium/kino 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE.md +157 -0
  2. package/db.json +106 -0
  3. package/dist/client/api.d.ts +49 -0
  4. package/dist/client/api.js +136 -0
  5. package/dist/client/frontend.d.ts +3 -0
  6. package/dist/client/frontend.js +33 -0
  7. package/dist/client/index.d.ts +1 -0
  8. package/dist/client/index.js +1 -0
  9. package/dist/client/web_hook.d.ts +8 -0
  10. package/dist/client/web_hook.js +4 -0
  11. package/dist/common.d.ts +749 -0
  12. package/dist/common.js +212 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -0
  15. package/dist/server/api/delete.d.ts +1 -0
  16. package/dist/server/api/delete.js +57 -0
  17. package/dist/server/api/metadata.d.ts +4 -0
  18. package/dist/server/api/metadata.js +153 -0
  19. package/dist/server/api/raw.d.ts +1 -0
  20. package/dist/server/api/raw.js +86 -0
  21. package/dist/server/api/search.d.ts +1 -0
  22. package/dist/server/api/search.js +108 -0
  23. package/dist/server/api/upload.d.ts +1 -0
  24. package/dist/server/api/upload.js +122 -0
  25. package/dist/server/api/views.d.ts +1 -0
  26. package/dist/server/api/views.js +128 -0
  27. package/dist/server/db.d.ts +11 -0
  28. package/dist/server/db.js +100 -0
  29. package/dist/server/hooks.d.ts +9 -0
  30. package/dist/server/hooks.js +19 -0
  31. package/dist/server/images.d.ts +12 -0
  32. package/dist/server/images.js +152 -0
  33. package/dist/server/media.d.ts +47 -0
  34. package/dist/server/media.js +136 -0
  35. package/dist/server/tmdb.d.ts +2 -0
  36. package/dist/server/tmdb.js +7 -0
  37. package/lib/EpisodeList.svelte +89 -0
  38. package/lib/MediaActions.svelte +110 -0
  39. package/lib/MediaCard.svelte +41 -0
  40. package/lib/MediaDetail.svelte +140 -0
  41. package/lib/MediaGrid.svelte +27 -0
  42. package/lib/Poster.svelte +49 -0
  43. package/lib/ProgressBar.svelte +22 -0
  44. package/lib/RecentGrid.svelte +82 -0
  45. package/lib/SearchBar.svelte +174 -0
  46. package/lib/SeasonList.svelte +50 -0
  47. package/lib/index.ts +10 -0
  48. package/lib/tsconfig.json +12 -0
  49. package/lib/watch.svelte.ts +93 -0
  50. package/locales/en.json +103 -0
  51. package/package.json +102 -0
  52. package/routes/+layout.svelte +22 -0
  53. package/routes/+layout.ts +21 -0
  54. package/routes/kino/+page.svelte +57 -0
  55. package/routes/kino/+page.ts +19 -0
  56. package/routes/movies/+page.svelte +22 -0
  57. package/routes/movies/+page.ts +12 -0
  58. package/routes/movies/[id]/+page.svelte +36 -0
  59. package/routes/movies/[id]/+page.ts +7 -0
  60. package/routes/movies/[id]/watch/+page.svelte +34 -0
  61. package/routes/movies/[id]/watch/+page.ts +18 -0
  62. package/routes/tsconfig.json +12 -0
  63. package/routes/tv/+page.svelte +22 -0
  64. package/routes/tv/+page.ts +12 -0
  65. package/routes/tv/[id]/+page.svelte +39 -0
  66. package/routes/tv/[id]/+page.ts +7 -0
  67. package/routes/tv/[id]/[season]/+page.svelte +43 -0
  68. package/routes/tv/[id]/[season]/+page.ts +11 -0
  69. package/routes/tv/[id]/[season]/[episode]/+page.svelte +46 -0
  70. package/routes/tv/[id]/[season]/[episode]/+page.ts +12 -0
  71. package/routes/tv/[id]/[season]/[episode]/watch/+page.svelte +46 -0
  72. package/routes/tv/[id]/[season]/[episode]/watch/+page.ts +22 -0
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { MediaGrid, RecentGrid } from '@axium/kino/components';
4
+
5
+ const { data } = $props();
6
+
7
+ const movies = $derived(data.movies.map(movie => ({ ...movie, type: 'movie' as const })));
8
+ const shows = $derived(data.shows.map(show => ({ ...show, type: 'tv' as const })));
9
+ </script>
10
+
11
+ <svelte:head>
12
+ <title>{text('page.kino.title')}</title>
13
+ </svelte:head>
14
+
15
+ <h1>{text('page.kino.heading')}</h1>
16
+
17
+ {#if data.views.length}
18
+ <section>
19
+ <h2>{text('page.kino.recently_watched')}</h2>
20
+ <RecentGrid views={data.views} empty={text('page.kino.no_recent')} />
21
+ </section>
22
+ {/if}
23
+
24
+ {#if !movies.length && !shows.length}
25
+ <p class="subtle">{text('page.kino.empty')}</p>
26
+ {:else}
27
+ {#if movies.length}
28
+ <section>
29
+ <h2>{text('page.kino.recent_movies')}</h2>
30
+ <MediaGrid items={movies} empty={text('page.kino.movies.empty')} />
31
+ </section>
32
+ {/if}
33
+
34
+ {#if shows.length}
35
+ <section>
36
+ <h2>{text('page.kino.recent_tv')}</h2>
37
+ <MediaGrid items={shows} empty={text('page.kino.tv.empty')} />
38
+ </section>
39
+ {/if}
40
+ {/if}
41
+
42
+ <style>
43
+ h1 {
44
+ margin: 0;
45
+ }
46
+
47
+ section {
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: 0.75em;
51
+ }
52
+
53
+ h2 {
54
+ margin: 0;
55
+ font-size: 1.15em;
56
+ }
57
+ </style>
@@ -0,0 +1,19 @@
1
+ import { getUploadedMovies, getUploadedShows, getViews } from '@axium/kino/client';
2
+ import { redirect } from '@sveltejs/kit';
3
+
4
+ export const ssr = false;
5
+
6
+ export async function load({ parent }) {
7
+ const { session } = await parent();
8
+
9
+ if (!session) redirect(307, '/login?after=/kino');
10
+
11
+ const [movies, shows, views] = await Promise.all([
12
+ getUploadedMovies(),
13
+ getUploadedShows(),
14
+ // The library is the point of the page, so don't fail it over the recently watched list
15
+ getViews().catch(() => []),
16
+ ]);
17
+
18
+ return { movies, shows, views };
19
+ }
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { MediaGrid } from '@axium/kino/components';
4
+
5
+ const { data } = $props();
6
+
7
+ const movies = $derived(data.movies.map(movie => ({ ...movie, type: 'movie' as const })));
8
+ </script>
9
+
10
+ <svelte:head>
11
+ <title>{text('page.kino.movies.title')}</title>
12
+ </svelte:head>
13
+
14
+ <h1>{text('page.kino.movies.title')}</h1>
15
+
16
+ <MediaGrid items={movies} empty={text('page.kino.movies.empty')} />
17
+
18
+ <style>
19
+ h1 {
20
+ margin: 0;
21
+ }
22
+ </style>
@@ -0,0 +1,12 @@
1
+ import { getUploadedMovies } from '@axium/kino/client';
2
+ import { redirect } from '@sveltejs/kit';
3
+
4
+ export const ssr = false;
5
+
6
+ export async function load({ parent }) {
7
+ const { session } = await parent();
8
+
9
+ if (!session) redirect(307, '/login?after=/movies');
10
+
11
+ return { movies: await getUploadedMovies() };
12
+ }
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { deleteMovieUpload, movieDataURL } from '@axium/kino/client';
4
+ import { uploadMovieFile } from '@axium/kino/client/frontend';
5
+ import { MediaActions, MediaDetail } from '@axium/kino/components';
6
+
7
+ const { data } = $props();
8
+
9
+ const movie = $derived(data.movie);
10
+
11
+ let upload = $state(data.movie.upload);
12
+ </script>
13
+
14
+ <svelte:head>
15
+ <title>{movie.title}</title>
16
+ </svelte:head>
17
+
18
+ <MediaDetail
19
+ title={movie.title}
20
+ imagePath={movie.poster_path}
21
+ backdropPath={movie.backdrop_path}
22
+ date={movie.release_date}
23
+ overview={movie.overview}
24
+ >
25
+ {#snippet actions()}
26
+ <MediaActions
27
+ bind:upload
28
+ watchHref="/movies/{movie.id}/watch"
29
+ dataURL={movieDataURL(movie.id, true)}
30
+ uploadText={text('kino.upload_movie')}
31
+ canDelete={data.session?.user.isAdmin}
32
+ uploadFile={file => uploadMovieFile(file, movie.id)}
33
+ deleteUpload={() => deleteMovieUpload(movie.id)}
34
+ />
35
+ {/snippet}
36
+ </MediaDetail>
@@ -0,0 +1,7 @@
1
+ import { getMovie } from '@axium/kino/client';
2
+
3
+ export const ssr = false;
4
+
5
+ export async function load({ params }) {
6
+ return { movie: await getMovie(Number(params.id)) };
7
+ }
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { Video } from '@axium/client/components';
4
+ import { MediaState } from '@axium/client/reactive';
5
+ import { movieDataURL } from '@axium/kino/client';
6
+ import { trackWatch } from '@axium/kino/watch';
7
+
8
+ const { data } = $props();
9
+
10
+ const { movie, upload } = $derived(data);
11
+
12
+ const media = new MediaState();
13
+
14
+ trackWatch({ media, target: { type: 'movie', id: data.movie.id }, resumeFrom: data.view?.position });
15
+ </script>
16
+
17
+ <svelte:head>
18
+ <title>{text('page.kino.watch_title', { name: movie.title })}</title>
19
+ </svelte:head>
20
+
21
+ <div class="watch">
22
+ <a class="subtle" href="/movies/{movie.id}">{movie.title}</a>
23
+ <Video {media} src={movieDataURL(movie.id)} size={upload.size} type={upload.type} name={movie.title} />
24
+ </div>
25
+
26
+ <style>
27
+ .watch {
28
+ display: flex;
29
+ flex-direction: column;
30
+ gap: 0.5em;
31
+ height: 80vh;
32
+ min-height: 0;
33
+ }
34
+ </style>
@@ -0,0 +1,18 @@
1
+ import { getMovie, getViews } from '@axium/kino/client';
2
+ import { error } from '@sveltejs/kit';
3
+
4
+ export const ssr = false;
5
+
6
+ export async function load({ params }) {
7
+ const id = Number(params.id);
8
+
9
+ const movie = await getMovie(id);
10
+
11
+ if (!movie.upload) error(404, 'This movie has not been uploaded');
12
+
13
+ // Used to resume where the last session left off; not worth failing the page over
14
+ const views = await getViews().catch(() => []);
15
+ const view = views.find(v => v.type == 'movie' && v.movie.id == id);
16
+
17
+ return { movie, upload: movie.upload, view };
18
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": ["../tsconfig.json", "../.svelte-kit/tsconfig.json"],
3
+ "compilerOptions": {
4
+ "moduleResolution": "bundler",
5
+ "module": "esnext",
6
+ "noEmit": true,
7
+ "target": "esnext",
8
+ "rootDir": ".."
9
+ },
10
+ "include": ["**/*", "../lib/*"],
11
+ "references": [{ "path": ".." }]
12
+ }
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { MediaGrid } from '@axium/kino/components';
4
+
5
+ const { data } = $props();
6
+
7
+ const shows = $derived(data.shows.map(show => ({ ...show, type: 'tv' as const })));
8
+ </script>
9
+
10
+ <svelte:head>
11
+ <title>{text('page.kino.tv.title')}</title>
12
+ </svelte:head>
13
+
14
+ <h1>{text('page.kino.tv.title')}</h1>
15
+
16
+ <MediaGrid items={shows} empty={text('page.kino.tv.empty')} />
17
+
18
+ <style>
19
+ h1 {
20
+ margin: 0;
21
+ }
22
+ </style>
@@ -0,0 +1,12 @@
1
+ import { getUploadedShows } from '@axium/kino/client';
2
+ import { redirect } from '@sveltejs/kit';
3
+
4
+ export const ssr = false;
5
+
6
+ export async function load({ parent }) {
7
+ const { session } = await parent();
8
+
9
+ if (!session) redirect(307, '/login?after=/tv');
10
+
11
+ return { shows: await getUploadedShows() };
12
+ }
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { MediaDetail, SeasonList } from '@axium/kino/components';
4
+
5
+ const { data } = $props();
6
+
7
+ const show = $derived(data.show);
8
+ </script>
9
+
10
+ <svelte:head>
11
+ <title>{show.name}</title>
12
+ </svelte:head>
13
+
14
+ <MediaDetail
15
+ title={show.name}
16
+ imagePath={show.poster_path}
17
+ backdropPath={show.backdrop_path}
18
+ date={show.first_air_date}
19
+ dateKind="aired"
20
+ overview={show.overview}
21
+ >
22
+ <section>
23
+ <h2>{text('kino.seasons')}</h2>
24
+ <SeasonList id={show.id} seasons={show.seasons} />
25
+ </section>
26
+ </MediaDetail>
27
+
28
+ <style>
29
+ section {
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: 0.75em;
33
+ }
34
+
35
+ h2 {
36
+ margin: 0;
37
+ font-size: 1.15em;
38
+ }
39
+ </style>
@@ -0,0 +1,7 @@
1
+ import { getTv } from '@axium/kino/client';
2
+
3
+ export const ssr = false;
4
+
5
+ export async function load({ params }) {
6
+ return { show: await getTv(Number(params.id)) };
7
+ }
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { EpisodeList, MediaDetail } from '@axium/kino/components';
4
+
5
+ const { data } = $props();
6
+
7
+ const { show, season } = $derived(data);
8
+ </script>
9
+
10
+ <svelte:head>
11
+ <title>{show.name} — {season.name}</title>
12
+ </svelte:head>
13
+
14
+ <MediaDetail
15
+ title={season.name}
16
+ imagePath={season.poster_path}
17
+ backdropPath={show.backdrop_path}
18
+ date={season.air_date}
19
+ dateKind="aired"
20
+ overview={season.overview}
21
+ >
22
+ {#snippet context()}
23
+ <a href="/tv/{show.id}">{show.name}</a>
24
+ {/snippet}
25
+
26
+ <section>
27
+ <h2>{text('kino.episodes')}</h2>
28
+ <EpisodeList id={show.id} season={season.season_number} episodes={season.episodes} />
29
+ </section>
30
+ </MediaDetail>
31
+
32
+ <style>
33
+ section {
34
+ display: flex;
35
+ flex-direction: column;
36
+ gap: 0.75em;
37
+ }
38
+
39
+ h2 {
40
+ margin: 0;
41
+ font-size: 1.15em;
42
+ }
43
+ </style>
@@ -0,0 +1,11 @@
1
+ import { getSeason, getTv } from '@axium/kino/client';
2
+
3
+ export const ssr = false;
4
+
5
+ export async function load({ params }) {
6
+ const id = Number(params.id);
7
+
8
+ const [show, season] = await Promise.all([getTv(id), getSeason(id, Number(params.season))]);
9
+
10
+ return { show, season };
11
+ }
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { deleteEpisodeUpload, episodeDataURL } from '@axium/kino/client';
4
+ import { uploadEpisodeFile } from '@axium/kino/client/frontend';
5
+ import { MediaActions, MediaDetail } from '@axium/kino/components';
6
+
7
+ const { data } = $props();
8
+
9
+ const { show, season, episode } = $derived(data);
10
+
11
+ const code = $derived(text('kino.episode_code', { season, episode: episode.episode_number }));
12
+
13
+ let upload = $state(data.episode.upload);
14
+ </script>
15
+
16
+ <svelte:head>
17
+ <title>{show.name} — {code}</title>
18
+ </svelte:head>
19
+
20
+ <MediaDetail
21
+ title={episode.name}
22
+ imagePath={episode.still_path}
23
+ imageType="still"
24
+ backdropPath={show.backdrop_path}
25
+ date={episode.air_date}
26
+ dateKind="aired"
27
+ overview={null}
28
+ >
29
+ {#snippet context()}
30
+ <a href="/tv/{show.id}">{show.name}</a>
31
+ ·
32
+ <a href="/tv/{show.id}/{season}">{code}</a>
33
+ {/snippet}
34
+
35
+ {#snippet actions()}
36
+ <MediaActions
37
+ bind:upload
38
+ watchHref="/tv/{show.id}/{season}/{episode.episode_number}/watch"
39
+ dataURL={episodeDataURL(show.id, season, episode.episode_number, true)}
40
+ uploadText={text('kino.upload_episode')}
41
+ canDelete={data.session?.user.isAdmin}
42
+ uploadFile={file => uploadEpisodeFile(file, show.id, season, episode.episode_number)}
43
+ deleteUpload={() => deleteEpisodeUpload(show.id, season, episode.episode_number)}
44
+ />
45
+ {/snippet}
46
+ </MediaDetail>
@@ -0,0 +1,12 @@
1
+ import { getEpisode, getTv } from '@axium/kino/client';
2
+
3
+ export const ssr = false;
4
+
5
+ export async function load({ params }) {
6
+ const id = Number(params.id),
7
+ season = Number(params.season);
8
+
9
+ const [show, episode] = await Promise.all([getTv(id), getEpisode(id, season, Number(params.episode))]);
10
+
11
+ return { show, season, episode };
12
+ }
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { Video } from '@axium/client/components';
4
+ import { MediaState } from '@axium/client/reactive';
5
+ import { episodeDataURL } from '@axium/kino/client';
6
+ import { trackWatch } from '@axium/kino/watch';
7
+
8
+ const { data } = $props();
9
+
10
+ const { show, season, episode, upload } = $derived(data);
11
+
12
+ const code = $derived(text('kino.episode_code', { season, episode: episode.episode_number }));
13
+
14
+ const media = new MediaState();
15
+
16
+ trackWatch({
17
+ media,
18
+ target: { type: 'tv', id: data.show.id, season: data.season, episode: data.episode.episode_number },
19
+ resumeFrom: data.view?.position,
20
+ });
21
+ </script>
22
+
23
+ <svelte:head>
24
+ <title>{text('page.kino.watch_title', { name: `${show.name} ${code}` })}</title>
25
+ </svelte:head>
26
+
27
+ <div class="watch">
28
+ <a class="subtle" href="/tv/{show.id}/{season}/{episode.episode_number}">{show.name} — {code} — {episode.name}</a>
29
+ <Video
30
+ {media}
31
+ src={episodeDataURL(show.id, season, episode.episode_number)}
32
+ size={upload.size}
33
+ type={upload.type}
34
+ name={episode.name}
35
+ />
36
+ </div>
37
+
38
+ <style>
39
+ .watch {
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: 0.5em;
43
+ height: 80vh;
44
+ min-height: 0;
45
+ }
46
+ </style>
@@ -0,0 +1,22 @@
1
+ import { getEpisode, getTv, getViews } from '@axium/kino/client';
2
+ import { error } from '@sveltejs/kit';
3
+
4
+ export const ssr = false;
5
+
6
+ export async function load({ params }) {
7
+ const id = Number(params.id),
8
+ season = Number(params.season),
9
+ episodeNumber = Number(params.episode);
10
+
11
+ const [show, episode] = await Promise.all([getTv(id), getEpisode(id, season, episodeNumber)]);
12
+
13
+ if (!episode.upload) error(404, 'This episode has not been uploaded');
14
+
15
+ // Used to resume where the last session left off; not worth failing the page over
16
+ const views = await getViews().catch(() => []);
17
+ const view = views.find(
18
+ v => v.type == 'tv' && v.show.id == id && v.episode.season_number == season && v.episode.episode_number == episodeNumber
19
+ );
20
+
21
+ return { show, season, episode, upload: episode.upload, view };
22
+ }