@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,82 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { type KinoView, viewProgress } from '@axium/kino/common';
4
+ import Poster from './Poster.svelte';
5
+ import ProgressBar from './ProgressBar.svelte';
6
+
7
+ const { views, empty }: { views: KinoView[]; empty: string } = $props();
8
+
9
+ function key(view: KinoView): string {
10
+ return view.type == 'movie'
11
+ ? `movie:${view.movie.id}`
12
+ : `tv:${view.show.id}:${view.episode.season_number}:${view.episode.episode_number}`;
13
+ }
14
+ </script>
15
+
16
+ <div class="RecentGrid">
17
+ {#each views as view (key(view))}
18
+ {const title = view.type == 'movie' ? view.movie.title : view.show.name}
19
+ <!-- Resume where they left off rather than starting over -->
20
+ <a
21
+ class="recent"
22
+ href={view.type == 'movie'
23
+ ? `/movies/${view.movie.id}/watch`
24
+ : `/tv/${view.show.id}/${view.episode.season_number}/${view.episode.episode_number}/watch`}
25
+ >
26
+ <Poster path={view.type == 'movie' ? view.movie.poster_path : view.show.poster_path} type="poster" size="w342" alt={title} />
27
+
28
+ {const progress = viewProgress(view)}
29
+ {#if progress !== null}
30
+ <ProgressBar value={progress} />
31
+ {/if}
32
+
33
+ <span class="title">{title}</span>
34
+
35
+ {#if view.type == 'tv'}
36
+ <span class="subtle detail">
37
+ {text('kino.episode_code', { season: view.episode.season_number, episode: view.episode.episode_number })}
38
+ · {view.episode.name}
39
+ </span>
40
+ {/if}
41
+ </a>
42
+ {:else}
43
+ <p class="subtle">{empty}</p>
44
+ {/each}
45
+ </div>
46
+
47
+ <style>
48
+ .RecentGrid {
49
+ display: grid;
50
+ grid-template-columns: repeat(auto-fill, minmax(9em, 1fr));
51
+ gap: 1em;
52
+ align-items: start;
53
+
54
+ &:has(> p) {
55
+ display: block;
56
+ }
57
+ }
58
+
59
+ .recent {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 0.25em;
63
+ text-decoration: none;
64
+ color: inherit;
65
+
66
+ &:hover .title {
67
+ color: var(--fg-strong);
68
+ }
69
+ }
70
+
71
+ .title {
72
+ text-wrap: balance;
73
+ line-height: 1.2;
74
+ }
75
+
76
+ .detail {
77
+ font-size: 0.85em;
78
+ overflow: hidden;
79
+ text-overflow: ellipsis;
80
+ white-space: nowrap;
81
+ }
82
+ </style>
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { Icon } from '@axium/client/components';
4
+ import { searchMedia } from '@axium/kino/client';
5
+ import type { KinoSearchResults } from '@axium/kino/common';
6
+ import Poster from './Poster.svelte';
7
+
8
+ let query = $state(''),
9
+ results = $state<KinoSearchResults>([]),
10
+ open = $state(false),
11
+ pending = $state(false),
12
+ failed = $state(false);
13
+
14
+ let debounce: ReturnType<typeof setTimeout> | undefined;
15
+
16
+ /** Responses can arrive out of order, so only the newest one is applied */
17
+ let latest = 0;
18
+
19
+ function search() {
20
+ clearTimeout(debounce);
21
+
22
+ const value = query.trim();
23
+
24
+ if (!value) {
25
+ latest++;
26
+ results = [];
27
+ pending = false;
28
+ failed = false;
29
+ open = false;
30
+ return;
31
+ }
32
+
33
+ open = true;
34
+ pending = true;
35
+ failed = false;
36
+
37
+ debounce = setTimeout(async () => {
38
+ const id = ++latest;
39
+ try {
40
+ const found = await searchMedia(value);
41
+ if (id != latest) return;
42
+ results = found;
43
+ } catch {
44
+ if (id != latest) return;
45
+ results = [];
46
+ failed = true;
47
+ } finally {
48
+ if (id == latest) pending = false;
49
+ }
50
+ }, 300);
51
+ }
52
+
53
+ function close() {
54
+ open = false;
55
+ }
56
+ </script>
57
+
58
+ <div
59
+ class="KinoSearch"
60
+ onfocusout={e => {
61
+ if (!e.currentTarget.contains(e.relatedTarget as Node | null)) close();
62
+ }}
63
+ >
64
+ <div class="input icon-text">
65
+ <Icon i="magnifying-glass" />
66
+ <input
67
+ type="search"
68
+ bind:value={query}
69
+ oninput={search}
70
+ onfocus={() => (open = !!query.trim())}
71
+ onkeydown={e => e.key == 'Escape' && close()}
72
+ placeholder={text('kino.search.placeholder')}
73
+ aria-label={text('kino.search.label')}
74
+ />
75
+ </div>
76
+
77
+ {#if open}
78
+ <div class="results">
79
+ {#if pending}
80
+ <i class="subtle message">{text('kino.search.searching')}</i>
81
+ {:else if failed}
82
+ <i class="subtle message">{text('kino.search.failed')}</i>
83
+ {:else}
84
+ {#each results as item (item.type + item.id)}
85
+ {const title = item.type == 'movie' ? item.title : item.name}
86
+ {const date = item.type == 'movie' ? item.release_date : item.first_air_date}
87
+ <a class="result" href={item.type == 'movie' ? `/movies/${item.id}` : `/tv/${item.id}`} onclick={close}>
88
+ <Poster path={item.poster_path} type="poster" size="w92" alt={title} />
89
+ <span class="name">{title}</span>
90
+ <span class="subtle meta">
91
+ {text(item.type == 'movie' ? 'kino.search.movie' : 'kino.search.tv')}
92
+ {#if date}· {date.getFullYear()}{/if}
93
+ </span>
94
+ </a>
95
+ {:else}
96
+ <i class="subtle message">{text('kino.search.no_results')}</i>
97
+ {/each}
98
+ {/if}
99
+ </div>
100
+ {/if}
101
+ </div>
102
+
103
+ <style>
104
+ .KinoSearch {
105
+ position: relative;
106
+ width: min(30em, 100%);
107
+ }
108
+
109
+ .input {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 0.5em;
113
+ padding: 0.5em 0.75em;
114
+ border-radius: 0.5em;
115
+ background-color: var(--bg-alt);
116
+ border: var(--border-accent);
117
+ }
118
+
119
+ input {
120
+ flex: 1 1 auto;
121
+ min-width: 0;
122
+ border: none;
123
+ outline: none;
124
+ background: none;
125
+ color: inherit;
126
+ font: inherit;
127
+ }
128
+
129
+ .results {
130
+ position: absolute;
131
+ inset: calc(100% + 0.25em) 0 auto 0;
132
+ z-index: 7;
133
+ max-height: 24em;
134
+ overflow-y: auto;
135
+ border-radius: 0.5em;
136
+ border: var(--border-accent);
137
+ background-color: var(--bg-elevated);
138
+ padding: 0.25em;
139
+ }
140
+
141
+ .message {
142
+ display: block;
143
+ padding: 0.75em;
144
+ }
145
+
146
+ .result {
147
+ display: grid;
148
+ grid-template-columns: 2.5em 1fr;
149
+ grid-template-rows: auto auto;
150
+ column-gap: 0.75em;
151
+ align-items: center;
152
+ padding: 0.5em;
153
+ border-radius: 0.5em;
154
+ text-decoration: none;
155
+ color: inherit;
156
+
157
+ &:hover {
158
+ background-color: var(--bg-strong);
159
+ }
160
+
161
+ & :global(.Poster) {
162
+ grid-row: 1 / 3;
163
+ }
164
+ }
165
+
166
+ .name {
167
+ align-self: end;
168
+ }
169
+
170
+ .meta {
171
+ align-self: start;
172
+ font-size: 0.85em;
173
+ }
174
+ </style>
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import type * as kt from 'kinotool';
4
+ import Poster from './Poster.svelte';
5
+
6
+ const { id, seasons }: { id: number; seasons?: kt.Season[] } = $props();
7
+ </script>
8
+
9
+ <div class="SeasonList">
10
+ {#each seasons || [] as season (season.season_number)}
11
+ <a class="season" href="/tv/{id}/{season.season_number}">
12
+ <Poster path={season.poster_path} type="poster" size="w185" alt={season.name} />
13
+ <span class="name">{season.name}</span>
14
+ {#if season.air_date}
15
+ <span class="subtle year">{season.air_date.getFullYear()}</span>
16
+ {/if}
17
+ </a>
18
+ {:else}
19
+ <p class="subtle">{text('kino.no_episodes')}</p>
20
+ {/each}
21
+ </div>
22
+
23
+ <style>
24
+ .SeasonList {
25
+ display: grid;
26
+ grid-template-columns: repeat(auto-fill, minmax(8em, 1fr));
27
+ gap: 1em;
28
+ align-items: start;
29
+
30
+ &:has(> p) {
31
+ display: block;
32
+ }
33
+ }
34
+
35
+ .season {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 0.25em;
39
+ text-decoration: none;
40
+ color: inherit;
41
+
42
+ &:hover .name {
43
+ color: var(--fg-strong);
44
+ }
45
+ }
46
+
47
+ .year {
48
+ font-size: 0.85em;
49
+ }
50
+ </style>
package/lib/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { default as EpisodeList } from './EpisodeList.svelte';
2
+ export { default as MediaActions } from './MediaActions.svelte';
3
+ export { default as MediaCard } from './MediaCard.svelte';
4
+ export { default as MediaDetail } from './MediaDetail.svelte';
5
+ export { default as MediaGrid } from './MediaGrid.svelte';
6
+ export { default as Poster } from './Poster.svelte';
7
+ export { default as ProgressBar } from './ProgressBar.svelte';
8
+ export { default as RecentGrid } from './RecentGrid.svelte';
9
+ export { default as SearchBar } from './SearchBar.svelte';
10
+ export { default as SeasonList } from './SeasonList.svelte';
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": ["../tsconfig.json", "../.svelte-kit/tsconfig.json"],
3
+ "compilerOptions": {
4
+ "rootDir": "..",
5
+ "noEmit": true,
6
+ "module": "preserve",
7
+ "moduleResolution": "Bundler"
8
+ },
9
+ "include": ["**/*.svelte", "**/*.ts"],
10
+ "exclude": [],
11
+ "references": [{ "path": ".." }]
12
+ }
@@ -0,0 +1,93 @@
1
+ import type { MediaState } from '@axium/client/reactive';
2
+ import { recordView, recordViewClosing } from '@axium/kino/client';
3
+ import type { KinoViewInit, KinoViewTarget } from '@axium/kino/common';
4
+ import * as io from 'ioium';
5
+
6
+ export interface WatchTrackerOptions {
7
+ /** Identifies the movie or episode, without the progress fields */
8
+ target: KinoViewTarget;
9
+ media: MediaState;
10
+ /** Position to seek to once the media is ready, in seconds */
11
+ resumeFrom?: number | null;
12
+ /** How often to report while playing, in milliseconds */
13
+ interval?: number;
14
+ }
15
+
16
+ /**
17
+ * Resume playback where the viewer left off and report progress as they watch.
18
+ *
19
+ * Registers effects, so it has to be called while a component is initializing.
20
+ */
21
+ export function trackWatch({ target, media, resumeFrom, interval = 15_000 }: WatchTrackerOptions): void {
22
+ let lastSent = -1;
23
+
24
+ function current(): KinoViewInit {
25
+ // `duration` is NaN until metadata loads, and Infinity for streams of unknown length
26
+ const duration = Number.isFinite(media.duration) && media.duration > 0 ? media.duration : undefined;
27
+
28
+ return { ...target, position: media.currentTime || 0, duration } as KinoViewInit;
29
+ }
30
+
31
+ /** Skip writes from a player sitting on the same frame */
32
+ function changed(init: KinoViewInit): boolean {
33
+ if (Math.abs(init.position - lastSent) < 1) return false;
34
+ lastSent = init.position;
35
+ return true;
36
+ }
37
+
38
+ function send(): void {
39
+ const init = current();
40
+ if (!changed(init)) return;
41
+
42
+ // A failed progress update should never interrupt playback
43
+ recordView(init).catch(e => io.debug('Kino: could not record progress: ' + io.errorText(e)));
44
+ }
45
+
46
+ function sendClosing(): void {
47
+ const init = current();
48
+ if (changed(init)) recordViewClosing(init);
49
+ }
50
+
51
+ let resumed = false;
52
+
53
+ $effect(() => {
54
+ /**
55
+ * Seeking needs a loaded element and a known duration, so this waits for both.
56
+ * Positions near the end are ignored so finishing something doesn't strand the next play at the credits.
57
+ */
58
+ if (resumed || !resumeFrom || !media.element || !media.duration) return;
59
+
60
+ resumed = true;
61
+ if (resumeFrom < media.duration - 10) media.currentTime = resumeFrom;
62
+ });
63
+
64
+ $effect(() => {
65
+ // Record straight away so it shows as recently watched even if they leave immediately
66
+ send();
67
+
68
+ const timer = setInterval(() => {
69
+ if (!media.paused) send();
70
+ }, interval);
71
+
72
+ /**
73
+ * A backgrounded tab can be killed without ever firing `pagehide` or `beforeunload`, so
74
+ * `visibilitychange` is the only reliable signal on mobile. `pagehide` covers desktop
75
+ * navigations and bfcache entry, where `visibilitychange` does not always fire first.
76
+ */
77
+ function onHide() {
78
+ if (document.visibilityState == 'hidden') sendClosing();
79
+ }
80
+
81
+ document.addEventListener('visibilitychange', onHide);
82
+ addEventListener('pagehide', sendClosing);
83
+
84
+ return () => {
85
+ clearInterval(timer);
86
+ document.removeEventListener('visibilitychange', onHide);
87
+ removeEventListener('pagehide', sendClosing);
88
+
89
+ // Client-side navigation away from the page: the document survives, so a normal request is fine
90
+ send();
91
+ };
92
+ });
93
+ }
@@ -0,0 +1,103 @@
1
+ {
2
+ "app_name": {
3
+ "kino": "Movies and TV"
4
+ },
5
+ "kino": {
6
+ "config": {
7
+ "remux": "Playback format",
8
+ "cache_mode": "Image cache mode",
9
+ "image_size": "Image size",
10
+ "poster_size": "Largest cached poster",
11
+ "backdrop_size": "Largest cached backdrop",
12
+ "logo_size": "Largest cached logo",
13
+ "profile_size": "Largest cached profile image",
14
+ "still_size": "Largest cached still"
15
+ },
16
+ "remux": {
17
+ "original": "Serve the uploaded file as-is",
18
+ "both": "Keep the original and serve a remuxed copy",
19
+ "replace": "Remux on upload and discard the original"
20
+ },
21
+ "cache_mode": {
22
+ "disabled": "Don't cache images",
23
+ "fallback": "Only when TMDB is unavailable",
24
+ "exact-size": "Only at the requested size",
25
+ "quality": "At the requested size or larger",
26
+ "prefer": "Always prefer cached images"
27
+ },
28
+ "image_size": {
29
+ "w45": "45px wide",
30
+ "w92": "92px wide",
31
+ "w154": "154px wide",
32
+ "w185": "185px wide",
33
+ "w300": "300px wide",
34
+ "w342": "342px wide",
35
+ "w500": "500px wide",
36
+ "w780": "780px wide",
37
+ "w1280": "1280px wide",
38
+ "h632": "632px tall",
39
+ "original": "Original"
40
+ },
41
+ "tab": {
42
+ "home": "Home",
43
+ "movies": "Movies",
44
+ "tv": "TV Shows"
45
+ },
46
+ "search": {
47
+ "label": "Search",
48
+ "placeholder": "Search movies and TV shows",
49
+ "searching": "Searching…",
50
+ "no_results": "No results",
51
+ "failed": "Search failed",
52
+ "movie": "Movie",
53
+ "tv": "TV Show"
54
+ },
55
+ "watch": "Watch",
56
+ "download": "Download",
57
+ "upload": "Upload",
58
+ "delete": "Delete upload",
59
+ "delete_confirm": "Delete this upload and its files? This cannot be undone.",
60
+ "delete_success": "Upload deleted",
61
+ "upload_movie": "Upload movie",
62
+ "upload_episode": "Upload episode",
63
+ "uploading": "Uploading {name}",
64
+ "upload_success": "Upload complete",
65
+ "upload_cancelled": "Upload cancelled",
66
+ "not_uploaded": "This has not been uploaded yet.",
67
+ "uploaded_detail": "Uploaded {date} · {size}",
68
+ "overview": "Overview",
69
+ "no_overview": "No overview available.",
70
+ "seasons": "Seasons",
71
+ "episodes": "Episodes",
72
+ "no_episodes": "No episodes are listed for this season.",
73
+ "season": "Season {number}",
74
+ "specials": "Specials",
75
+ "episode_number": "Episode {number}",
76
+ "episode_code": "S{season}E{episode}",
77
+ "aired": "Aired {date}",
78
+ "released": "Released {date}",
79
+ "unreleased": "Release date unknown",
80
+ "back_to_show": "Back to show",
81
+ "back_to_season": "Back to season"
82
+ },
83
+ "page": {
84
+ "kino": {
85
+ "title": "Movies and TV",
86
+ "heading": "Your library",
87
+ "empty": "Nothing has been uploaded yet.",
88
+ "recently_watched": "Recently watched",
89
+ "no_recent": "You haven't watched anything yet.",
90
+ "recent_movies": "Movies",
91
+ "recent_tv": "TV Shows",
92
+ "movies": {
93
+ "title": "Movies",
94
+ "empty": "No movies have been uploaded yet."
95
+ },
96
+ "tv": {
97
+ "title": "TV Shows",
98
+ "empty": "No TV shows have been uploaded yet."
99
+ },
100
+ "watch_title": "Watching {name}"
101
+ }
102
+ }
103
+ }
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "@axium/kino",
3
+ "version": "0.0.1",
4
+ "author": "James Prevett <axium@jamespre.dev>",
5
+ "description": "Movies and TV for Axium",
6
+ "funding": {
7
+ "type": "individual",
8
+ "url": "https://github.com/sponsors/james-pre"
9
+ },
10
+ "license": "LGPL-3.0-or-later",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/james-pre/axium.git"
14
+ },
15
+ "homepage": "https://github.com/james-pre/axium#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/james-pre/axium/issues"
18
+ },
19
+ "type": "module",
20
+ "main": "dist/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "exports": {
23
+ ".": "./dist/index.js",
24
+ "./client": "./dist/client/index.js",
25
+ "./*": "./dist/*.js",
26
+ "./components": "./lib/index.js",
27
+ "./components/*": "./lib/*.svelte",
28
+ "./watch": "./lib/watch.svelte.js"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "lib",
33
+ "routes",
34
+ "locales",
35
+ "db.json"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc"
39
+ },
40
+ "peerDependencies": {
41
+ "@axium/client": ">=0.36.0",
42
+ "@axium/core": ">=0.38.0",
43
+ "@axium/server": ">=0.54.0",
44
+ "@sveltejs/kit": "^2.27.3",
45
+ "kysely": "^0.29.0",
46
+ "utilium": "^3.0.0"
47
+ },
48
+ "dependencies": {
49
+ "commander": "^15.0.0",
50
+ "ioium": "^1.6.0",
51
+ "kinotool": "^0.2.1",
52
+ "mime": "^4.1.0",
53
+ "tmdb-ts": "^2.3.0",
54
+ "zod": "^4.0.5"
55
+ },
56
+ "axium": {
57
+ "server": {
58
+ "routes": "routes",
59
+ "hooks": "./dist/server/hooks.js",
60
+ "db": "./db.json",
61
+ "web_client_hooks": "./dist/client/web_hook.js"
62
+ },
63
+ "client": {
64
+ "hooks": "./dist/client/hooks.js",
65
+ "cli": "./dist/client/cli.js"
66
+ },
67
+ "apps": [
68
+ {
69
+ "id": "kino",
70
+ "name": "Movies and TV",
71
+ "icon": "popcorn"
72
+ }
73
+ ],
74
+ "update_checks": true,
75
+ "config": {
76
+ "allow_user_uploads": true,
77
+ "data_dir": "/srv/axium/kino",
78
+ "tmdb_api_key": "",
79
+ "remux": "original",
80
+ "upload": {
81
+ "temp_dir": "/tmp/axium",
82
+ "timeout": 15
83
+ },
84
+ "images": {
85
+ "cache": {
86
+ "mode": "quality",
87
+ "max_size": 1000,
88
+ "largest_only": false,
89
+ "directory": "/var/cache/axium/kino/images",
90
+ "max_image_sizes": {
91
+ "poster": "w500",
92
+ "backdrop": "w1280",
93
+ "logo": "w185",
94
+ "profile": "w185",
95
+ "still": "w300"
96
+ }
97
+ },
98
+ "base_url": "https://image.tmdb.org/t/p/"
99
+ }
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import { SidebarLayout } from '@axium/client/components';
3
+ import { SearchBar } from '@axium/kino/components';
4
+
5
+ let { children, data } = $props();
6
+ </script>
7
+
8
+ <SidebarLayout tabs={data.tabs}>
9
+ <div class="kino">
10
+ <SearchBar />
11
+ {@render children()}
12
+ </div>
13
+ </SidebarLayout>
14
+
15
+ <style>
16
+ .kino {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 1.5em;
20
+ padding: 0.5em;
21
+ }
22
+ </style>
@@ -0,0 +1,21 @@
1
+ import { text } from '@axium/client';
2
+ import { getCurrentSession } from '@axium/client/user';
3
+ import type { Session, User } from '@axium/core';
4
+
5
+ export const ssr = false;
6
+
7
+ export async function load({ route, parent }) {
8
+ let { session }: { session?: (Session & { user: User }) | null } = await parent();
9
+
10
+ session ||= await getCurrentSession().catch(() => null);
11
+
12
+ const id = route.id || '';
13
+
14
+ const tabs = [
15
+ { name: text('kino.tab.home'), href: '/kino', icon: 'house', active: id.endsWith('/kino') },
16
+ { name: text('kino.tab.movies'), href: '/movies', icon: 'film', active: id.includes('/movies') },
17
+ { name: text('kino.tab.tv'), href: '/tv', icon: 'tv', active: id.includes('/tv') },
18
+ ];
19
+
20
+ return { session, tabs };
21
+ }