mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-03 05:09:43 -05:00
feat(rebase): rebase
This commit is contained in:
@@ -14,6 +14,7 @@ export interface JellyfinLoginResponse {
|
||||
User: JellyfinUserResponse;
|
||||
AccessToken: string;
|
||||
}
|
||||
|
||||
export interface JellyfinLibrary {
|
||||
type: 'show' | 'movie';
|
||||
key: string;
|
||||
@@ -45,6 +46,7 @@ export interface JellyfinMediaStream {
|
||||
Language?: string;
|
||||
DisplayTitle: string;
|
||||
}
|
||||
|
||||
export interface JellyfinMediaSource {
|
||||
Protocol: string;
|
||||
Id: string;
|
||||
@@ -66,6 +68,7 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
||||
IsHD?: boolean;
|
||||
DateCreated?: string;
|
||||
}
|
||||
|
||||
class JellyfinAPI {
|
||||
private authToken?: string;
|
||||
private jellyfinHost: string;
|
||||
@@ -78,12 +81,12 @@ class JellyfinAPI {
|
||||
let authHeaderVal = '';
|
||||
if (this.authToken) {
|
||||
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 +
|
||||
'"';
|
||||
} else {
|
||||
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({
|
||||
@@ -131,8 +134,7 @@ class JellyfinAPI {
|
||||
try {
|
||||
const account = await this.axios.get<any>('/Library/MediaFolders');
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let response: JellyfinLibrary[] = [];
|
||||
const response: JellyfinLibrary[] = [];
|
||||
|
||||
account.data.Items.forEach((Item: any) => {
|
||||
const library: JellyfinLibrary = {
|
||||
|
||||
5
server/constants/server.ts
Normal file
5
server/constants/server.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum MediaServerType {
|
||||
PLEX = 1,
|
||||
JELLYFIN, //also works for emby (identical APIs, etc)
|
||||
NOT_CONFIGURED,
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { getSettings } from '../lib/settings';
|
||||
import RadarrAPI from '../api/radarr';
|
||||
import downloadTracker, { DownloadingItem } from '../lib/downloadtracker';
|
||||
import SonarrAPI from '../api/sonarr';
|
||||
import { MediaServerType } from '../constants/server';
|
||||
|
||||
@Entity()
|
||||
class Media {
|
||||
@@ -134,45 +135,40 @@ class Media {
|
||||
public ratingKey4k?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public jellyfinMediaID?: string;
|
||||
public jellyfinMediaId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public jellyfinMediaID4k?: string;
|
||||
public jellyfinMediaId4k?: string;
|
||||
|
||||
public serviceUrl?: string;
|
||||
public serviceUrl4k?: string;
|
||||
public downloadStatus?: DownloadingItem[] = [];
|
||||
public downloadStatus4k?: DownloadingItem[] = [];
|
||||
|
||||
public plexUrl?: string;
|
||||
public plexUrl4k?: string;
|
||||
|
||||
public jellyfinUrl?: string;
|
||||
public jellyfinUrl4k?: string;
|
||||
public mediaUrl?: string;
|
||||
public mediaUrl4k?: string;
|
||||
|
||||
constructor(init?: Partial<Media>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
public setPlexUrls(): void {
|
||||
const machineId = getSettings().plex.machineId;
|
||||
if (this.ratingKey) {
|
||||
this.plexUrl = `https://app.plex.tv/desktop#!/server/${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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
public setJellyfinUrls(): void {
|
||||
const jellyfinSettings = getSettings().jellyfin;
|
||||
if (this.jellyfinMediaID) {
|
||||
this.jellyfinUrl = `${jellyfinSettings.hostname}/web/#!/details?id=${this.jellyfinMediaID}&context=home&serverId=${jellyfinSettings.serverID}`;
|
||||
}
|
||||
if (this.jellyfinMediaID4k) {
|
||||
this.jellyfinUrl4k = `${jellyfinSettings.hostname}/web/#!/details?id=${this.jellyfinMediaID4k}&context=home&serverId=${jellyfinSettings.serverID}`;
|
||||
public setMediaUrls(): void {
|
||||
const settings = getSettings();
|
||||
if (settings.main.mediaServerType == MediaServerType.PLEX) {
|
||||
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.mediaUrl4k = `https://app.plex.tv/desktop#!/server/${settings.plex.machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}`;
|
||||
}
|
||||
} else {
|
||||
if (this.jellyfinMediaId) {
|
||||
this.mediaUrl = `${settings.jellyfin.hostname}/web/#!/details?id=${this.jellyfinMediaId}&context=home&serverId=${settings.jellyfin.serverID}`;
|
||||
}
|
||||
if (this.jellyfinMediaId4k) {
|
||||
this.mediaUrl4k = `${settings.jellyfin.hostname}/web/#!/details?id=${this.jellyfinMediaId4k}&context=home&serverId=${settings.jellyfin.serverID}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@ export class User {
|
||||
|
||||
@AfterLoad()
|
||||
public setDisplayName(): void {
|
||||
this.displayName = this.username || this.plexUsername;
|
||||
this.displayName =
|
||||
this.username || this.plexUsername || this.jellyfinUsername;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,6 @@ app
|
||||
* OpenAPI validator. Otherwise, they are treated as objects instead of strings
|
||||
* and response validation will fail
|
||||
*/
|
||||
|
||||
server.use((_req, res, next) => {
|
||||
const original = res.json;
|
||||
res.json = function jsonp(json) {
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface SettingsAboutResponse {
|
||||
}
|
||||
|
||||
export interface PublicSettingsResponse {
|
||||
jfHost?: string;
|
||||
jellyfinHost?: string;
|
||||
initialized: boolean;
|
||||
applicationTitle: string;
|
||||
hideAvailable: boolean;
|
||||
@@ -15,7 +15,7 @@ export interface PublicSettingsResponse {
|
||||
series4kEnabled: boolean;
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
mediaServerType: string;
|
||||
mediaServerType: number;
|
||||
}
|
||||
|
||||
export interface CacheItem {
|
||||
|
||||
@@ -11,6 +11,7 @@ import Season from '../../entity/Season';
|
||||
import { uniqWith } from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import AsyncLock from '../../utils/asyncLock';
|
||||
import { MediaServerType } from '../../constants/server';
|
||||
|
||||
const BUNDLE_SIZE = 20;
|
||||
const UPDATE_RATE = 4 * 1000;
|
||||
@@ -125,18 +126,18 @@ class JobJellyfinSync {
|
||||
|
||||
if (
|
||||
(hasOtherResolution || (has4k && !this.enable4kMovie)) &&
|
||||
existing.jellyfinMediaID !== metadata.Id
|
||||
existing.jellyfinMediaId !== metadata.Id
|
||||
) {
|
||||
existing.jellyfinMediaID = metadata.Id;
|
||||
existing.jellyfinMediaId = metadata.Id;
|
||||
changedExisting = true;
|
||||
}
|
||||
|
||||
if (
|
||||
has4k &&
|
||||
this.enable4kMovie &&
|
||||
existing.jellyfinMediaID4k !== metadata.Id
|
||||
existing.jellyfinMediaId4k !== metadata.Id
|
||||
) {
|
||||
existing.jellyfinMediaID4k = metadata.Id;
|
||||
existing.jellyfinMediaId4k = metadata.Id;
|
||||
changedExisting = true;
|
||||
}
|
||||
|
||||
@@ -162,11 +163,11 @@ class JobJellyfinSync {
|
||||
: MediaStatus.UNKNOWN;
|
||||
newMedia.mediaType = MediaType.MOVIE;
|
||||
newMedia.mediaAddedAt = new Date(metadata.DateCreated ?? '');
|
||||
newMedia.jellyfinMediaID =
|
||||
newMedia.jellyfinMediaId =
|
||||
hasOtherResolution || (!this.enable4kMovie && has4k)
|
||||
? metadata.Id
|
||||
: undefined;
|
||||
newMedia.jellyfinMediaID4k =
|
||||
newMedia.jellyfinMediaId4k =
|
||||
has4k && this.enable4kMovie ? metadata.Id : undefined;
|
||||
await mediaRepository.save(newMedia);
|
||||
this.log(`Saved ${metadata.Name}`);
|
||||
@@ -212,7 +213,7 @@ class JobJellyfinSync {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lets get the available seasons from Plex
|
||||
// Lets get the available seasons from Jellyfin
|
||||
const seasons = tvShow.seasons;
|
||||
const media = await this.getExisting(tvShow.id, MediaType.TV);
|
||||
|
||||
@@ -229,7 +230,6 @@ class JobJellyfinSync {
|
||||
) ?? []
|
||||
).length;
|
||||
|
||||
//girl bye idk what's happening here! LMFAO
|
||||
for (const season of seasons) {
|
||||
const JellyfinSeasons = await this.jfClient.getSeasons(Id);
|
||||
const matchedJellyfinSeason = JellyfinSeasons.find(
|
||||
@@ -242,7 +242,7 @@ class JobJellyfinSync {
|
||||
|
||||
// Check if we found the matching season and it has all the available episodes
|
||||
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(
|
||||
Id,
|
||||
matchedJellyfinSeason.Id
|
||||
@@ -279,18 +279,18 @@ class JobJellyfinSync {
|
||||
if (
|
||||
media &&
|
||||
(totalStandard > 0 || (total4k > 0 && !this.enable4kShow)) &&
|
||||
media.jellyfinMediaID !== Id
|
||||
media.jellyfinMediaId !== Id
|
||||
) {
|
||||
media.jellyfinMediaID = Id;
|
||||
media.jellyfinMediaId = Id;
|
||||
}
|
||||
|
||||
if (
|
||||
media &&
|
||||
total4k > 0 &&
|
||||
this.enable4kShow &&
|
||||
media.jellyfinMediaID4k !== Id
|
||||
media.jellyfinMediaId4k !== Id
|
||||
) {
|
||||
media.jellyfinMediaID4k = Id;
|
||||
media.jellyfinMediaId4k = Id;
|
||||
}
|
||||
|
||||
if (existingSeason) {
|
||||
@@ -438,8 +438,8 @@ class JobJellyfinSync {
|
||||
tmdbId: tvShow.id,
|
||||
tvdbId: tvShow.external_ids.tvdb_id,
|
||||
mediaAddedAt: new Date(metadata.DateCreated ?? ''),
|
||||
jellyfinMediaID: Id,
|
||||
jellyfinMediaID4k: Id,
|
||||
jellyfinMediaId: Id,
|
||||
jellyfinMediaId4k: Id,
|
||||
status: isAllStandardSeasons
|
||||
? MediaStatus.AVAILABLE
|
||||
: newSeasons.some(
|
||||
@@ -538,7 +538,7 @@ class JobJellyfinSync {
|
||||
public async run(): Promise<void> {
|
||||
const settings = getSettings();
|
||||
|
||||
if (settings.main.mediaServerType != 'JELLYFIN') {
|
||||
if (settings.main.mediaServerType != MediaServerType.JELLYFIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { uniqWith } from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import animeList from '../../api/animelist';
|
||||
import AsyncLock from '../../utils/asyncLock';
|
||||
import { MediaServerType } from '../../constants/server';
|
||||
|
||||
const BUNDLE_SIZE = 20;
|
||||
const UPDATE_RATE = 4 * 1000;
|
||||
@@ -803,7 +804,7 @@ class JobPlexSync {
|
||||
public async run(): Promise<void> {
|
||||
const settings = getSettings();
|
||||
|
||||
if (settings.main.mediaServerType != 'PLEX') {
|
||||
if (settings.main.mediaServerType != MediaServerType.PLEX) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from 'path';
|
||||
import { merge } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Permission } from './permissions';
|
||||
import { MediaServerType } from '../constants/server';
|
||||
|
||||
export interface Library {
|
||||
id: string;
|
||||
@@ -81,7 +82,7 @@ export interface MainSettings {
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
trustProxy: boolean;
|
||||
mediaServerType: string;
|
||||
mediaServerType: number;
|
||||
}
|
||||
|
||||
interface PublicSettings {
|
||||
@@ -96,8 +97,8 @@ interface FullPublicSettings extends PublicSettings {
|
||||
series4kEnabled: boolean;
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
mediaServerType: string;
|
||||
jfHost?: string;
|
||||
mediaServerType: number;
|
||||
jellyfinHost?: string;
|
||||
}
|
||||
|
||||
export interface NotificationAgentConfig {
|
||||
@@ -208,7 +209,7 @@ class Settings {
|
||||
region: '',
|
||||
originalLanguage: '',
|
||||
trustProxy: false,
|
||||
mediaServerType: '',
|
||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||
},
|
||||
plex: {
|
||||
name: '',
|
||||
@@ -372,8 +373,12 @@ class Settings {
|
||||
originalLanguage: this.data.main.originalLanguage,
|
||||
=======
|
||||
mediaServerType: this.main.mediaServerType,
|
||||
<<<<<<< HEAD
|
||||
jfHost: this.jellyfin.hostname ?? '',
|
||||
>>>>>>> feat(all): add initial Jellyfin/Emby support
|
||||
=======
|
||||
jellyfinHost: this.jellyfin.hostname,
|
||||
>>>>>>> feat(rebase): rebase
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Permission } from '../lib/permissions';
|
||||
import logger from '../logger';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import { UserType } from '../constants/user';
|
||||
import { MediaServerType } from '../constants/server';
|
||||
|
||||
const authRoutes = Router();
|
||||
|
||||
@@ -32,13 +33,18 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
const userRepository = getRepository(User);
|
||||
const body = req.body as {
|
||||
authToken?: string;
|
||||
mediaServerType?: string;
|
||||
jellyfinHostname?: string;
|
||||
};
|
||||
|
||||
if (!body.authToken) {
|
||||
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 {
|
||||
// First we need to use this auth token to get the users email from plex.tv
|
||||
const plextv = new PlexTvAPI(body.authToken);
|
||||
@@ -80,6 +86,9 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
userType: UserType.PLEX,
|
||||
});
|
||||
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
|
||||
@@ -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) => {
|
||||
//lgtm [js/missing-rate-limiting]
|
||||
const settings = getSettings();
|
||||
const userRepository = getRepository(User);
|
||||
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
|
||||
if (
|
||||
settings.main.mediaServerType != 'JELLYFIN' &&
|
||||
settings.main.mediaServerType != MediaServerType.JELLYFIN &&
|
||||
settings.jellyfin.hostname != ''
|
||||
) {
|
||||
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
|
||||
: body.hostname;
|
||||
// First we need to use this auth token to get the users email from plex tv
|
||||
const plextv = new JellyfinAPI(hostname ?? '');
|
||||
// First we need to attempt to log the user in to jellyfin
|
||||
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
|
||||
let user = await userRepository.findOne({
|
||||
@@ -181,12 +193,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
});
|
||||
|
||||
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) {
|
||||
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.email = 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)
|
||||
//Also set mediaservertype to JELLYFIN
|
||||
if (settings.jellyfin.hostname == '') {
|
||||
settings.main.mediaServerType = 'JELLYFIN';
|
||||
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
||||
settings.jellyfin.hostname = body.hostname ?? '';
|
||||
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
|
||||
if (req.session) {
|
||||
req.session.userId = user.id;
|
||||
req.session.userId = user?.id;
|
||||
}
|
||||
|
||||
return res.status(200).json(user?.filter() ?? {});
|
||||
|
||||
Reference in New Issue
Block a user