mirror of
https://github.com/FJurmanovic/sugarr.git
synced 2026-11-10 23:31:08 +00:00
Add JellyfinClient
This commit is contained in:
61
src/app/clients/jellyfin/JellyfinClient.ts
Normal file
61
src/app/clients/jellyfin/JellyfinClient.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { ApiClient } from '../../../core/http/ApiClient';
|
||||
import type { Movie } from '../../models';
|
||||
import { ItemsEndpoint } from './endpoints/ItemsEndpoint';
|
||||
|
||||
export class JellyfinClient extends ApiClient {
|
||||
private readonly endpoint = new ItemsEndpoint();
|
||||
|
||||
constructor(
|
||||
baseUrl: string,
|
||||
private readonly apiKey: string,
|
||||
private readonly userId: string
|
||||
) {
|
||||
super(baseUrl);
|
||||
}
|
||||
|
||||
protected buildHeaders(): Record<string, string> {
|
||||
return {
|
||||
...super.buildHeaders(),
|
||||
Authorization: `MediaBrowser Token="${this.apiKey}"`,
|
||||
};
|
||||
}
|
||||
|
||||
getFavoriteMovies(): Promise<Movie[]> {
|
||||
return this.request(this.endpoint, {
|
||||
userId: this.userId,
|
||||
query: {
|
||||
IncludeItemTypes: 'Movie',
|
||||
Recursive: 'true',
|
||||
IsFavorite: 'true',
|
||||
Fields: 'ProviderIds,Genres,CommunityRating,UserData',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getRecentlyWatched(): Promise<Movie[]> {
|
||||
return this.request(this.endpoint, {
|
||||
userId: this.userId,
|
||||
query: {
|
||||
IncludeItemTypes: 'Movie',
|
||||
Recursive: 'true',
|
||||
Filters: 'IsPlayed',
|
||||
SortBy: 'DatePlayed',
|
||||
SortOrder: 'Descending',
|
||||
Limit: '50',
|
||||
Fields: 'ProviderIds,Genres,CommunityRating,UserData',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getAllMovies(): Promise<Movie[]> {
|
||||
return this.request(this.endpoint, {
|
||||
userId: this.userId,
|
||||
query: {
|
||||
IncludeItemTypes: 'Movie',
|
||||
Recursive: 'true',
|
||||
Fields: 'ProviderIds',
|
||||
Limit: '10000',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
46
src/app/clients/jellyfin/endpoints/ItemsEndpoint.ts
Normal file
46
src/app/clients/jellyfin/endpoints/ItemsEndpoint.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Endpoint, type HttpMethod } from '../../../../core/http/Endpoint';
|
||||
import type { Movie } from '../../../models';
|
||||
|
||||
interface JellyfinItem {
|
||||
Name: string;
|
||||
ProductionYear: number;
|
||||
ProviderIds: Record<string, string>;
|
||||
Genres: string[];
|
||||
CommunityRating: number;
|
||||
UserData: { IsFavorite: boolean; PlayCount: number };
|
||||
}
|
||||
|
||||
interface JellyfinItemsResponse {
|
||||
Items: JellyfinItem[];
|
||||
}
|
||||
|
||||
export interface ItemsParams {
|
||||
userId: string;
|
||||
query: Record<string, string>;
|
||||
}
|
||||
|
||||
export class ItemsEndpoint extends Endpoint<Movie[], ItemsParams> {
|
||||
readonly method: HttpMethod = 'GET';
|
||||
|
||||
path(params: ItemsParams): string {
|
||||
return `/Users/${params.userId}/Items`;
|
||||
}
|
||||
|
||||
query(params: ItemsParams): Record<string, string> {
|
||||
return params.query;
|
||||
}
|
||||
|
||||
parseResponse(json: unknown): Movie[] {
|
||||
const response = json as JellyfinItemsResponse;
|
||||
return response.Items.map((item) => ({
|
||||
name: item.Name,
|
||||
year: item.ProductionYear,
|
||||
imdbId: item.ProviderIds?.Imdb ?? '',
|
||||
tmdbId: item.ProviderIds?.Tmdb ?? '',
|
||||
genres: item.Genres ?? [],
|
||||
communityRating: item.CommunityRating ?? 0,
|
||||
isFavorite: item.UserData?.IsFavorite ?? false,
|
||||
playCount: item.UserData?.PlayCount ?? 0,
|
||||
}));
|
||||
}
|
||||
}
|
||||
72
test/app/clients/jellyfin/JellyfinClient.test.ts
Normal file
72
test/app/clients/jellyfin/JellyfinClient.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { JellyfinClient } from '../../../../src/app/clients/jellyfin/JellyfinClient';
|
||||
|
||||
describe('JellyfinClient', () => {
|
||||
const fetchMock = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
fetchMock.mockReset();
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () =>
|
||||
JSON.stringify({
|
||||
Items: [
|
||||
{
|
||||
Name: 'Dune',
|
||||
ProductionYear: 2021,
|
||||
ProviderIds: { Imdb: 'tt1160419', Tmdb: '438631' },
|
||||
Genres: ['Sci-Fi'],
|
||||
CommunityRating: 8.0,
|
||||
UserData: { IsFavorite: true, PlayCount: 2 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('getFavoriteMovies requests favorites and maps the response to Movie[]', async () => {
|
||||
const client = new JellyfinClient('http://jellyfin', 'jf-key', 'user-1');
|
||||
const movies = await client.getFavoriteMovies();
|
||||
|
||||
expect(movies).toEqual([
|
||||
{
|
||||
name: 'Dune',
|
||||
year: 2021,
|
||||
imdbId: 'tt1160419',
|
||||
tmdbId: '438631',
|
||||
genres: ['Sci-Fi'],
|
||||
communityRating: 8.0,
|
||||
isFavorite: true,
|
||||
playCount: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toContain('/Users/user-1/Items');
|
||||
expect(url).toContain('IsFavorite=true');
|
||||
expect(init.headers.Authorization).toBe('MediaBrowser Token="jf-key"');
|
||||
});
|
||||
|
||||
it('getRecentlyWatched requests played items sorted by date', async () => {
|
||||
const client = new JellyfinClient('http://jellyfin', 'jf-key', 'user-1');
|
||||
await client.getRecentlyWatched();
|
||||
|
||||
const [url] = fetchMock.mock.calls[0];
|
||||
expect(url).toContain('Filters=IsPlayed');
|
||||
expect(url).toContain('SortBy=DatePlayed');
|
||||
});
|
||||
|
||||
it('getAllMovies requests the full library', async () => {
|
||||
const client = new JellyfinClient('http://jellyfin', 'jf-key', 'user-1');
|
||||
await client.getAllMovies();
|
||||
|
||||
const [url] = fetchMock.mock.calls[0];
|
||||
expect(url).toContain('Limit=10000');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user