diff --git a/.all-contributorsrc b/.all-contributorsrc index fac1d1f01..31a4cc63b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -97,6 +97,33 @@ "contributions": [ "infra" ] + }, + { + "login": "Shutruk", + "name": "Shutruk", + "avatar_url": "https://avatars2.githubusercontent.com/u/9198633?v=4", + "profile": "https://github.com/Shutruk", + "contributions": [ + "translation" + ] + }, + { + "login": "krystiancharubin", + "name": "Krystian Charubin", + "avatar_url": "https://avatars2.githubusercontent.com/u/17775600?v=4", + "profile": "https://github.com/krystiancharubin", + "contributions": [ + "design" + ] + }, + { + "login": "kieron", + "name": "Kieron Boswell", + "avatar_url": "https://avatars2.githubusercontent.com/u/8655212?v=4", + "profile": "https://github.com/kieron", + "contributions": [ + "code" + ] } ], "badgeTemplate": "\"All-orange.svg\"/>", diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 40ae9700e..6d369e18a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,8 +9,10 @@ assignees: '' **Describe the bug** A clear and concise description of what the bug is. -**Are you on latest or develop branch?** -Please fill in which docker image you are currently using. +**What version of Overseerr are you running?** +Please fill in the version you are currently running. + +You can find it under: Settings -> About -> Version **To Reproduce** Steps to reproduce the behavior: diff --git a/.github/workflows/invalid_template.yml b/.github/workflows/invalid_template.yml new file mode 100644 index 000000000..c98d3174b --- /dev/null +++ b/.github/workflows/invalid_template.yml @@ -0,0 +1,19 @@ +name: 'Invalid Template' + +on: + issues: + types: [labeled, unlabeled, reopened] + +jobs: + support: + runs-on: ubuntu-latest + steps: + - uses: dessant/support-requests@v2 + with: + github-token: ${{ github.token }} + support-label: 'invalid:template-incomplete' + issue-comment: > + :wave: @{issue-author}, please edit your issue and follow the template provided. + close-issue: false + lock-issue: true + issue-lock-reason: 'resolved' diff --git a/README.md b/README.md index 544b2cd68..6c52d0153 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Language grade: JavaScript GitHub -All Contributors +All Contributors

@@ -86,7 +86,13 @@ If you would like to chat with community members you can join the [Overseerr Dis Our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md) applies to all Overseerr community channels. -## Contributors +## Contributing + +You can help build Overseerr too! Check out our [Contribution Guide](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md) to get started. + +## Contributors ✨ + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): @@ -105,13 +111,13 @@ Our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_COND
jvennik

🌍
darknessgp

💻
salty

🚇 +
Shutruk

🌍 +
Krystian Charubin

🎨 +
Kieron Boswell

