mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-02 20:58:56 -05:00
feat(rebase): rebase
This commit is contained in:
@@ -75,17 +75,16 @@ class JellyfinAPI {
|
||||
private jellyfinHost: string;
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor(jellyfinHost: string, authToken?: string, userId?: string) {
|
||||
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
|
||||
console.log(jellyfinHost, deviceId, authToken);
|
||||
this.jellyfinHost = jellyfinHost;
|
||||
this.authToken = authToken;
|
||||
this.userId = userId;
|
||||
|
||||
let authHeaderVal = '';
|
||||
if (this.authToken) {
|
||||
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0", Token="${authToken}"`;
|
||||
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
|
||||
} else {
|
||||
authHeaderVal =
|
||||
'MediaBrowser Client="Overseerr", Device="Axios", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0"';
|
||||
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
|
||||
}
|
||||
|
||||
this.axios = axios.create({
|
||||
@@ -116,6 +115,11 @@ class JellyfinAPI {
|
||||
}
|
||||
}
|
||||
|
||||
public setUserId(userId: string): void {
|
||||
this.userId = userId;
|
||||
return;
|
||||
}
|
||||
|
||||
public async getServerName(): Promise<string> {
|
||||
try {
|
||||
const account = await this.axios.get<JellyfinUserResponse>(
|
||||
@@ -150,19 +154,21 @@ class JellyfinAPI {
|
||||
try {
|
||||
const account = await this.axios.get<any>('/Library/MediaFolders');
|
||||
|
||||
const response: JellyfinLibrary[] = [];
|
||||
|
||||
account.data.Items.forEach((Item: any) => {
|
||||
const library: JellyfinLibrary = {
|
||||
const response: JellyfinLibrary[] = account.data.Items.filter(
|
||||
(Item: any) => {
|
||||
return (
|
||||
Item.Type === 'CollectionFolder' &&
|
||||
(Item.CollectionType === 'tvshows' ||
|
||||
Item.CollectionType === 'movies')
|
||||
);
|
||||
}
|
||||
).map((Item: any) => {
|
||||
return <JellyfinLibrary>{
|
||||
key: Item.Id,
|
||||
title: Item.Name,
|
||||
type: Item.CollectionType == 'movies' ? 'movie' : 'show',
|
||||
type: Item.CollectionType === 'movies' ? 'movie' : 'show',
|
||||
agent: 'jellyfin',
|
||||
};
|
||||
|
||||
if (Item.Type == 'CollectionFolder') {
|
||||
response.push(library);
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
@@ -178,9 +184,7 @@ class JellyfinAPI {
|
||||
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
`/Users/${
|
||||
(await this.getUser()).Id
|
||||
}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&StartIndex=0&ParentId=${id}`
|
||||
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&StartIndex=0&ParentId=${id}`
|
||||
);
|
||||
|
||||
return contents.data.Items;
|
||||
@@ -196,9 +200,7 @@ class JellyfinAPI {
|
||||
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
`/Users/${
|
||||
(await this.getUser()).Id
|
||||
}/Items/Latest?Limit=50&ParentId=${id}`
|
||||
`/Users/${this.userId}/Items/Latest?Limit=50&ParentId=${id}`
|
||||
);
|
||||
|
||||
return contents.data.Items;
|
||||
@@ -214,7 +216,7 @@ class JellyfinAPI {
|
||||
public async getItemData(id: string): Promise<JellyfinLibraryItemExtended> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
`/Users/${(await this.getUser()).Id}/Items/${id}`
|
||||
`/Users/${this.userId}/Items/${id}`
|
||||
);
|
||||
|
||||
return contents.data;
|
||||
|
||||
@@ -164,10 +164,10 @@ class Media {
|
||||
}
|
||||
} else {
|
||||
if (this.jellyfinMediaId) {
|
||||
this.mediaUrl = `${settings.jellyfin.hostname}/web/#!/details?id=${this.jellyfinMediaId}&context=home&serverId=${settings.jellyfin.serverID}`;
|
||||
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}`;
|
||||
this.mediaUrl4k = `${settings.jellyfin.hostname}/web/#!/details?id=${this.jellyfinMediaId4k}&context=home&serverId=${settings.jellyfin.serverId}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,16 +65,19 @@ export class User {
|
||||
@Column({ type: 'integer', default: UserType.PLEX })
|
||||
public userType: UserType;
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
@Column({ nullable: true })
|
||||
public plexId?: number;
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
public jellyfinId?: string;
|
||||
@Column({ nullable: true })
|
||||
public jellyfinUserId?: string;
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
@Column({ nullable: true })
|
||||
public jellyfinDeviceId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public jellyfinAuthToken?: string;
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
@Column({ nullable: true })
|
||||
public plexToken?: string;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
|
||||
@@ -264,7 +264,7 @@ class JobJellyfinSync {
|
||||
|
||||
ExtendedEpisodeData.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
if (MediaStream.Type == 'Video') {
|
||||
if (MediaStream.Type === 'Video') {
|
||||
if (MediaStream.Width ?? 0 < 2000) {
|
||||
totalStandard++;
|
||||
}
|
||||
@@ -552,7 +552,12 @@ class JobJellyfinSync {
|
||||
this.running = true;
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOne({
|
||||
select: ['id', 'jellyfinAuthToken', 'jellyfinId'],
|
||||
select: [
|
||||
'id',
|
||||
'jellyfinAuthToken',
|
||||
'jellyfinUserId',
|
||||
'jellyfinDeviceId',
|
||||
],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
|
||||
@@ -562,10 +567,12 @@ class JobJellyfinSync {
|
||||
|
||||
this.jfClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinId ?? ''
|
||||
admin.jellyfinAuthToken,
|
||||
admin.jellyfinDeviceId
|
||||
);
|
||||
|
||||
this.jfClient.setUserId(admin.jellyfinUserId ?? '');
|
||||
|
||||
this.libraries = settings.jellyfin.libraries.filter(
|
||||
(library) => library.enabled
|
||||
);
|
||||
|
||||
@@ -35,9 +35,7 @@ export interface JellyfinSettings {
|
||||
name: string;
|
||||
hostname?: string;
|
||||
libraries: Library[];
|
||||
adminUser: string;
|
||||
adminPass: string;
|
||||
serverID: string;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
interface DVRSettings {
|
||||
@@ -223,9 +221,7 @@ class Settings {
|
||||
name: '',
|
||||
hostname: '',
|
||||
libraries: [],
|
||||
adminUser: '',
|
||||
adminPass: '',
|
||||
serverID: '',
|
||||
serverId: '',
|
||||
},
|
||||
radarr: [],
|
||||
sonarr: [],
|
||||
|
||||
@@ -43,7 +43,7 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
settings.main.mediaServerType != MediaServerType.PLEX &&
|
||||
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED
|
||||
) {
|
||||
return res.status(500).json({ error: 'Plex login disabled' });
|
||||
return res.status(500).json({ error: 'Plex login is disabled' });
|
||||
}
|
||||
try {
|
||||
// First we need to use this auth token to get the users email from plex.tv
|
||||
@@ -154,40 +154,52 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
username?: string;
|
||||
password?: string;
|
||||
hostname?: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
//Make sure jellyfin login is enabled, but only if jellyfin is not already configured
|
||||
if (
|
||||
settings.main.mediaServerType != MediaServerType.JELLYFIN &&
|
||||
settings.jellyfin.hostname != ''
|
||||
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
|
||||
settings.jellyfin.hostname !== ''
|
||||
) {
|
||||
return res.status(500).json({ error: 'Jellyfin login is disabled' });
|
||||
} else if (!body.username || !body.password) {
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'You must provide an username and a password' });
|
||||
} else if (settings.jellyfin.hostname != '' && body.hostname) {
|
||||
} else if (settings.jellyfin.hostname !== '' && body.hostname) {
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'Jellyfin hostname already configured' });
|
||||
} else if (settings.jellyfin.hostname == '' && !body.hostname) {
|
||||
} else if (settings.jellyfin.hostname === '' && !body.hostname) {
|
||||
return res.status(500).json({ error: 'No hostname provided.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const hostname =
|
||||
settings.jellyfin.hostname != ''
|
||||
settings.jellyfin.hostname !== ''
|
||||
? settings.jellyfin.hostname
|
||||
: body.hostname;
|
||||
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
|
||||
let user = await userRepository.findOne({
|
||||
where: { jellyfinUsername: body.username },
|
||||
});
|
||||
|
||||
let deviceId = '';
|
||||
if (user) {
|
||||
deviceId = user.jellyfinDeviceId ?? '';
|
||||
} else {
|
||||
deviceId = Buffer.from(
|
||||
`Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0|${Date.now()}`
|
||||
).toString('base64');
|
||||
}
|
||||
// First we need to attempt to log the user in to jellyfin
|
||||
const jellyfinserver = new JellyfinAPI(hostname ?? '');
|
||||
settings.jellyfin.name = await jellyfinserver.getServerName();
|
||||
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
|
||||
|
||||
const account = await jellyfinserver.login(body.username, body.password);
|
||||
|
||||
// Next let's see if the user already exists
|
||||
let user = await userRepository.findOne({
|
||||
where: { jellyfinId: account.User.Id },
|
||||
user = await userRepository.findOne({
|
||||
where: { jellyfinUserId: account.User.Id },
|
||||
});
|
||||
|
||||
if (user) {
|
||||
@@ -200,9 +212,9 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
if (typeof account.User.PrimaryImageTag !== undefined) {
|
||||
user.avatar = `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
|
||||
} else {
|
||||
user.avatar = '/images/os_logo_square.png';
|
||||
user.avatar = '/os_logo_square.png';
|
||||
}
|
||||
user.email = account.User.Name;
|
||||
|
||||
user.jellyfinUsername = account.User.Name;
|
||||
|
||||
if (user.username === account.User.Name) {
|
||||
@@ -213,30 +225,52 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
// Here we check if it's the first user. If it is, we create the user with no check
|
||||
// and give them admin permissions
|
||||
const totalUsers = await userRepository.count();
|
||||
|
||||
if (totalUsers === 0) {
|
||||
user = new User({
|
||||
email: account.User.Name,
|
||||
email: body.email,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinId: account.User.Id,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: Permission.ADMIN,
|
||||
avatar:
|
||||
typeof account.User.PrimaryImageTag !== undefined
|
||||
? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||
: '/images/os_logo_square.png',
|
||||
: '/os_logo_square.png',
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
await userRepository.save(user);
|
||||
|
||||
//Update hostname in settings if it doesn't exist (initial configuration)
|
||||
//Also set mediaservertype to JELLYFIN
|
||||
if (settings.jellyfin.hostname == '') {
|
||||
if (settings.jellyfin.hostname === '') {
|
||||
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
||||
settings.jellyfin.hostname = body.hostname ?? '';
|
||||
settings.jellyfin.serverId = account.User.ServerId;
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
if (!body.email) {
|
||||
throw new Error('add_email');
|
||||
}
|
||||
|
||||
user = new User({
|
||||
email: body.email,
|
||||
jellyfinUsername: account.User.Name,
|
||||
jellyfinUserId: account.User.Id,
|
||||
jellyfinDeviceId: deviceId,
|
||||
jellyfinAuthToken: account.AccessToken,
|
||||
permissions: settings.main.defaultPermissions,
|
||||
avatar:
|
||||
typeof account.User.PrimaryImageTag !== undefined
|
||||
? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
|
||||
: '/os_logo_square.png',
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
await userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
||||
// Set logged in session
|
||||
@@ -246,16 +280,32 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
|
||||
return res.status(200).json(user?.filter() ?? {});
|
||||
} catch (e) {
|
||||
if (e.message != 'Unauthorized') {
|
||||
if (e.message === 'Unauthorized') {
|
||||
logger.info(
|
||||
'Failed login attempt from user with incorrect Jellyfin credentials',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
ip: req.ip,
|
||||
email: body.username,
|
||||
password: '__REDACTED__',
|
||||
},
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
} else if (e.message === 'add_email') {
|
||||
return next({
|
||||
status: 406,
|
||||
message: 'CREDENTIAL_ERROR_ADD_EMAIL',
|
||||
});
|
||||
} else {
|
||||
logger.error(e.message, { label: 'Auth' });
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong. Is your auth token valid?',
|
||||
});
|
||||
} else {
|
||||
return next({
|
||||
status: 401,
|
||||
message: 'CREDENTIAL_ERROR',
|
||||
message: 'Something went wrong.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,8 +228,7 @@ settingsRoutes.post('/plex/sync', (req, res) => {
|
||||
settingsRoutes.get('/jellyfin', (_req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
//DO NOT RETURN ADMIN USER CREDENTIALS!!
|
||||
res.status(200).json(omit(settings.jellyfin, ['adminUser', 'adminPass']));
|
||||
res.status(200).json(settings.jellyfin);
|
||||
});
|
||||
|
||||
settingsRoutes.post('/jellyfin', (req, res) => {
|
||||
@@ -247,30 +246,28 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => {
|
||||
if (req.query.sync) {
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'jellyfinAuthToken'],
|
||||
select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
admin.jellyfinAuthToken ?? ''
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinDeviceId ?? ''
|
||||
);
|
||||
|
||||
const libraries = await jellyfinClient.getLibraries();
|
||||
|
||||
const newLibraries: Library[] = libraries
|
||||
// Remove libraries that are not movie or show
|
||||
.filter((library) => library.type === 'movie' || library.type === 'show')
|
||||
.map((library) => {
|
||||
const existing = settings.plex.libraries.find(
|
||||
(l) => l.id === library.key && l.name === library.title
|
||||
);
|
||||
const newLibraries: Library[] = libraries.map((library) => {
|
||||
const existing = settings.jellyfin.libraries.find(
|
||||
(l) => l.id === library.key && l.name === library.title
|
||||
);
|
||||
|
||||
return {
|
||||
id: library.key,
|
||||
name: library.title,
|
||||
enabled: existing?.enabled ?? false,
|
||||
};
|
||||
});
|
||||
return {
|
||||
id: library.key,
|
||||
name: library.title,
|
||||
enabled: existing?.enabled ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
settings.jellyfin.libraries = newLibraries;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user