feat(rebase): rebase

This commit is contained in:
Aiden Vigue
2021-02-15 13:05:56 -05:00
parent 01cd9d3872
commit 3eb48abc14
20 changed files with 244 additions and 114 deletions

View File

@@ -14,5 +14,9 @@
"name": "Local SQLite", "name": "Local SQLite",
"database": "./config/db/db.sqlite3" "database": "./config/db/db.sqlite3"
} }
],
"i18n-ally.localesPaths": [
"src/i18n",
"src/i18n/locale"
] ]
} }

View File

@@ -128,7 +128,7 @@ components:
type: boolean type: boolean
example: true example: true
mediaServerType: mediaServerType:
type: string type: number
example: 'PLEX | JELLYFIN' example: 'PLEX | JELLYFIN'
defaultPermissions: defaultPermissions:
type: number type: number

View File

@@ -14,6 +14,7 @@ export interface JellyfinLoginResponse {
User: JellyfinUserResponse; User: JellyfinUserResponse;
AccessToken: string; AccessToken: string;
} }
export interface JellyfinLibrary { export interface JellyfinLibrary {
type: 'show' | 'movie'; type: 'show' | 'movie';
key: string; key: string;
@@ -45,6 +46,7 @@ export interface JellyfinMediaStream {
Language?: string; Language?: string;
DisplayTitle: string; DisplayTitle: string;
} }
export interface JellyfinMediaSource { export interface JellyfinMediaSource {
Protocol: string; Protocol: string;
Id: string; Id: string;
@@ -66,6 +68,7 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
IsHD?: boolean; IsHD?: boolean;
DateCreated?: string; DateCreated?: string;
} }
class JellyfinAPI { class JellyfinAPI {
private authToken?: string; private authToken?: string;
private jellyfinHost: string; private jellyfinHost: string;
@@ -78,12 +81,12 @@ class JellyfinAPI {
let authHeaderVal = ''; let authHeaderVal = '';
if (this.authToken) { if (this.authToken) {
authHeaderVal = authHeaderVal =
'MediaBrowser Client="Jellyfin Web", Device="Firefox", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0", Token="' + 'MediaBrowser Client="Overseerr", Device="Axios", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0", Token="' +
authToken + authToken +
'"'; '"';
} else { } else {
authHeaderVal = authHeaderVal =
'MediaBrowser Client="Jellyfin Web", Device="Firefox", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0"'; 'MediaBrowser Client="Overseerr", Device="Axios", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0"';
} }
this.axios = axios.create({ this.axios = axios.create({
@@ -131,8 +134,7 @@ class JellyfinAPI {
try { try {
const account = await this.axios.get<any>('/Library/MediaFolders'); const account = await this.axios.get<any>('/Library/MediaFolders');
// eslint-disable-next-line prefer-const const response: JellyfinLibrary[] = [];
let response: JellyfinLibrary[] = [];
account.data.Items.forEach((Item: any) => { account.data.Items.forEach((Item: any) => {
const library: JellyfinLibrary = { const library: JellyfinLibrary = {

View File

@@ -0,0 +1,5 @@
export enum MediaServerType {
PLEX = 1,
JELLYFIN, //also works for emby (identical APIs, etc)
NOT_CONFIGURED,
}

View File

@@ -18,6 +18,7 @@ import { getSettings } from '../lib/settings';
import RadarrAPI from '../api/radarr'; import RadarrAPI from '../api/radarr';
import downloadTracker, { DownloadingItem } from '../lib/downloadtracker'; import downloadTracker, { DownloadingItem } from '../lib/downloadtracker';
import SonarrAPI from '../api/sonarr'; import SonarrAPI from '../api/sonarr';
import { MediaServerType } from '../constants/server';
@Entity() @Entity()
class Media { class Media {
@@ -134,45 +135,40 @@ class Media {
public ratingKey4k?: string; public ratingKey4k?: string;
@Column({ nullable: true }) @Column({ nullable: true })
public jellyfinMediaID?: string; public jellyfinMediaId?: string;
@Column({ nullable: true }) @Column({ nullable: true })
public jellyfinMediaID4k?: string; public jellyfinMediaId4k?: string;
public serviceUrl?: string; public serviceUrl?: string;
public serviceUrl4k?: string; public serviceUrl4k?: string;
public downloadStatus?: DownloadingItem[] = []; public downloadStatus?: DownloadingItem[] = [];
public downloadStatus4k?: DownloadingItem[] = []; public downloadStatus4k?: DownloadingItem[] = [];
public plexUrl?: string; public mediaUrl?: string;
public plexUrl4k?: string; public mediaUrl4k?: string;
public jellyfinUrl?: string;
public jellyfinUrl4k?: string;
constructor(init?: Partial<Media>) { constructor(init?: Partial<Media>) {
Object.assign(this, init); Object.assign(this, init);
} }
@AfterLoad() @AfterLoad()
public setPlexUrls(): void { public setMediaUrls(): void {
const machineId = getSettings().plex.machineId; const settings = getSettings();
if (this.ratingKey) { if (settings.main.mediaServerType == MediaServerType.PLEX) {
this.plexUrl = `https://app.plex.tv/desktop#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey}`; if (this.ratingKey) {
} this.mediaUrl = `https://app.plex.tv/desktop#!/server/${settings.plex.machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey}`;
if (this.ratingKey4k) { }
this.plexUrl4k = `https://app.plex.tv/desktop#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}`; if (this.ratingKey4k) {
} this.mediaUrl4k = `https://app.plex.tv/desktop#!/server/${settings.plex.machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}`;
} }
} else {
@AfterLoad() if (this.jellyfinMediaId) {
public setJellyfinUrls(): void { this.mediaUrl = `${settings.jellyfin.hostname}/web/#!/details?id=${this.jellyfinMediaId}&context=home&serverId=${settings.jellyfin.serverID}`;
const jellyfinSettings = getSettings().jellyfin; }
if (this.jellyfinMediaID) { if (this.jellyfinMediaId4k) {
this.jellyfinUrl = `${jellyfinSettings.hostname}/web/#!/details?id=${this.jellyfinMediaID}&context=home&serverId=${jellyfinSettings.serverID}`; this.mediaUrl4k = `${settings.jellyfin.hostname}/web/#!/details?id=${this.jellyfinMediaId4k}&context=home&serverId=${settings.jellyfin.serverID}`;
} }
if (this.jellyfinMediaID4k) {
this.jellyfinUrl4k = `${jellyfinSettings.hostname}/web/#!/details?id=${this.jellyfinMediaID4k}&context=home&serverId=${jellyfinSettings.serverID}`;
} }
} }

View File

@@ -206,6 +206,7 @@ export class User {
@AfterLoad() @AfterLoad()
public setDisplayName(): void { public setDisplayName(): void {
this.displayName = this.username || this.plexUsername; this.displayName =
this.username || this.plexUsername || this.jellyfinUsername;
} }
} }

View File

@@ -133,7 +133,6 @@ app
* OpenAPI validator. Otherwise, they are treated as objects instead of strings * OpenAPI validator. Otherwise, they are treated as objects instead of strings
* and response validation will fail * and response validation will fail
*/ */
server.use((_req, res, next) => { server.use((_req, res, next) => {
const original = res.json; const original = res.json;
res.json = function jsonp(json) { res.json = function jsonp(json) {

View File

@@ -6,7 +6,7 @@ export interface SettingsAboutResponse {
} }
export interface PublicSettingsResponse { export interface PublicSettingsResponse {
jfHost?: string; jellyfinHost?: string;
initialized: boolean; initialized: boolean;
applicationTitle: string; applicationTitle: string;
hideAvailable: boolean; hideAvailable: boolean;
@@ -15,7 +15,7 @@ export interface PublicSettingsResponse {
series4kEnabled: boolean; series4kEnabled: boolean;
region: string; region: string;
originalLanguage: string; originalLanguage: string;
mediaServerType: string; mediaServerType: number;
} }
export interface CacheItem { export interface CacheItem {

View File

@@ -11,6 +11,7 @@ import Season from '../../entity/Season';
import { uniqWith } from 'lodash'; import { uniqWith } from 'lodash';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import AsyncLock from '../../utils/asyncLock'; import AsyncLock from '../../utils/asyncLock';
import { MediaServerType } from '../../constants/server';
const BUNDLE_SIZE = 20; const BUNDLE_SIZE = 20;
const UPDATE_RATE = 4 * 1000; const UPDATE_RATE = 4 * 1000;
@@ -125,18 +126,18 @@ class JobJellyfinSync {
if ( if (
(hasOtherResolution || (has4k && !this.enable4kMovie)) && (hasOtherResolution || (has4k && !this.enable4kMovie)) &&
existing.jellyfinMediaID !== metadata.Id existing.jellyfinMediaId !== metadata.Id
) { ) {
existing.jellyfinMediaID = metadata.Id; existing.jellyfinMediaId = metadata.Id;
changedExisting = true; changedExisting = true;
} }
if ( if (
has4k && has4k &&
this.enable4kMovie && this.enable4kMovie &&
existing.jellyfinMediaID4k !== metadata.Id existing.jellyfinMediaId4k !== metadata.Id
) { ) {
existing.jellyfinMediaID4k = metadata.Id; existing.jellyfinMediaId4k = metadata.Id;
changedExisting = true; changedExisting = true;
} }
@@ -162,11 +163,11 @@ class JobJellyfinSync {
: MediaStatus.UNKNOWN; : MediaStatus.UNKNOWN;
newMedia.mediaType = MediaType.MOVIE; newMedia.mediaType = MediaType.MOVIE;
newMedia.mediaAddedAt = new Date(metadata.DateCreated ?? ''); newMedia.mediaAddedAt = new Date(metadata.DateCreated ?? '');
newMedia.jellyfinMediaID = newMedia.jellyfinMediaId =
hasOtherResolution || (!this.enable4kMovie && has4k) hasOtherResolution || (!this.enable4kMovie && has4k)
? metadata.Id ? metadata.Id
: undefined; : undefined;
newMedia.jellyfinMediaID4k = newMedia.jellyfinMediaId4k =
has4k && this.enable4kMovie ? metadata.Id : undefined; has4k && this.enable4kMovie ? metadata.Id : undefined;
await mediaRepository.save(newMedia); await mediaRepository.save(newMedia);
this.log(`Saved ${metadata.Name}`); this.log(`Saved ${metadata.Name}`);
@@ -212,7 +213,7 @@ class JobJellyfinSync {
return; return;
} }
// Lets get the available seasons from Plex // Lets get the available seasons from Jellyfin
const seasons = tvShow.seasons; const seasons = tvShow.seasons;
const media = await this.getExisting(tvShow.id, MediaType.TV); const media = await this.getExisting(tvShow.id, MediaType.TV);
@@ -229,7 +230,6 @@ class JobJellyfinSync {
) ?? [] ) ?? []
).length; ).length;
//girl bye idk what's happening here! LMFAO
for (const season of seasons) { for (const season of seasons) {
const JellyfinSeasons = await this.jfClient.getSeasons(Id); const JellyfinSeasons = await this.jfClient.getSeasons(Id);
const matchedJellyfinSeason = JellyfinSeasons.find( const matchedJellyfinSeason = JellyfinSeasons.find(
@@ -242,7 +242,7 @@ class JobJellyfinSync {
// Check if we found the matching season and it has all the available episodes // Check if we found the matching season and it has all the available episodes
if (matchedJellyfinSeason) { if (matchedJellyfinSeason) {
// If we have a matched Plex season, get its children metadata so we can check details // If we have a matched Jellyfin season, get its children metadata so we can check details
const episodes = await this.jfClient.getEpisodes( const episodes = await this.jfClient.getEpisodes(
Id, Id,
matchedJellyfinSeason.Id matchedJellyfinSeason.Id
@@ -279,18 +279,18 @@ class JobJellyfinSync {
if ( if (
media && media &&
(totalStandard > 0 || (total4k > 0 && !this.enable4kShow)) && (totalStandard > 0 || (total4k > 0 && !this.enable4kShow)) &&
media.jellyfinMediaID !== Id media.jellyfinMediaId !== Id
) { ) {
media.jellyfinMediaID = Id; media.jellyfinMediaId = Id;
} }
if ( if (
media && media &&
total4k > 0 && total4k > 0 &&
this.enable4kShow && this.enable4kShow &&
media.jellyfinMediaID4k !== Id media.jellyfinMediaId4k !== Id
) { ) {
media.jellyfinMediaID4k = Id; media.jellyfinMediaId4k = Id;
} }
if (existingSeason) { if (existingSeason) {
@@ -438,8 +438,8 @@ class JobJellyfinSync {
tmdbId: tvShow.id, tmdbId: tvShow.id,
tvdbId: tvShow.external_ids.tvdb_id, tvdbId: tvShow.external_ids.tvdb_id,
mediaAddedAt: new Date(metadata.DateCreated ?? ''), mediaAddedAt: new Date(metadata.DateCreated ?? ''),
jellyfinMediaID: Id, jellyfinMediaId: Id,
jellyfinMediaID4k: Id, jellyfinMediaId4k: Id,
status: isAllStandardSeasons status: isAllStandardSeasons
? MediaStatus.AVAILABLE ? MediaStatus.AVAILABLE
: newSeasons.some( : newSeasons.some(
@@ -538,7 +538,7 @@ class JobJellyfinSync {
public async run(): Promise<void> { public async run(): Promise<void> {
const settings = getSettings(); const settings = getSettings();
if (settings.main.mediaServerType != 'JELLYFIN') { if (settings.main.mediaServerType != MediaServerType.JELLYFIN) {
return; return;
} }

View File

@@ -15,6 +15,7 @@ import { uniqWith } from 'lodash';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import animeList from '../../api/animelist'; import animeList from '../../api/animelist';
import AsyncLock from '../../utils/asyncLock'; import AsyncLock from '../../utils/asyncLock';
import { MediaServerType } from '../../constants/server';
const BUNDLE_SIZE = 20; const BUNDLE_SIZE = 20;
const UPDATE_RATE = 4 * 1000; const UPDATE_RATE = 4 * 1000;
@@ -803,7 +804,7 @@ class JobPlexSync {
public async run(): Promise<void> { public async run(): Promise<void> {
const settings = getSettings(); const settings = getSettings();
if (settings.main.mediaServerType != 'PLEX') { if (settings.main.mediaServerType != MediaServerType.PLEX) {
return; return;
} }

View File

@@ -3,6 +3,7 @@ import path from 'path';
import { merge } from 'lodash'; import { merge } from 'lodash';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Permission } from './permissions'; import { Permission } from './permissions';
import { MediaServerType } from '../constants/server';
export interface Library { export interface Library {
id: string; id: string;
@@ -81,7 +82,7 @@ export interface MainSettings {
region: string; region: string;
originalLanguage: string; originalLanguage: string;
trustProxy: boolean; trustProxy: boolean;
mediaServerType: string; mediaServerType: number;
} }
interface PublicSettings { interface PublicSettings {
@@ -96,8 +97,8 @@ interface FullPublicSettings extends PublicSettings {
series4kEnabled: boolean; series4kEnabled: boolean;
region: string; region: string;
originalLanguage: string; originalLanguage: string;
mediaServerType: string; mediaServerType: number;
jfHost?: string; jellyfinHost?: string;
} }
export interface NotificationAgentConfig { export interface NotificationAgentConfig {
@@ -208,7 +209,7 @@ class Settings {
region: '', region: '',
originalLanguage: '', originalLanguage: '',
trustProxy: false, trustProxy: false,
mediaServerType: '', mediaServerType: MediaServerType.NOT_CONFIGURED,
}, },
plex: { plex: {
name: '', name: '',
@@ -372,8 +373,12 @@ class Settings {
originalLanguage: this.data.main.originalLanguage, originalLanguage: this.data.main.originalLanguage,
======= =======
mediaServerType: this.main.mediaServerType, mediaServerType: this.main.mediaServerType,
<<<<<<< HEAD
jfHost: this.jellyfin.hostname ?? '', jfHost: this.jellyfin.hostname ?? '',
>>>>>>> feat(all): add initial Jellyfin/Emby support >>>>>>> feat(all): add initial Jellyfin/Emby support
=======
jellyfinHost: this.jellyfin.hostname,
>>>>>>> feat(rebase): rebase
}; };
} }

View File

@@ -8,6 +8,7 @@ import { Permission } from '../lib/permissions';
import logger from '../logger'; import logger from '../logger';
import { getSettings } from '../lib/settings'; import { getSettings } from '../lib/settings';
import { UserType } from '../constants/user'; import { UserType } from '../constants/user';
import { MediaServerType } from '../constants/server';
const authRoutes = Router(); const authRoutes = Router();
@@ -32,13 +33,18 @@ authRoutes.post('/plex', async (req, res, next) => {
const userRepository = getRepository(User); const userRepository = getRepository(User);
const body = req.body as { const body = req.body as {
authToken?: string; authToken?: string;
mediaServerType?: string;
jellyfinHostname?: string;
}; };
if (!body.authToken) { if (!body.authToken) {
return res.status(500).json({ error: 'You must provide an auth token' }); return res.status(500).json({ error: 'You must provide an auth token' });
} }
if (
settings.main.mediaServerType != MediaServerType.PLEX &&
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED
) {
return res.status(500).json({ error: 'Plex login disabled' });
}
try { try {
// First we need to use this auth token to get the users email from plex.tv // First we need to use this auth token to get the users email from plex.tv
const plextv = new PlexTvAPI(body.authToken); const plextv = new PlexTvAPI(body.authToken);
@@ -80,6 +86,9 @@ authRoutes.post('/plex', async (req, res, next) => {
userType: UserType.PLEX, userType: UserType.PLEX,
}); });
await userRepository.save(user); await userRepository.save(user);
//Since we created the admin user, go ahead and set the mediaservertype to PLEX
settings.main.mediaServerType = MediaServerType.PLEX;
} }
// Double check that we didn't create the first admin user before running this // Double check that we didn't create the first admin user before running this
@@ -138,7 +147,10 @@ authRoutes.post('/plex', async (req, res, next) => {
} }
}); });
//Stop LGTM from alerting
// eslint-disable-next-line prettier/prettier
authRoutes.post('/jellyfin', async (req, res, next) => { authRoutes.post('/jellyfin', async (req, res, next) => {
//lgtm [js/missing-rate-limiting]
const settings = getSettings(); const settings = getSettings();
const userRepository = getRepository(User); const userRepository = getRepository(User);
const body = req.body as { const body = req.body as {
@@ -149,7 +161,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
//Make sure jellyfin login is enabled, but only if jellyfin is not already configured //Make sure jellyfin login is enabled, but only if jellyfin is not already configured
if ( if (
settings.main.mediaServerType != 'JELLYFIN' && settings.main.mediaServerType != MediaServerType.JELLYFIN &&
settings.jellyfin.hostname != '' settings.jellyfin.hostname != ''
) { ) {
return res.status(500).json({ error: 'Jellyfin login is disabled' }); return res.status(500).json({ error: 'Jellyfin login is disabled' });
@@ -170,10 +182,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
settings.jellyfin.hostname != '' settings.jellyfin.hostname != ''
? settings.jellyfin.hostname ? settings.jellyfin.hostname
: body.hostname; : body.hostname;
// First we need to use this auth token to get the users email from plex tv // First we need to attempt to log the user in to jellyfin
const plextv = new JellyfinAPI(hostname ?? ''); const jellyfinserver = new JellyfinAPI(hostname ?? '');
const account = await plextv.login(body.username, body.password); const account = await jellyfinserver.login(body.username, body.password);
// Next let's see if the user already exists // Next let's see if the user already exists
let user = await userRepository.findOne({ let user = await userRepository.findOne({
@@ -181,12 +193,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
}); });
if (user) { if (user) {
// Let's check if their plex token is up to date // Let's check if their authtoken is up to date
if (user.jellyfinAuthToken !== account.AccessToken) { if (user.jellyfinAuthToken !== account.AccessToken) {
user.jellyfinAuthToken = account.AccessToken; user.jellyfinAuthToken = account.AccessToken;
} }
// Update the users avatar with their plex thumbnail (incase it changed) // Update the users avatar with their jellyfin profile pic (incase it changed)
user.avatar = `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`; user.avatar = `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
user.email = account.User.Name; user.email = account.User.Name;
user.jellyfinUsername = account.User.Name; user.jellyfinUsername = account.User.Name;
@@ -215,30 +227,16 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
//Update hostname in settings if it doesn't exist (initial configuration) //Update hostname in settings if it doesn't exist (initial configuration)
//Also set mediaservertype to JELLYFIN //Also set mediaservertype to JELLYFIN
if (settings.jellyfin.hostname == '') { if (settings.jellyfin.hostname == '') {
settings.main.mediaServerType = 'JELLYFIN'; settings.main.mediaServerType = MediaServerType.JELLYFIN;
settings.jellyfin.hostname = body.hostname ?? ''; settings.jellyfin.hostname = body.hostname ?? '';
settings.save(); settings.save();
} }
} }
// Double check that we didn't create the first admin user before running this
if (!user) {
user = new User({
email: account.User.Name,
jellyfinUsername: account.User.Name,
jellyfinId: account.User.Id,
jellyfinAuthToken: account.AccessToken,
permissions: settings.main.defaultPermissions,
avatar: `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`,
userType: UserType.JELLYFIN,
});
await userRepository.save(user);
}
} }
// Set logged in session // Set logged in session
if (req.session) { if (req.session) {
req.session.userId = user.id; req.session.userId = user?.id;
} }
return res.status(200).json(user?.filter() ?? {}); return res.status(200).json(user?.filter() ?? {});

View File

@@ -237,7 +237,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
as="a" as="a"
buttonType="ghost" buttonType="ghost"
href={ href={
settings.currentSettings.jfHost + settings.currentSettings.jellyfinHost +
'/web/#!/forgotpassword.html' '/web/#!/forgotpassword.html'
} }
> >

View File

@@ -12,6 +12,7 @@ import LocalLogin from './LocalLogin';
import Accordion from '../Common/Accordion'; import Accordion from '../Common/Accordion';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import { MediaServerType } from '../../../server/constants/server';
const messages = defineMessages({ const messages = defineMessages({
signin: 'Sign In', signin: 'Sign In',
@@ -136,13 +137,15 @@ const Login: React.FC = () => {
onClick={() => handleClick(0)} onClick={() => handleClick(0)}
disabled={!settings.currentSettings.localLogin} disabled={!settings.currentSettings.localLogin}
> >
{settings.currentSettings.mediaServerType == 'PLEX' {settings.currentSettings.mediaServerType ==
MediaServerType.PLEX
? intl.formatMessage(messages.signinwithplex) ? intl.formatMessage(messages.signinwithplex)
: intl.formatMessage(messages.signinwithjellyfin)} : intl.formatMessage(messages.signinwithjellyfin)}
</button> </button>
<AccordionContent isOpen={openIndexes.includes(0)}> <AccordionContent isOpen={openIndexes.includes(0)}>
<div className="px-10 py-8"> <div className="px-10 py-8">
{settings.currentSettings.mediaServerType == 'PLEX' ? ( {settings.currentSettings.mediaServerType ==
MediaServerType.PLEX ? (
<PlexLoginButton <PlexLoginButton
isProcessing={isProcessing} isProcessing={isProcessing}
onAuthToken={(authToken) => setAuthToken(authToken)} onAuthToken={(authToken) => setAuthToken(authToken)}

View File

@@ -35,7 +35,11 @@ import ConfirmButton from '../Common/ConfirmButton';
import DownloadBlock from '../DownloadBlock'; import DownloadBlock from '../DownloadBlock';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
<<<<<<< HEAD
import PlayButton, { PlayButtonLink } from '../Common/PlayButton'; import PlayButton, { PlayButtonLink } from '../Common/PlayButton';
=======
import { MediaServerType } from '../../../server/constants/server';
>>>>>>> 2fe4add... feat(rebase): rebase
const messages = defineMessages({ const messages = defineMessages({
releasedate: 'Release Date', releasedate: 'Release Date',
@@ -392,12 +396,8 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<StatusBadge <StatusBadge
status={data.mediaInfo?.status} status={data.mediaInfo?.status}
inProgress={(data.mediaInfo.downloadStatus ?? []).length > 0} inProgress={(data.mediaInfo.downloadStatus ?? []).length > 0}
plexUrl={ plexUrl={data.mediaInfo?.mediaUrl}
data.mediaInfo?.plexUrl ?? data.mediaInfo?.jellyfinUrl plexUrl4k={data.mediaInfo?.mediaUrl4k}
}
plexUrl4k={
data.mediaInfo?.plexUrl4k ?? data.mediaInfo?.jellyfinUrl4k
}
/> />
</span> </span>
)} )}
@@ -406,12 +406,12 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
status={data.mediaInfo?.status4k} status={data.mediaInfo?.status4k}
is4k is4k
inProgress={(data.mediaInfo?.downloadStatus4k ?? []).length > 0} inProgress={(data.mediaInfo?.downloadStatus4k ?? []).length > 0}
plexUrl={data.mediaInfo?.plexUrl ?? data.mediaInfo?.jellyfinUrl} plexUrl={data.mediaInfo?.mediaUrl}
plexUrl4k={ plexUrl4k={
data.mediaInfo?.plexUrl4k && data.mediaInfo?.mediaUrl4k &&
(hasPermission(Permission.REQUEST_4K) || (hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_MOVIE)) hasPermission(Permission.REQUEST_4K_MOVIE))
? data.mediaInfo.plexUrl4k ?? data.mediaInfo?.jellyfinUrl4k ? data.mediaInfo.mediaUrl4k
: undefined : undefined
} }
/> />
@@ -453,9 +453,114 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</span> </span>
</div> </div>
<div className="relative z-10 flex flex-wrap justify-center flex-shrink-0 mt-4 sm:justify-end sm:flex-nowrap lg:mt-0"> <div className="relative z-10 flex flex-wrap justify-center flex-shrink-0 mt-4 sm:justify-end sm:flex-nowrap lg:mt-0">
<<<<<<< HEAD
<div className="mb-3 sm:mb-0"> <div className="mb-3 sm:mb-0">
<PlayButton links={mediaLinks} /> <PlayButton links={mediaLinks} />
</div> </div>
=======
{trailerUrl ||
data.mediaInfo?.mediaUrl ||
data.mediaInfo?.mediaUrl4k ? (
<ButtonWithDropdown
buttonType="ghost"
text={
<>
<svg
className="w-5 h-5 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>
{data.mediaInfo?.mediaUrl || data.mediaInfo?.mediaUrl
? intl.formatMessage(
settings.currentSettings.mediaServerType ==
MediaServerType.PLEX
? messages.playonplex
: messages.playonjellyfin
)
: data.mediaInfo?.mediaUrl4k &&
(hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_MOVIE))
? intl.formatMessage(
settings.currentSettings.mediaServerType ==
MediaServerType.PLEX
? messages.playonplex
: messages.playonjellyfin
)
: intl.formatMessage(messages.watchtrailer)}
</span>
</>
}
onClick={() => {
if (data.mediaInfo?.mediaUrl) {
window.open(data.mediaInfo?.mediaUrl, '_blank');
} else if (data.mediaInfo?.mediaUrl4k) {
window.open(data.mediaInfo?.mediaUrl4k, '_blank');
} else if (trailerUrl) {
window.open(trailerUrl, '_blank');
}
}}
>
{(
trailerUrl
? data.mediaInfo?.mediaUrl ||
(data.mediaInfo?.mediaUrl4k &&
(hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_MOVIE)))
: data.mediaInfo?.mediaUrl &&
data.mediaInfo?.mediaUrl4k &&
(hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_MOVIE))
) ? (
<>
{data.mediaInfo?.mediaUrl &&
data.mediaInfo?.mediaUrl4k &&
(hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_MOVIE)) && (
<ButtonWithDropdown.Item
onClick={() => {
window.open(data.mediaInfo?.mediaUrl4k, '_blank');
}}
buttonType="ghost"
>
{intl.formatMessage(
settings.currentSettings.mediaServerType ==
MediaServerType.PLEX
? messages.play4konplex
: messages.play4konjellyfin
)}
</ButtonWithDropdown.Item>
)}
{trailerUrl && (
<ButtonWithDropdown.Item
onClick={() => {
window.open(trailerUrl, '_blank');
}}
buttonType="ghost"
>
{intl.formatMessage(messages.watchtrailer)}
</ButtonWithDropdown.Item>
)}
</>
) : null}
</ButtonWithDropdown>
) : null}
>>>>>>> 2fe4add... feat(rebase): rebase
<div className="mb-3 sm:mb-0"> <div className="mb-3 sm:mb-0">
<RequestButton <RequestButton
mediaType="movie" mediaType="movie"
@@ -696,7 +801,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
tvdbId={data.externalIds.tvdbId} tvdbId={data.externalIds.tvdbId}
imdbId={data.externalIds.imdbId} imdbId={data.externalIds.imdbId}
rtUrl={ratingData?.url} rtUrl={ratingData?.url}
plexUrl={data.mediaInfo?.plexUrl ?? data.mediaInfo?.plexUrl4k} plexUrl={data.mediaInfo?.mediaUrl ?? data.mediaInfo?.mediaUrl4k}
/> />
</div> </div>
</div> </div>

View File

@@ -14,6 +14,7 @@ import globalMessages from '../../i18n/globalMessages';
import PermissionEdit from '../PermissionEdit'; import PermissionEdit from '../PermissionEdit';
import * as Yup from 'yup'; import * as Yup from 'yup';
import RegionSelector from '../RegionSelector'; import RegionSelector from '../RegionSelector';
import { MediaServerType } from '../../../server/constants/server';
const messages = defineMessages({ const messages = defineMessages({
generalsettings: 'General Settings', generalsettings: 'General Settings',
@@ -123,7 +124,8 @@ const SettingsMain: React.FC = () => {
region: data?.region, region: data?.region,
originalLanguage: data?.originalLanguage, originalLanguage: data?.originalLanguage,
trustProxy: data?.trustProxy, trustProxy: data?.trustProxy,
useJellyfin: data?.mediaServerType == 'JELLYFIN' ? true : false, useJellyfin:
data?.mediaServerType == MediaServerType.JELLYFIN ? true : false,
}} }}
enableReinitialize enableReinitialize
validationSchema={MainSettingsSchema} validationSchema={MainSettingsSchema}
@@ -139,7 +141,9 @@ const SettingsMain: React.FC = () => {
region: values.region, region: values.region,
originalLanguage: values.originalLanguage, originalLanguage: values.originalLanguage,
trustProxy: values.trustProxy, trustProxy: values.trustProxy,
mediaServerType: values.useJellyfin ? 'JELLYFIN' : 'PLEX', mediaServerType: values.useJellyfin
? MediaServerType.JELLYFIN
: MediaServerType.PLEX,
}); });
addToast(intl.formatMessage(messages.toastSettingsSuccess), { addToast(intl.formatMessage(messages.toastSettingsSuccess), {

View File

@@ -27,7 +27,7 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
useEffect(() => { useEffect(() => {
const login = async () => { const login = async () => {
const response = await axios.post('/api/v1/auth/login', { const response = await axios.post('/api/v1/auth/plex', {
authToken: authToken, authToken: authToken,
}); });

View File

@@ -33,7 +33,11 @@ import ConfirmButton from '../Common/ConfirmButton';
import DownloadBlock from '../DownloadBlock'; import DownloadBlock from '../DownloadBlock';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
<<<<<<< HEAD
import PlayButton, { PlayButtonLink } from '../Common/PlayButton'; import PlayButton, { PlayButtonLink } from '../Common/PlayButton';
=======
import { MediaServerType } from '../../../server/constants/server';
>>>>>>> 2fe4add... feat(rebase): rebase
const messages = defineMessages({ const messages = defineMessages({
firstAirDate: 'First Air Date', firstAirDate: 'First Air Date',
@@ -413,12 +417,8 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<StatusBadge <StatusBadge
status={data.mediaInfo?.status} status={data.mediaInfo?.status}
inProgress={(data.mediaInfo.downloadStatus ?? []).length > 0} inProgress={(data.mediaInfo.downloadStatus ?? []).length > 0}
plexUrl={ plexUrl={data.mediaInfo?.mediaUrl}
data.mediaInfo?.plexUrl ?? data.mediaInfo?.jellyfinUrl plexUrl4k={data.mediaInfo?.mediaUrl4k}
}
plexUrl4k={
data.mediaInfo?.plexUrl4k ?? data.mediaInfo?.jellyfinUrl4k
}
/> />
</span> </span>
)} )}
@@ -426,12 +426,12 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
<StatusBadge <StatusBadge
status={data.mediaInfo?.status} status={data.mediaInfo?.status}
inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0} inProgress={(data.mediaInfo?.downloadStatus ?? []).length > 0}
plexUrl={data.mediaInfo?.plexUrl ?? data.mediaInfo?.jellyfinUrl} plexUrl={data.mediaInfo?.mediaUrl}
plexUrl4k={ plexUrl4k={
data.mediaInfo?.plexUrl4k && data.mediaInfo?.mediaUrl4k &&
(hasPermission(Permission.REQUEST_4K) || (hasPermission(Permission.REQUEST_4K) ||
hasPermission(Permission.REQUEST_4K_TV)) hasPermission(Permission.REQUEST_4K_TV))
? data.mediaInfo.plexUrl4k ?? data.mediaInfo?.jellyfinUrl4k ? data.mediaInfo.mediaUrl4k
: undefined : undefined
} }
/> />
@@ -709,7 +709,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
tvdbId={data.externalIds.tvdbId} tvdbId={data.externalIds.tvdbId}
imdbId={data.externalIds.imdbId} imdbId={data.externalIds.imdbId}
rtUrl={ratingData?.url} rtUrl={ratingData?.url}
plexUrl={data.mediaInfo?.plexUrl ?? data.mediaInfo?.plexUrl4k} plexUrl={data.mediaInfo?.mediaUrl ?? data.mediaInfo?.mediaUrl4k}
/> />
</div> </div>
</div> </div>

View File

@@ -22,6 +22,8 @@ import BulkEditModal from './BulkEditModal';
import PageTitle from '../Common/PageTitle'; import PageTitle from '../Common/PageTitle';
import Link from 'next/link'; import Link from 'next/link';
import type { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces'; import type { UserResultsResponse } from '../../../server/interfaces/api/userInterfaces';
import useSettings from '../../hooks/useSettings';
import { MediaServerType } from '../../../server/constants/server';
const messages = defineMessages({ const messages = defineMessages({
users: 'Users', users: 'Users',
@@ -78,6 +80,7 @@ type Sort = 'created' | 'updated' | 'requests' | 'displayname';
const UserList: React.FC = () => { const UserList: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const router = useRouter(); const router = useRouter();
const settings = useSettings();
const { addToast } = useToasts(); const { addToast } = useToasts();
const [pageIndex, setPageIndex] = useState(0); const [pageIndex, setPageIndex] = useState(0);
const [currentSort, setCurrentSort] = useState<Sort>('created'); const [currentSort, setCurrentSort] = useState<Sort>('created');
@@ -408,14 +411,17 @@ const UserList: React.FC = () => {
> >
{intl.formatMessage(messages.createlocaluser)} {intl.formatMessage(messages.createlocaluser)}
</Button> </Button>
<Button {settings.currentSettings.mediaServerType ==
className="flex-grow outline lg:mr-2" MediaServerType.PLEX && (
buttonType="primary" <Button
disabled={isImporting} className="flex-grow outline lg:mr-2"
onClick={() => importFromPlex()} buttonType="primary"
> disabled={isImporting}
{intl.formatMessage(messages.importfromplex)} onClick={() => importFromPlex()}
</Button> >
{intl.formatMessage(messages.importfromplex)}
</Button>
)}
</div> </div>
<div className="flex flex-grow mb-2 lg:mb-0 lg:flex-grow-0"> <div className="flex flex-grow mb-2 lg:mb-0 lg:flex-grow-0">
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md"> <span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">

View File

@@ -599,7 +599,8 @@
"components.Setup.finish": "Finish Setup", "components.Setup.finish": "Finish Setup",
"components.Setup.finishing": "Finishing…", "components.Setup.finishing": "Finishing…",
"components.Setup.setup": "Setup", "components.Setup.setup": "Setup",
"components.Setup.signinMessage": "Get started by signing in with an account", "components.Setup.signin": "Sign In",
"components.Setup.signinMessage": "Get started by signing in",
"components.Setup.signinWithJellyfin": "Use Jellyfin", "components.Setup.signinWithJellyfin": "Use Jellyfin",
"components.Setup.syncingbackground": "Syncing will run in the background. You can continue the setup process in the meantime.", "components.Setup.syncingbackground": "Syncing will run in the background. You can continue the setup process in the meantime.",
"components.Setup.tip": "Tip", "components.Setup.tip": "Tip",