💻 - - -## Contributing -You can help build Overseerr too! Check out our [Contribution Guide](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md) to get started. + diff --git a/overseerr-api.yml b/overseerr-api.yml index 07da768af..90c2bcb77 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -643,7 +643,6 @@ components: readOnly: true requestedBy: $ref: '#/components/schemas/User' - readOnly: true modifiedBy: anyOf: - $ref: '#/components/schemas/User' @@ -967,6 +966,10 @@ components: type: apiKey name: connect.sid in: cookie + apiKey: + type: apiKey + in: header + name: X-Api-Key paths: /settings/main: @@ -2486,3 +2489,4 @@ paths: security: - cookieAuth: [] + - apiKey: [] diff --git a/public/site.webmanifest b/public/site.webmanifest index 45dc8a206..3f47bc7cb 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1 +1 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#1e2937","display":"standalone"} diff --git a/server/api/plexapi.ts b/server/api/plexapi.ts index c8e123710..cc71b07e7 100644 --- a/server/api/plexapi.ts +++ b/server/api/plexapi.ts @@ -4,10 +4,12 @@ import { getSettings } from '../lib/settings'; export interface PlexLibraryItem { ratingKey: string; parentRatingKey?: string; + grandparentRatingKey?: string; title: string; guid: string; parentGuid?: string; - type: 'movie' | 'show' | 'season'; + grandparentGuid?: string; + type: 'movie' | 'show' | 'season' | 'episode'; } interface PlexLibraryResponse { @@ -20,6 +22,7 @@ export interface PlexLibrary { type: 'show' | 'movie'; key: string; title: string; + agent: string; } interface PlexLibrariesResponse { @@ -120,9 +123,9 @@ class PlexAPI { return response.MediaContainer.Metadata[0]; } - public async getRecentlyAdded(): Promise { + public async getRecentlyAdded(id: string): Promise { const response = await this.plexClient.query( - '/library/recentlyAdded' + `/library/sections/${id}/recentlyAdded` ); return response.MediaContainer.Metadata; diff --git a/server/index.ts b/server/index.ts index 6c469371e..657c2bfeb 100644 --- a/server/index.ts +++ b/server/index.ts @@ -17,9 +17,11 @@ import { startJobs } from './job/schedule'; import notificationManager from './lib/notifications'; import DiscordAgent from './lib/notifications/agents/discord'; import EmailAgent from './lib/notifications/agents/email'; +import { getAppVersion } from './utils/appVersion'; const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml'); +logger.info(`Starting Overseerr version ${getAppVersion()}`); const dev = process.env.NODE_ENV !== 'production'; const app = next({ dev }); const handle = app.getRequestHandler(); @@ -101,7 +103,7 @@ app const port = Number(process.env.PORT) || 3000; server.listen(port, () => { logger.info(`Server ready on port ${port}`, { - label: 'SERVER', + label: 'Server', }); }); }) diff --git a/server/job/plexsync/index.ts b/server/job/plexsync/index.ts index cf1b3823b..87e078d40 100644 --- a/server/job/plexsync/index.ts +++ b/server/job/plexsync/index.ts @@ -8,7 +8,8 @@ import logger from '../../logger'; import { getSettings, Library } from '../../lib/settings'; import Season from '../../entity/Season'; -const BUNDLE_SIZE = 10; +const BUNDLE_SIZE = 20; +const UPDATE_RATE = 4 * 1000; const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/); const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/); @@ -136,7 +137,9 @@ class JobPlexSync { try { const metadata = await this.plexClient.getMetadata( - plexitem.parentRatingKey ?? plexitem.ratingKey, + plexitem.grandparentRatingKey ?? + plexitem.parentRatingKey ?? + plexitem.ratingKey, { includeChildren: true } ); if (metadata.guid.match(tvdbRegex)) { @@ -239,9 +242,15 @@ class JobPlexSync { } catch (e) { this.log( `Failed to process plex item. ratingKey: ${ - plexitem.parentRatingKey ?? plexitem.ratingKey + plexitem.grandparentRatingKey ?? + plexitem.parentRatingKey ?? + plexitem.ratingKey }`, - 'error' + 'error', + { + errorMessage: e.message, + plexitem, + } ); } } @@ -251,7 +260,11 @@ class JobPlexSync { slicedItems.map(async (plexitem) => { if (plexitem.type === 'movie') { await this.processMovie(plexitem); - } else if (plexitem.type === 'show') { + } else if ( + plexitem.type === 'show' || + plexitem.type === 'episode' || + plexitem.type === 'season' + ) { await this.processShow(plexitem); } }) @@ -277,16 +290,17 @@ class JobPlexSync { end: end + BUNDLE_SIZE, }); resolve(); - }, 5000) + }, UPDATE_RATE) ); } } private log( message: string, - level: 'info' | 'error' | 'debug' = 'debug' + level: 'info' | 'error' | 'debug' = 'debug', + optional?: Record ): void { - logger[level](message, { label: 'Plex Sync' }); + logger[level](message, { label: 'Plex Sync', ...optional }); } public async run(): Promise { @@ -300,20 +314,22 @@ class JobPlexSync { }); this.plexClient = new PlexAPI({ plexToken: admin.plexToken }); + + this.libraries = settings.plex.libraries.filter( + (library) => library.enabled + ); + if (this.isRecentOnly) { - this.currentLibrary = { - id: '0', - name: 'Recently Added', - enabled: true, - }; - this.log(`Beginning to process recently added`, 'info'); - this.items = await this.plexClient.getRecentlyAdded(); - await this.loop(); + for (const library of this.libraries) { + this.currentLibrary = library; + this.log( + `Beginning to process recently added for library: ${library.name}`, + 'info' + ); + this.items = await this.plexClient.getRecentlyAdded(library.id); + await this.loop(); + } } else { - this.libraries = settings.plex.libraries.filter( - (library) => library.enabled - ); - for (const library of this.libraries) { this.currentLibrary = library; this.log(`Beginning to process library: ${library.name}`, 'info'); diff --git a/server/job/schedule.ts b/server/job/schedule.ts index 43ef1113e..4ce2f8f87 100644 --- a/server/job/schedule.ts +++ b/server/job/schedule.ts @@ -13,7 +13,7 @@ export const startJobs = (): void => { // Run recently added plex sync every 5 minutes scheduledJobs.push({ name: 'Plex Recently Added Sync', - job: schedule.scheduleJob('0 */10 * * * *', () => { + job: schedule.scheduleJob('0 */5 * * * *', () => { logger.info('Starting scheduled job: Plex Recently Added Sync', { label: 'Jobs', }); @@ -23,7 +23,7 @@ export const startJobs = (): void => { // Run full plex sync every 6 hours scheduledJobs.push({ name: 'Plex Full Library Sync', - job: schedule.scheduleJob('0 0 */6 * * *', () => { + job: schedule.scheduleJob('0 0 3 * * *', () => { logger.info('Starting scheduled job: Plex Full Sync', { label: 'Jobs' }); jobPlexFullSync.run(); }), diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 050c98e70..6ba96f608 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -135,7 +135,7 @@ class EmailAgent implements NotificationAgent { to: payload.notifyUser.email, }, locals: { - body: 'Your requsested media is now available!', + body: 'Your requested media is now available!', mediaName: payload.subject, imageUrl: payload.image, timestamp: new Date().toTimeString(), diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts index 97814772b..f541c3d60 100644 --- a/server/middleware/auth.ts +++ b/server/middleware/auth.ts @@ -1,9 +1,25 @@ import { getRepository } from 'typeorm'; import { User } from '../entity/User'; import { Permission } from '../lib/permissions'; +import { getSettings } from '../lib/settings'; export const checkUser: Middleware = async (req, _res, next) => { - if (req.session?.userId) { + const settings = getSettings(); + if (req.header('X-API-Key') === settings.main.apiKey) { + const userRepository = getRepository(User); + + let userId = 1; // Work on original administrator account + + // If a User ID is provided, we will act on that users behalf + if (req.header('X-API-User')) { + userId = Number(req.header('X-API-User')); + } + const user = await userRepository.findOne({ where: { id: userId } }); + + if (user) { + req.user = user; + } + } else if (req.session?.userId) { const userRepository = getRepository(User); const user = await userRepository.findOne({ diff --git a/server/routes/settings.ts b/server/routes/settings.ts index fee4e5c78..25a702b07 100644 --- a/server/routes/settings.ts +++ b/server/routes/settings.ts @@ -19,6 +19,7 @@ import { isAuthenticated } from '../middleware/auth'; import { merge } from 'lodash'; import Media from '../entity/Media'; import { MediaRequest } from '../entity/MediaRequest'; +import { getAppVersion } from '../utils/appVersion'; const settingsRoutes = Router(); @@ -93,17 +94,22 @@ settingsRoutes.get('/plex/library', async (req, res) => { const libraries = await plexapi.getLibraries(); - const newLibraries: Library[] = libraries.map((library) => { - const existing = settings.plex.libraries.find( - (l) => l.id === library.key - ); - - return { - id: library.key, - name: library.title, - enabled: existing?.enabled ?? false, - }; - }); + const newLibraries: Library[] = libraries + // Remove libraries that are not movie or show + .filter((library) => library.type === 'movie' || library.type === 'show') + // Remove libraries that do not have a metadata agent set (usually personal video libraries) + .filter((library) => library.agent !== 'com.plexapp.agents.none') + .map((library) => { + const existing = settings.plex.libraries.find( + (l) => l.id === library.key && l.name === library.title + ); + + return { + id: library.key, + name: library.title, + enabled: existing?.enabled ?? false, + }; + }); settings.plex.libraries = newLibraries; } @@ -440,16 +446,8 @@ settingsRoutes.get('/about', async (req, res) => { const totalMediaItems = await mediaRepository.count(); const totalRequests = await mediaRequestRepository.count(); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { version } = require('../../package.json'); - - let finalVersion = version; - - if (version === '0.1.0') { - finalVersion = `develop-${process.env.COMMIT_TAG ?? 'local'}`; - } return res.status(200).json({ - version: finalVersion, + version: getAppVersion(), totalMediaItems, totalRequests, }); diff --git a/server/utils/appVersion.ts b/server/utils/appVersion.ts new file mode 100644 index 000000000..ef9f35c3b --- /dev/null +++ b/server/utils/appVersion.ts @@ -0,0 +1,12 @@ +export const getAppVersion = (): string => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { version } = require('../../package.json'); + + let finalVersion = version; + + if (version === '0.1.0') { + finalVersion = `develop-${process.env.COMMIT_TAG ?? 'local'}`; + } + + return finalVersion; +}; diff --git a/src/components/Layout/LanguagePicker/index.tsx b/src/components/Layout/LanguagePicker/index.tsx index a3c065449..13f66de8e 100644 --- a/src/components/Layout/LanguagePicker/index.tsx +++ b/src/components/Layout/LanguagePicker/index.tsx @@ -45,6 +45,10 @@ const availableLanguages: AvailableLanguageObject = { code: 'nl', display: 'Nederlands', }, + es: { + code: 'es', + display: 'Spanish', + }, }; const LanguagePicker: React.FC = () => { diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index b5de5f2e8..b6e44f37d 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -1,9 +1,10 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useRef } from 'react'; import Transition from '../../Transition'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { defineMessages, FormattedMessage } from 'react-intl'; import { useUser, Permission } from '../../../hooks/useUser'; +import useClickOutside from '../../../hooks/useClickOutside'; const messages = defineMessages({ dashboard: 'Discover', @@ -116,8 +117,10 @@ const SidebarLinks: SidebarLinkProps[] = [ ]; const Sidebar: React.FC = ({ open, setClosed }) => { + const navRef = useRef(null); const router = useRouter(); const { hasPermission } = useUser(); + useClickOutside(navRef, () => setClosed()); return ( <>
@@ -166,7 +169,10 @@ const Sidebar: React.FC = ({ open, setClosed }) => {
-
+
diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 57e4779db..df99de949 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -193,14 +193,14 @@ const MovieDetails: React.FC = ({ movie }) => { )}
-
+
-
+
{data.mediaInfo?.status === MediaStatus.AVAILABLE && ( @@ -352,7 +352,7 @@ const MovieDetails: React.FC = ({ movie }) => { {hasPermission(Permission.MANAGE_REQUESTS) && (
- diff --git a/src/components/Settings/SettingsLogs/index.tsx b/src/components/Settings/SettingsLogs/index.tsx new file mode 100644 index 000000000..9c6d9cf79 --- /dev/null +++ b/src/components/Settings/SettingsLogs/index.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +// We will localize this file when the complete version is released. + +const SettingsLogs: React.FC = () => { + return ( + <> +
+ Logs page is still being built. For now, you can access your logs + directly in stdout (container logs) or looking in{' '} + /app/config/logs/overseerr.logs +
+ + ); +}; + +export default SettingsLogs; diff --git a/src/components/Setup/index.tsx b/src/components/Setup/index.tsx index d4cacd43f..7fc0b2d6a 100644 --- a/src/components/Setup/index.tsx +++ b/src/components/Setup/index.tsx @@ -8,6 +8,7 @@ import LoginWithPlex from './LoginWithPlex'; import SetupSteps from './SetupSteps'; import axios from 'axios'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import Badge from '../Common/Badge'; const messages = defineMessages({ finish: 'Finish Setup', @@ -16,6 +17,9 @@ const messages = defineMessages({ loginwithplex: 'Login with Plex', configureplex: 'Configure Plex', configureservices: 'Configure Services', + tip: 'Tip', + syncingbackground: + 'Syncing will run in the background. You can continue the setup process in the meantime.', }); const Setup: React.FC = () => { @@ -85,6 +89,12 @@ const Setup: React.FC = () => { {currentStep === 2 && (
setPlexSettingsComplete(true)} /> +
+ + {intl.formatMessage(messages.tip)} + + {intl.formatMessage(messages.syncingbackground)} +
diff --git a/src/components/TitleCard/index.tsx b/src/components/TitleCard/index.tsx index 5202c1928..443f06a1b 100644 --- a/src/components/TitleCard/index.tsx +++ b/src/components/TitleCard/index.tsx @@ -10,10 +10,11 @@ import Link from 'next/link'; import { MediaStatus } from '../../../server/constants/media'; import RequestModal from '../RequestModal'; import { defineMessages, useIntl } from 'react-intl'; +import { useIsTouch } from '../../hooks/useIsTouch'; const messages = defineMessages({ - movie: 'MOVIE', - tvshow: 'SERIES', + movie: 'Movie', + tvshow: 'Series', }); interface TitleCardProps { @@ -38,6 +39,7 @@ const TitleCard: React.FC = ({ mediaType, canExpand = false, }) => { + const isTouch = useIsTouch(); const intl = useIntl(); const [isUpdating, setIsUpdating] = useState(false); const [currentStatus, setCurrentStatus] = useState(status); @@ -74,9 +76,13 @@ const TitleCard: React.FC = ({
{ + if (!isTouch) { + setShowDetail(true); + } }} - onMouseEnter={() => setShowDetail(true)} onMouseLeave={() => setShowDetail(false)} onClick={() => setShowDetail(true)} onKeyDown={(e) => { @@ -146,158 +152,162 @@ const TitleCard: React.FC = ({ -
-
-
-
{year}
+ + +
+
+
{year}
-

- {title} -

-
- {summary} +

+ {title} +

+
+ {summary} +
-
-
- - - - - - - - - {(!currentStatus || - currentStatus === MediaStatus.UNKNOWN) && ( - - )} - {currentStatus === MediaStatus.PENDING && ( - + )} + {currentStatus === MediaStatus.PENDING && ( + - )} - {currentStatus === MediaStatus.PROCESSING && ( - + )} + {currentStatus === MediaStatus.PROCESSING && ( + - )} - {(currentStatus === MediaStatus.AVAILABLE || - currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && ( - + )} + {(currentStatus === MediaStatus.AVAILABLE || + currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && ( + - )} + + + + + )} +
-
-
+ +
diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 85fec8981..451a119f8 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -196,14 +196,14 @@ const TvDetails: React.FC = ({ tv }) => { )}
-
+
-
+
{data.mediaInfo?.status === MediaStatus.AVAILABLE && ( @@ -344,7 +344,7 @@ const TvDetails: React.FC = ({ tv }) => { {hasPermission(Permission.MANAGE_REQUESTS) && (