├── config
├── .gitkeep
├── db
│ └── .gitkeep
└── logs
│ └── .gitkeep
├── .github
├── FUNDING.yml
├── CODEOWNERS
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.md
│ └── bug_report.md
├── workflows
│ ├── invalid_template.yml
│ ├── deploy_docs.yml
│ ├── support.yml
│ └── preview.yml
├── stale.yml
└── lock.yml
├── server
├── api
│ └── themoviedb
│ │ └── constants.ts
├── templates
│ └── email
│ │ ├── resetpassword
│ │ └── subject.pug
│ │ ├── test-email
│ │ └── subject.pug
│ │ ├── generatedpassword
│ │ └── subject.pug
│ │ └── media-request
│ │ └── subject.pug
├── constants
│ ├── user.ts
│ ├── server.ts
│ └── media.ts
├── interfaces
│ └── api
│ │ ├── discoverInterfaces.ts
│ │ ├── common.ts
│ │ ├── mediaInterfaces.ts
│ │ ├── personInterfaces.ts
│ │ ├── requestInterfaces.ts
│ │ ├── userInterfaces.ts
│ │ ├── serviceInterfaces.ts
│ │ ├── userSettingsInterfaces.ts
│ │ ├── plexInterfaces.ts
│ │ └── settingsInterfaces.ts
├── tsconfig.json
├── entity
│ ├── Session.ts
│ ├── UserPushSubscription.ts
│ ├── SeasonRequest.ts
│ └── Season.ts
├── utils
│ ├── appDataVolume.ts
│ ├── typeHelpers.ts
│ ├── appVersion.ts
│ └── asyncLock.ts
├── migration
│ ├── 1607928251245-DropImdbIdConstraint.ts
│ └── 1605085519544-SeasonStatus.ts
├── types
│ ├── express.d.ts
│ └── plex-api.d.ts
├── routes
│ ├── collection.ts
│ └── search.ts
├── lib
│ ├── notifications
│ │ ├── agents
│ │ │ └── agent.ts
│ │ └── index.ts
│ ├── email
│ │ └── index.ts
│ ├── cache.ts
│ └── permissions.ts
├── models
│ └── Collection.ts
├── middleware
│ └── auth.ts
└── logger.ts
├── public
├── favicon.ico
├── preview.jpg
├── logo_full.png
├── badge-128x128.png
├── favicon-16x16.png
├── favicon-32x32.png
├── images
│ ├── rotate1.jpg
│ ├── rotate2.jpg
│ ├── rotate3.jpg
│ ├── rotate4.jpg
│ ├── rotate5.jpg
│ ├── rotate6.jpg
│ ├── overseerr_poster_not_found.png
│ ├── overseerr_poster_not_found_logo_top.png
│ └── overseerr_poster_not_found_logo_center.png
├── os_logo_filled.png
├── apple-touch-icon.png
├── clock-icon-192x192.png
├── cog-icon-192x192.png
├── user-icon-192x192.png
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-splash-1125-2436.jpg
├── apple-splash-1136-640.jpg
├── apple-splash-1170-2532.jpg
├── apple-splash-1242-2208.jpg
├── apple-splash-1242-2688.jpg
├── apple-splash-1284-2778.jpg
├── apple-splash-1334-750.jpg
├── apple-splash-1536-2048.jpg
├── apple-splash-1620-2160.jpg
├── apple-splash-1668-2224.jpg
├── apple-splash-1668-2388.jpg
├── apple-splash-1792-828.jpg
├── apple-splash-2048-1536.jpg
├── apple-splash-2048-2732.jpg
├── apple-splash-2160-1620.jpg
├── apple-splash-2208-1242.jpg
├── apple-splash-2224-1668.jpg
├── apple-splash-2388-1668.jpg
├── apple-splash-2436-1125.jpg
├── apple-splash-2532-1170.jpg
├── apple-splash-2688-1242.jpg
├── apple-splash-2732-2048.jpg
├── apple-splash-2778-1284.jpg
├── apple-splash-640-1136.jpg
├── apple-splash-750-1334.jpg
├── apple-splash-828-1792.jpg
├── sparkles-icon-192x192.png
├── android-chrome-192x192_maskable.png
├── android-chrome-512x512_maskable.png
├── os_icon.svg
└── site.webmanifest
├── .gitbook.yaml
├── Dockerfile.local
├── postcss.config.js
├── .prettierignore
├── .stoplight.json
├── src
├── pages
│ ├── search.tsx
│ ├── setup.tsx
│ ├── index.tsx
│ ├── login
│ │ ├── index.tsx
│ │ └── plex
│ │ │ └── loading.tsx
│ ├── profile
│ │ ├── index.tsx
│ │ └── settings
│ │ │ ├── index.tsx
│ │ │ ├── password.tsx
│ │ │ ├── permissions.tsx
│ │ │ ├── main.tsx
│ │ │ └── notifications
│ │ │ ├── email.tsx
│ │ │ ├── discord.tsx
│ │ │ ├── telegram.tsx
│ │ │ └── webpush.tsx
│ ├── tv
│ │ └── [tvId]
│ │ │ ├── cast.tsx
│ │ │ ├── crew.tsx
│ │ │ ├── similar.tsx
│ │ │ ├── recommendations.tsx
│ │ │ └── index.tsx
│ ├── users
│ │ ├── [userId]
│ │ │ ├── index.tsx
│ │ │ ├── requests.tsx
│ │ │ └── settings
│ │ │ │ ├── index.tsx
│ │ │ │ ├── permissions.tsx
│ │ │ │ ├── password.tsx
│ │ │ │ ├── main.tsx
│ │ │ │ └── notifications
│ │ │ │ ├── email.tsx
│ │ │ │ ├── discord.tsx
│ │ │ │ ├── telegram.tsx
│ │ │ │ └── webpush.tsx
│ │ └── index.tsx
│ ├── discover
│ │ ├── trending.tsx
│ │ ├── tv
│ │ │ ├── genres.tsx
│ │ │ ├── index.tsx
│ │ │ ├── upcoming.tsx
│ │ │ ├── genre
│ │ │ │ └── [genreId]
│ │ │ │ │ └── index.tsx
│ │ │ ├── network
│ │ │ │ └── [networkId]
│ │ │ │ │ └── index.tsx
│ │ │ └── language
│ │ │ │ └── [language]
│ │ │ │ └── index.tsx
│ │ └── movies
│ │ │ ├── genres.tsx
│ │ │ ├── upcoming.tsx
│ │ │ ├── index.tsx
│ │ │ ├── genre
│ │ │ └── [genreId]
│ │ │ │ └── index.tsx
│ │ │ ├── studio
│ │ │ └── [studioId]
│ │ │ │ └── index.tsx
│ │ │ └── language
│ │ │ └── [language]
│ │ │ └── index.tsx
│ ├── requests
│ │ └── index.tsx
│ ├── person
│ │ └── [personId]
│ │ │ └── index.tsx
│ ├── movie
│ │ └── [movieId]
│ │ │ ├── cast.tsx
│ │ │ ├── crew.tsx
│ │ │ ├── similar.tsx
│ │ │ ├── recommendations.tsx
│ │ │ └── index.tsx
│ ├── resetpassword
│ │ ├── [guid]
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── settings
│ │ ├── index.tsx
│ │ ├── logs.tsx
│ │ ├── main.tsx
│ │ ├── about.tsx
│ │ ├── plex.tsx
│ │ ├── users.tsx
│ │ ├── jobs.tsx
│ │ ├── jellyfin.tsx
│ │ ├── services.tsx
│ │ └── notifications
│ │ │ ├── email.tsx
│ │ │ ├── discord.tsx
│ │ │ ├── lunasea.tsx
│ │ │ ├── pushover.tsx
│ │ │ ├── slack.tsx
│ │ │ ├── telegram.tsx
│ │ │ ├── webhook.tsx
│ │ │ ├── pushbullet.tsx
│ │ │ └── webpush.tsx
│ ├── _document.tsx
│ ├── collection
│ │ └── [collectionId]
│ │ │ └── index.tsx
│ └── 404.tsx
├── hooks
│ ├── useIsTouch.ts
│ ├── useSettings.ts
│ ├── useLocale.ts
│ ├── useRouteGuard.ts
│ ├── useLockBodyScroll.ts
│ ├── useClickOutside.ts
│ ├── useDebouncedState.ts
│ └── useRequestOverride.ts
├── assets
│ ├── ellipsis.svg
│ ├── spinner.svg
│ ├── extlogos
│ │ ├── lunasea.svg
│ │ ├── pushbullet.svg
│ │ ├── telegram.svg
│ │ ├── slack.svg
│ │ ├── pushover.svg
│ │ └── discord.svg
│ ├── services
│ │ ├── radarr.svg
│ │ ├── plex.svg
│ │ └── imdb.svg
│ ├── rt_fresh.svg
│ ├── rt_rotten.svg
│ └── tmdb_logo.svg
├── types
│ ├── react-intl-auto.d.ts
│ └── custom.d.ts
├── utils
│ ├── numberHelpers.ts
│ ├── creditHelpers.ts
│ ├── typeHelpers.ts
│ └── jellyfin.ts
├── components
│ ├── Layout
│ │ └── Notifications
│ │ │ └── index.tsx
│ ├── TitleCard
│ │ ├── Placeholder.tsx
│ │ └── TmdbTitleCard.tsx
│ ├── Common
│ │ ├── PageTitle
│ │ │ └── index.tsx
│ │ ├── CachedImage
│ │ │ └── index.tsx
│ │ ├── Header
│ │ │ └── index.tsx
│ │ ├── Badge
│ │ │ └── index.tsx
│ │ ├── List
│ │ │ └── index.tsx
│ │ ├── PlayButton
│ │ │ └── index.tsx
│ │ ├── SensitiveInput
│ │ │ └── index.tsx
│ │ ├── ConfirmButton
│ │ │ └── index.tsx
│ │ └── Alert
│ │ │ └── index.tsx
│ ├── ToastContainer
│ │ └── index.tsx
│ ├── JSONEditor
│ │ └── index.tsx
│ ├── Settings
│ │ └── CopyButton.tsx
│ ├── AppDataWarning
│ │ └── index.tsx
│ ├── Discover
│ │ ├── DiscoverTv.tsx
│ │ ├── DiscoverMovies.tsx
│ │ ├── DiscoverTvUpcoming.tsx
│ │ ├── Upcoming.tsx
│ │ ├── Trending.tsx
│ │ ├── TvGenreList
│ │ │ └── index.tsx
│ │ ├── MovieGenreList
│ │ │ └── index.tsx
│ │ ├── TvGenreSlider
│ │ │ └── index.tsx
│ │ ├── DiscoverTvGenre
│ │ │ └── index.tsx
│ │ ├── MovieGenreSlider
│ │ │ └── index.tsx
│ │ └── DiscoverMovieGenre
│ │ │ └── index.tsx
│ ├── CompanyCard
│ │ └── index.tsx
│ ├── Search
│ │ └── index.tsx
│ ├── StatusChacker
│ │ └── index.tsx
│ ├── ServiceWorkerSetup
│ │ └── index.tsx
│ └── PlexLoginButton
│ │ └── index.tsx
├── context
│ ├── InteractionContext.tsx
│ ├── UserContext.tsx
│ └── SettingsContext.tsx
└── i18n
│ └── globalMessages.ts
├── docs
├── using-overseerr
│ └── notifications
│ │ ├── pushbullet.md
│ │ ├── slack.md
│ │ ├── lunasea.md
│ │ ├── pushover.md
│ │ ├── discord.md
│ │ ├── README.md
│ │ ├── webpush.md
│ │ └── telegram.md
├── extending-overseerr
│ ├── fail2ban.md
│ └── third-party.md
├── SUMMARY.md
└── README.md
├── next-env.d.ts
├── .editorconfig
├── docker-compose.yml
├── next.config.js
├── stylelint.config.js
├── .dockerignore
├── .gitattributes
├── babel.config.js
├── .vscode
├── settings.json
└── extensions.json
├── tsconfig.json
├── .gitignore
├── Dockerfile
├── LICENSE
├── ormconfig.js
├── .eslintrc.js
└── tailwind.config.js
/config/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/db/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/logs/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [sct]
2 | patreon: overseerr
3 |
--------------------------------------------------------------------------------
/server/api/themoviedb/constants.ts:
--------------------------------------------------------------------------------
1 | export const ANIME_KEYWORD_ID = 210024;
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/preview.jpg
--------------------------------------------------------------------------------
/public/logo_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/logo_full.png
--------------------------------------------------------------------------------
/server/templates/email/resetpassword/subject.pug:
--------------------------------------------------------------------------------
1 | != `Password Reset [${applicationTitle}]`
2 |
--------------------------------------------------------------------------------
/server/templates/email/test-email/subject.pug:
--------------------------------------------------------------------------------
1 | != `Test Notification [${applicationTitle}]`
2 |
--------------------------------------------------------------------------------
/.gitbook.yaml:
--------------------------------------------------------------------------------
1 | root: ./docs
2 |
3 | structure:
4 | readme: README.md
5 | summary: SUMMARY.md
6 |
--------------------------------------------------------------------------------
/public/badge-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/badge-128x128.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/favicon-32x32.png
--------------------------------------------------------------------------------
/server/templates/email/generatedpassword/subject.pug:
--------------------------------------------------------------------------------
1 | != `Account Information [${applicationTitle}]`
2 |
--------------------------------------------------------------------------------
/public/images/rotate1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/images/rotate1.jpg
--------------------------------------------------------------------------------
/public/images/rotate2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/images/rotate2.jpg
--------------------------------------------------------------------------------
/public/images/rotate3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/images/rotate3.jpg
--------------------------------------------------------------------------------
/public/images/rotate4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/images/rotate4.jpg
--------------------------------------------------------------------------------
/public/images/rotate5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/images/rotate5.jpg
--------------------------------------------------------------------------------
/public/images/rotate6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/images/rotate6.jpg
--------------------------------------------------------------------------------
/public/os_logo_filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/os_logo_filled.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/clock-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/clock-icon-192x192.png
--------------------------------------------------------------------------------
/public/cog-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/cog-icon-192x192.png
--------------------------------------------------------------------------------
/public/user-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/user-icon-192x192.png
--------------------------------------------------------------------------------
/server/constants/user.ts:
--------------------------------------------------------------------------------
1 | export enum UserType {
2 | PLEX = 1,
3 | LOCAL = 2,
4 | JELLYFIN = 3,
5 | }
6 |
--------------------------------------------------------------------------------
/server/templates/email/media-request/subject.pug:
--------------------------------------------------------------------------------
1 | != `${requestType} - ${mediaName} [${applicationTitle}]`
2 |
--------------------------------------------------------------------------------
/Dockerfile.local:
--------------------------------------------------------------------------------
1 | FROM node:14.17-alpine
2 |
3 | COPY . /app
4 | WORKDIR /app
5 |
6 | RUN yarn
7 |
8 | CMD yarn dev
9 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-splash-1125-2436.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1125-2436.jpg
--------------------------------------------------------------------------------
/public/apple-splash-1136-640.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1136-640.jpg
--------------------------------------------------------------------------------
/public/apple-splash-1170-2532.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1170-2532.jpg
--------------------------------------------------------------------------------
/public/apple-splash-1242-2208.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1242-2208.jpg
--------------------------------------------------------------------------------
/public/apple-splash-1242-2688.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1242-2688.jpg
--------------------------------------------------------------------------------
/public/apple-splash-1284-2778.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1284-2778.jpg
--------------------------------------------------------------------------------
/public/apple-splash-1334-750.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1334-750.jpg
--------------------------------------------------------------------------------
/public/apple-splash-1536-2048.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1536-2048.jpg
--------------------------------------------------------------------------------
/public/apple-splash-1620-2160.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1620-2160.jpg
--------------------------------------------------------------------------------
/public/apple-splash-1668-2224.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1668-2224.jpg
--------------------------------------------------------------------------------
/public/apple-splash-1668-2388.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1668-2388.jpg
--------------------------------------------------------------------------------
/public/apple-splash-1792-828.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-1792-828.jpg
--------------------------------------------------------------------------------
/public/apple-splash-2048-1536.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-2048-1536.jpg
--------------------------------------------------------------------------------
/public/apple-splash-2048-2732.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-2048-2732.jpg
--------------------------------------------------------------------------------
/public/apple-splash-2160-1620.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-2160-1620.jpg
--------------------------------------------------------------------------------
/public/apple-splash-2208-1242.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-2208-1242.jpg
--------------------------------------------------------------------------------
/public/apple-splash-2224-1668.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-2224-1668.jpg
--------------------------------------------------------------------------------
/public/apple-splash-2388-1668.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-2388-1668.jpg
--------------------------------------------------------------------------------
/public/apple-splash-2436-1125.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-2436-1125.jpg
--------------------------------------------------------------------------------
/public/apple-splash-2532-1170.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-2532-1170.jpg
--------------------------------------------------------------------------------
/public/apple-splash-2688-1242.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-2688-1242.jpg
--------------------------------------------------------------------------------
/public/apple-splash-2732-2048.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-2732-2048.jpg
--------------------------------------------------------------------------------
/public/apple-splash-2778-1284.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-2778-1284.jpg
--------------------------------------------------------------------------------
/public/apple-splash-640-1136.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-640-1136.jpg
--------------------------------------------------------------------------------
/public/apple-splash-750-1334.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-750-1334.jpg
--------------------------------------------------------------------------------
/public/apple-splash-828-1792.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/apple-splash-828-1792.jpg
--------------------------------------------------------------------------------
/public/sparkles-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/sparkles-icon-192x192.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/server/constants/server.ts:
--------------------------------------------------------------------------------
1 | export enum MediaServerType {
2 | PLEX = 1,
3 | JELLYFIN,
4 | EMBY,
5 | NOT_CONFIGURED,
6 | }
7 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192_maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/android-chrome-192x192_maskable.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512_maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/android-chrome-512x512_maskable.png
--------------------------------------------------------------------------------
/public/images/overseerr_poster_not_found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/images/overseerr_poster_not_found.png
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Generated files which we would not like to format
2 | .next/
3 | dist/
4 | config/
5 |
6 | # assets
7 | src/assets/
8 | public/
9 |
--------------------------------------------------------------------------------
/server/interfaces/api/discoverInterfaces.ts:
--------------------------------------------------------------------------------
1 | export interface GenreSliderItem {
2 | id: number;
3 | name: string;
4 | backdrops: string[];
5 | }
6 |
--------------------------------------------------------------------------------
/public/images/overseerr_poster_not_found_logo_top.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/images/overseerr_poster_not_found_logo_top.png
--------------------------------------------------------------------------------
/public/images/overseerr_poster_not_found_logo_center.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juandjara/jellyseer/HEAD/public/images/overseerr_poster_not_found_logo_center.png
--------------------------------------------------------------------------------
/.stoplight.json:
--------------------------------------------------------------------------------
1 | {
2 | "formats": {
3 | "openapi": {
4 | "rootDir": ".",
5 | "include": ["**"]
6 | }
7 | },
8 | "exclude": ["docs"]
9 | }
10 |
--------------------------------------------------------------------------------
/src/pages/search.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Search from '../components/Search';
3 |
4 | const SearchPage: React.FC = () => {
5 | return ;
6 | };
7 |
8 | export default SearchPage;
9 |
--------------------------------------------------------------------------------
/server/interfaces/api/common.ts:
--------------------------------------------------------------------------------
1 | interface PageInfo {
2 | pages: number;
3 | page: number;
4 | results: number;
5 | pageSize: number;
6 | }
7 |
8 | export interface PaginatedResponse {
9 | pageInfo: PageInfo;
10 | }
11 |
--------------------------------------------------------------------------------
/server/interfaces/api/mediaInterfaces.ts:
--------------------------------------------------------------------------------
1 | import type Media from '../../entity/Media';
2 | import { PaginatedResponse } from './common';
3 |
4 | export interface MediaResultsResponse extends PaginatedResponse {
5 | results: Media[];
6 | }
7 |
--------------------------------------------------------------------------------
/src/pages/setup.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import Setup from '../components/Setup';
4 |
5 | const SetupPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default SetupPage;
10 |
--------------------------------------------------------------------------------
/docs/using-overseerr/notifications/pushbullet.md:
--------------------------------------------------------------------------------
1 | # Pushbullet
2 |
3 | ## Configuration
4 |
5 | ### Access Token
6 |
7 | [Create an access token](https://www.pushbullet.com/#settings) and set it here to grant Overseerr access to the Pushbullet API.
8 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { NextPage } from 'next';
3 | import Discover from '../components/Discover';
4 |
5 | const Index: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default Index;
10 |
--------------------------------------------------------------------------------
/server/interfaces/api/personInterfaces.ts:
--------------------------------------------------------------------------------
1 | import { PersonCreditCast, PersonCreditCrew } from '../../models/Person';
2 |
3 | export interface PersonCombinedCreditsResponse {
4 | id: number;
5 | cast: PersonCreditCast[];
6 | crew: PersonCreditCrew[];
7 | }
8 |
--------------------------------------------------------------------------------
/src/pages/login/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { NextPage } from 'next';
3 | import Login from '../../components/Login';
4 |
5 | const LoginPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default LoginPage;
10 |
--------------------------------------------------------------------------------
/server/interfaces/api/requestInterfaces.ts:
--------------------------------------------------------------------------------
1 | import type { PaginatedResponse } from './common';
2 | import type { MediaRequest } from '../../entity/MediaRequest';
3 |
4 | export interface RequestResultsResponse extends PaginatedResponse {
5 | results: MediaRequest[];
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/useIsTouch.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { InteractionContext } from '../context/InteractionContext';
3 |
4 | export const useIsTouch = (): boolean => {
5 | const { isTouch } = useContext(InteractionContext);
6 | return isTouch;
7 | };
8 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/basic-features/typescript for more information.
7 |
--------------------------------------------------------------------------------
/src/pages/profile/index.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserProfile from '../../components/UserProfile';
4 |
5 | const UserPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default UserPage;
10 |
--------------------------------------------------------------------------------
/src/pages/tv/[tvId]/cast.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import TvCast from '../../../components/TvDetails/TvCast';
4 |
5 | const TvCastPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default TvCastPage;
10 |
--------------------------------------------------------------------------------
/src/pages/tv/[tvId]/crew.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import TvCrew from '../../../components/TvDetails/TvCrew';
4 |
5 | const TvCrewPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default TvCrewPage;
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | indent_style = space
10 | indent_size = 2
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "ES2019",
5 | "lib": ["ES2019"],
6 | "module": "commonjs",
7 | "outDir": "../dist",
8 | "noEmit": false
9 | },
10 | "include": ["**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | overseerr:
4 | build:
5 | context: .
6 | dockerfile: Dockerfile.local
7 | ports:
8 | - 5055:5055
9 | volumes:
10 | - .:/app:rw,cached
11 | - /app/node_modules
12 | - /app/.next
13 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/index.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserProfile from '../../../components/UserProfile';
4 |
5 | const UserPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default UserPage;
10 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Global code ownership
2 | * @sct
3 |
4 | # Documentation
5 | docs/ @TheCatLady @samwiseg0
6 |
7 | # Snap-related files
8 | .github/workflows/snap.yaml @samwiseg0
9 | snap/ @samwiseg0
10 |
11 | # i18n locale files
12 | src/i18n/locale/ @sct @TheCatLady
13 |
--------------------------------------------------------------------------------
/src/pages/discover/trending.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { NextPage } from 'next';
3 | import Trending from '../../components/Discover/Trending';
4 |
5 | const TrendingPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default TrendingPage;
10 |
--------------------------------------------------------------------------------
/src/pages/requests/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { NextPage } from 'next';
3 | import RequestList from '../../components/RequestList';
4 |
5 | const RequestsPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default RequestsPage;
10 |
--------------------------------------------------------------------------------
/src/pages/person/[personId]/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import PersonDetails from '../../../components/PersonDetails';
4 |
5 | const MoviePage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default MoviePage;
10 |
--------------------------------------------------------------------------------
/src/pages/tv/[tvId]/similar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import TvSimilar from '../../../components/TvDetails/TvSimilar';
4 |
5 | const TvSimilarPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default TvSimilarPage;
10 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/genres.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import TvGenreList from '../../../components/Discover/TvGenreList';
4 |
5 | const TvGenresPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default TvGenresPage;
10 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import DiscoverTv from '../../../components/Discover/DiscoverTv';
4 |
5 | const DiscoverTvPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default DiscoverTvPage;
10 |
--------------------------------------------------------------------------------
/src/pages/movie/[movieId]/cast.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import MovieCast from '../../../components/MovieDetails/MovieCast';
4 |
5 | const MovieCastPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default MovieCastPage;
10 |
--------------------------------------------------------------------------------
/src/pages/movie/[movieId]/crew.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import MovieCrew from '../../../components/MovieDetails/MovieCrew';
4 |
5 | const MovieCrewPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default MovieCrewPage;
10 |
--------------------------------------------------------------------------------
/src/assets/ellipsis.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | #### Description
2 |
3 | #### Screenshot (if UI-related)
4 |
5 | #### To-Dos
6 |
7 | - [ ] Successful build `yarn build`
8 | - [ ] Translation keys `yarn i18n:extract`
9 | - [ ] Database migration (if required)
10 |
11 | #### Issues Fixed or Closed
12 |
13 | - Fixes #XXXX
14 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/genres.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import MovieGenreList from '../../../components/Discover/MovieGenreList';
4 |
5 | const MovieGenresPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default MovieGenresPage;
10 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/upcoming.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import UpcomingMovies from '../../../components/Discover/Upcoming';
4 |
5 | const UpcomingMoviesPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default UpcomingMoviesPage;
10 |
--------------------------------------------------------------------------------
/src/pages/login/plex/loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LoadingSpinner from '../../../components/Common/LoadingSpinner';
3 |
4 | const PlexLoading: React.FC = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
12 | export default PlexLoading;
13 |
--------------------------------------------------------------------------------
/src/pages/movie/[movieId]/similar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import MovieSimilar from '../../../components/MovieDetails/MovieSimilar';
4 |
5 | const MovieSimilarPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default MovieSimilarPage;
10 |
--------------------------------------------------------------------------------
/src/pages/resetpassword/[guid]/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { NextPage } from 'next';
3 | import ResetPassword from '../../../components/ResetPassword';
4 |
5 | const ResetPasswordPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default ResetPasswordPage;
10 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import DiscoverMovies from '../../../components/Discover/DiscoverMovies';
4 |
5 | const DiscoverMoviesPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default DiscoverMoviesPage;
10 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/upcoming.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import DiscoverTvUpcoming from '../../../components/Discover/DiscoverTvUpcoming';
4 |
5 | const DiscoverTvPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default DiscoverTvPage;
10 |
--------------------------------------------------------------------------------
/src/pages/resetpassword/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { NextPage } from 'next';
3 | import RequestResetLink from '../../components/ResetPassword/RequestResetLink';
4 |
5 | const RequestResetLinkPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default RequestResetLinkPage;
10 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/genre/[genreId]/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import DiscoverTvGenre from '../../../../../components/Discover/DiscoverTvGenre';
4 |
5 | const DiscoverTvGenrePage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default DiscoverTvGenrePage;
10 |
--------------------------------------------------------------------------------
/src/pages/tv/[tvId]/recommendations.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import TvRecommendations from '../../../components/TvDetails/TvRecommendations';
4 |
5 | const TvRecommendationsPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default TvRecommendationsPage;
10 |
--------------------------------------------------------------------------------
/server/constants/media.ts:
--------------------------------------------------------------------------------
1 | export enum MediaRequestStatus {
2 | PENDING = 1,
3 | APPROVED,
4 | DECLINED,
5 | }
6 |
7 | export enum MediaType {
8 | MOVIE = 'movie',
9 | TV = 'tv',
10 | }
11 |
12 | export enum MediaStatus {
13 | UNKNOWN = 1,
14 | PENDING,
15 | PROCESSING,
16 | PARTIALLY_AVAILABLE,
17 | AVAILABLE,
18 | }
19 |
--------------------------------------------------------------------------------
/src/types/react-intl-auto.d.ts:
--------------------------------------------------------------------------------
1 | import { MessageDescriptor } from 'react-intl';
2 |
3 | declare module 'react-intl' {
4 | interface ExtractableMessage {
5 | [key: string]: string;
6 | }
7 |
8 | export function defineMessages(
9 | messages: T
10 | ): { [K in keyof T]: MessageDescriptor };
11 | }
12 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/network/[networkId]/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import DiscoverNetwork from '../../../../../components/Discover/DiscoverNetwork';
4 |
5 | const DiscoverTvNetworkPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default DiscoverTvNetworkPage;
10 |
--------------------------------------------------------------------------------
/src/hooks/useSettings.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import {
3 | SettingsContext,
4 | SettingsContextProps,
5 | } from '../context/SettingsContext';
6 |
7 | const useSettings = (): SettingsContextProps => {
8 | const settings = useContext(SettingsContext);
9 |
10 | return settings;
11 | };
12 |
13 | export default useSettings;
14 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/genre/[genreId]/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import DiscoverMovieGenre from '../../../../../components/Discover/DiscoverMovieGenre';
4 |
5 | const DiscoverMoviesGenrePage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default DiscoverMoviesGenrePage;
10 |
--------------------------------------------------------------------------------
/src/pages/discover/tv/language/[language]/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import DiscoverTvLanguage from '../../../../../components/Discover/DiscoverTvLanguage';
4 |
5 | const DiscoverTvLanguagePage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default DiscoverTvLanguagePage;
10 |
--------------------------------------------------------------------------------
/src/pages/movie/[movieId]/recommendations.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import MovieRecommendations from '../../../components/MovieDetails/MovieRecommendations';
4 |
5 | const MovieRecommendationsPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default MovieRecommendationsPage;
10 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/studio/[studioId]/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import DiscoverMovieStudio from '../../../../../components/Discover/DiscoverStudio';
4 |
5 | const DiscoverMoviesStudioPage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default DiscoverMoviesStudioPage;
10 |
--------------------------------------------------------------------------------
/src/hooks/useLocale.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import {
3 | LanguageContext,
4 | LanguageContextProps,
5 | } from '../context/LanguageContext';
6 |
7 | const useLocale = (): Omit => {
8 | const languageContext = useContext(LanguageContext);
9 |
10 | return languageContext;
11 | };
12 |
13 | export default useLocale;
14 |
--------------------------------------------------------------------------------
/src/pages/discover/movies/language/[language]/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import DiscoverMovieLanguage from '../../../../../components/Discover/DiscoverMovieLanguage';
4 |
5 | const DiscoverMovieLanguagePage: NextPage = () => {
6 | return ;
7 | };
8 |
9 | export default DiscoverMovieLanguagePage;
10 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: '/'
5 | schedule:
6 | interval: daily
7 | time: '20:00'
8 | open-pull-requests-limit: 10
9 | - package-ecosystem: github-actions
10 | directory: '/'
11 | schedule:
12 | interval: daily
13 | time: '20:00'
14 | open-pull-requests-limit: 10
15 |
--------------------------------------------------------------------------------
/src/assets/spinner.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | commitTag: process.env.COMMIT_TAG || 'local',
4 | },
5 | images: {
6 | domains: ['image.tmdb.org'],
7 | },
8 | webpack(config) {
9 | config.module.rules.push({
10 | test: /\.svg$/,
11 | issuer: /\.(js|ts)x?$/,
12 | use: ['@svgr/webpack'],
13 | });
14 |
15 | return config;
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Support via Discord
4 | url: https://discord.gg/overseerr
5 | about: Chat with users and devs on support and setup related topics.
6 | - name: Support via GitHub Discussions
7 | url: https://github.com/sct/overseerr/discussions
8 | about: Ask questions and discuss with other community members
9 |
--------------------------------------------------------------------------------
/server/entity/Session.ts:
--------------------------------------------------------------------------------
1 | import { ISession } from 'connect-typeorm';
2 | import { Index, Column, PrimaryColumn, Entity } from 'typeorm';
3 |
4 | @Entity()
5 | export class Session implements ISession {
6 | @Index()
7 | @Column('bigint')
8 | public expiredAt = Date.now();
9 |
10 | @PrimaryColumn('varchar', { length: 255 })
11 | public id = '';
12 |
13 | @Column('text')
14 | public json = '';
15 | }
16 |
--------------------------------------------------------------------------------
/docs/using-overseerr/notifications/slack.md:
--------------------------------------------------------------------------------
1 | # Slack
2 |
3 | ## Configuration
4 |
5 | ### Webhook URL
6 |
7 | Simply [create a webhook](https://my.slack.com/services/new/incoming-webhook/) and enter the URL in this field.
8 |
9 | {% hint style="info" %}
10 | Please refer to the [Slack API documentation](https://api.slack.com/messaging/webhooks) for more details on configuring these notifications.
11 | {% endhint %}
12 |
--------------------------------------------------------------------------------
/src/pages/users/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { NextPage } from 'next';
3 | import UserList from '../../components/UserList';
4 | import useRouteGuard from '../../hooks/useRouteGuard';
5 | import { Permission } from '../../hooks/useUser';
6 |
7 | const UsersPage: NextPage = () => {
8 | useRouteGuard(Permission.MANAGE_USERS);
9 | return ;
10 | };
11 |
12 | export default UsersPage;
13 |
--------------------------------------------------------------------------------
/src/utils/numberHelpers.ts:
--------------------------------------------------------------------------------
1 | export const formatBytes = (bytes: number, decimals = 2): string => {
2 | if (bytes === 0) return '0 Bytes';
3 |
4 | const k = 1024;
5 | const dm = decimals < 0 ? 0 : decimals;
6 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
7 |
8 | const i = Math.floor(Math.log(bytes) / Math.log(k));
9 |
10 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
11 | };
12 |
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ignoreFiles: ['**/*.js'],
3 | rules: {
4 | 'at-rule-no-unknown': [
5 | true,
6 | {
7 | ignoreAtRules: [
8 | 'tailwind',
9 | 'apply',
10 | 'variants',
11 | 'responsive',
12 | 'screen',
13 | ],
14 | },
15 | ],
16 | 'declaration-block-trailing-semicolon': null,
17 | 'no-descending-specificity': null,
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/*.md
2 | **/.gitkeep
3 | **/.vscode
4 | .all-contributorsrc
5 | .dockerignore
6 | .editorconfig
7 | .eslintrc.js
8 | .git
9 | .gitbook.yaml
10 | .gitconfig
11 | .github
12 | .gitignore
13 | .next
14 | .prettierignore
15 | config/db/*
16 | config/logs/*
17 | config/*.json
18 | dist
19 | Dockerfile*
20 | docker-compose.yml
21 | docs
22 | LICENSE
23 | node_modules
24 | public/os_logo_filled.png
25 | public/preview.jpg
26 | snap
27 | stylelint.config.js
28 |
--------------------------------------------------------------------------------
/server/utils/appDataVolume.ts:
--------------------------------------------------------------------------------
1 | import { existsSync } from 'fs';
2 | import path from 'path';
3 |
4 | const CONFIG_PATH = process.env.CONFIG_DIRECTORY
5 | ? process.env.CONFIG_DIRECTORY
6 | : path.join(__dirname, '../../config');
7 |
8 | const DOCKER_PATH = `${CONFIG_PATH}/DOCKER`;
9 |
10 | export const appDataStatus = (): boolean => {
11 | return !existsSync(DOCKER_PATH);
12 | };
13 |
14 | export const appDataPath = (): string => {
15 | return CONFIG_PATH;
16 | };
17 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 |
3 | #
4 | ## These files are binary and should be left untouched
5 | #
6 |
7 | # (binary is a macro for -text -diff)
8 | *.png binary
9 | *.jpg binary
10 | *.jpeg binary
11 | *.gif binary
12 | *.ico binary
13 | *.mov binary
14 | *.mp4 binary
15 | *.mp3 binary
16 | *.flv binary
17 | *.fla binary
18 | *.swf binary
19 | *.gz binary
20 | *.zip binary
21 | *.7z binary
22 | *.ttf binary
23 | *.eot binary
24 | *.woff binary
25 | *.pyc binary
26 | *.pdf binary
27 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/index.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../components/UserProfile/UserSettings';
4 | import UserGeneralSettings from '../../../components/UserProfile/UserSettings/UserGeneralSettings';
5 |
6 | const UserSettingsPage: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default UserSettingsPage;
15 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/password.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../components/UserProfile/UserSettings';
4 | import UserPasswordChange from '../../../components/UserProfile/UserSettings/UserPasswordChange';
5 |
6 | const UserPassswordPage: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default UserPassswordPage;
15 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/permissions.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../components/UserProfile/UserSettings';
4 | import UserPermissions from '../../../components/UserProfile/UserSettings/UserPermissions';
5 |
6 | const UserPermissionsPage: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default UserPermissionsPage;
15 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 |
4 | return {
5 | presets: [
6 | [
7 | 'next/babel',
8 | {
9 | 'preset-env': {
10 | useBuiltIns: 'entry',
11 | corejs: '3',
12 | },
13 | },
14 | ],
15 | ],
16 | plugins: [
17 | [
18 | 'react-intl-auto',
19 | {
20 | removePrefix: 'src/',
21 | },
22 | ],
23 | ],
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/main.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../components/UserProfile/UserSettings';
4 | import UserGeneralSettings from '../../../components/UserProfile/UserSettings/UserGeneralSettings';
5 |
6 | const UserSettingsMainPage: NextPage = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default UserSettingsMainPage;
15 |
--------------------------------------------------------------------------------
/src/components/Layout/Notifications/index.tsx:
--------------------------------------------------------------------------------
1 | import { BellIcon } from '@heroicons/react/outline';
2 | import React from 'react';
3 |
4 | const Notifications: React.FC = () => {
5 | return (
6 |
12 | );
13 | };
14 |
15 | export default Notifications;
16 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/requests.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import RequestList from '../../../components/RequestList';
4 | import useRouteGuard from '../../../hooks/useRouteGuard';
5 | import { Permission } from '../../../hooks/useUser';
6 |
7 | const UserRequestsPage: NextPage = () => {
8 | useRouteGuard([Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], {
9 | type: 'or',
10 | });
11 | return ;
12 | };
13 |
14 | export default UserRequestsPage;
15 |
--------------------------------------------------------------------------------
/src/assets/extlogos/lunasea.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/TitleCard/Placeholder.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface PlaceholderProps {
4 | canExpand?: boolean;
5 | }
6 |
7 | const Placeholder: React.FC = ({ canExpand = false }) => {
8 | return (
9 |
16 | );
17 | };
18 |
19 | export default Placeholder;
20 |
--------------------------------------------------------------------------------
/server/utils/typeHelpers.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | TmdbMovieResult,
3 | TmdbTvResult,
4 | TmdbPersonResult,
5 | } from '../api/themoviedb/interfaces';
6 |
7 | export const isMovie = (
8 | movie: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
9 | ): movie is TmdbMovieResult => {
10 | return (movie as TmdbMovieResult).title !== undefined;
11 | };
12 |
13 | export const isPerson = (
14 | person: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
15 | ): person is TmdbPersonResult => {
16 | return (person as TmdbPersonResult).known_for !== undefined;
17 | };
18 |
--------------------------------------------------------------------------------
/src/assets/extlogos/pushbullet.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/context/InteractionContext.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useInteraction from '../hooks/useInteraction';
3 |
4 | interface InteractionContextProps {
5 | isTouch: boolean;
6 | }
7 |
8 | export const InteractionContext = React.createContext({
9 | isTouch: false,
10 | });
11 |
12 | export const InteractionProvider: React.FC = ({ children }) => {
13 | const isTouch = useInteraction();
14 |
15 | return (
16 |
17 | {children}
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/pages/settings/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import SettingsLayout from '../../components/Settings/SettingsLayout';
4 | import SettingsMain from '../../components/Settings/SettingsMain';
5 | import useRouteGuard from '../../hooks/useRouteGuard';
6 | import { Permission } from '../../hooks/useUser';
7 |
8 | const SettingsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_SETTINGS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default SettingsPage;
18 |
--------------------------------------------------------------------------------
/.github/workflows/invalid_template.yml:
--------------------------------------------------------------------------------
1 | name: 'Invalid Template'
2 |
3 | on:
4 | issues:
5 | types: [labeled, unlabeled, reopened]
6 |
7 | jobs:
8 | support:
9 | runs-on: ubuntu-20.04
10 | steps:
11 | - uses: dessant/support-requests@v2.0.1
12 | with:
13 | github-token: ${{ github.token }}
14 | support-label: 'invalid:template-incomplete'
15 | issue-comment: >
16 | :wave: @{issue-author}, please follow the template provided.
17 | close-issue: true
18 | lock-issue: true
19 | issue-lock-reason: 'resolved'
20 |
--------------------------------------------------------------------------------
/src/pages/settings/logs.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import SettingsLayout from '../../components/Settings/SettingsLayout';
4 | import SettingsLogs from '../../components/Settings/SettingsLogs';
5 | import useRouteGuard from '../../hooks/useRouteGuard';
6 | import { Permission } from '../../hooks/useUser';
7 |
8 | const SettingsLogsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_SETTINGS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default SettingsLogsPage;
18 |
--------------------------------------------------------------------------------
/src/pages/settings/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import SettingsLayout from '../../components/Settings/SettingsLayout';
4 | import SettingsMain from '../../components/Settings/SettingsMain';
5 | import { Permission } from '../../hooks/useUser';
6 | import useRouteGuard from '../../hooks/useRouteGuard';
7 |
8 | const SettingsMainPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_SETTINGS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default SettingsMainPage;
18 |
--------------------------------------------------------------------------------
/src/pages/settings/about.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import SettingsAbout from '../../components/Settings/SettingsAbout';
4 | import SettingsLayout from '../../components/Settings/SettingsLayout';
5 | import useRouteGuard from '../../hooks/useRouteGuard';
6 | import { Permission } from '../../hooks/useUser';
7 |
8 | const SettingsAboutPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_SETTINGS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default SettingsAboutPage;
18 |
--------------------------------------------------------------------------------
/src/pages/settings/plex.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { NextPage } from 'next';
3 | import SettingsLayout from '../../components/Settings/SettingsLayout';
4 | import SettingsPlex from '../../components/Settings/SettingsPlex';
5 | import { Permission } from '../../hooks/useUser';
6 | import useRouteGuard from '../../hooks/useRouteGuard';
7 |
8 | const PlexSettingsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_SETTINGS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default PlexSettingsPage;
18 |
--------------------------------------------------------------------------------
/src/pages/settings/users.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NextPage } from 'next';
3 | import SettingsLayout from '../../components/Settings/SettingsLayout';
4 | import SettingsUsers from '../../components/Settings/SettingsUsers';
5 | import { Permission } from '../../hooks/useUser';
6 | import useRouteGuard from '../../hooks/useRouteGuard';
7 |
8 | const SettingsUsersPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_SETTINGS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default SettingsUsersPage;
18 |
--------------------------------------------------------------------------------
/src/assets/extlogos/telegram.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/settings/jobs.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { NextPage } from 'next';
3 | import SettingsLayout from '../../components/Settings/SettingsLayout';
4 | import SettingsJobs from '../../components/Settings/SettingsJobsCache';
5 | import { Permission } from '../../hooks/useUser';
6 | import useRouteGuard from '../../hooks/useRouteGuard';
7 |
8 | const SettingsMainPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_SETTINGS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default SettingsMainPage;
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: 'awaiting-triage, type:enhancement'
6 | assignees: ''
7 | ---
8 |
9 | #### Description
10 |
11 | Is your feature request related to a problem? If so, please provide a clear and concise description of the problem. E.g., "I'm always frustrated when [...]."
12 |
13 | #### Desired Behavior
14 |
15 | Provide a clear and concise description of what you want to happen.
16 |
17 | #### Additional Context
18 |
19 | Provide any additional information or screenshots that may be relevant or helpful.
20 |
--------------------------------------------------------------------------------
/src/pages/settings/jellyfin.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { NextPage } from 'next';
3 | import SettingsLayout from '../../components/Settings/SettingsLayout';
4 | import SettingsJellyfin from '../../components/Settings/SettingsJellyfin';
5 | import { Permission } from '../../hooks/useUser';
6 | import useRouteGuard from '../../hooks/useRouteGuard';
7 |
8 | const JellyfinSettingsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_SETTINGS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default JellyfinSettingsPage;
18 |
--------------------------------------------------------------------------------
/src/pages/settings/services.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { NextPage } from 'next';
3 | import SettingsLayout from '../../components/Settings/SettingsLayout';
4 | import SettingsServices from '../../components/Settings/SettingsServices';
5 | import { Permission } from '../../hooks/useUser';
6 | import useRouteGuard from '../../hooks/useRouteGuard';
7 |
8 | const ServicesSettingsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_SETTINGS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default ServicesSettingsPage;
18 |
--------------------------------------------------------------------------------
/src/hooks/useRouteGuard.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { useEffect } from 'react';
3 | import { Permission, PermissionCheckOptions, useUser } from './useUser';
4 |
5 | const useRouteGuard = (
6 | permission: Permission | Permission[],
7 | options?: PermissionCheckOptions
8 | ): void => {
9 | const router = useRouter();
10 | const { user, hasPermission } = useUser();
11 |
12 | useEffect(() => {
13 | if (user && !hasPermission(permission, options)) {
14 | router.push('/');
15 | }
16 | }, [user, permission, router, hasPermission, options]);
17 | };
18 |
19 | export default useRouteGuard;
20 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.enable": true,
3 | "eslint.validate": [
4 | "javascript",
5 | "javascriptreact",
6 | "typescript",
7 | "typescriptreact"
8 | ],
9 | "typescript.tsdk": "node_modules/typescript/lib",
10 | "sqltools.connections": [
11 | {
12 | "previewLimit": 50,
13 | "driver": "SQLite",
14 | "name": "Local SQLite",
15 | "database": "./config/db/db.sqlite3"
16 | }
17 | ],
18 | "i18n-ally.localesPaths": ["src/i18n", "src/i18n/locale"],
19 | "editor.codeActionsOnSave": {
20 | "source.organizeImports": true
21 | },
22 | "editor.formatOnSave": true
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Common/PageTitle/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useSettings from '../../../hooks/useSettings';
3 | import Head from 'next/head';
4 |
5 | interface PageTitleProps {
6 | title: string | (string | undefined)[];
7 | }
8 |
9 | const PageTitle: React.FC = ({ title }) => {
10 | const settings = useSettings();
11 |
12 | return (
13 |
14 |
15 | {Array.isArray(title) ? title.filter(Boolean).join(' - ') : title} -{' '}
16 | {settings.currentSettings.applicationTitle}
17 |
18 |
19 | );
20 | };
21 |
22 | export default PageTitle;
23 |
--------------------------------------------------------------------------------
/src/components/ToastContainer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ToastContainerProps } from 'react-toast-notifications';
3 |
4 | const ToastContainer: React.FC = ({
5 | hasToasts,
6 | ...props
7 | }) => {
8 | return (
9 |
19 | );
20 | };
21 |
22 | export default ToastContainer;
23 |
--------------------------------------------------------------------------------
/.github/workflows/deploy_docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy API Docs
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Generate Swagger UI
14 | uses: Legion2/swagger-ui-action@v1.1.2
15 | with:
16 | output: swagger-ui
17 | spec-file: overseerr-api.yml
18 | - name: Deploy to GitHub Pages
19 | uses: peaceiris/actions-gh-pages@v3.8.0
20 | with:
21 | github_token: ${{ secrets.GITHUB_TOKEN }}
22 | publish_dir: swagger-ui
23 | cname: api-docs.overseerr.dev
24 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/index.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../components/UserProfile/UserSettings';
4 | import UserGeneralSettings from '../../../../components/UserProfile/UserSettings/UserGeneralSettings';
5 | import useRouteGuard from '../../../../hooks/useRouteGuard';
6 | import { Permission } from '../../../../hooks/useUser';
7 |
8 | const UserSettingsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_USERS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default UserSettingsPage;
18 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/permissions.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../components/UserProfile/UserSettings';
4 | import UserPermissions from '../../../../components/UserProfile/UserSettings/UserPermissions';
5 | import useRouteGuard from '../../../../hooks/useRouteGuard';
6 | import { Permission } from '../../../../hooks/useUser';
7 |
8 | const UserPermissionsPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_USERS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default UserPermissionsPage;
18 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/password.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../components/UserProfile/UserSettings';
4 | import UserPasswordChange from '../../../../components/UserProfile/UserSettings/UserPasswordChange';
5 | import useRouteGuard from '../../../../hooks/useRouteGuard';
6 | import { Permission } from '../../../../hooks/useUser';
7 |
8 | const UserPassswordPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_USERS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default UserPassswordPage;
18 |
--------------------------------------------------------------------------------
/server/interfaces/api/userInterfaces.ts:
--------------------------------------------------------------------------------
1 | import { MediaRequest } from '../../entity/MediaRequest';
2 | import type { User } from '../../entity/User';
3 | import { PaginatedResponse } from './common';
4 |
5 | export interface UserResultsResponse extends PaginatedResponse {
6 | results: User[];
7 | }
8 |
9 | export interface UserRequestsResponse extends PaginatedResponse {
10 | results: MediaRequest[];
11 | }
12 |
13 | export interface QuotaStatus {
14 | days?: number;
15 | limit?: number;
16 | used: number;
17 | remaining?: number;
18 | restricted: boolean;
19 | }
20 |
21 | export interface QuotaResponse {
22 | movie: QuotaStatus;
23 | tv: QuotaStatus;
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/main.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../components/UserProfile/UserSettings';
4 | import UserGeneralSettings from '../../../../components/UserProfile/UserSettings/UserGeneralSettings';
5 | import useRouteGuard from '../../../../hooks/useRouteGuard';
6 | import { Permission } from '../../../../hooks/useUser';
7 |
8 | const UserSettingsMainPage: NextPage = () => {
9 | useRouteGuard(Permission.MANAGE_USERS);
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default UserSettingsMainPage;
18 |
--------------------------------------------------------------------------------
/src/utils/creditHelpers.ts:
--------------------------------------------------------------------------------
1 | import { Crew } from '../../server/models/common';
2 | const priorityJobs = [
3 | 'Director',
4 | 'Creator',
5 | 'Screenplay',
6 | 'Writer',
7 | 'Composer',
8 | 'Editor',
9 | 'Producer',
10 | 'Co-Producer',
11 | 'Executive Producer',
12 | 'Animation',
13 | ];
14 |
15 | export const sortCrewPriority = (crew: Crew[]): Crew[] => {
16 | return crew
17 | .filter((person) => priorityJobs.includes(person.job))
18 | .sort((a, b) => {
19 | const aScore = priorityJobs.findIndex((job) => job.includes(a.job));
20 | const bScore = priorityJobs.findIndex((job) => job.includes(b.job));
21 |
22 | return aScore - bScore;
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/docs/using-overseerr/notifications/lunasea.md:
--------------------------------------------------------------------------------
1 | # LunaSea
2 |
3 | ## Configuration
4 |
5 | ### Webhook URL
6 |
7 | Copy either a device- or user-based webhook URL from the LunaSea app into this field.
8 |
9 | ### Profile Name (optional)
10 |
11 | If not using the `default` profile in the LunaSea app, specify the name of the profile here.
12 |
13 | Note that the entered profile name **_must_** match the name in LunaSea exactly (including any capitalization, punctuation, and/or whitespace).
14 |
15 | {% hint style="info" %}
16 | Please refer to the [LunaSea documentation](https://docs.lunasea.app/lunasea/notifications/overseerr) for more details on configuring these notifications.
17 | {% endhint %}
18 |
--------------------------------------------------------------------------------
/src/components/Common/CachedImage/index.tsx:
--------------------------------------------------------------------------------
1 | import Image, { ImageProps } from 'next/image';
2 | import React from 'react';
3 | import useSettings from '../../../hooks/useSettings';
4 |
5 | /**
6 | * The CachedImage component should be used wherever
7 | * we want to offer the option to locally cache images.
8 | *
9 | * It uses the `next/image` Image component but overrides
10 | * the `unoptimized` prop based on the application setting `cacheImages`.
11 | **/
12 | const CachedImage: React.FC = (props) => {
13 | const { currentSettings } = useSettings();
14 |
15 | return ;
16 | };
17 |
18 | export default CachedImage;
19 |
--------------------------------------------------------------------------------
/docs/extending-overseerr/fail2ban.md:
--------------------------------------------------------------------------------
1 | # Fail2ban Filter
2 |
3 | {% hint style="warning" %}
4 | If you are running Overseerr behind a reverse proxy, make sure that the **Enable Proxy Support** setting is **enabled**.
5 | {% endhint %}
6 |
7 | To use Fail2ban with Overseerr, create a new file named `overseerr.local` in your Fail2ban `filter.d` directory with the following filter definition:
8 |
9 | ```
10 | [Definition]
11 | failregex = .*\[info\]\[Auth\]\: Failed sign-in attempt.*"ip":""
12 | ```
13 |
14 | You can then add a jail using this filter in `jail.local`. Please see the [Fail2ban documetation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jails) for details on how to configure the jail.
15 |
--------------------------------------------------------------------------------
/server/entity/UserPushSubscription.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
2 | import { User } from './User';
3 |
4 | @Entity()
5 | export class UserPushSubscription {
6 | @PrimaryGeneratedColumn()
7 | public id: number;
8 |
9 | @ManyToOne(() => User, (user) => user.pushSubscriptions, {
10 | eager: true,
11 | onDelete: 'CASCADE',
12 | })
13 | public user: User;
14 |
15 | @Column()
16 | public endpoint: string;
17 |
18 | @Column()
19 | public p256dh: string;
20 |
21 | @Column({ unique: true })
22 | public auth: string;
23 |
24 | constructor(init?: Partial) {
25 | Object.assign(this, init);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/server/migration/1607928251245-DropImdbIdConstraint.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner, TableUnique } from 'typeorm';
2 |
3 | export class DropImdbIdConstraint1607928251245 implements MigrationInterface {
4 | public async up(queryRunner: QueryRunner): Promise {
5 | await queryRunner.dropUniqueConstraint(
6 | 'media',
7 | 'UQ_7ff2d11f6a83cb52386eaebe74b'
8 | );
9 | }
10 |
11 | public async down(queryRunner: QueryRunner): Promise {
12 | await queryRunner.createUniqueConstraint(
13 | 'media',
14 | new TableUnique({
15 | name: 'UQ_7ff2d11f6a83cb52386eaebe74b',
16 | columnNames: ['imdbId'],
17 | })
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/types/custom.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | declare module '*.svg' {
3 | const content: any;
4 | export default content;
5 | }
6 |
7 | declare module '*.jpg' {
8 | const content: any;
9 | export default content;
10 | }
11 | declare module '*.jpeg' {
12 | const content: any;
13 | export default content;
14 | }
15 |
16 | declare module '*.gif' {
17 | const content: any;
18 | export default content;
19 | }
20 |
21 | declare module '*.png' {
22 | const content: any;
23 | export default content;
24 | }
25 |
26 | declare module '*.css' {
27 | interface IClassNames {
28 | [className: string]: string;
29 | }
30 | const classNames: IClassNames;
31 | export = classNames;
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/typeHelpers.ts:
--------------------------------------------------------------------------------
1 | export type Undefinable = T | undefined;
2 | export type Nullable = T | null;
3 | export type Maybe = T | null | undefined;
4 |
5 | /**
6 | * Helps type objects with an arbitrary number of properties that are
7 | * usually being defined at export.
8 | *
9 | * @param component Main object you want to apply properties to
10 | * @param properties Object of properties you want to type on the main component
11 | */
12 | export function withProperties(component: A, properties: B): A & B {
13 | (Object.keys(properties) as (keyof B)[]).forEach((key) => {
14 | Object.assign(component, { [key]: properties[key] });
15 | });
16 | return component as A & B;
17 | }
18 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/notifications/email.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../components/UserProfile/UserSettings';
4 | import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings';
5 | import UserNotificationsEmail from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail';
6 |
7 | const NotificationsPage: NextPage = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default NotificationsPage;
18 |
--------------------------------------------------------------------------------
/src/assets/services/radarr.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/notifications/discord.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../components/UserProfile/UserSettings';
4 | import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings';
5 | import UserNotificationsDiscord from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord';
6 |
7 | const NotificationsPage: NextPage = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default NotificationsPage;
18 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/notifications/telegram.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../components/UserProfile/UserSettings';
4 | import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings';
5 | import UserNotificationsTelegram from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram';
6 |
7 | const NotificationsPage: NextPage = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default NotificationsPage;
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "strictPropertyInitialization": false,
17 | "experimentalDecorators": true,
18 | "emitDecoratorMetadata": true,
19 | "useUnknownInCatchVariables": false
20 | },
21 | "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/src/pages/profile/settings/notifications/webpush.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../components/UserProfile/UserSettings';
4 | import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings';
5 | import UserWebPushSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush';
6 |
7 | const WebPushProfileNotificationsPage: NextPage = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default WebPushProfileNotificationsPage;
18 |
--------------------------------------------------------------------------------
/server/migration/1605085519544-SeasonStatus.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner } from 'typeorm';
2 |
3 | export class SeasonStatus1605085519544 implements MigrationInterface {
4 | name = 'SeasonStatus1605085519544';
5 |
6 | public async up(queryRunner: QueryRunner): Promise {
7 | await queryRunner.query(
8 | `CREATE TABLE "season" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "mediaId" integer)`
9 | );
10 | }
11 |
12 | public async down(queryRunner: QueryRunner): Promise {
13 | await queryRunner.query(`DROP TABLE "season"`);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | - dependencies
10 | # Label to use when marking an issue as stale
11 | staleLabel: stale
12 | # Comment to post when marking an issue as stale. Set to `false` to disable
13 | markComment: >
14 | This issue has been automatically marked as stale because it has not had
15 | recent activity. It will be closed if no further activity occurs. Thank you
16 | for your contributions.
17 | # Comment to post when closing a stale issue. Set to `false` to disable
18 | closeComment: false
19 |
--------------------------------------------------------------------------------
/docs/using-overseerr/notifications/pushover.md:
--------------------------------------------------------------------------------
1 | # Pushover
2 |
3 | ## Configuration
4 |
5 | ### Application/API Token
6 |
7 | [Register an application](https://pushover.net/apps/build) and enter the API token in this field. (You can use one of the [official icons in our GitHub repository](https://github.com/sct/overseerr/tree/develop/public) when configuring the application.)
8 |
9 | For more details on registering applications or the API token, please see the [Pushover API documentation](https://pushover.net/api#registration).
10 |
11 | ### User Key
12 |
13 | Set this to the user key for your Pushover account. Alternatively, you can set this to a group key to deliver notifications to multiple users.
14 |
15 | For more details, please see the [Pushover API documentation](https://pushover.net/api#identifiers).
16 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/email.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import NotificationsEmail from '../../../components/Settings/Notifications/NotificationsEmail';
4 | import SettingsLayout from '../../../components/Settings/SettingsLayout';
5 | import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
6 | import useRouteGuard from '../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../hooks/useUser';
8 |
9 | const NotificationsPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_SETTINGS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsPage;
21 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/discord.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import NotificationsDiscord from '../../../components/Settings/Notifications/NotificationsDiscord';
4 | import SettingsLayout from '../../../components/Settings/SettingsLayout';
5 | import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
6 | import useRouteGuard from '../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../hooks/useUser';
8 |
9 | const NotificationsPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_SETTINGS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsPage;
21 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/lunasea.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import NotificationsLunaSea from '../../../components/Settings/Notifications/NotificationsLunaSea';
4 | import SettingsLayout from '../../../components/Settings/SettingsLayout';
5 | import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
6 | import useRouteGuard from '../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../hooks/useUser';
8 |
9 | const NotificationsPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_SETTINGS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsPage;
21 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/pushover.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import NotificationsPushover from '../../../components/Settings/Notifications/NotificationsPushover';
4 | import SettingsLayout from '../../../components/Settings/SettingsLayout';
5 | import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
6 | import useRouteGuard from '../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../hooks/useUser';
8 |
9 | const NotificationsPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_SETTINGS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsPage;
21 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/slack.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import NotificationsSlack from '../../../components/Settings/Notifications/NotificationsSlack';
4 | import SettingsLayout from '../../../components/Settings/SettingsLayout';
5 | import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
6 | import useRouteGuard from '../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../hooks/useUser';
8 |
9 | const NotificationsSlackPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_SETTINGS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsSlackPage;
21 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/telegram.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import NotificationsTelegram from '../../../components/Settings/Notifications/NotificationsTelegram';
4 | import SettingsLayout from '../../../components/Settings/SettingsLayout';
5 | import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
6 | import useRouteGuard from '../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../hooks/useUser';
8 |
9 | const NotificationsPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_SETTINGS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsPage;
21 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/webhook.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import NotificationsWebhook from '../../../components/Settings/Notifications/NotificationsWebhook';
4 | import SettingsLayout from '../../../components/Settings/SettingsLayout';
5 | import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
6 | import useRouteGuard from '../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../hooks/useUser';
8 |
9 | const NotificationsPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_SETTINGS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsPage;
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # next.js
10 | /.next/
11 | /out/
12 |
13 | # production
14 | /build
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 |
20 | # debug
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # local env files
26 | .env.local
27 | .env.development.local
28 | .env.test.local
29 | .env.production.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # database
35 | config/db/*.sqlite3*
36 | config/settings.json
37 |
38 | # logs
39 | config/logs/*.log*
40 | config/logs/*.json
41 | config/logs/*.log.gz
42 | config/logs/*-audit.json
43 |
44 | # anidb mapping file
45 | config/anime-list.xml
46 |
47 | # dist files
48 | dist
49 |
50 | # sqlite journal
51 | config/db/db.sqlite3-journal
52 |
53 | # VS Code
54 | .vscode/launch.json
55 |
--------------------------------------------------------------------------------
/server/interfaces/api/serviceInterfaces.ts:
--------------------------------------------------------------------------------
1 | import { QualityProfile, RootFolder, Tag } from '../../api/servarr/base';
2 | import { LanguageProfile } from '../../api/servarr/sonarr';
3 |
4 | export interface ServiceCommonServer {
5 | id: number;
6 | name: string;
7 | is4k: boolean;
8 | isDefault: boolean;
9 | activeProfileId: number;
10 | activeDirectory: string;
11 | activeLanguageProfileId?: number;
12 | activeAnimeProfileId?: number;
13 | activeAnimeDirectory?: string;
14 | activeAnimeLanguageProfileId?: number;
15 | activeTags: number[];
16 | activeAnimeTags?: number[];
17 | }
18 |
19 | export interface ServiceCommonServerWithDetails {
20 | server: ServiceCommonServer;
21 | profiles: QualityProfile[];
22 | rootFolders: Partial[];
23 | languageProfiles?: LanguageProfile[];
24 | tags: Tag[];
25 | }
26 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/pushbullet.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import NotificationsPushbullet from '../../../components/Settings/Notifications/NotificationsPushbullet';
4 | import SettingsLayout from '../../../components/Settings/SettingsLayout';
5 | import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
6 | import useRouteGuard from '../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../hooks/useUser';
8 |
9 | const NotificationsPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_SETTINGS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsPage;
21 |
--------------------------------------------------------------------------------
/src/pages/settings/notifications/webpush.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import NotificationsWebPush from '../../../components/Settings/Notifications/NotificationsWebPush';
4 | import SettingsLayout from '../../../components/Settings/SettingsLayout';
5 | import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
6 | import useRouteGuard from '../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../hooks/useUser';
8 |
9 | const NotificationsWebPushPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_SETTINGS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsWebPushPage;
21 |
--------------------------------------------------------------------------------
/server/types/express.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import type { NextFunction, Request, Response } from 'express';
3 | import type { User } from '../entity/User';
4 |
5 | declare global {
6 | namespace Express {
7 | export interface Request {
8 | user?: User;
9 | locale?: string;
10 | }
11 | }
12 |
13 | export type Middleware = (
14 | req: Request,
15 | res: Response,
16 | next: NextFunction
17 | ) => Promise | void | NextFunction;
18 | }
19 |
20 | // Declaration merging to apply our own types to SessionData
21 | // See: (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/express-session/index.d.ts#L23)
22 | declare module 'express-session' {
23 | export interface SessionData {
24 | userId: number;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/server/utils/appVersion.ts:
--------------------------------------------------------------------------------
1 | import { existsSync } from 'fs';
2 | import path from 'path';
3 | import logger from '../logger';
4 |
5 | const COMMIT_TAG_PATH = path.join(__dirname, '../../committag.json');
6 | let commitTag = 'local';
7 |
8 | if (existsSync(COMMIT_TAG_PATH)) {
9 | // eslint-disable-next-line @typescript-eslint/no-var-requires
10 | commitTag = require(COMMIT_TAG_PATH).commitTag;
11 | logger.info(`Commit Tag: ${commitTag}`);
12 | }
13 |
14 | export const getCommitTag = (): string => {
15 | return commitTag;
16 | };
17 |
18 | export const getAppVersion = (): string => {
19 | // eslint-disable-next-line @typescript-eslint/no-var-requires
20 | const { version } = require('../../package.json');
21 |
22 | let finalVersion = version;
23 |
24 | if (version === '0.1.0') {
25 | finalVersion = `develop-${getCommitTag()}`;
26 | }
27 |
28 | return finalVersion;
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/Common/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface HeaderProps {
4 | extraMargin?: number;
5 | subtext?: React.ReactNode;
6 | }
7 |
8 | const Header: React.FC = ({
9 | children,
10 | extraMargin = 0,
11 | subtext,
12 | }) => {
13 | return (
14 |
15 |
16 |
17 |
18 | {children}
19 |
20 |
21 | {subtext &&
{subtext}
}
22 |
23 |
24 | );
25 | };
26 |
27 | export default Header;
28 |
--------------------------------------------------------------------------------
/server/routes/collection.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 | import TheMovieDb from '../api/themoviedb';
3 | import Media from '../entity/Media';
4 | import { mapCollection } from '../models/Collection';
5 |
6 | const collectionRoutes = Router();
7 |
8 | collectionRoutes.get<{ id: string }>('/:id', async (req, res, next) => {
9 | const tmdb = new TheMovieDb();
10 |
11 | try {
12 | const collection = await tmdb.getCollection({
13 | collectionId: Number(req.params.id),
14 | language: req.locale ?? (req.query.language as string),
15 | });
16 |
17 | const media = await Media.getRelatedMedia(
18 | collection.parts.map((part) => part.id)
19 | );
20 |
21 | return res.status(200).json(mapCollection(collection, media));
22 | } catch (e) {
23 | return next({ status: 404, message: 'Collection does not exist' });
24 | }
25 | });
26 |
27 | export default collectionRoutes;
28 |
--------------------------------------------------------------------------------
/docs/using-overseerr/notifications/discord.md:
--------------------------------------------------------------------------------
1 | # Discord
2 |
3 | The Discord notification agent enables you to post notifications to a channel in a server you manage.
4 |
5 | {% hint style="info" %}
6 | Users can optionally opt-in to being mentioned in Discord notifications by configuring their [Discord user ID](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-) in their user settings.
7 | {% endhint %}
8 |
9 | ## Configuration
10 |
11 | ### Webhook URL
12 |
13 | You can find the webhook URL in the Discord application, at **Server Settings → Integrations → Webhooks**.
14 |
15 | ### Bot Username (optional)
16 |
17 | If you would like to override the name you configured for your bot in Discord, you may set this value to whatever you like!
18 |
19 | ### Bot Avatar URL (optional)
20 |
21 | Similar to the bot username, you can override the avatar for your bot.
22 |
--------------------------------------------------------------------------------
/.github/workflows/support.yml:
--------------------------------------------------------------------------------
1 | name: 'Support requests'
2 |
3 | on:
4 | issues:
5 | types: [labeled, unlabeled, reopened]
6 |
7 | jobs:
8 | support:
9 | runs-on: ubuntu-20.04
10 | steps:
11 | - uses: dessant/support-requests@v2.0.1
12 | with:
13 | github-token: ${{ github.token }}
14 | support-label: 'support'
15 | issue-comment: >
16 | :wave: @{issue-author}, we use the issue tracker exclusively
17 | for bug reports and feature requests. However, this issue appears
18 | to be a support request. Please use our support channels
19 | to get help with Overseerr.
20 |
21 | - [Discord](https://discord.gg/overseerr)
22 |
23 | - [GitHub Discussions](https://github.com/sct/overseerr/discussions)
24 | close-issue: true
25 | lock-issue: true
26 | issue-lock-reason: 'off-topic'
27 |
--------------------------------------------------------------------------------
/server/routes/search.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 | import TheMovieDb from '../api/themoviedb';
3 | import Media from '../entity/Media';
4 | import { mapSearchResults } from '../models/Search';
5 |
6 | const searchRoutes = Router();
7 |
8 | searchRoutes.get('/', async (req, res) => {
9 | const tmdb = new TheMovieDb();
10 |
11 | const results = await tmdb.searchMulti({
12 | query: req.query.query as string,
13 | page: Number(req.query.page),
14 | language: req.locale ?? (req.query.language as string),
15 | });
16 |
17 | const media = await Media.getRelatedMedia(
18 | results.results.map((result) => result.id)
19 | );
20 |
21 | return res.status(200).json({
22 | page: results.page,
23 | totalPages: results.total_pages,
24 | totalResults: results.total_results,
25 | results: mapSearchResults(results.results, media),
26 | });
27 | });
28 |
29 | export default searchRoutes;
30 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/notifications/email.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../../components/UserProfile/UserSettings';
4 | import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings';
5 | import UserNotificationsEmail from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail';
6 | import useRouteGuard from '../../../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../../../hooks/useUser';
8 |
9 | const NotificationsPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_USERS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsPage;
21 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/notifications/discord.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../../components/UserProfile/UserSettings';
4 | import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings';
5 | import UserNotificationsDiscord from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord';
6 | import useRouteGuard from '../../../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../../../hooks/useUser';
8 |
9 | const NotificationsPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_USERS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsPage;
21 |
--------------------------------------------------------------------------------
/src/hooks/useLockBodyScroll.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | /**
4 | * Hook to lock the body scroll whenever a component is mounted or
5 | * whenever isLocked is set to true.
6 | *
7 | * You can pass in true always to cause a lock on mount/dismount of the component
8 | * using this hook.
9 | *
10 | * @param isLocked Toggle the scroll lock
11 | * @param disabled Disables the entire hook (allows conditional skipping of the lock)
12 | */
13 | export const useLockBodyScroll = (
14 | isLocked: boolean,
15 | disabled?: boolean
16 | ): void => {
17 | useEffect(() => {
18 | const originalStyle = window.getComputedStyle(document.body).overflow;
19 | if (isLocked && !disabled) {
20 | document.body.style.overflow = 'hidden';
21 | }
22 | return () => {
23 | if (!disabled) {
24 | document.body.style.overflow = originalStyle;
25 | }
26 | };
27 | }, [isLocked, disabled]);
28 | };
29 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/notifications/telegram.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../../components/UserProfile/UserSettings';
4 | import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings';
5 | import UserNotificationsTelegram from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram';
6 | import useRouteGuard from '../../../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../../../hooks/useUser';
8 |
9 | const NotificationsPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_USERS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default NotificationsPage;
21 |
--------------------------------------------------------------------------------
/src/pages/users/[userId]/settings/notifications/webpush.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import React from 'react';
3 | import UserSettings from '../../../../../components/UserProfile/UserSettings';
4 | import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings';
5 | import UserWebPushSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush';
6 | import useRouteGuard from '../../../../../hooks/useRouteGuard';
7 | import { Permission } from '../../../../../hooks/useUser';
8 |
9 | const WebPushNotificationsPage: NextPage = () => {
10 | useRouteGuard(Permission.MANAGE_USERS);
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default WebPushNotificationsPage;
21 |
--------------------------------------------------------------------------------
/docs/extending-overseerr/third-party.md:
--------------------------------------------------------------------------------
1 | # Third-Party Integrations
2 |
3 | {% hint style="warning" %}
4 | We do not officially support these third-party integrations. If you run into any issues, please seek help on the appropriate support channels for the integration itself!
5 | {% endhint %}
6 |
7 | - [Organizr](https://organizr.app/), a HTPC/homelab services organizer
8 | - [Heimdall](https://github.com/linuxserver/Heimdall), an application dashboard and launcher
9 | - [LunaSea](https://docs.lunasea.app/modules/overseerr), a self-hosted controller for mobile and macOS
10 | - [Requestrr](https://github.com/darkalfx/requestrr/wiki/Configuring-Overseerr), a Discord chatbot
11 | - [ha-overseerr](https://github.com/vaparr/ha-overseerr), a custom Home Assistant component
12 | - [OverCLIrr](https://github.com/WillFantom/OverCLIrr), a command-line tool
13 | - [Overseerr Exporter](https://github.com/WillFantom/overseerr-exporter), a Prometheus exporter
14 |
--------------------------------------------------------------------------------
/server/lib/notifications/agents/agent.ts:
--------------------------------------------------------------------------------
1 | import { Notification } from '..';
2 | import Media from '../../../entity/Media';
3 | import { MediaRequest } from '../../../entity/MediaRequest';
4 | import { User } from '../../../entity/User';
5 | import { NotificationAgentConfig } from '../../settings';
6 |
7 | export interface NotificationPayload {
8 | subject: string;
9 | notifyUser?: User;
10 | media?: Media;
11 | image?: string;
12 | message?: string;
13 | extra?: { name: string; value: string }[];
14 | request?: MediaRequest;
15 | }
16 |
17 | export abstract class BaseAgent {
18 | protected settings?: T;
19 | public constructor(settings?: T) {
20 | this.settings = settings;
21 | }
22 |
23 | protected abstract getSettings(): T;
24 | }
25 |
26 | export interface NotificationAgent {
27 | shouldSend(): boolean;
28 | send(type: Notification, payload: NotificationPayload): Promise;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/JSONEditor/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes } from 'react';
2 | import AceEditor from 'react-ace';
3 | import 'ace-builds/src-noconflict/mode-json';
4 | import 'ace-builds/src-noconflict/theme-dracula';
5 |
6 | interface JSONEditorProps extends HTMLAttributes {
7 | name: string;
8 | value: string;
9 | onUpdate: (value: string) => void;
10 | }
11 |
12 | const JSONEditor: React.FC = ({
13 | name,
14 | value,
15 | onUpdate,
16 | onBlur,
17 | }) => {
18 | return (
19 |
32 | );
33 | };
34 |
35 | export default JSONEditor;
36 |
--------------------------------------------------------------------------------
/server/models/Collection.ts:
--------------------------------------------------------------------------------
1 | import type { TmdbCollection } from '../api/themoviedb/interfaces';
2 | import { MediaType } from '../constants/media';
3 | import Media from '../entity/Media';
4 | import { mapMovieResult, MovieResult } from './Search';
5 |
6 | export interface Collection {
7 | id: number;
8 | name: string;
9 | overview?: string;
10 | posterPath?: string;
11 | backdropPath?: string;
12 | parts: MovieResult[];
13 | }
14 |
15 | export const mapCollection = (
16 | collection: TmdbCollection,
17 | media: Media[]
18 | ): Collection => ({
19 | id: collection.id,
20 | name: collection.name,
21 | overview: collection.overview,
22 | posterPath: collection.poster_path,
23 | backdropPath: collection.backdrop_path,
24 | parts: collection.parts.map((part) =>
25 | mapMovieResult(
26 | part,
27 | media?.find(
28 | (req) => req.tmdbId === part.id && req.mediaType === MediaType.MOVIE
29 | )
30 | )
31 | ),
32 | });
33 |
--------------------------------------------------------------------------------
/src/pages/tv/[tvId]/index.tsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { NextPage } from 'next';
3 | import React from 'react';
4 | import type { TvDetails as TvDetailsType } from '../../../../server/models/Tv';
5 | import TvDetails from '../../../components/TvDetails';
6 |
7 | interface TvPageProps {
8 | tv?: TvDetailsType;
9 | }
10 |
11 | const TvPage: NextPage = ({ tv }) => {
12 | return ;
13 | };
14 |
15 | TvPage.getInitialProps = async (ctx) => {
16 | if (ctx.req) {
17 | const response = await axios.get(
18 | `http://localhost:${process.env.PORT || 5055}/api/v1/tv/${
19 | ctx.query.tvId
20 | }`,
21 | {
22 | headers: ctx.req?.headers?.cookie
23 | ? { cookie: ctx.req.headers.cookie }
24 | : undefined,
25 | }
26 | );
27 |
28 | return {
29 | tv: response.data,
30 | };
31 | }
32 |
33 | return {};
34 | };
35 |
36 | export default TvPage;
37 |
--------------------------------------------------------------------------------
/src/hooks/useClickOutside.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | /**
4 | * useClickOutside
5 | *
6 | * Simple hook to add an event listener to the body and allow a callback to
7 | * be triggered when clicking outside of the target ref
8 | *
9 | * @param ref Any HTML Element ref
10 | * @param callback Callback triggered when clicking outside of ref element
11 | */
12 | const useClickOutside = (
13 | ref: React.RefObject,
14 | callback: (e: MouseEvent) => void
15 | ): void => {
16 | useEffect(() => {
17 | const handleBodyClick = (e: MouseEvent) => {
18 | if (ref.current && !ref.current.contains(e.target as Node)) {
19 | callback(e);
20 | }
21 | };
22 | document.body.addEventListener('click', handleBodyClick, { capture: true });
23 |
24 | return () => {
25 | document.body.removeEventListener('click', handleBodyClick);
26 | };
27 | }, [ref, callback]);
28 | };
29 |
30 | export default useClickOutside;
31 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // see
3 | // - https://code.visualstudio.com/docs/editor/extension-gallery#_workspace-recommended-extensions
4 | "recommendations": [
5 | // https://marketplace.visualstudio.com/items?itemName=EditorConfig.editorconfig
6 | "EditorConfig.editorconfig",
7 |
8 | // https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint
9 | "dbaeumer.vscode-eslint",
10 |
11 | // https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode
12 | "esbenp.prettier-vscode",
13 |
14 | // https://marketplace.visualstudio.com/items?itemName=eg2.vscode-npm-script
15 | "eg2.vscode-npm-script",
16 |
17 | // https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest
18 | "Orta.vscode-jest",
19 |
20 | "stylelint.vscode-stylelint",
21 |
22 | "bradlc.vscode-tailwindcss",
23 |
24 | // https://marketplace.visualstudio.com/items?itemName=heybourn.headwind
25 | "heybourn.headwind"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/server/entity/SeasonRequest.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | CreateDateColumn,
6 | UpdateDateColumn,
7 | ManyToOne,
8 | } from 'typeorm';
9 | import { MediaRequestStatus } from '../constants/media';
10 | import { MediaRequest } from './MediaRequest';
11 |
12 | @Entity()
13 | class SeasonRequest {
14 | @PrimaryGeneratedColumn()
15 | public id: number;
16 |
17 | @Column()
18 | public seasonNumber: number;
19 |
20 | @Column({ type: 'int', default: MediaRequestStatus.PENDING })
21 | public status: MediaRequestStatus;
22 |
23 | @ManyToOne(() => MediaRequest, (request) => request.seasons, {
24 | onDelete: 'CASCADE',
25 | })
26 | public request: MediaRequest;
27 |
28 | @CreateDateColumn()
29 | public createdAt: Date;
30 |
31 | @UpdateDateColumn()
32 | public updatedAt: Date;
33 |
34 | constructor(init?: Partial) {
35 | Object.assign(this, init);
36 | }
37 | }
38 |
39 | export default SeasonRequest;
40 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, {
2 | DocumentContext,
3 | DocumentInitialProps,
4 | Head,
5 | Html,
6 | Main,
7 | NextScript,
8 | } from 'next/document';
9 | import React from 'react';
10 |
11 | class MyDocument extends Document {
12 | static async getInitialProps(
13 | ctx: DocumentContext
14 | ): Promise {
15 | const initialProps = await Document.getInitialProps(ctx);
16 |
17 | return initialProps;
18 | }
19 |
20 | render(): JSX.Element {
21 | return (
22 |
23 |
24 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | export default MyDocument;
40 |
--------------------------------------------------------------------------------
/server/entity/Season.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Entity,
3 | PrimaryGeneratedColumn,
4 | Column,
5 | ManyToOne,
6 | CreateDateColumn,
7 | UpdateDateColumn,
8 | } from 'typeorm';
9 | import { MediaStatus } from '../constants/media';
10 | import Media from './Media';
11 |
12 | @Entity()
13 | class Season {
14 | @PrimaryGeneratedColumn()
15 | public id: number;
16 |
17 | @Column()
18 | public seasonNumber: number;
19 |
20 | @Column({ type: 'int', default: MediaStatus.UNKNOWN })
21 | public status: MediaStatus;
22 |
23 | @Column({ type: 'int', default: MediaStatus.UNKNOWN })
24 | public status4k: MediaStatus;
25 |
26 | @ManyToOne(() => Media, (media) => media.seasons, { onDelete: 'CASCADE' })
27 | public media: Promise;
28 |
29 | @CreateDateColumn()
30 | public createdAt: Date;
31 |
32 | @UpdateDateColumn()
33 | public updatedAt: Date;
34 |
35 | constructor(init?: Partial) {
36 | Object.assign(this, init);
37 | }
38 | }
39 |
40 | export default Season;
41 |
--------------------------------------------------------------------------------
/server/types/plex-api.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'plex-api' {
2 | export default class PlexAPI {
3 | constructor(intiialOptions: {
4 | hostname: string;
5 | port: number;
6 | token?: string;
7 | https?: boolean;
8 | timeout?: number;
9 | authenticator: {
10 | authenticate: (
11 | _plexApi: PlexAPI,
12 | cb: (err?: string, token?: string) => void
13 | ) => void;
14 | };
15 | options: {
16 | identifier: string;
17 | product: string;
18 | deviceName: string;
19 | platform: string;
20 | };
21 | requestOptions?: Record;
22 | });
23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
24 | query: >(
25 | endpoint:
26 | | string
27 | | {
28 | uri: string;
29 | extraHeaders?: Record;
30 | }
31 | ) => Promise;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/assets/extlogos/slack.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/using-overseerr/notifications/README.md:
--------------------------------------------------------------------------------
1 | # Notifications
2 |
3 | ## Supported Notification Agents
4 |
5 | Overseerr currently supports the following notification agents:
6 |
7 | - [Email](./email.md)
8 | - [Web Push](./webpush.md)
9 | - [Discord](./discord.md)
10 | - [LunaSea](./lunasea.md)
11 | - [Pushbullet](./pushbullet.md)
12 | - [Pushover](./pushover.md)
13 | - [Slack](./slack.md)
14 | - [Telegram](./telegram.md)
15 | - [Webhooks](./webhooks.md)
16 |
17 | ## Setting Up Notifications
18 |
19 | Simply configure your desired notification agents in **Settings → Notifications**.
20 |
21 | Users can customize their personal notification preferences in their own user notification settings.
22 |
23 | ## Requesting New Notification Agents
24 |
25 | If we do not currently support your preferred notification agent, feel free to [submit a feature request on GitHub](https://github.com/sct/overseerr/issues). However, please be sure to search first and confirm that there is not already an existing request for the agent!
26 |
--------------------------------------------------------------------------------
/src/pages/movie/[movieId]/index.tsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { NextPage } from 'next';
3 | import React from 'react';
4 | import type { MovieDetails as MovieDetailsType } from '../../../../server/models/Movie';
5 | import MovieDetails from '../../../components/MovieDetails';
6 |
7 | interface MoviePageProps {
8 | movie?: MovieDetailsType;
9 | }
10 |
11 | const MoviePage: NextPage = ({ movie }) => {
12 | return ;
13 | };
14 |
15 | MoviePage.getInitialProps = async (ctx) => {
16 | if (ctx.req) {
17 | const response = await axios.get(
18 | `http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
19 | ctx.query.movieId
20 | }`,
21 | {
22 | headers: ctx.req?.headers?.cookie
23 | ? { cookie: ctx.req.headers.cookie }
24 | : undefined,
25 | }
26 | );
27 |
28 | return {
29 | movie: response.data,
30 | };
31 | }
32 |
33 | return {};
34 | };
35 |
36 | export default MoviePage;
37 |
--------------------------------------------------------------------------------
/src/hooks/useDebouncedState.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, Dispatch, SetStateAction } from 'react';
2 |
3 | /**
4 | * A hook to help with debouncing state
5 | *
6 | * This hook basically acts the same as useState except it is also
7 | * returning a deobuncedValue that can be used for things like
8 | * debouncing input into a search field
9 | *
10 | * @param initialValue Initial state value
11 | * @param debounceTime Debounce time in ms
12 | */
13 | const useDebouncedState = (
14 | initialValue: S,
15 | debounceTime = 300
16 | ): [S, S, Dispatch>] => {
17 | const [value, setValue] = useState(initialValue);
18 | const [finalValue, setFinalValue] = useState(initialValue);
19 |
20 | useEffect(() => {
21 | const timeout = setTimeout(() => {
22 | setFinalValue(value);
23 | }, debounceTime);
24 |
25 | return () => {
26 | clearTimeout(timeout);
27 | };
28 | }, [value, debounceTime]);
29 |
30 | return [value, finalValue, setValue];
31 | };
32 |
33 | export default useDebouncedState;
34 |
--------------------------------------------------------------------------------
/src/components/Common/Badge/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface BadgeProps {
4 | badgeType?: 'default' | 'primary' | 'danger' | 'warning' | 'success';
5 | className?: string;
6 | }
7 |
8 | const Badge: React.FC = ({
9 | badgeType = 'default',
10 | className,
11 | children,
12 | }) => {
13 | const badgeStyle = [
14 | 'px-2 inline-flex text-xs leading-5 font-semibold rounded-full cursor-default',
15 | ];
16 |
17 | switch (badgeType) {
18 | case 'danger':
19 | badgeStyle.push('bg-red-600 text-red-100');
20 | break;
21 | case 'warning':
22 | badgeStyle.push('bg-yellow-500 text-yellow-100');
23 | break;
24 | case 'success':
25 | badgeStyle.push('bg-green-500 text-green-100');
26 | break;
27 | default:
28 | badgeStyle.push('bg-indigo-500 text-indigo-100');
29 | }
30 |
31 | if (className) {
32 | badgeStyle.push(className);
33 | }
34 |
35 | return {children};
36 | };
37 |
38 | export default Badge;
39 |
--------------------------------------------------------------------------------
/server/interfaces/api/userSettingsInterfaces.ts:
--------------------------------------------------------------------------------
1 | import { NotificationAgentKey } from '../../lib/settings';
2 |
3 | export interface UserSettingsGeneralResponse {
4 | username?: string;
5 | locale?: string;
6 | region?: string;
7 | originalLanguage?: string;
8 | movieQuotaLimit?: number;
9 | movieQuotaDays?: number;
10 | tvQuotaLimit?: number;
11 | tvQuotaDays?: number;
12 | globalMovieQuotaDays?: number;
13 | globalMovieQuotaLimit?: number;
14 | globalTvQuotaLimit?: number;
15 | globalTvQuotaDays?: number;
16 | }
17 |
18 | export type NotificationAgentTypes = Record;
19 | export interface UserSettingsNotificationsResponse {
20 | emailEnabled?: boolean;
21 | pgpKey?: string;
22 | discordEnabled?: boolean;
23 | discordEnabledTypes?: number;
24 | discordId?: string;
25 | telegramEnabled?: boolean;
26 | telegramBotUsername?: string;
27 | telegramChatId?: string;
28 | telegramSendSilently?: boolean;
29 | webPushEnabled?: boolean;
30 | notificationTypes: Partial;
31 | }
32 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14.17-alpine AS BUILD_IMAGE
2 |
3 | WORKDIR /app
4 |
5 | ARG TARGETPLATFORM
6 | ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
7 |
8 | RUN \
9 | case "${TARGETPLATFORM}" in \
10 | 'linux/arm64') apk add --no-cache python make g++ ;; \
11 | 'linux/arm/v7') apk add --no-cache python make g++ ;; \
12 | esac
13 |
14 | COPY package.json yarn.lock ./
15 | RUN yarn install --frozen-lockfile --network-timeout 1000000
16 |
17 | COPY . ./
18 |
19 | ARG COMMIT_TAG
20 | ENV COMMIT_TAG=${COMMIT_TAG}
21 |
22 | RUN yarn build
23 |
24 | # remove development dependencies
25 | RUN yarn install --production --ignore-scripts --prefer-offline
26 |
27 | RUN rm -rf src server
28 |
29 | RUN touch config/DOCKER
30 |
31 | RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
32 |
33 |
34 | FROM node:14.17-alpine
35 |
36 | WORKDIR /app
37 |
38 | RUN apk add --no-cache tzdata tini
39 |
40 | # copy from build image
41 | COPY --from=BUILD_IMAGE /app ./
42 |
43 | ENTRYPOINT [ "/sbin/tini", "--" ]
44 | CMD [ "yarn", "start" ]
45 |
46 | EXPOSE 5055
47 |
--------------------------------------------------------------------------------
/src/assets/services/plex.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/using-overseerr/notifications/webpush.md:
--------------------------------------------------------------------------------
1 | # Web Push
2 |
3 | The web push notification agent enables you and your users to receive Overseerr notifications in a supported browser.
4 |
5 | This notification agent does not require any configuration, but is not enabled in Overseerr by default.
6 |
7 | {% hint style="warning" %}
8 | **The web push agent only works via HTTPS.** Refer to our [reverse proxy examples](../../extending-overseerr/reverse-proxy.md) for help on proxying Overseerr traffic via HTTPS.
9 | {% endhint %}
10 |
11 | To set up web push notifications, simply enable the agent in **Settings → Notifications → Web Push**. You and your users will then be prompted to allow notifications in your web browser.
12 |
13 | Users can opt out of these notifications, or customize the notification types they would like to subscribe to, in their user settings.
14 |
15 | {% hint style="info" %}
16 | Web push notifications offer a native notification experience without the need to install an app. iOS devices do not have support for these notifications at this time, however.
17 | {% endhint %}
18 |
--------------------------------------------------------------------------------
/src/pages/collection/[collectionId]/index.tsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { GetServerSideProps, NextPage } from 'next';
3 | import React from 'react';
4 | import type { Collection } from '../../../../server/models/Collection';
5 | import CollectionDetails from '../../../components/CollectionDetails';
6 |
7 | interface CollectionPageProps {
8 | collection?: Collection;
9 | }
10 |
11 | const CollectionPage: NextPage = ({ collection }) => {
12 | return ;
13 | };
14 |
15 | export const getServerSideProps: GetServerSideProps = async (
16 | ctx
17 | ) => {
18 | const response = await axios.get(
19 | `http://localhost:${process.env.PORT || 5055}/api/v1/collection/${
20 | ctx.query.collectionId
21 | }`,
22 | {
23 | headers: ctx.req?.headers?.cookie
24 | ? { cookie: ctx.req.headers.cookie }
25 | : undefined,
26 | }
27 | );
28 |
29 | return {
30 | props: {
31 | collection: response.data,
32 | },
33 | };
34 | };
35 |
36 | export default CollectionPage;
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 sct
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/server/interfaces/api/plexInterfaces.ts:
--------------------------------------------------------------------------------
1 | import { PlexSettings } from '../../lib/settings';
2 |
3 | export interface PlexStatus {
4 | settings: PlexSettings;
5 | status: number;
6 | message: string;
7 | }
8 |
9 | export interface PlexConnection {
10 | protocol: string;
11 | address: string;
12 | port: number;
13 | uri: string;
14 | local: boolean;
15 | status?: number;
16 | message?: string;
17 | }
18 |
19 | export interface PlexDevice {
20 | name: string;
21 | product: string;
22 | productVersion: string;
23 | platform: string;
24 | platformVersion: string;
25 | device: string;
26 | clientIdentifier: string;
27 | createdAt: Date;
28 | lastSeenAt: Date;
29 | provides: string[];
30 | owned: boolean;
31 | accessToken?: string;
32 | publicAddress?: string;
33 | httpsRequired?: boolean;
34 | synced?: boolean;
35 | relay?: boolean;
36 | dnsRebindingProtection?: boolean;
37 | natLoopbackSupported?: boolean;
38 | publicAddressMatches?: boolean;
39 | presence?: boolean;
40 | ownerID?: string;
41 | home?: boolean;
42 | sourceTitle?: string;
43 | connection: PlexConnection[];
44 | }
45 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowCircleRightIcon } from '@heroicons/react/outline';
2 | import Link from 'next/link';
3 | import React from 'react';
4 | import { defineMessages, useIntl } from 'react-intl';
5 | import PageTitle from '../components/Common/PageTitle';
6 |
7 | const messages = defineMessages({
8 | errormessagewithcode: '{statusCode} - {error}',
9 | pagenotfound: 'Page Not Found',
10 | returnHome: 'Return Home',
11 | });
12 |
13 | const Custom404: React.FC = () => {
14 | const intl = useIntl();
15 |
16 | return (
17 |
32 | );
33 | };
34 |
35 | export default Custom404;
36 |
--------------------------------------------------------------------------------
/src/context/UserContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { useUser, User } from '../hooks/useUser';
3 | import { useRouter } from 'next/dist/client/router';
4 |
5 | interface UserContextProps {
6 | initialUser: User;
7 | }
8 |
9 | /**
10 | * This UserContext serves the purpose of just preparing the useUser hooks
11 | * cache on server side render. It also will handle redirecting the user to
12 | * the login page if their session ever becomes invalid.
13 | */
14 | export const UserContext: React.FC = ({
15 | initialUser,
16 | children,
17 | }) => {
18 | const { user, error, revalidate } = useUser({ initialData: initialUser });
19 | const router = useRouter();
20 | const routing = useRef(false);
21 |
22 | useEffect(() => {
23 | revalidate();
24 | }, [router.pathname, revalidate]);
25 |
26 | useEffect(() => {
27 | if (
28 | !router.pathname.match(/(setup|login|resetpassword)/) &&
29 | (!user || error) &&
30 | !routing.current
31 | ) {
32 | routing.current = true;
33 | location.href = '/login';
34 | }
35 | }, [router, user, error]);
36 |
37 | return <>{children}>;
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/Settings/CopyButton.tsx:
--------------------------------------------------------------------------------
1 | import { ClipboardCopyIcon } from '@heroicons/react/solid';
2 | import React, { useEffect } from 'react';
3 | import { defineMessages, useIntl } from 'react-intl';
4 | import { useToasts } from 'react-toast-notifications';
5 | import useClipboard from 'react-use-clipboard';
6 |
7 | const messages = defineMessages({
8 | copied: 'Copied API key to clipboard.',
9 | });
10 |
11 | const CopyButton: React.FC<{ textToCopy: string }> = ({ textToCopy }) => {
12 | const intl = useIntl();
13 | const [isCopied, setCopied] = useClipboard(textToCopy, {
14 | successDuration: 1000,
15 | });
16 | const { addToast } = useToasts();
17 |
18 | useEffect(() => {
19 | if (isCopied) {
20 | addToast(intl.formatMessage(messages.copied), {
21 | appearance: 'info',
22 | autoDismiss: true,
23 | });
24 | }
25 | }, [isCopied, addToast, intl]);
26 |
27 | return (
28 |
37 | );
38 | };
39 |
40 | export default CopyButton;
41 |
--------------------------------------------------------------------------------
/ormconfig.js:
--------------------------------------------------------------------------------
1 | const devConfig = {
2 | type: 'sqlite',
3 | database: process.env.CONFIG_DIRECTORY
4 | ? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
5 | : 'config/db/db.sqlite3',
6 | synchronize: true,
7 | migrationsRun: false,
8 | logging: false,
9 | enableWAL: true,
10 | entities: ['server/entity/**/*.ts'],
11 | migrations: ['server/migration/**/*.ts'],
12 | subscribers: ['server/subscriber/**/*.ts'],
13 | cli: {
14 | entitiesDir: 'server/entity',
15 | migrationsDir: 'server/migration',
16 | },
17 | };
18 |
19 | const prodConfig = {
20 | type: 'sqlite',
21 | database: process.env.CONFIG_DIRECTORY
22 | ? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
23 | : 'config/db/db.sqlite3',
24 | synchronize: false,
25 | logging: false,
26 | enableWAL: true,
27 | entities: ['dist/entity/**/*.js'],
28 | migrations: ['dist/migration/**/*.js'],
29 | migrationsRun: false,
30 | subscribers: ['dist/subscriber/**/*.js'],
31 | cli: {
32 | entitiesDir: 'dist/entity',
33 | migrationsDir: 'dist/migration',
34 | },
35 | };
36 |
37 | const finalConfig =
38 | process.env.NODE_ENV !== 'production' ? devConfig : prodConfig;
39 |
40 | module.exports = finalConfig;
41 |
--------------------------------------------------------------------------------
/src/assets/rt_fresh.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/AppDataWarning/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { defineMessages, useIntl } from 'react-intl';
3 | import useSWR from 'swr';
4 | import Alert from '../Common/Alert';
5 |
6 | const messages = defineMessages({
7 | dockerVolumeMissingDescription:
8 | 'The {appDataPath} volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.',
9 | });
10 |
11 | const AppDataWarning: React.FC = () => {
12 | const intl = useIntl();
13 | const { data, error } = useSWR<{ appData: boolean; appDataPath: string }>(
14 | '/api/v1/status/appdata'
15 | );
16 |
17 | if (!data && !error) {
18 | return null;
19 | }
20 |
21 | if (!data) {
22 | return null;
23 | }
24 |
25 | return (
26 | <>
27 | {!data.appData && (
28 | {msg};
32 | },
33 | appDataPath: data.appDataPath,
34 | })}
35 | />
36 | )}
37 | >
38 | );
39 | };
40 |
41 | export default AppDataWarning;
42 |
--------------------------------------------------------------------------------
/docs/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of Contents
2 |
3 | - [Introduction](README.md)
4 |
5 | ## Getting Started
6 |
7 | - [Installation](getting-started/installation.md)
8 |
9 | ## Using Overseerr
10 |
11 | - [Settings](using-overseerr/settings/README.md)
12 | - [Users](using-overseerr/users/README.md)
13 | - [Notifications](using-overseerr/notifications/README.md)
14 | - [Email](using-overseerr/notifications/email.md)
15 | - [Web Push](using-overseerr/notifications/webpush.md)
16 | - [Discord](using-overseerr/notifications/discord.md)
17 | - [LunaSea](using-overseerr/notifications/lunasea.md)
18 | - [Pushbullet](using-overseerr/notifications/pushbullet.md)
19 | - [Pushover](using-overseerr/notifications/pushover.md)
20 | - [Slack](using-overseerr/notifications/slack.md)
21 | - [Telegram](using-overseerr/notifications/telegram.md)
22 | - [Webhook](using-overseerr/notifications/webhooks.md)
23 |
24 | ## Support
25 |
26 | - [Frequently Asked Questions (FAQ)](support/faq.md)
27 | - [Need Help?](support/need-help.md)
28 |
29 | ## Extending Overseerr
30 |
31 | - [Reverse Proxy](extending-overseerr/reverse-proxy.md)
32 | - [Fail2ban](extending-overseerr/fail2ban.md)
33 | - [Third-Party Integrations](extending-overseerr/third-party.md)
34 |
--------------------------------------------------------------------------------
/src/components/Common/List/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withProperties } from '../../../utils/typeHelpers';
3 |
4 | interface ListItemProps {
5 | title: string;
6 | className?: string;
7 | }
8 |
9 | const ListItem: React.FC = ({ title, className, children }) => {
10 | return (
11 |
12 |
13 |
{title}
14 |
15 | {children}
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | interface ListProps {
23 | title: string;
24 | subTitle?: string;
25 | }
26 |
27 | const List: React.FC = ({ title, subTitle, children }) => {
28 | return (
29 | <>
30 |
31 |
{title}
32 | {subTitle &&
{subTitle}
}
33 |
34 |
37 | >
38 | );
39 | };
40 |
41 | export default withProperties(List, { Item: ListItem });
42 |
--------------------------------------------------------------------------------
/docs/using-overseerr/notifications/telegram.md:
--------------------------------------------------------------------------------
1 | # Telegram
2 |
3 | {% hint style="info" %}
4 | Users can optionally configure their own notifications in their user settings.
5 | {% endhint %}
6 |
7 | ## Configuration
8 |
9 | {% hint style="info" %}
10 | In order to configure Telegram notifications, you first need to [create a bot](https://telegram.me/BotFather).
11 |
12 | Bots **cannot** initiate conversations with users, so users must have your bot added to a conversation in order to receive notifications.
13 | {% endhint %}
14 |
15 | ### Bot Username (optional)
16 |
17 | If this value is configured, users will be able to click a link to start a chat with your bot and configure their own personal notifications.
18 |
19 | The bot username should end with `_bot`, and the `@` prefix should be omitted.
20 |
21 | ### Bot Authentication Token
22 |
23 | At the end of the bot creation process, [@BotFather](https://telegram.me/botfather) will provide an authentication token.
24 |
25 | ### Chat ID
26 |
27 | To obtain your chat ID, simply create a new group chat, add [@get_id_bot](https://telegram.me/get_id_bot), and issue the `/my_id` command.
28 |
29 | ### Send Silently (optional)
30 |
31 | Optionally, notifications can be sent silently. Silent notifications send messages without notification sounds.
32 |
--------------------------------------------------------------------------------
/src/assets/extlogos/pushover.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/lock.yml:
--------------------------------------------------------------------------------
1 | # Configuration for Lock Threads - https://github.com/dessant/lock-threads-app
2 |
3 | # Number of days of inactivity before a closed issue or pull request is locked
4 | daysUntilLock: 365
5 |
6 | # Skip issues and pull requests created before a given timestamp. Timestamp must
7 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
8 | skipCreatedBefore: false
9 |
10 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable
11 | exemptLabels:
12 | - dependencies
13 |
14 | # Label to add before locking, such as `outdated`. Set to `false` to disable
15 | lockLabel: false
16 |
17 | # Comment to post before locking. Set to `false` to disable
18 | lockComment: >
19 | This thread has been automatically locked since there has not been
20 | any recent activity after it was closed. Please open a new issue for
21 | related bugs.
22 |
23 | # Assign `resolved` as the reason for locking. Set to `false` to disable
24 | setLockReason: true
25 | # Limit to only `issues` or `pulls`
26 | # only: issues
27 |
28 | # Optionally, specify configuration settings just for `issues` or `pulls`
29 | # issues:
30 | # exemptLabels:
31 | # - help-wanted
32 | # lockLabel: outdated
33 |
34 | # pulls:
35 | # daysUntilLock: 30
36 |
37 | # Repository to extend settings from
38 | # _extends: repo
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Submit a report to help us improve
4 | title: ''
5 | labels: 'awaiting-triage, type:bug'
6 | assignees: ''
7 | ---
8 |
9 | #### Description
10 |
11 | Please provide a clear and concise description of the bug or issue.
12 |
13 | #### Version
14 |
15 | What version of Overseerr are you running? (You can find this in Settings → About → Version.)
16 |
17 | #### Steps to Reproduce
18 |
19 | Please tell us how we can reproduce the undesired behavior.
20 |
21 | 1. Go to [...]
22 | 2. Click on [...]
23 | 3. Scroll down to [...]
24 | 4. See error in [...]
25 |
26 | #### Expected Behavior
27 |
28 | Please provide a clear and concise description of what you expected to happen.
29 |
30 | #### Screenshots
31 |
32 | If applicable, please provide screenshots depicting the problem.
33 |
34 | #### Device
35 |
36 | What device were you using when you encountered this issue? Please provide this information to help us reproduce and investigate the bug.
37 |
38 | - **Platform:** [e.g., desktop, smartphone, tablet]
39 | - **Device:** [e.g., iPhone X, Surface Pro, Samsung Galaxy Tab]
40 | - **OS:** [e.g., iOS 8.1, Windows 10, Android 11]
41 | - **Browser:** [e.g., Chrome, Safari, Edge, Firefox]
42 |
43 | #### Additional Context
44 |
45 | Please provide any additional information that may be relevant or helpful.
46 |
--------------------------------------------------------------------------------
/src/components/Common/PlayButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import ButtonWithDropdown from '../ButtonWithDropdown';
3 |
4 | interface PlayButtonProps {
5 | links: PlayButtonLink[];
6 | }
7 |
8 | export interface PlayButtonLink {
9 | text: string;
10 | url: string;
11 | svg: ReactNode;
12 | }
13 |
14 | const PlayButton: React.FC = ({ links }) => {
15 | if (!links || !links.length) {
16 | return null;
17 | }
18 |
19 | return (
20 |
24 | {links[0].svg}
25 | {links[0].text}
26 | >
27 | }
28 | onClick={() => {
29 | window.open(links[0].url, '_blank');
30 | }}
31 | >
32 | {links.length > 1 &&
33 | links.slice(1).map((link, i) => {
34 | return (
35 | {
38 | window.open(link.url, '_blank');
39 | }}
40 | buttonType="ghost"
41 | >
42 | {link.svg}
43 | {link.text}
44 |
45 | );
46 | })}
47 |
48 | );
49 | };
50 |
51 | export default PlayButton;
52 |
--------------------------------------------------------------------------------
/src/hooks/useRequestOverride.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr';
2 | import { MediaRequest } from '../../server/entity/MediaRequest';
3 | import { ServiceCommonServer } from '../../server/interfaces/api/serviceInterfaces';
4 |
5 | interface OverrideStatus {
6 | server: string | null;
7 | profile: number | null;
8 | rootFolder: string | null;
9 | }
10 |
11 | const useRequestOverride = (request: MediaRequest): OverrideStatus => {
12 | const { data } = useSWR(
13 | `/api/v1/service/${request.type === 'movie' ? 'radarr' : 'sonarr'}`
14 | );
15 |
16 | if (!data) {
17 | return {
18 | server: null,
19 | profile: null,
20 | rootFolder: null,
21 | };
22 | }
23 |
24 | const defaultServer = data.find(
25 | (server) => server.is4k === request.is4k && server.isDefault
26 | );
27 |
28 | const activeServer = data.find((server) => server.id === request.serverId);
29 |
30 | return {
31 | server:
32 | activeServer && request.serverId !== defaultServer?.id
33 | ? activeServer.name
34 | : null,
35 | profile:
36 | defaultServer?.activeProfileId !== request.profileId
37 | ? request.profileId
38 | : null,
39 | rootFolder:
40 | defaultServer?.activeDirectory !== request.rootFolder
41 | ? request.rootFolder
42 | : null,
43 | };
44 | };
45 |
46 | export default useRequestOverride;
47 |
--------------------------------------------------------------------------------
/src/assets/rt_rotten.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Discover/DiscoverTv.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { TvResult } from '../../../server/models/Search';
3 | import ListView from '../Common/ListView';
4 | import { defineMessages, useIntl } from 'react-intl';
5 | import Header from '../Common/Header';
6 | import PageTitle from '../Common/PageTitle';
7 | import useDiscover from '../../hooks/useDiscover';
8 | import Error from '../../pages/_error';
9 |
10 | const messages = defineMessages({
11 | discovertv: 'Popular Series',
12 | });
13 |
14 | const DiscoverTv: React.FC = () => {
15 | const intl = useIntl();
16 |
17 | const {
18 | isLoadingInitialData,
19 | isEmpty,
20 | isLoadingMore,
21 | isReachingEnd,
22 | titles,
23 | fetchMore,
24 | error,
25 | } = useDiscover('/api/v1/discover/tv');
26 |
27 | if (error) {
28 | return ;
29 | }
30 |
31 | const title = intl.formatMessage(messages.discovertv);
32 |
33 | return (
34 | <>
35 |
36 |
37 |
38 |
39 | 0)
45 | }
46 | onScrollBottom={fetchMore}
47 | />
48 | >
49 | );
50 | };
51 |
52 | export default DiscoverTv;
53 |
--------------------------------------------------------------------------------
/src/components/Discover/DiscoverMovies.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { MovieResult } from '../../../server/models/Search';
3 | import ListView from '../Common/ListView';
4 | import { defineMessages, useIntl } from 'react-intl';
5 | import Header from '../Common/Header';
6 | import PageTitle from '../Common/PageTitle';
7 | import useDiscover from '../../hooks/useDiscover';
8 | import Error from '../../pages/_error';
9 |
10 | const messages = defineMessages({
11 | discovermovies: 'Popular Movies',
12 | });
13 |
14 | const DiscoverMovies: React.FC = () => {
15 | const intl = useIntl();
16 |
17 | const {
18 | isLoadingInitialData,
19 | isEmpty,
20 | isLoadingMore,
21 | isReachingEnd,
22 | titles,
23 | fetchMore,
24 | error,
25 | } = useDiscover('/api/v1/discover/movies');
26 |
27 | if (error) {
28 | return ;
29 | }
30 |
31 | const title = intl.formatMessage(messages.discovermovies);
32 |
33 | return (
34 | <>
35 |
36 |
37 |
38 |
39 | 0)
44 | }
45 | isReachingEnd={isReachingEnd}
46 | onScrollBottom={fetchMore}
47 | />
48 | >
49 | );
50 | };
51 |
52 | export default DiscoverMovies;
53 |
--------------------------------------------------------------------------------
/src/components/Discover/DiscoverTvUpcoming.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { TvResult } from '../../../server/models/Search';
3 | import ListView from '../Common/ListView';
4 | import { defineMessages, useIntl } from 'react-intl';
5 | import Header from '../Common/Header';
6 | import PageTitle from '../Common/PageTitle';
7 | import useDiscover from '../../hooks/useDiscover';
8 | import Error from '../../pages/_error';
9 |
10 | const messages = defineMessages({
11 | upcomingtv: 'Upcoming Series',
12 | });
13 |
14 | const DiscoverTvUpcoming: React.FC = () => {
15 | const intl = useIntl();
16 |
17 | const {
18 | isLoadingInitialData,
19 | isEmpty,
20 | isLoadingMore,
21 | isReachingEnd,
22 | titles,
23 | fetchMore,
24 | error,
25 | } = useDiscover('/api/v1/discover/tv/upcoming');
26 |
27 | if (error) {
28 | return ;
29 | }
30 |
31 | return (
32 | <>
33 |
34 |
35 | {intl.formatMessage(messages.upcomingtv)}
36 |
37 | 0)
43 | }
44 | onScrollBottom={fetchMore}
45 | />
46 | >
47 | );
48 | };
49 |
50 | export default DiscoverTvUpcoming;
51 |
--------------------------------------------------------------------------------
/src/components/Discover/Upcoming.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { MovieResult } from '../../../server/models/Search';
3 | import ListView from '../Common/ListView';
4 | import { defineMessages, useIntl } from 'react-intl';
5 | import Header from '../Common/Header';
6 | import PageTitle from '../Common/PageTitle';
7 | import useDiscover from '../../hooks/useDiscover';
8 | import Error from '../../pages/_error';
9 |
10 | const messages = defineMessages({
11 | upcomingmovies: 'Upcoming Movies',
12 | });
13 |
14 | const UpcomingMovies: React.FC = () => {
15 | const intl = useIntl();
16 |
17 | const {
18 | isLoadingInitialData,
19 | isEmpty,
20 | isLoadingMore,
21 | isReachingEnd,
22 | titles,
23 | fetchMore,
24 | error,
25 | } = useDiscover('/api/v1/discover/movies/upcoming');
26 |
27 | if (error) {
28 | return ;
29 | }
30 |
31 | return (
32 | <>
33 |
34 |
35 | {intl.formatMessage(messages.upcomingmovies)}
36 |
37 | 0)
42 | }
43 | isReachingEnd={isReachingEnd}
44 | onScrollBottom={fetchMore}
45 | />
46 | >
47 | );
48 | };
49 |
50 | export default UpcomingMovies;
51 |
--------------------------------------------------------------------------------
/server/interfaces/api/settingsInterfaces.ts:
--------------------------------------------------------------------------------
1 | import type { PaginatedResponse } from './common';
2 |
3 | export type LogMessage = {
4 | timestamp: string;
5 | level: string;
6 | label: string;
7 | message: string;
8 | data?: Record;
9 | };
10 |
11 | export interface LogsResultsResponse extends PaginatedResponse {
12 | results: LogMessage[];
13 | }
14 |
15 | export interface SettingsAboutResponse {
16 | version: string;
17 | totalRequests: number;
18 | totalMediaItems: number;
19 | tz?: string;
20 | }
21 |
22 | export interface PublicSettingsResponse {
23 | jellyfinHost?: string;
24 | jellyfinServerName?: string;
25 | initialized: boolean;
26 | applicationTitle: string;
27 | applicationUrl: string;
28 | hideAvailable: boolean;
29 | localLogin: boolean;
30 | movie4kEnabled: boolean;
31 | series4kEnabled: boolean;
32 | region: string;
33 | originalLanguage: string;
34 | mediaServerType: number;
35 | partialRequestsEnabled: boolean;
36 | cacheImages: boolean;
37 | vapidPublic: string;
38 | enablePushRegistration: boolean;
39 | locale: string;
40 | emailEnabled: boolean;
41 | }
42 |
43 | export interface CacheItem {
44 | id: string;
45 | name: string;
46 | stats: {
47 | hits: number;
48 | misses: number;
49 | keys: number;
50 | ksize: number;
51 | vsize: number;
52 | };
53 | }
54 |
55 | export interface StatusResponse {
56 | version: string;
57 | commitTag: string;
58 | updateAvailable: boolean;
59 | commitsBehind: number;
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/CompanyCard/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import React, { useState } from 'react';
3 |
4 | interface CompanyCardProps {
5 | name: string;
6 | image: string;
7 | url: string;
8 | }
9 |
10 | const CompanyCard: React.FC = ({ image, url, name }) => {
11 | const [isHovered, setHovered] = useState(false);
12 |
13 | return (
14 |
15 | {
22 | setHovered(true);
23 | }}
24 | onMouseLeave={() => setHovered(false)}
25 | onKeyDown={(e) => {
26 | if (e.key === 'Enter') {
27 | setHovered(true);
28 | }
29 | }}
30 | role="link"
31 | tabIndex={0}
32 | >
33 |
38 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default CompanyCard;
49 |
--------------------------------------------------------------------------------
/.github/workflows/preview.yml:
--------------------------------------------------------------------------------
1 | name: Overseerr Preview
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'preview-*'
7 |
8 | jobs:
9 | build_and_push:
10 | name: Build & Publish Docker Preview Images
11 | runs-on: ubuntu-20.04
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v2.3.4
15 | - name: Get the version
16 | id: get_version
17 | run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
18 | - name: Set up QEMU
19 | uses: docker/setup-qemu-action@v1.2.0
20 | - name: Set up Docker Buildx
21 | uses: docker/setup-buildx-action@v1.3.0
22 | - name: Log in to Docker Hub
23 | uses: docker/login-action@v1.9.0
24 | with:
25 | username: ${{ secrets.DOCKER_USERNAME }}
26 | password: ${{ secrets.DOCKER_TOKEN }}
27 | - name: Log in to GitHub Container Registry
28 | uses: docker/login-action@v1.9.0
29 | with:
30 | registry: ghcr.io
31 | username: ${{ github.repository_owner }}
32 | password: ${{ secrets.GITHUB_TOKEN }}
33 | - name: Build and push
34 | uses: docker/build-push-action@v2.5.0
35 | with:
36 | context: .
37 | file: ./Dockerfile
38 | platforms: linux/amd64
39 | push: true
40 | build-args: |
41 | COMMIT_TAG=${{ github.sha }}
42 | tags: |
43 | sctx/overseerr:${{ steps.get_version.outputs.VERSION }}
44 | ghcr.io/sct/overseerr:${{ steps.get_version.outputs.VERSION }}
45 |
--------------------------------------------------------------------------------
/src/components/Discover/Trending.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type {
3 | MovieResult,
4 | TvResult,
5 | PersonResult,
6 | } from '../../../server/models/Search';
7 | import ListView from '../Common/ListView';
8 | import { defineMessages, useIntl } from 'react-intl';
9 | import Header from '../Common/Header';
10 | import PageTitle from '../Common/PageTitle';
11 | import useDiscover from '../../hooks/useDiscover';
12 | import Error from '../../pages/_error';
13 |
14 | const messages = defineMessages({
15 | trending: 'Trending',
16 | });
17 |
18 | const Trending: React.FC = () => {
19 | const intl = useIntl();
20 | const {
21 | isLoadingInitialData,
22 | isEmpty,
23 | isLoadingMore,
24 | isReachingEnd,
25 | titles,
26 | fetchMore,
27 | error,
28 | } = useDiscover(
29 | '/api/v1/discover/trending'
30 | );
31 |
32 | if (error) {
33 | return ;
34 | }
35 |
36 | return (
37 | <>
38 |
39 |
40 | {intl.formatMessage(messages.trending)}
41 |
42 | 0)
47 | }
48 | isReachingEnd={isReachingEnd}
49 | onScrollBottom={fetchMore}
50 | />
51 | >
52 | );
53 | };
54 |
55 | export default Trending;
56 |
--------------------------------------------------------------------------------
/src/context/SettingsContext.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useSWR from 'swr';
3 | import { MediaServerType } from '../../server/constants/server';
4 | import { PublicSettingsResponse } from '../../server/interfaces/api/settingsInterfaces';
5 |
6 | export interface SettingsContextProps {
7 | currentSettings: PublicSettingsResponse;
8 | }
9 |
10 | const defaultSettings = {
11 | initialized: false,
12 | applicationTitle: 'Overseerr',
13 | applicationUrl: '',
14 | hideAvailable: false,
15 | localLogin: true,
16 | movie4kEnabled: false,
17 | series4kEnabled: false,
18 | region: '',
19 | originalLanguage: '',
20 | mediaServerType: MediaServerType.NOT_CONFIGURED,
21 | partialRequestsEnabled: true,
22 | cacheImages: false,
23 | vapidPublic: '',
24 | enablePushRegistration: false,
25 | locale: 'en',
26 | emailEnabled: false,
27 | };
28 |
29 | export const SettingsContext = React.createContext({
30 | currentSettings: defaultSettings,
31 | });
32 |
33 | export const SettingsProvider: React.FC = ({
34 | children,
35 | currentSettings,
36 | }) => {
37 | const { data, error } = useSWR(
38 | '/api/v1/settings/public',
39 | { initialData: currentSettings }
40 | );
41 |
42 | let newSettings = defaultSettings;
43 |
44 | if (data && !error) {
45 | newSettings = data;
46 | }
47 |
48 | return (
49 |
50 | {children}
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/i18n/globalMessages.ts:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 |
3 | const globalMessages = defineMessages({
4 | available: 'Available',
5 | partiallyavailable: 'Partially Available',
6 | processing: 'Processing',
7 | unavailable: 'Unavailable',
8 | notrequested: 'Not Requested',
9 | requested: 'Requested',
10 | requesting: 'Requesting…',
11 | request: 'Request',
12 | request4k: 'Request in 4K',
13 | failed: 'Failed',
14 | pending: 'Pending',
15 | declined: 'Declined',
16 | approved: 'Approved',
17 | movie: 'Movie',
18 | movies: 'Movies',
19 | tvshow: 'Series',
20 | tvshows: 'Series',
21 | cancel: 'Cancel',
22 | canceling: 'Canceling…',
23 | approve: 'Approve',
24 | decline: 'Decline',
25 | delete: 'Delete',
26 | retry: 'Retry',
27 | retrying: 'Retrying…',
28 | view: 'View',
29 | deleting: 'Deleting…',
30 | test: 'Test',
31 | testing: 'Testing…',
32 | save: 'Save Changes',
33 | saving: 'Saving…',
34 | close: 'Close',
35 | edit: 'Edit',
36 | areyousure: 'Are you sure?',
37 | back: 'Back',
38 | next: 'Next',
39 | previous: 'Previous',
40 | status: 'Status',
41 | all: 'All',
42 | experimental: 'Experimental',
43 | advanced: 'Advanced',
44 | loading: 'Loading…',
45 | settings: 'Settings',
46 | usersettings: 'User Settings',
47 | delimitedlist: '{a}, {b}',
48 | showingresults:
49 | 'Showing {from} to {to} of {total} results',
50 | resultsperpage: 'Display {pageSize} results per page',
51 | noresults: 'No results.',
52 | });
53 |
54 | export default globalMessages;
55 |
--------------------------------------------------------------------------------
/public/os_icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/jellyfin.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosError, AxiosResponse } from 'axios';
2 |
3 | interface JellyfinAuthenticationResult {
4 | Id: string;
5 | AccessToken: string;
6 | ServerId: string;
7 | }
8 |
9 | class JellyAPI {
10 | public login(
11 | Hostname?: string,
12 | Username?: string,
13 | Password?: string
14 | ): Promise {
15 | return new Promise(
16 | (
17 | resolve: (result: JellyfinAuthenticationResult) => void,
18 | reject: (e: Error) => void
19 | ) => {
20 | axios
21 | .post(
22 | Hostname + '/Users/AuthenticateByName',
23 | {
24 | Username: Username,
25 | Pw: Password,
26 | },
27 | {
28 | headers: {
29 | 'X-Emby-Authorization':
30 | 'MediaBrowser Client="Jellyfin Web", Device="Firefox", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0"',
31 | },
32 | }
33 | )
34 | .then((resp: AxiosResponse) => {
35 | const response: JellyfinAuthenticationResult = {
36 | Id: resp.data.User.Id,
37 | AccessToken: resp.data.AccessToken,
38 | ServerId: resp.data.ServerId,
39 | };
40 | resolve(response);
41 | })
42 | .catch((e: AxiosError) => {
43 | reject(e);
44 | });
45 | }
46 | );
47 | }
48 | }
49 |
50 | export default JellyAPI;
51 |
--------------------------------------------------------------------------------
/src/components/Common/SensitiveInput/index.tsx:
--------------------------------------------------------------------------------
1 | import { EyeIcon, EyeOffIcon } from '@heroicons/react/solid';
2 | import { Field } from 'formik';
3 | import React, { useState } from 'react';
4 |
5 | interface CustomInputProps extends React.ComponentProps<'input'> {
6 | as?: 'input';
7 | }
8 |
9 | interface CustomFieldProps extends React.ComponentProps {
10 | as?: 'field';
11 | }
12 |
13 | type SensitiveInputProps = CustomInputProps | CustomFieldProps;
14 |
15 | const SensitiveInput: React.FC = ({
16 | as = 'input',
17 | ...props
18 | }) => {
19 | const [isHidden, setHidden] = useState(true);
20 | const Component = as === 'input' ? 'input' : Field;
21 | const componentProps =
22 | as === 'input'
23 | ? props
24 | : {
25 | ...props,
26 | as: props.type === 'textarea' && !isHidden ? 'textarea' : undefined,
27 | };
28 | return (
29 | <>
30 |
41 |
51 | >
52 | );
53 | };
54 |
55 | export default SensitiveInput;
56 |
--------------------------------------------------------------------------------
/src/assets/extlogos/discord.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/services/imdb.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Common/ConfirmButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react';
2 | import useClickOutside from '../../../hooks/useClickOutside';
3 | import Button from '../Button';
4 |
5 | interface ConfirmButtonProps {
6 | onClick: () => void;
7 | confirmText: React.ReactNode;
8 | className?: string;
9 | }
10 |
11 | const ConfirmButton: React.FC = ({
12 | onClick,
13 | children,
14 | confirmText,
15 | className,
16 | }) => {
17 | const ref = useRef(null);
18 | useClickOutside(ref, () => setIsClicked(false));
19 | const [isClicked, setIsClicked] = useState(false);
20 | return (
21 |
54 | );
55 | };
56 |
57 | export default ConfirmButton;
58 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
7 | 'plugin:jsx-a11y/recommended',
8 | 'plugin:react/recommended',
9 | 'plugin:react-hooks/recommended',
10 | 'prettier',
11 | ],
12 | parserOptions: {
13 | ecmaVersion: 6,
14 | sourceType: 'module',
15 | ecmaFeatures: {
16 | jsx: true,
17 | },
18 | },
19 | rules: {
20 | '@typescript-eslint/camelcase': 0,
21 | '@typescript-eslint/no-use-before-define': 0,
22 | 'jsx-a11y/no-noninteractive-tabindex': 0,
23 | 'arrow-parens': 'off',
24 | 'jsx-a11y/anchor-is-valid': 'off',
25 | 'no-console': 1,
26 | 'react-hooks/rules-of-hooks': 'error',
27 | 'react-hooks/exhaustive-deps': 'warn',
28 | '@typescript-eslint/explicit-function-return-type': 'off',
29 | 'prettier/prettier': ['error', { endOfLine: 'auto' }],
30 | 'formatjs/no-offset': 'error',
31 | 'no-unused-vars': 'off',
32 | '@typescript-eslint/no-unused-vars': ['error'],
33 | 'jsx-a11y/no-onchange': 'off',
34 | },
35 | overrides: [
36 | {
37 | files: ['**/*.tsx'],
38 | rules: {
39 | 'react/prop-types': 'off',
40 | },
41 | },
42 | ],
43 | plugins: ['jsx-a11y', 'prettier', 'react-hooks', 'formatjs'],
44 | settings: {
45 | react: {
46 | pragma: 'React',
47 | version: '16.8',
48 | },
49 | },
50 | env: {
51 | browser: true,
52 | node: true,
53 | jest: true,
54 | es6: true,
55 | },
56 | reportUnusedDisableDirectives: true,
57 | };
58 |
--------------------------------------------------------------------------------
/server/utils/asyncLock.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 |
3 | // whenever you need to run async code on tv show or movie that does "get existing" / "check if need to create new" / "save"
4 | // then you need to put all of that code in "await asyncLock.dispatch" callback based on media id
5 | // this will guarantee that only one part of code will run at the same for this media id to avoid code
6 | // trying to create two or more entries for same movie/tvshow (which would result in sqlite unique constraint failrue)
7 |
8 | class AsyncLock {
9 | private locked: { [key: string]: boolean } = {};
10 | private ee = new EventEmitter();
11 |
12 | constructor() {
13 | this.ee.setMaxListeners(0);
14 | }
15 |
16 | private acquire = async (key: string) => {
17 | return new Promise((resolve) => {
18 | if (!this.locked[key]) {
19 | this.locked[key] = true;
20 | return resolve(undefined);
21 | }
22 |
23 | const nextAcquire = () => {
24 | if (!this.locked[key]) {
25 | this.locked[key] = true;
26 | this.ee.removeListener(key, nextAcquire);
27 | return resolve(undefined);
28 | }
29 | };
30 |
31 | this.ee.on(key, nextAcquire);
32 | });
33 | };
34 |
35 | private release = (key: string): void => {
36 | delete this.locked[key];
37 | setImmediate(() => this.ee.emit(key));
38 | };
39 |
40 | public dispatch = async (
41 | key: string | number,
42 | callback: () => Promise
43 | ): Promise => {
44 | const skey = String(key);
45 | await this.acquire(skey);
46 | try {
47 | await callback();
48 | } finally {
49 | this.release(skey);
50 | }
51 | };
52 | }
53 |
54 | export default AsyncLock;
55 |
--------------------------------------------------------------------------------
/src/components/Search/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useRouter } from 'next/router';
3 | import {
4 | TvResult,
5 | MovieResult,
6 | PersonResult,
7 | } from '../../../server/models/Search';
8 | import ListView from '../Common/ListView';
9 | import { defineMessages, useIntl } from 'react-intl';
10 | import Header from '../Common/Header';
11 | import PageTitle from '../Common/PageTitle';
12 | import Error from '../../pages/_error';
13 | import useDiscover from '../../hooks/useDiscover';
14 |
15 | const messages = defineMessages({
16 | search: 'Search',
17 | searchresults: 'Search Results',
18 | });
19 |
20 | const Search: React.FC = () => {
21 | const intl = useIntl();
22 | const router = useRouter();
23 |
24 | const {
25 | isLoadingInitialData,
26 | isEmpty,
27 | isLoadingMore,
28 | isReachingEnd,
29 | titles,
30 | fetchMore,
31 | error,
32 | } = useDiscover(
33 | `/api/v1/search`,
34 | {
35 | query: router.query.query,
36 | },
37 | { hideAvailable: false }
38 | );
39 |
40 | if (error) {
41 | return ;
42 | }
43 |
44 | return (
45 | <>
46 |
47 |
48 | {intl.formatMessage(messages.searchresults)}
49 |
50 | 0)
55 | }
56 | isReachingEnd={isReachingEnd}
57 | onScrollBottom={fetchMore}
58 | />
59 | >
60 | );
61 | };
62 |
63 | export default Search;
64 |
--------------------------------------------------------------------------------
/server/middleware/auth.ts:
--------------------------------------------------------------------------------
1 | import { getRepository } from 'typeorm';
2 | import { User } from '../entity/User';
3 | import { Permission, PermissionCheckOptions } from '../lib/permissions';
4 | import { getSettings } from '../lib/settings';
5 |
6 | export const checkUser: Middleware = async (req, _res, next) => {
7 | const settings = getSettings();
8 | let user: User | undefined;
9 |
10 | if (req.header('X-API-Key') === settings.main.apiKey) {
11 | const userRepository = getRepository(User);
12 |
13 | let userId = 1; // Work on original administrator account
14 |
15 | // If a User ID is provided, we will act on that user's behalf
16 | if (req.header('X-API-User')) {
17 | userId = Number(req.header('X-API-User'));
18 | }
19 |
20 | user = await userRepository.findOne({ where: { id: userId } });
21 | } else if (req.session?.userId) {
22 | const userRepository = getRepository(User);
23 |
24 | user = await userRepository.findOne({
25 | where: { id: req.session.userId },
26 | });
27 | }
28 |
29 | if (user) {
30 | req.user = user;
31 | }
32 |
33 | req.locale = user?.settings?.locale
34 | ? user.settings.locale
35 | : settings.main.locale;
36 |
37 | next();
38 | };
39 |
40 | export const isAuthenticated = (
41 | permissions?: Permission | Permission[],
42 | options?: PermissionCheckOptions
43 | ): Middleware => {
44 | const authMiddleware: Middleware = (req, res, next) => {
45 | if (!req.user || !req.user.hasPermission(permissions ?? 0, options)) {
46 | res.status(403).json({
47 | status: 403,
48 | error: 'You do not have permission to access this endpoint',
49 | });
50 | } else {
51 | next();
52 | }
53 | };
54 | return authMiddleware;
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/StatusChacker/index.tsx:
--------------------------------------------------------------------------------
1 | import { SparklesIcon } from '@heroicons/react/outline';
2 | import React from 'react';
3 | import { defineMessages, useIntl } from 'react-intl';
4 | import useSWR from 'swr';
5 | import { StatusResponse } from '../../../server/interfaces/api/settingsInterfaces';
6 | import Modal from '../Common/Modal';
7 | import Transition from '../Transition';
8 |
9 | const messages = defineMessages({
10 | newversionavailable: 'Application Update',
11 | newversionDescription:
12 | 'Overseerr has been updated! Please click the button below to reload the page.',
13 | reloadOverseerr: 'Reload',
14 | });
15 |
16 | const StatusChecker: React.FC = () => {
17 | const intl = useIntl();
18 | const { data, error } = useSWR('/api/v1/status', {
19 | refreshInterval: 60 * 1000,
20 | });
21 |
22 | if (!data && !error) {
23 | return null;
24 | }
25 |
26 | if (!data) {
27 | return null;
28 | }
29 |
30 | return (
31 |
41 | }
43 | title={intl.formatMessage(messages.newversionavailable)}
44 | onOk={() => location.reload()}
45 | okText={intl.formatMessage(messages.reloadOverseerr)}
46 | backgroundClickable={false}
47 | >
48 | {intl.formatMessage(messages.newversionDescription)}
49 |
50 |
51 | );
52 | };
53 |
54 | export default StatusChecker;
55 |
--------------------------------------------------------------------------------
/src/components/ServiceWorkerSetup/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import axios from 'axios';
3 | import React, { useEffect } from 'react';
4 | import useSettings from '../../hooks/useSettings';
5 | import { useUser } from '../../hooks/useUser';
6 |
7 | const ServiceWorkerSetup: React.FC = () => {
8 | const { currentSettings } = useSettings();
9 | const { user } = useUser();
10 | useEffect(() => {
11 | if ('serviceWorker' in navigator && user?.id) {
12 | navigator.serviceWorker
13 | .register('/sw.js')
14 | .then(async (registration) => {
15 | console.log(
16 | '[SW] Registration successful, scope is:',
17 | registration.scope
18 | );
19 |
20 | if (currentSettings.enablePushRegistration) {
21 | const sub = await registration.pushManager.subscribe({
22 | userVisibleOnly: true,
23 | applicationServerKey: currentSettings.vapidPublic,
24 | });
25 |
26 | const parsedSub = JSON.parse(JSON.stringify(sub));
27 |
28 | if (parsedSub.keys.p256dh && parsedSub.keys.auth) {
29 | await axios.post('/api/v1/user/registerPushSubscription', {
30 | endpoint: parsedSub.endpoint,
31 | p256dh: parsedSub.keys.p256dh,
32 | auth: parsedSub.keys.auth,
33 | });
34 | }
35 | }
36 | })
37 | .catch(function (error) {
38 | console.log('[SW] Service worker registration failed, error:', error);
39 | });
40 | }
41 | }, [
42 | user,
43 | currentSettings.vapidPublic,
44 | currentSettings.enablePushRegistration,
45 | ]);
46 | return null;
47 | };
48 |
49 | export default ServiceWorkerSetup;
50 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | Welcome to the Overseerr Documentation.
4 |
5 | ## Features
6 |
7 | - **Full Plex integration**. Login and manage user access with Plex.
8 | - **Syncs to your Plex library** to show what titles you already have.
9 | - **Integrates with Sonarr and Radarr**. With more services to come in the future.
10 | - **Easy to use request system** allowing users to request individual seasons or movies in a friendly, clean UI.
11 | - **Simple request management UI**. Don't dig through the app to approve recent requests.
12 | - **Mobile-friendly design**, for when you need to approve requests on the go.
13 | - Granular permission system.
14 | - Localization into other languages.
15 |
16 | ## Motivation
17 |
18 | The primary motivation for starting this project was to have an incredibly performant and easy to use application. There is a heavy focus on the user experience for both the server owner and the users. We feel requesting should be **effortless for the user**. Find the media you want, click request, and branch off efficiently into other titles that interest you, all in one seamless flow. For the server owner, Overseerr takes all the hassle out of approving your users' requests.
19 |
20 | ## We need your help!
21 |
22 | Overseerr is an ambitious project. We have already poured a lot of work into this, and have a lot more to do. We need your valuable feedback and help to find and fix bugs. Also, with Overseerr being an open-source project, anyone is welcome to contribute. Contribution includes building new features, patching bugs, translating the application, or even just writing documentation.
23 |
24 | If you would like to contribute, please be sure to review our [contribution guidelines](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md).
25 |
--------------------------------------------------------------------------------
/server/logger.ts:
--------------------------------------------------------------------------------
1 | import * as winston from 'winston';
2 | import 'winston-daily-rotate-file';
3 | import path from 'path';
4 | import fs from 'fs';
5 |
6 | // Migrate away from old log
7 | const OLD_LOG_FILE = path.join(__dirname, '../config/logs/overseerr.log');
8 | if (fs.existsSync(OLD_LOG_FILE)) {
9 | const file = fs.lstatSync(OLD_LOG_FILE);
10 |
11 | if (!file.isSymbolicLink()) {
12 | fs.unlinkSync(OLD_LOG_FILE);
13 | }
14 | }
15 |
16 | const hformat = winston.format.printf(
17 | ({ level, label, message, timestamp, ...metadata }) => {
18 | let msg = `${timestamp} [${level}]${
19 | label ? `[${label}]` : ''
20 | }: ${message} `;
21 | if (Object.keys(metadata).length > 0) {
22 | msg += JSON.stringify(metadata);
23 | }
24 | return msg;
25 | }
26 | );
27 |
28 | const logger = winston.createLogger({
29 | level: process.env.LOG_LEVEL || 'debug',
30 | format: winston.format.combine(
31 | winston.format.splat(),
32 | winston.format.timestamp(),
33 | hformat
34 | ),
35 | transports: [
36 | new winston.transports.Console({
37 | format: winston.format.combine(
38 | winston.format.colorize(),
39 | winston.format.splat(),
40 | winston.format.timestamp(),
41 | hformat
42 | ),
43 | }),
44 | new winston.transports.DailyRotateFile({
45 | filename: process.env.CONFIG_DIRECTORY
46 | ? `${process.env.CONFIG_DIRECTORY}/logs/overseerr-%DATE%.log`
47 | : path.join(__dirname, '../config/logs/overseerr-%DATE%.log'),
48 | datePattern: 'YYYY-MM-DD',
49 | zippedArchive: true,
50 | maxSize: '20m',
51 | maxFiles: '7d',
52 | createSymlink: true,
53 | symlinkName: 'overseerr.log',
54 | }),
55 | ],
56 | });
57 |
58 | export default logger;
59 |
--------------------------------------------------------------------------------
/src/components/Common/Alert/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ExclamationIcon,
3 | InformationCircleIcon,
4 | XCircleIcon,
5 | } from '@heroicons/react/solid';
6 | import React from 'react';
7 |
8 | interface AlertProps {
9 | title?: React.ReactNode;
10 | type?: 'warning' | 'info' | 'error';
11 | }
12 |
13 | const Alert: React.FC = ({ title, children, type }) => {
14 | let design = {
15 | bgColor: 'bg-yellow-600',
16 | titleColor: 'text-yellow-100',
17 | textColor: 'text-yellow-300',
18 | svg: ,
19 | };
20 |
21 | switch (type) {
22 | case 'info':
23 | design = {
24 | bgColor: 'bg-indigo-600',
25 | titleColor: 'text-indigo-100',
26 | textColor: 'text-indigo-300',
27 | svg: ,
28 | };
29 | break;
30 | case 'error':
31 | design = {
32 | bgColor: 'bg-red-600',
33 | titleColor: 'text-red-100',
34 | textColor: 'text-red-300',
35 | svg: ,
36 | };
37 | break;
38 | }
39 |
40 | return (
41 |
42 |
43 |
{design.svg}
44 |
45 | {title && (
46 |
47 | {title}
48 |
49 | )}
50 | {children && (
51 |
52 | {children}
53 |
54 | )}
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default Alert;
62 |
--------------------------------------------------------------------------------
/server/lib/email/index.ts:
--------------------------------------------------------------------------------
1 | import Email from 'email-templates';
2 | import nodemailer from 'nodemailer';
3 | import { URL } from 'url';
4 | import { getSettings, NotificationAgentEmail } from '../settings';
5 | import { openpgpEncrypt } from './openpgpEncrypt';
6 |
7 | class PreparedEmail extends Email {
8 | public constructor(settings: NotificationAgentEmail, pgpKey?: string) {
9 | const { applicationUrl } = getSettings().main;
10 |
11 | const transport = nodemailer.createTransport({
12 | name: applicationUrl ? new URL(applicationUrl).hostname : undefined,
13 | host: settings.options.smtpHost,
14 | port: settings.options.smtpPort,
15 | secure: settings.options.secure,
16 | ignoreTLS: settings.options.ignoreTls,
17 | requireTLS: settings.options.requireTls,
18 | tls: settings.options.allowSelfSigned
19 | ? {
20 | rejectUnauthorized: false,
21 | }
22 | : undefined,
23 | auth:
24 | settings.options.authUser && settings.options.authPass
25 | ? {
26 | user: settings.options.authUser,
27 | pass: settings.options.authPass,
28 | }
29 | : undefined,
30 | });
31 |
32 | if (pgpKey) {
33 | transport.use(
34 | 'stream',
35 | openpgpEncrypt({
36 | signingKey: settings.options.pgpPrivateKey,
37 | password: settings.options.pgpPassword,
38 | encryptionKeys: [pgpKey],
39 | })
40 | );
41 | }
42 |
43 | super({
44 | message: {
45 | from: {
46 | name: settings.options.senderName,
47 | address: settings.options.emailFrom,
48 | },
49 | },
50 | send: true,
51 | transport: transport,
52 | });
53 | }
54 | }
55 |
56 | export default PreparedEmail;
57 |
--------------------------------------------------------------------------------
/src/components/Discover/TvGenreList/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { defineMessages, useIntl } from 'react-intl';
3 | import useSWR from 'swr';
4 | import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
5 | import Error from '../../../pages/_error';
6 | import Header from '../../Common/Header';
7 | import LoadingSpinner from '../../Common/LoadingSpinner';
8 | import PageTitle from '../../Common/PageTitle';
9 | import GenreCard from '../../GenreCard';
10 | import { genreColorMap } from '../constants';
11 |
12 | const messages = defineMessages({
13 | seriesgenres: 'Series Genres',
14 | });
15 |
16 | const TvGenreList: React.FC = () => {
17 | const intl = useIntl();
18 | const { data, error } = useSWR(
19 | `/api/v1/discover/genreslider/tv`
20 | );
21 |
22 | if (!data && !error) {
23 | return ;
24 | }
25 |
26 | if (!data) {
27 | return ;
28 | }
29 |
30 | return (
31 | <>
32 |
33 |
34 | {intl.formatMessage(messages.seriesgenres)}
35 |
36 |
37 | {data.map((genre, index) => (
38 | -
39 |
47 |
48 | ))}
49 |
50 | >
51 | );
52 | };
53 |
54 | export default TvGenreList;
55 |
--------------------------------------------------------------------------------
/src/components/Discover/MovieGenreList/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { defineMessages, useIntl } from 'react-intl';
3 | import useSWR from 'swr';
4 | import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
5 | import Error from '../../../pages/_error';
6 | import Header from '../../Common/Header';
7 | import LoadingSpinner from '../../Common/LoadingSpinner';
8 | import PageTitle from '../../Common/PageTitle';
9 | import GenreCard from '../../GenreCard';
10 | import { genreColorMap } from '../constants';
11 |
12 | const messages = defineMessages({
13 | moviegenres: 'Movie Genres',
14 | });
15 |
16 | const MovieGenreList: React.FC = () => {
17 | const intl = useIntl();
18 | const { data, error } = useSWR(
19 | `/api/v1/discover/genreslider/movie`
20 | );
21 |
22 | if (!data && !error) {
23 | return ;
24 | }
25 |
26 | if (!data) {
27 | return ;
28 | }
29 |
30 | return (
31 | <>
32 |
33 |
34 | {intl.formatMessage(messages.moviegenres)}
35 |
36 |
37 | {data.map((genre, index) => (
38 | -
39 |
47 |
48 | ))}
49 |
50 | >
51 | );
52 | };
53 |
54 | export default MovieGenreList;
55 |
--------------------------------------------------------------------------------
/src/assets/tmdb_logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/TitleCard/TmdbTitleCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useInView } from 'react-intersection-observer';
3 | import useSWR from 'swr';
4 | import TitleCard from '.';
5 | import type { MovieDetails } from '../../../server/models/Movie';
6 | import type { TvDetails } from '../../../server/models/Tv';
7 |
8 | interface TmdbTitleCardProps {
9 | tmdbId: number;
10 | type: 'movie' | 'tv';
11 | }
12 |
13 | const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
14 | return (movie as MovieDetails).title !== undefined;
15 | };
16 |
17 | const TmdbTitleCard: React.FC = ({ tmdbId, type }) => {
18 | const { ref, inView } = useInView({
19 | triggerOnce: true,
20 | });
21 | const url =
22 | type === 'movie' ? `/api/v1/movie/${tmdbId}` : `/api/v1/tv/${tmdbId}`;
23 | const { data: title, error } = useSWR(
24 | inView ? `${url}` : null
25 | );
26 |
27 | if (!title && !error) {
28 | return (
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | if (!title) {
36 | return ;
37 | }
38 |
39 | return isMovie(title) ? (
40 |
50 | ) : (
51 |
61 | );
62 | };
63 |
64 | export default TmdbTitleCard;
65 |
--------------------------------------------------------------------------------
/src/components/Discover/TvGenreSlider/index.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowCircleRightIcon } from '@heroicons/react/outline';
2 | import Link from 'next/link';
3 | import React from 'react';
4 | import { defineMessages, useIntl } from 'react-intl';
5 | import useSWR from 'swr';
6 | import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
7 | import GenreCard from '../../GenreCard';
8 | import Slider from '../../Slider';
9 | import { genreColorMap } from '../constants';
10 |
11 | const messages = defineMessages({
12 | tvgenres: 'Series Genres',
13 | });
14 |
15 | const TvGenreSlider: React.FC = () => {
16 | const intl = useIntl();
17 | const { data, error } = useSWR(
18 | `/api/v1/discover/genreslider/tv`,
19 | {
20 | refreshInterval: 0,
21 | revalidateOnFocus: false,
22 | }
23 | );
24 |
25 | return (
26 | <>
27 |
35 | (
40 |
48 | ))}
49 | placeholder={}
50 | emptyMessage=""
51 | />
52 | >
53 | );
54 | };
55 |
56 | export default React.memo(TvGenreSlider);
57 |
--------------------------------------------------------------------------------
/src/components/Discover/DiscoverTvGenre/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { TvResult } from '../../../../server/models/Search';
3 | import ListView from '../../Common/ListView';
4 | import { defineMessages, useIntl } from 'react-intl';
5 | import Header from '../../Common/Header';
6 | import PageTitle from '../../Common/PageTitle';
7 | import { useRouter } from 'next/router';
8 | import globalMessages from '../../../i18n/globalMessages';
9 | import useDiscover from '../../../hooks/useDiscover';
10 | import Error from '../../../pages/_error';
11 |
12 | const messages = defineMessages({
13 | genreSeries: '{genre} Series',
14 | });
15 |
16 | const DiscoverTvGenre: React.FC = () => {
17 | const router = useRouter();
18 | const intl = useIntl();
19 |
20 | const {
21 | isLoadingInitialData,
22 | isEmpty,
23 | isLoadingMore,
24 | isReachingEnd,
25 | titles,
26 | fetchMore,
27 | error,
28 | firstResultData,
29 | } = useDiscover(
30 | `/api/v1/discover/tv/genre/${router.query.genreId}`
31 | );
32 |
33 | if (error) {
34 | return ;
35 | }
36 |
37 | const title = isLoadingInitialData
38 | ? intl.formatMessage(globalMessages.loading)
39 | : intl.formatMessage(messages.genreSeries, {
40 | genre: firstResultData?.genre.name,
41 | });
42 |
43 | return (
44 | <>
45 |
46 |
47 |
48 |
49 | 0)
54 | }
55 | isReachingEnd={isReachingEnd}
56 | onScrollBottom={fetchMore}
57 | />
58 | >
59 | );
60 | };
61 |
62 | export default DiscoverTvGenre;
63 |
--------------------------------------------------------------------------------
/src/components/Discover/MovieGenreSlider/index.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowCircleRightIcon } from '@heroicons/react/outline';
2 | import Link from 'next/link';
3 | import React from 'react';
4 | import { defineMessages, useIntl } from 'react-intl';
5 | import useSWR from 'swr';
6 | import { GenreSliderItem } from '../../../../server/interfaces/api/discoverInterfaces';
7 | import GenreCard from '../../GenreCard';
8 | import Slider from '../../Slider';
9 | import { genreColorMap } from '../constants';
10 |
11 | const messages = defineMessages({
12 | moviegenres: 'Movie Genres',
13 | });
14 |
15 | const MovieGenreSlider: React.FC = () => {
16 | const intl = useIntl();
17 | const { data, error } = useSWR(
18 | `/api/v1/discover/genreslider/movie`,
19 | {
20 | refreshInterval: 0,
21 | revalidateOnFocus: false,
22 | }
23 | );
24 |
25 | return (
26 | <>
27 |
35 | (
40 |
48 | ))}
49 | placeholder={}
50 | emptyMessage=""
51 | />
52 | >
53 | );
54 | };
55 |
56 | export default React.memo(MovieGenreSlider);
57 |
--------------------------------------------------------------------------------
/src/components/Discover/DiscoverMovieGenre/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { MovieResult } from '../../../../server/models/Search';
3 | import ListView from '../../Common/ListView';
4 | import { defineMessages, useIntl } from 'react-intl';
5 | import Header from '../../Common/Header';
6 | import PageTitle from '../../Common/PageTitle';
7 | import { useRouter } from 'next/router';
8 | import globalMessages from '../../../i18n/globalMessages';
9 | import useDiscover from '../../../hooks/useDiscover';
10 | import Error from '../../../pages/_error';
11 |
12 | const messages = defineMessages({
13 | genreMovies: '{genre} Movies',
14 | });
15 |
16 | const DiscoverMovieGenre: React.FC = () => {
17 | const router = useRouter();
18 | const intl = useIntl();
19 |
20 | const {
21 | isLoadingInitialData,
22 | isEmpty,
23 | isLoadingMore,
24 | isReachingEnd,
25 | titles,
26 | fetchMore,
27 | error,
28 | firstResultData,
29 | } = useDiscover(
30 | `/api/v1/discover/movies/genre/${router.query.genreId}`
31 | );
32 |
33 | if (error) {
34 | return ;
35 | }
36 |
37 | const title = isLoadingInitialData
38 | ? intl.formatMessage(globalMessages.loading)
39 | : intl.formatMessage(messages.genreMovies, {
40 | genre: firstResultData?.genre.name,
41 | });
42 |
43 | return (
44 | <>
45 |
46 |
47 |
48 |
49 | 0)
54 | }
55 | isReachingEnd={isReachingEnd}
56 | onScrollBottom={fetchMore}
57 | />
58 | >
59 | );
60 | };
61 |
62 | export default DiscoverMovieGenre;
63 |
--------------------------------------------------------------------------------
/src/components/PlexLoginButton/index.tsx:
--------------------------------------------------------------------------------
1 | import { LoginIcon } from '@heroicons/react/outline';
2 | import React, { useState } from 'react';
3 | import { defineMessages, useIntl } from 'react-intl';
4 | import globalMessages from '../../i18n/globalMessages';
5 | import PlexOAuth from '../../utils/plex';
6 |
7 | const messages = defineMessages({
8 | signinwithplex: 'Sign In',
9 | signingin: 'Signing In…',
10 | });
11 |
12 | const plexOAuth = new PlexOAuth();
13 |
14 | interface PlexLoginButtonProps {
15 | onAuthToken: (authToken: string) => void;
16 | isProcessing?: boolean;
17 | onError?: (message: string) => void;
18 | }
19 |
20 | const PlexLoginButton: React.FC = ({
21 | onAuthToken,
22 | onError,
23 | isProcessing,
24 | }) => {
25 | const intl = useIntl();
26 | const [loading, setLoading] = useState(false);
27 |
28 | const getPlexLogin = async () => {
29 | setLoading(true);
30 | try {
31 | const authToken = await plexOAuth.login();
32 | setLoading(false);
33 | onAuthToken(authToken);
34 | } catch (e) {
35 | if (onError) {
36 | onError(e.message);
37 | }
38 | setLoading(false);
39 | }
40 | };
41 | return (
42 |
43 |
61 |
62 | );
63 | };
64 |
65 | export default PlexLoginButton;
66 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const defaultTheme = require('tailwindcss/defaultTheme');
3 |
4 | module.exports = {
5 | mode: 'jit',
6 | purge: ['./src/pages/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}'],
7 | theme: {
8 | extend: {
9 | transitionProperty: {
10 | 'max-height': 'max-height',
11 | width: 'width',
12 | },
13 | fontFamily: {
14 | sans: ['Inter', ...defaultTheme.fontFamily.sans],
15 | },
16 | typography: (theme) => ({
17 | DEFAULT: {
18 | css: {
19 | color: theme('colors.gray.300'),
20 | a: {
21 | color: theme('colors.indigo.500'),
22 | '&:hover': {
23 | color: theme('colors.indigo.400'),
24 | },
25 | },
26 |
27 | h1: {
28 | color: theme('colors.gray.300'),
29 | },
30 | h2: {
31 | color: theme('colors.gray.300'),
32 | },
33 | h3: {
34 | color: theme('colors.gray.300'),
35 | },
36 | h4: {
37 | color: theme('colors.gray.300'),
38 | },
39 | h5: {
40 | color: theme('colors.gray.300'),
41 | },
42 | h6: {
43 | color: theme('colors.gray.300'),
44 | },
45 |
46 | strong: {
47 | color: theme('colors.gray.400'),
48 | },
49 |
50 | code: {
51 | color: theme('colors.gray.300'),
52 | },
53 |
54 | figcaption: {
55 | color: theme('colors.gray.500'),
56 | },
57 | },
58 | },
59 | }),
60 | },
61 | },
62 | plugins: [
63 | require('@tailwindcss/forms'),
64 | require('@tailwindcss/typography'),
65 | require('@tailwindcss/aspect-ratio'),
66 | ],
67 | };
68 |
--------------------------------------------------------------------------------
/server/lib/cache.ts:
--------------------------------------------------------------------------------
1 | import NodeCache from 'node-cache';
2 |
3 | export type AvailableCacheIds =
4 | | 'tmdb'
5 | | 'radarr'
6 | | 'sonarr'
7 | | 'rt'
8 | | 'github'
9 | | 'plexguid';
10 |
11 | const DEFAULT_TTL = 300;
12 | const DEFAULT_CHECK_PERIOD = 120;
13 |
14 | class Cache {
15 | public id: AvailableCacheIds;
16 | public data: NodeCache;
17 | public name: string;
18 |
19 | constructor(
20 | id: AvailableCacheIds,
21 | name: string,
22 | options: { stdTtl?: number; checkPeriod?: number } = {}
23 | ) {
24 | this.id = id;
25 | this.name = name;
26 | this.data = new NodeCache({
27 | stdTTL: options.stdTtl ?? DEFAULT_TTL,
28 | checkperiod: options.checkPeriod ?? DEFAULT_CHECK_PERIOD,
29 | });
30 | }
31 |
32 | public getStats() {
33 | return this.data.getStats();
34 | }
35 |
36 | public flush(): void {
37 | this.data.flushAll();
38 | }
39 | }
40 |
41 | class CacheManager {
42 | private availableCaches: Record = {
43 | tmdb: new Cache('tmdb', 'TMDb API', {
44 | stdTtl: 21600,
45 | checkPeriod: 60 * 30,
46 | }),
47 | radarr: new Cache('radarr', 'Radarr API'),
48 | sonarr: new Cache('sonarr', 'Sonarr API'),
49 | rt: new Cache('rt', 'Rotten Tomatoes API', {
50 | stdTtl: 43200,
51 | checkPeriod: 60 * 30,
52 | }),
53 | github: new Cache('github', 'GitHub API', {
54 | stdTtl: 21600,
55 | checkPeriod: 60 * 30,
56 | }),
57 | plexguid: new Cache('plexguid', 'Plex GUID Cache', {
58 | stdTtl: 86400 * 7, // 1 week cache
59 | checkPeriod: 60 * 30,
60 | }),
61 | };
62 |
63 | public getCache(id: AvailableCacheIds): Cache {
64 | return this.availableCaches[id];
65 | }
66 |
67 | public getAllCaches(): Record {
68 | return this.availableCaches;
69 | }
70 | }
71 |
72 | const cacheManager = new CacheManager();
73 |
74 | export default cacheManager;
75 |
--------------------------------------------------------------------------------
/server/lib/notifications/index.ts:
--------------------------------------------------------------------------------
1 | import logger from '../../logger';
2 | import type { NotificationAgent, NotificationPayload } from './agents/agent';
3 |
4 | export enum Notification {
5 | NONE = 0,
6 | MEDIA_PENDING = 2,
7 | MEDIA_APPROVED = 4,
8 | MEDIA_AVAILABLE = 8,
9 | MEDIA_FAILED = 16,
10 | TEST_NOTIFICATION = 32,
11 | MEDIA_DECLINED = 64,
12 | MEDIA_AUTO_APPROVED = 128,
13 | }
14 |
15 | export const hasNotificationType = (
16 | types: Notification | Notification[],
17 | value: number
18 | ): boolean => {
19 | let total = 0;
20 |
21 | // If we are not checking any notifications, bail out and return true
22 | if (types === 0) {
23 | return true;
24 | }
25 |
26 | if (Array.isArray(types)) {
27 | // Combine all notification values into one
28 | total = types.reduce((a, v) => a + v, 0);
29 | } else {
30 | total = types;
31 | }
32 |
33 | // Test notifications don't need to be enabled
34 | if (!(value & Notification.TEST_NOTIFICATION)) {
35 | value += Notification.TEST_NOTIFICATION;
36 | }
37 |
38 | return !!(value & total);
39 | };
40 |
41 | class NotificationManager {
42 | private activeAgents: NotificationAgent[] = [];
43 |
44 | public registerAgents = (agents: NotificationAgent[]): void => {
45 | this.activeAgents = [...this.activeAgents, ...agents];
46 | logger.info('Registered notification agents', { label: 'Notifications' });
47 | };
48 |
49 | public sendNotification(
50 | type: Notification,
51 | payload: NotificationPayload
52 | ): void {
53 | logger.info(`Sending notification(s) for ${Notification[type]}`, {
54 | label: 'Notifications',
55 | subject: payload.subject,
56 | });
57 |
58 | this.activeAgents.forEach((agent) => {
59 | if (agent.shouldSend()) {
60 | agent.send(type, payload);
61 | }
62 | });
63 | }
64 | }
65 |
66 | const notificationManager = new NotificationManager();
67 |
68 | export default notificationManager;
69 |
--------------------------------------------------------------------------------
/server/lib/permissions.ts:
--------------------------------------------------------------------------------
1 | export enum Permission {
2 | NONE = 0,
3 | ADMIN = 2,
4 | MANAGE_SETTINGS = 4,
5 | MANAGE_USERS = 8,
6 | MANAGE_REQUESTS = 16,
7 | REQUEST = 32,
8 | VOTE = 64,
9 | AUTO_APPROVE = 128,
10 | AUTO_APPROVE_MOVIE = 256,
11 | AUTO_APPROVE_TV = 512,
12 | REQUEST_4K = 1024,
13 | REQUEST_4K_MOVIE = 2048,
14 | REQUEST_4K_TV = 4096,
15 | REQUEST_ADVANCED = 8192,
16 | REQUEST_VIEW = 16384,
17 | AUTO_APPROVE_4K = 32768,
18 | AUTO_APPROVE_4K_MOVIE = 65536,
19 | AUTO_APPROVE_4K_TV = 131072,
20 | REQUEST_MOVIE = 262144,
21 | REQUEST_TV = 524288,
22 | }
23 |
24 | export interface PermissionCheckOptions {
25 | type: 'and' | 'or';
26 | }
27 |
28 | /**
29 | * Takes a Permission and the users permission value and determines
30 | * if the user has access to the permission provided. If the user has
31 | * the admin permission, true will always be returned from this check!
32 | *
33 | * @param permissions Single permission or array of permissions
34 | * @param value users current permission value
35 | * @param options Extra options to control permission check behavior (mainly for arrays)
36 | */
37 | export const hasPermission = (
38 | permissions: Permission | Permission[],
39 | value: number,
40 | options: PermissionCheckOptions = { type: 'and' }
41 | ): boolean => {
42 | let total = 0;
43 |
44 | // If we are not checking any permissions, bail out and return true
45 | if (permissions === 0) {
46 | return true;
47 | }
48 |
49 | if (Array.isArray(permissions)) {
50 | if (value & Permission.ADMIN) {
51 | return true;
52 | }
53 | switch (options.type) {
54 | case 'and':
55 | return permissions.every((permission) => !!(value & permission));
56 | case 'or':
57 | return permissions.some((permission) => !!(value & permission));
58 | }
59 | } else {
60 | total = permissions;
61 | }
62 |
63 | return !!(value & Permission.ADMIN) || !!(value & total);
64 | };
65 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Overseerr",
3 | "short_name": "Overseerr",
4 | "start_url": "./",
5 | "icons": [
6 | {
7 | "src": "./android-chrome-192x192.png",
8 | "sizes": "192x192",
9 | "type": "image/png",
10 | "purpose": "any"
11 | },
12 | {
13 | "src": "./android-chrome-192x192_maskable.png",
14 | "sizes": "192x192",
15 | "type": "image/png",
16 | "purpose": "maskable"
17 | },
18 | {
19 | "src": "./android-chrome-512x512.png",
20 | "sizes": "512x512",
21 | "type": "image/png",
22 | "purpose": "any"
23 | },
24 | {
25 | "src": "./android-chrome-512x512_maskable.png",
26 | "sizes": "512x512",
27 | "type": "image/png",
28 | "purpose": "maskable"
29 | }
30 | ],
31 | "theme_color": "#1f2937",
32 | "background_color": "#1f2937",
33 | "display": "standalone",
34 | "shortcuts": [
35 | {
36 | "name": "Discover",
37 | "url": "./",
38 | "icons": [
39 | {
40 | "src": "./sparkles-icon-192x192.png",
41 | "sizes": "192x192",
42 | "type": "image/png"
43 | }
44 | ]
45 | },
46 | {
47 | "name": "Requests",
48 | "url": "./requests",
49 | "icons": [
50 | {
51 | "src": "./clock-icon-192x192.png",
52 | "sizes": "192x192",
53 | "type": "image/png"
54 | }
55 | ]
56 | },
57 | {
58 | "name": "Profile",
59 | "url": "./profile",
60 | "icons": [
61 | {
62 | "src": "./user-icon-192x192.png",
63 | "sizes": "192x192",
64 | "type": "image/png"
65 | }
66 | ]
67 | },
68 | {
69 | "name": "Settings",
70 | "url": "./profile/settings",
71 | "icons": [
72 | {
73 | "src": "./cog-icon-192x192.png",
74 | "sizes": "192x192",
75 | "type": "image/png"
76 | }
77 | ]
78 | }
79 | ]
80 | }
81 |
--------------------------------------------------------------------------------