diff --git a/README.md b/README.md index 195d892..4cb9ea2 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,9 @@ Reiverr is a project that aims to create a single UI for interacting with TMDB, Jellyfin, Radarr and Sonarr, as well as be an alternative to Overseerr. -This project is still in alpha, and many features are still missing. Contributions are welcome and necessary for the project to achieve it's full potential! If you would like to contribute, see [contributing](#Contributing). +This project is still in alpha, and many features are still missing. Contributions are welcome! See [contributing](#Contributing) for more information. -![Landing Page](images/screenshot-1.png) - -![Discover Page](images/screenshot-2.png) +![Demo Videi](images/reiverr-demo.gif) # List of major featuers @@ -40,7 +38,7 @@ Before you contribute: - If the ticket is vague or missing information, please ask for clarification in the comments. - UI style must match the rest of the project and it is a good idea to discuss the design beforehand, especially for larger design choices (issues labelled with "design") -I'm not a designer, so if you have any ideas for improving the UI, I'd love to implement them. If you are a designer and would like to help, contributions are much appreciated! +I'm not a designer, so if you have any ideas for improving the UI, I'd love to learn about them. If you are a designer and would like to help, contributions are much appreciated! # Development @@ -68,3 +66,11 @@ PUBLIC_SONARR_BASE_URL=http://192.168.0.129:8989 PUBLIC_JELLYFIN_API_KEY= PUBLIC_JELLYFIN_URL=http://192.168.0.129:8096 ``` + +# Additional Screenshots + +![Landing Page](images/screenshot-1.png) + +![Series Page](images/screenshot-2.png) + +![Library Page](images/screenshot-3.png) diff --git a/images/reiverr-demo.gif b/images/reiverr-demo.gif new file mode 100644 index 0000000..f133e80 Binary files /dev/null and b/images/reiverr-demo.gif differ diff --git a/images/screenshot-1.png b/images/screenshot-1.png index f5c7c0b..68fc20b 100644 Binary files a/images/screenshot-1.png and b/images/screenshot-1.png differ diff --git a/images/screenshot-2.png b/images/screenshot-2.png index 5ed36a4..48036b4 100644 Binary files a/images/screenshot-2.png and b/images/screenshot-2.png differ diff --git a/images/screenshot-3.png b/images/screenshot-3.png new file mode 100644 index 0000000..79c88df Binary files /dev/null and b/images/screenshot-3.png differ diff --git a/src/lib/apis/tmdb/tmdbApi.ts b/src/lib/apis/tmdb/tmdbApi.ts index 8492fbb..9d2d666 100644 --- a/src/lib/apis/tmdb/tmdbApi.ts +++ b/src/lib/apis/tmdb/tmdbApi.ts @@ -1,11 +1,10 @@ +import { browser } from '$app/environment'; import { TMDB_API_KEY } from '$lib/constants'; import { getIncludedLanguagesQuery, settings } from '$lib/stores/settings.store'; import { formatDateToYearMonthDay } from '$lib/utils'; -import axios from 'axios'; import createClient from 'openapi-fetch'; import { get } from 'svelte/store'; import type { operations, paths } from './tmdb.generated'; -import { browser } from '$app/environment'; const CACHE_ONE_DAY = 'max-age=86400'; const CACHE_FOUR_DAYS = 'max-age=345600'; @@ -188,6 +187,8 @@ export const getTmdbSeriesPoster = async (tmdbId: number) => export const getTmdbMoviePoster = async (tmdbId: number) => getTmdbCache(posterCache, tmdbId, () => getTmdbMovie(tmdbId).then((m) => m?.poster_path)); +/** Discover */ + export const getTmdbPopularMovies = () => TmdbApiOpen.get('/3/movie/popular', { params: { @@ -324,238 +325,148 @@ export const searchTmdbTitles = (query: string) => } }).then((res) => res.data?.results || []); -// Deprecated hereon forward - -/** @deprecated */ -export const TmdbApi = axios.create({ - baseURL: 'https://api.themoviedb.org/3', - headers: { - Authorization: `Bearer ${TMDB_API_KEY}` +export const TMDB_MOVIE_GENRES = [ + { + id: 28, + name: 'Action' + }, + { + id: 12, + name: 'Adventure' + }, + { + id: 16, + name: 'Animation' + }, + { + id: 35, + name: 'Comedy' + }, + { + id: 80, + name: 'Crime' + }, + { + id: 99, + name: 'Documentary' + }, + { + id: 18, + name: 'Drama' + }, + { + id: 10751, + name: 'Family' + }, + { + id: 14, + name: 'Fantasy' + }, + { + id: 36, + name: 'History' + }, + { + id: 27, + name: 'Horror' + }, + { + id: 10402, + name: 'Music' + }, + { + id: 9648, + name: 'Mystery' + }, + { + id: 10749, + name: 'Romance' + }, + { + id: 878, + name: 'Science Fiction' + }, + { + id: 10770, + name: 'TV Movie' + }, + { + id: 53, + name: 'Thriller' + }, + { + id: 10752, + name: 'War' + }, + { + id: 37, + name: 'Western' } -}); +]; -/** @deprecated */ -export const fetchTmdbMovie = async (tmdbId: string): Promise => - await TmdbApi.get('/movie/' + tmdbId).then((r) => r.data); - -/** @deprecated */ -export const fetchTmdbMovieVideos = async (tmdbId: string): Promise => - await TmdbApi.get('/movie/' + tmdbId + '/videos').then((res) => res.data.results); - -/** @deprecated */ -export const fetchTmdbMovieImages = async (tmdbId: string): Promise => - await TmdbApi.get('/movie/' + tmdbId + '/images', { - headers: { - 'Cache-Control': CACHE_FOUR_DAYS // 4 days - } - }).then((res) => res.data); - -/** @deprecated */ -export const fetchTmdbMovieCredits = async (tmdbId: string): Promise => - await TmdbApi.get('/movie/' + tmdbId + '/credits').then((res) => res.data.cast); - -export interface TmdbMovieFull extends TmdbMovie { - videos: { - results: Video[]; - }; - // images: { - // backdrops: Backdrop[]; - // logos: Logo[]; - // posters: Poster[]; - // }; - credits: { - cast: CastMember[]; - }; -} - -export type MovieDetailsResponse = TmdbMovie; - -export interface TmdbMovie { - adult: boolean; - backdrop_path: string; - belongs_to_collection: any; - budget: number; - genres: Genre[]; - homepage: string; - id: number; - imdb_id: string; - original_language: string; - original_title: string; - overview: string; - popularity: number; - poster_path: string; - production_companies: ProductionCompany[]; - production_countries: ProductionCountry[]; - release_date: string; - revenue: number; - runtime: number; - spoken_languages: SpokenLanguage[]; - status: string; - tagline: string; - title: string; - video: boolean; - vote_average: number; - vote_count: number; -} - -export interface Genre { - id: number; - name: string; -} - -export interface ProductionCompany { - id: number; - logo_path?: string; - name: string; - origin_country: string; -} - -export interface ProductionCountry { - iso_3166_1: string; - name: string; -} - -export interface SpokenLanguage { - english_name: string; - iso_639_1: string; - name: string; -} - -export interface CreditsResponse { - id: number; - cast: CastMember[]; - crew: CrewMember[]; -} - -export interface CastMember { - adult: boolean; - gender: number; - id: number; - known_for_department: string; - name: string; - original_name: string; - popularity: number; - profile_path?: string; - cast_id: number; - character: string; - credit_id: string; - order: number; -} - -export interface CrewMember { - adult: boolean; - gender: number; - id: number; - known_for_department: string; - name: string; - original_name: string; - popularity: number; - profile_path?: string; - credit_id: string; - department: string; - job: string; -} - -export interface VideosResponse { - id: number; - results: Video[]; -} - -export interface Video { - iso_639_1: string; - iso_3166_1: string; - name: string; - key: string; - site: string; - size: number; - type: string; - official: boolean; - published_at: string; - id: string; -} - -export interface ImagesResponse { - backdrops: Backdrop[]; - id: number; - logos: Logo[]; - posters: Poster[]; -} - -export interface Backdrop { - aspect_ratio: number; - height: number; - iso_639_1?: string; - file_path: string; - vote_average: number; - vote_count: number; - width: number; -} - -export interface Logo { - aspect_ratio: number; - height: number; - iso_639_1: string; - file_path: string; - vote_average: number; - vote_count: number; - width: number; -} - -export interface Poster { - aspect_ratio: number; - height: number; - iso_639_1?: string; - file_path: string; - vote_average: number; - vote_count: number; - width: number; -} - -export interface MultiSearchResponse { - page: number; - results: MultiSearchResult[]; - total_pages: number; - total_results: number; -} - -export interface MultiSearchResult { - adult: boolean; - backdrop_path?: string; - id: number; - title: string; - original_language: string; - original_title: string; - overview: string; - poster_path?: string; - media_type: string; - genre_ids: number[]; - popularity: number; - release_date: string; - video: boolean; - vote_average: number; - vote_count: number; -} - -export interface PopularMoviesResponse { - page: number; - results: PopularMovieResult[]; - total_pages: number; - total_results: number; -} - -export interface PopularMovieResult { - adult: boolean; - backdrop_path: string; - genre_ids: number[]; - id: number; - original_language: string; - original_title: string; - overview: string; - popularity: number; - poster_path: string; - release_date: string; - title: string; - video: boolean; - vote_average: number; - vote_count: number; -} +export const TMDB_SERIES_GENRES = [ + { + id: 10759, + name: 'Action & Adventure' + }, + { + id: 16, + name: 'Animation' + }, + { + id: 35, + name: 'Comedy' + }, + { + id: 80, + name: 'Crime' + }, + { + id: 99, + name: 'Documentary' + }, + { + id: 18, + name: 'Drama' + }, + { + id: 10751, + name: 'Family' + }, + { + id: 10762, + name: 'Kids' + }, + { + id: 9648, + name: 'Mystery' + }, + { + id: 10763, + name: 'News' + }, + { + id: 10764, + name: 'Reality' + }, + { + id: 10765, + name: 'Sci-Fi & Fantasy' + }, + { + id: 10766, + name: 'Soap' + }, + { + id: 10767, + name: 'Talk' + }, + { + id: 10768, + name: 'War & Politics' + }, + { + id: 37, + name: 'Western' + } +]; diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte index 6a5bd9a..6d12a26 100644 --- a/src/lib/components/Button.svelte +++ b/src/lib/components/Button.svelte @@ -16,7 +16,8 @@ 'flex items-center gap-1 rounded-xl font-medium select-none cursor-pointer selectable transition-all flex-shrink-0', { 'bg-white text-zinc-900 font-extrabold backdrop-blur-lg': type === 'primary', - 'hover:bg-amber-400 hover:border-amber-400': type === 'primary' && !disabled, + 'hover:bg-amber-400 focus-within:bg-amber-400 hover:border-amber-400 focus-within:border-amber-400': + type === 'primary' && !disabled, 'text-zinc-200 bg-zinc-400 bg-opacity-20 backdrop-blur-lg': type === 'secondary', 'focus-visible:bg-zinc-200 focus-visible:text-zinc-800 hover:bg-zinc-200 hover:text-zinc-800': (type === 'secondary' || type === 'tertiary') && !disabled, diff --git a/src/lib/components/Card/Card.svelte b/src/lib/components/Card/Card.svelte index 341194b..6b6099a 100644 --- a/src/lib/components/Card/Card.svelte +++ b/src/lib/components/Card/Card.svelte @@ -9,6 +9,7 @@ import ContextMenuItem from '../ContextMenu/ContextMenuItem.svelte'; import type { TitleType } from '$lib/types'; import { openTitleModal } from '../Modal/Modal'; + import ProgressBar from '../ProgressBar.svelte'; export let tmdbId: number; export let jellyfinId: string | undefined = undefined; @@ -53,7 +54,8 @@ diff --git a/src/lib/components/Card/card.ts b/src/lib/components/Card/card.ts index 1031742..cb96b60 100644 --- a/src/lib/components/Card/card.ts +++ b/src/lib/components/Card/card.ts @@ -1,15 +1,28 @@ import type { TmdbMovie2, TmdbSeries2 } from '$lib/apis/tmdb/tmdbApi'; -import { getTmdbMovieBackdrop, getTmdbSeriesBackdrop } from '$lib/apis/tmdb/tmdbApi'; +import { + TMDB_MOVIE_GENRES, + TMDB_SERIES_GENRES, + getTmdbMovieBackdrop, + getTmdbSeriesBackdrop +} from '$lib/apis/tmdb/tmdbApi'; import type { ComponentProps } from 'svelte'; import type Card from './Card.svelte'; export const fetchCardTmdbMovieProps = async (movie: TmdbMovie2): Promise> => { const backdropUri = getTmdbMovieBackdrop(movie.id || 0); + const movieAny = movie as any; + const genres = + movie.genres?.map((g) => g.name || '') || + movieAny?.genre_ids?.map( + (id: number) => TMDB_MOVIE_GENRES.find((g) => g.id === id)?.name || '' + ) || + []; + return { tmdbId: movie.id || 0, title: movie.title || '', - genres: movie.genres?.map((g) => g.name || '') || [], + genres, runtimeMinutes: movie.runtime, backdropUri: (await backdropUri) || '', rating: movie.vote_average || 0 @@ -21,10 +34,18 @@ export const fetchCardTmdbSeriesProps = async ( ): Promise> => { const backdropUri = getTmdbSeriesBackdrop(series.id || 0); + const seriesAny = series as any; + const genres = + series.genres?.map((g) => g.name || '') || + seriesAny?.genre_ids?.map( + (id: number) => TMDB_SERIES_GENRES.find((g) => g.id === id)?.name || '' + ) || + []; + return { tmdbId: series.id || 0, title: series.name || '', - genres: series.genres?.map((g) => g.name || '') || [], + genres, runtimeMinutes: series.episode_run_time?.[0], backdropUri: (await backdropUri) || '', rating: series.vote_average || 0, diff --git a/src/lib/components/EpisodeCard/EpisodeCard.svelte b/src/lib/components/EpisodeCard/EpisodeCard.svelte index bd5d3fe..13bb09f 100644 --- a/src/lib/components/EpisodeCard/EpisodeCard.svelte +++ b/src/lib/components/EpisodeCard/EpisodeCard.svelte @@ -63,7 +63,8 @@ diff --git a/src/lib/components/Modal/DynamicModal.svelte b/src/lib/components/Modal/DynamicModal.svelte index aa2d0e5..cd60d1f 100644 --- a/src/lib/components/Modal/DynamicModal.svelte +++ b/src/lib/components/Modal/DynamicModal.svelte @@ -10,9 +10,9 @@ } } - // onDestroy(() => { - // modalStack.reset(); - // }); + onDestroy(() => { + modalStack.reset(); + }); @@ -33,7 +33,7 @@ {#if modal.group === modal.id}
{/if} @@ -47,7 +47,6 @@ } )} on:click|self={() => modalStack.close(modal.id)} - transition:fade|global={{ duration: 100 }} >
diff --git a/src/lib/components/PeopleCard/PeopleCard.svelte b/src/lib/components/PeopleCard/PeopleCard.svelte index be4ef40..4cba1e1 100644 --- a/src/lib/components/PeopleCard/PeopleCard.svelte +++ b/src/lib/components/PeopleCard/PeopleCard.svelte @@ -12,7 +12,7 @@
@@ -36,60 +36,4 @@ {name}
-
- - diff --git a/src/lib/components/PlayButton.svelte b/src/lib/components/PlayButton.svelte index 24bce28..fa592aa 100644 --- a/src/lib/components/PlayButton.svelte +++ b/src/lib/components/PlayButton.svelte @@ -4,8 +4,14 @@ import classNames from 'classnames'; -
- - - + + +
+
diff --git a/src/lib/components/Poster/Poster.svelte b/src/lib/components/Poster/Poster.svelte index 1a4ba4a..c33f978 100644 --- a/src/lib/components/Poster/Poster.svelte +++ b/src/lib/components/Poster/Poster.svelte @@ -1,14 +1,10 @@