feat: Implemented jellyfin api
This commit is contained in:
2
.idea/reiverr.iml
generated
2
.idea/reiverr.iml
generated
@@ -9,6 +9,8 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/.svelte-kit/output" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tizen/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tizen/.buildResult" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type createClient from 'openapi-fetch';
|
||||
|
||||
export abstract class Api<Paths extends NonNullable<unknown>> {
|
||||
protected abstract baseUrl: string;
|
||||
protected abstract client: ReturnType<typeof createClient<Paths>>;
|
||||
protected abstract isLoggedIn: boolean;
|
||||
|
||||
getApi() {
|
||||
return this.client;
|
||||
}
|
||||
export interface Api<Paths extends NonNullable<unknown>> {
|
||||
getClient(): ReturnType<typeof createClient<Paths>>;
|
||||
}
|
||||
|
||||
// export abstract class Api<Paths extends NonNullable<unknown>> {
|
||||
// protected abstract baseUrl: string;
|
||||
// protected abstract client: ReturnType<typeof createClient<Paths>>;
|
||||
// protected abstract isLoggedIn: boolean;
|
||||
//
|
||||
// getApi() {
|
||||
// return this.client;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -4,11 +4,82 @@ import { get } from 'svelte/store';
|
||||
import type { components, paths } from './jellyfin.generated';
|
||||
import { settings } from '../../stores/settings.store';
|
||||
import type { DeviceProfile } from './playback-profiles';
|
||||
import type { Api } from '../api.interface';
|
||||
import { appState } from '../../stores/app-state.store';
|
||||
|
||||
export type JellyfinItem = components['schemas']['BaseItemDto'];
|
||||
|
||||
export const JELLYFIN_DEVICE_ID = 'Reiverr Client';
|
||||
|
||||
export class JellyfinApi implements Api<paths> {
|
||||
getClient() {
|
||||
const jellyfinSettings = get(appState).user?.settings.jellyfin;
|
||||
const baseUrl = jellyfinSettings?.baseUrl;
|
||||
const apiKey = jellyfinSettings?.apiKey;
|
||||
|
||||
return createClient<paths>({
|
||||
baseUrl,
|
||||
headers: {
|
||||
Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${apiKey}"`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getUserId() {
|
||||
return get(appState).user?.settings.jellyfin.userId || '';
|
||||
}
|
||||
|
||||
async getContinueWatching(): Promise<JellyfinItem[] | undefined> {
|
||||
return this.getClient()
|
||||
.GET('/Users/{userId}/Items/Resume', {
|
||||
params: {
|
||||
path: {
|
||||
userId: this.getUserId()
|
||||
},
|
||||
query: {
|
||||
mediaTypes: ['Video'],
|
||||
fields: ['ProviderIds', 'Genres']
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => r.data?.Items || []);
|
||||
}
|
||||
|
||||
async getLibraryItems() {
|
||||
return (
|
||||
this.getClient()
|
||||
.GET('/Users/{userId}/Items', {
|
||||
params: {
|
||||
path: {
|
||||
userId: this.getUserId()
|
||||
},
|
||||
query: {
|
||||
hasTmdbId: true,
|
||||
recursive: true,
|
||||
includeItemTypes: ['Movie', 'Series'],
|
||||
fields: ['ProviderIds', 'Genres', 'DateLastMediaAdded', 'DateCreated']
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => r.data?.Items || []) || Promise.resolve([])
|
||||
);
|
||||
}
|
||||
|
||||
getPosterUrl(item: JellyfinItem, quality = 100, original = false) {
|
||||
return item.ImageTags?.Primary
|
||||
? `${get(appState).user?.settings.jellyfin.baseUrl}/Items/${
|
||||
item?.Id
|
||||
}/Images/Primary?quality=${quality}${original ? '' : '&fillWidth=432'}&tag=${
|
||||
item?.ImageTags?.Primary
|
||||
}`
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
export const jellyfinApi = new JellyfinApi();
|
||||
export const getReiverrApiClient = jellyfinApi.getClient;
|
||||
|
||||
/*
|
||||
function getJellyfinApi() {
|
||||
const baseUrl = get(settings)?.jellyfin.baseUrl;
|
||||
const apiKey = get(settings)?.jellyfin.apiKey;
|
||||
@@ -51,23 +122,6 @@ export const getJellyfinNextUp = async () =>
|
||||
})
|
||||
.then((r) => r.data?.Items || []);
|
||||
|
||||
export const getJellyfinItems = async () =>
|
||||
getJellyfinApi()
|
||||
?.GET('/Users/{userId}/Items', {
|
||||
params: {
|
||||
path: {
|
||||
userId: get(settings)?.jellyfin.userId || ''
|
||||
},
|
||||
query: {
|
||||
hasTmdbId: true,
|
||||
recursive: true,
|
||||
includeItemTypes: ['Movie', 'Series'],
|
||||
fields: ['ProviderIds', 'Genres', 'DateLastMediaAdded', 'DateCreated']
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => r.data?.Items || []) || Promise.resolve([]);
|
||||
|
||||
// export const getJellyfinSeries = () =>
|
||||
// JellyfinApi.get('/Users/{userId}/Items', {
|
||||
// params: {
|
||||
@@ -285,13 +339,6 @@ export const getJellyfinUsers = async (
|
||||
.then((res) => res.data || [])
|
||||
.catch(() => []);
|
||||
|
||||
export const getJellyfinPosterUrl = (item: JellyfinItem, quality = 100, original = false) =>
|
||||
item.ImageTags?.Primary
|
||||
? `${get(settings).jellyfin.baseUrl}/Items/${item?.Id}/Images/Primary?quality=${quality}${
|
||||
original ? '' : '&fillWidth=432'
|
||||
}&tag=${item?.ImageTags?.Primary}`
|
||||
: '';
|
||||
|
||||
export const getJellyfinBackdrop = (item: JellyfinItem, quality = 100) => {
|
||||
if (item.BackdropImageTags?.length) {
|
||||
return `${get(settings).jellyfin.baseUrl}/Items/${
|
||||
@@ -303,3 +350,4 @@ export const getJellyfinBackdrop = (item: JellyfinItem, quality = 100) => {
|
||||
}/Images/Primary?quality=${quality}&tag=${item?.ImageTags?.Primary}`;
|
||||
}
|
||||
};
|
||||
*/
|
||||
@@ -2,11 +2,9 @@ import createClient from 'openapi-fetch';
|
||||
import type { paths } from './reiverr.generated';
|
||||
import { get } from 'svelte/store';
|
||||
import { appState } from '../../stores/app-state.store';
|
||||
import type { Api } from '../api.interface';
|
||||
|
||||
interface ApiInterface<Paths extends NonNullable<unknown>> {
|
||||
getClient(): ReturnType<typeof createClient<Paths>>;
|
||||
}
|
||||
export class ReiverrApi implements ApiInterface<paths> {
|
||||
export class ReiverrApi implements Api<paths> {
|
||||
getClient(basePath?: string) {
|
||||
const token = get(appState).token;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {
|
||||
setJellyfinItemUnwatched,
|
||||
setJellyfinItemWatched
|
||||
} from '../../../lib/apis/jellyfin/jellyfinApi';
|
||||
} from '../../apis/jellyfin/jellyfin-api';
|
||||
import { jellyfinItemsStore } from '../../../lib/stores/data.store';
|
||||
import classNames from 'classnames';
|
||||
import { Check } from 'radix-icons-svelte';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getJellyfinEpisodes, type JellyfinItem } from '../../lib/apis/jellyfin/jellyfinApi';
|
||||
import { getJellyfinEpisodes, type JellyfinItem } from '../apis/jellyfin/jellyfin-api';
|
||||
import { addSeriesToSonarr, type SonarrSeries } from '../../lib/apis/sonarr/sonarrApi';
|
||||
import {
|
||||
getTmdbIdFromTvdbId,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
reportJellyfinPlaybackProgress,
|
||||
reportJellyfinPlaybackStarted,
|
||||
reportJellyfinPlaybackStopped
|
||||
} from '../../apis/jellyfin/jellyfinApi';
|
||||
} from '../../apis/jellyfin/jellyfin-api';
|
||||
import getDeviceProfile from '../../apis/jellyfin/playback-profiles';
|
||||
import { getQualities } from '../../apis/jellyfin/qualities';
|
||||
import { settings } from '../../stores/settings.store';
|
||||
|
||||
30
src/lib/components/Card/JellyfinCard.svelte
Normal file
30
src/lib/components/Card/JellyfinCard.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { jellyfinApi, type JellyfinItem } from '../../apis/jellyfin/jellyfin-api';
|
||||
import Card from './Card.svelte';
|
||||
|
||||
export let item: JellyfinItem;
|
||||
|
||||
// return {
|
||||
// tmdbId: Number(item.ProviderIds?.Tmdb) || 0,
|
||||
// jellyfinId: item.Id,
|
||||
// title: item.Name || undefined,
|
||||
// subtitle: item.Genres?.join(', ') || undefined,
|
||||
// backdropUrl: getJellyfinPosterUrl(item, 80),
|
||||
// size: 'dynamic',
|
||||
// ...(item.Type === 'Movie' ? { type: 'movie' } : { type: 'series' }),
|
||||
// orientation: 'portrait',
|
||||
// rating: item.CommunityRating || undefined
|
||||
// };
|
||||
</script>
|
||||
|
||||
<Card
|
||||
tmdbId={Number(item.ProviderIds?.Tmdb) || 0}
|
||||
jellyfinId={item.Id}
|
||||
title={item.Name || undefined}
|
||||
subtitle={item.Genres?.join(', ') || undefined}
|
||||
backdropUrl={jellyfinApi.getPosterUrl(item, 80)}
|
||||
size="dynamic"
|
||||
type={item.Type === 'Movie' ? 'movie' : 'series'}
|
||||
orientation="portrait"
|
||||
rating={item.CommunityRating || undefined}
|
||||
/>
|
||||
3
src/lib/components/CardGrid.svelte
Normal file
3
src/lib/components/CardGrid.svelte
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="grid gap-x-4 gap-y-8 grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { ChevronLeft, ChevronRight } from 'radix-icons-svelte';
|
||||
import classNames from 'classnames';
|
||||
@@ -13,14 +12,15 @@
|
||||
export let scrollClass = '';
|
||||
</script>
|
||||
|
||||
<div class={classNames('flex flex-col gap-4 group/carousel', $$restProps.class)}>
|
||||
<div class={'flex justify-between items-center gap-4 ' + scrollClass}>
|
||||
<div class={classNames('flex flex-col group/carousel', $$restProps.class)}>
|
||||
<div class={'flex justify-between items-center mb-2 ' + scrollClass}>
|
||||
<slot name="title">
|
||||
<div class="font-semibold text-xl">{heading}</div>
|
||||
</slot>
|
||||
<div
|
||||
class={classNames(
|
||||
'flex gap-2 sm:opacity-0 transition-opacity sm:group-hover/carousel:opacity-100',
|
||||
'flex gap-2 ml-4',
|
||||
//'sm:opacity-0 transition-opacity sm:group-hover/carousel:opacity-100',
|
||||
{
|
||||
hidden: (carousel?.scrollWidth || 0) === (carousel?.clientWidth || 0)
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
<Container horizontal>
|
||||
<div
|
||||
class={classNames(
|
||||
'flex overflow-x-scroll items-center overflow-y-visible relative scrollbar-hide p-1',
|
||||
'flex overflow-x-scroll items-center overflow-y-visible relative scrollbar-hide',
|
||||
scrollClass
|
||||
)}
|
||||
bind:this={carousel}
|
||||
@@ -59,14 +59,12 @@
|
||||
</Container>
|
||||
{#if scrollX > 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class={'absolute inset-y-0 left-0 w-0 sm:w-16 md:w-24 bg-gradient-to-r ' +
|
||||
gradientFromColor}
|
||||
/>
|
||||
{/if}
|
||||
{#if carousel && scrollX < carousel?.scrollWidth - carousel?.clientWidth - 50}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class={'absolute inset-y-0 right-0 w-0 sm:w-16 md:w-24 bg-gradient-to-l ' +
|
||||
gradientFromColor}
|
||||
/>
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
<Container class="z-10">
|
||||
<Container class="z-10 pt-8">
|
||||
<slot />
|
||||
</Container>
|
||||
</Container>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
reportJellyfinPlaybackProgress,
|
||||
reportJellyfinPlaybackStarted,
|
||||
reportJellyfinPlaybackStopped
|
||||
} from '../../apis/jellyfin/jellyfinApi';
|
||||
} from '../../apis/jellyfin/jellyfin-api';
|
||||
import getDeviceProfile from '../../apis/jellyfin/playback-profiles';
|
||||
import { getQualities } from '../../apis/jellyfin/qualities';
|
||||
import { settings } from '../../stores/settings.store';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import type { TitleType } from '../types';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import Poster from '../components/Card/Card.svelte';
|
||||
import { getJellyfinItems, type JellyfinItem } from '../apis/jellyfin/jellyfinApi';
|
||||
import { type JellyfinItem } from '../apis/jellyfin/jellyfin-api';
|
||||
import { jellyfinItemsStore } from '../stores/data.store';
|
||||
import Carousel from '../components/Carousel/Carousel.svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
@@ -62,12 +62,12 @@
|
||||
|
||||
const fetchPopularMovies = () => getTmdbPopularMovies();
|
||||
|
||||
const fetchLibraryItems = async () => {
|
||||
const items = await getJellyfinItems();
|
||||
const props = await fetchCardProps(items, 'series');
|
||||
console.log('JellyfinItems', items, props);
|
||||
return props;
|
||||
};
|
||||
// const fetchLibraryItems = async () => {
|
||||
// const items = await getJellyfinItems();
|
||||
// const props = await fetchCardProps(items, 'series');
|
||||
// console.log('JellyfinItems', items, props);
|
||||
// return props;
|
||||
// };
|
||||
|
||||
function parseIncludedLanguages(includedLanguages: string) {
|
||||
return includedLanguages.replace(' ', '').split(',').join('|');
|
||||
@@ -76,8 +76,8 @@
|
||||
|
||||
<Container focusOnMount>
|
||||
<HeroShowcase items={getTmdbPopularMovies().then(getShowcasePropsFromTmdb)}>
|
||||
<Carousel scrollClass="px-2 sm:px-8 2xl:px-16">
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
<Carousel scrollClass="px-8">
|
||||
<div slot="title" class="text-xl font-semibold text-zinc-300">
|
||||
{$_('discover.streamingNow')}
|
||||
</div>
|
||||
{#await fetchNowStreaming()}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { settings } from '../stores/settings.store';
|
||||
import { jellyfinItemsStore } from '../stores/data.store';
|
||||
import Carousel from '../components/Carousel/Carousel.svelte';
|
||||
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
|
||||
import Container from '../../Container.svelte';
|
||||
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
|
||||
import CardGrid from '../components/CardGrid.svelte';
|
||||
import JellyfinCard from '../components/Card/JellyfinCard.svelte';
|
||||
|
||||
const libraryItemsP = jellyfinApi.getLibraryItems();
|
||||
|
||||
settings.update((prev) => ({
|
||||
...prev,
|
||||
@@ -21,9 +25,15 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Container focusOnMount>
|
||||
<Container focusOnMount class="pl-20">
|
||||
<div>LibraryPage</div>
|
||||
<Carousel>
|
||||
<CarouselPlaceholderItems />
|
||||
</Carousel>
|
||||
<CardGrid>
|
||||
{#await libraryItemsP}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then items}
|
||||
{#each items as item}
|
||||
<JellyfinCard {item} />
|
||||
{/each}
|
||||
{/await}
|
||||
</CardGrid>
|
||||
</Container>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { settings } from './settings.store';
|
||||
import { getJellyfinItems, type JellyfinItem } from '../apis/jellyfin/jellyfinApi';
|
||||
import { jellyfinApi, type JellyfinItem } from '../apis/jellyfin/jellyfin-api';
|
||||
import {
|
||||
getSonarrDownloads,
|
||||
getSonarrSeries,
|
||||
@@ -59,7 +59,7 @@ function _createDataFetchStore<T>(fn: () => Promise<T>) {
|
||||
};
|
||||
}
|
||||
|
||||
export const jellyfinItemsStore = _createDataFetchStore(getJellyfinItems);
|
||||
export const jellyfinItemsStore = _createDataFetchStore(jellyfinApi.getLibraryItems);
|
||||
|
||||
export function createJellyfinItemStore(tmdbId: number | Promise<number>) {
|
||||
const store = writable<{ loading: boolean; item?: JellyfinItem }>({
|
||||
|
||||
Reference in New Issue
Block a user