mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(api): plex Sync (Movies)
Also adds winston logging
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -35,5 +35,8 @@ yarn-error.log*
|
||||
config/db/db.sqlite3
|
||||
config/settings.json
|
||||
|
||||
# logs
|
||||
config/logs/*.log
|
||||
|
||||
# dist files
|
||||
dist
|
||||
|
0
config/logs/.gitkeep
Normal file
0
config/logs/.gitkeep
Normal file
@@ -39,6 +39,7 @@
|
||||
"swr": "^0.3.2",
|
||||
"typeorm": "^0.2.26",
|
||||
"uuid": "^8.3.0",
|
||||
"winston": "^3.3.3",
|
||||
"xml2js": "^0.4.23",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
|
@@ -1,8 +1,49 @@
|
||||
import NodePlexAPI from 'plex-api';
|
||||
import { getSettings } from '../lib/settings';
|
||||
|
||||
export interface PlexLibraryItem {
|
||||
ratingKey: string;
|
||||
title: string;
|
||||
guid: string;
|
||||
type: 'movie' | 'show';
|
||||
}
|
||||
|
||||
interface PlexLibraryResponse {
|
||||
MediaContainer: {
|
||||
Metadata: PlexLibraryItem[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlexLibrary {
|
||||
type: 'show' | 'movie';
|
||||
key: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface PlexLibrariesResponse {
|
||||
MediaContainer: {
|
||||
Directory: PlexLibrary[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlexMetadata {
|
||||
ratingKey: string;
|
||||
guid: string;
|
||||
type: 'movie' | 'show';
|
||||
title: string;
|
||||
Guid: {
|
||||
id: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface PlexMetadataResponse {
|
||||
MediaContainer: {
|
||||
Metadata: PlexMetadata[];
|
||||
};
|
||||
}
|
||||
|
||||
class PlexAPI {
|
||||
private plexClient: typeof NodePlexAPI;
|
||||
private plexClient: NodePlexAPI;
|
||||
|
||||
constructor({ plexToken }: { plexToken?: string }) {
|
||||
const settings = getSettings();
|
||||
@@ -13,7 +54,7 @@ class PlexAPI {
|
||||
token: plexToken,
|
||||
authenticator: {
|
||||
authenticate: (
|
||||
_plexApi: typeof PlexAPI,
|
||||
_plexApi,
|
||||
cb: (err?: string, token?: string) => void
|
||||
) => {
|
||||
if (!plexToken) {
|
||||
@@ -34,6 +75,36 @@ class PlexAPI {
|
||||
public async getStatus() {
|
||||
return await this.plexClient.query('/');
|
||||
}
|
||||
|
||||
public async getLibraries(): Promise<PlexLibrary[]> {
|
||||
const response = await this.plexClient.query<PlexLibrariesResponse>(
|
||||
'/library/sections'
|
||||
);
|
||||
|
||||
return response.MediaContainer.Directory;
|
||||
}
|
||||
|
||||
public async getLibraryContents(id: string): Promise<PlexLibraryItem[]> {
|
||||
const response = await this.plexClient.query<PlexLibraryResponse>(
|
||||
`/library/sections/${id}/all`
|
||||
);
|
||||
|
||||
return response.MediaContainer.Metadata;
|
||||
}
|
||||
|
||||
public async getMetadata(key: string): Promise<PlexMetadata> {
|
||||
const response = await this.plexClient.query<PlexMetadataResponse>(
|
||||
`/library/metadata/${key}`
|
||||
);
|
||||
|
||||
return response.MediaContainer.Metadata[0];
|
||||
}
|
||||
|
||||
public async getRecentlyAdded() {
|
||||
const response = await this.plexClient.query('/library/recentlyAdded');
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export default PlexAPI;
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import xml2js from 'xml2js';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
|
||||
interface PlexAccountResponse {
|
||||
user: PlexUser;
|
||||
@@ -79,9 +80,9 @@ class PlexTvAPI {
|
||||
|
||||
return account.data.user;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Something broke when getting account from plex.tv',
|
||||
e.message
|
||||
logger.error(
|
||||
`Something went wrong getting the account from plex.tv: ${e.message}`,
|
||||
{ label: 'Plex.tv API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
}
|
||||
@@ -124,7 +125,7 @@ class PlexTvAPI {
|
||||
(server) => server.$.machineIdentifier === settings.plex.machineId
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(`Error checking user access: ${e.message}`);
|
||||
logger.error(`Error checking user access: ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -100,6 +100,11 @@ interface TmdbSearchTvResponse extends TmdbPaginatedResponse {
|
||||
results: TmdbTvResult[];
|
||||
}
|
||||
|
||||
interface TmdbExternalIdResponse {
|
||||
movie_results: TmdbMovieResult[];
|
||||
tv_results: TmdbTvResult[];
|
||||
}
|
||||
|
||||
export interface TmdbCreditCast {
|
||||
cast_id: number;
|
||||
character: string;
|
||||
@@ -549,6 +554,70 @@ class TheMovieDb {
|
||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public async getByExternalId({
|
||||
externalId,
|
||||
type,
|
||||
language = 'en-US',
|
||||
}:
|
||||
| {
|
||||
externalId: string;
|
||||
type: 'imdb';
|
||||
language?: string;
|
||||
}
|
||||
| {
|
||||
externalId: number;
|
||||
type: 'tvdb';
|
||||
language?: string;
|
||||
}): Promise<TmdbExternalIdResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbExternalIdResponse>(
|
||||
`/find/${externalId}`,
|
||||
{
|
||||
params: {
|
||||
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMovieByImdbId({
|
||||
imdbId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
imdbId: string;
|
||||
language?: string;
|
||||
}): Promise<TmdbMovieDetails> {
|
||||
try {
|
||||
const extResponse = await this.getByExternalId({
|
||||
externalId: imdbId,
|
||||
type: 'imdb',
|
||||
});
|
||||
|
||||
if (extResponse.movie_results[0]) {
|
||||
const movie = await this.getMovie({
|
||||
movieId: extResponse.movie_results[0].id,
|
||||
language,
|
||||
});
|
||||
|
||||
return movie;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'[TMDB] Failed to find a title with the provided IMDB id'
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to get movie by external imdb ID: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TheMovieDb;
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
} from 'typeorm';
|
||||
import { MediaRequest } from './MediaRequest';
|
||||
import { MediaStatus, MediaType } from '../constants/media';
|
||||
import logger from '../logger';
|
||||
|
||||
@Entity()
|
||||
class Media {
|
||||
@@ -33,7 +34,7 @@ class Media {
|
||||
|
||||
return media;
|
||||
} catch (e) {
|
||||
console.error(e.messaage);
|
||||
logger.error(e.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -48,7 +49,7 @@ class Media {
|
||||
|
||||
return media;
|
||||
} catch (e) {
|
||||
console.error(e.messaage);
|
||||
logger.error(e.messaage);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -65,7 +66,11 @@ class Media {
|
||||
|
||||
@Column({ unique: true, nullable: true })
|
||||
@Index()
|
||||
public tvdbId: number;
|
||||
public tvdbId?: number;
|
||||
|
||||
@Column({ unique: true, nullable: true })
|
||||
@Index()
|
||||
public imdbId?: string;
|
||||
|
||||
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
||||
public status: MediaStatus;
|
||||
|
@@ -4,6 +4,7 @@ import TheMovieDb from '../api/themoviedb';
|
||||
import RadarrAPI from '../api/radarr';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import { MediaType, MediaRequestStatus } from '../constants/media';
|
||||
import logger from '../logger';
|
||||
|
||||
@ChildEntity(MediaType.MOVIE)
|
||||
class MovieRequest extends MediaRequest {
|
||||
@@ -18,8 +19,9 @@ class MovieRequest extends MediaRequest {
|
||||
try {
|
||||
const settings = getSettings();
|
||||
if (settings.radarr.length === 0 && !settings.radarr[0]) {
|
||||
console.log(
|
||||
'[MediaRequest] Skipped radarr request as there is no radarr configured'
|
||||
logger.info(
|
||||
'Skipped radarr request as there is no radarr configured',
|
||||
{ label: 'Media Request' }
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -44,7 +46,7 @@ class MovieRequest extends MediaRequest {
|
||||
monitored: true,
|
||||
searchNow: true,
|
||||
});
|
||||
console.log('[MediaRequest] Sent request to Radarr');
|
||||
logger.info('Sent request to Radarr', { label: 'Media Request' });
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[MediaRequest] Request failed to send to radarr: ${e.message}`
|
||||
|
@@ -12,6 +12,7 @@ import swaggerUi from 'swagger-ui-express';
|
||||
import { OpenApiValidator } from 'express-openapi-validator';
|
||||
import { Session } from './entity/Session';
|
||||
import { getSettings } from './lib/settings';
|
||||
import logger from './logger';
|
||||
|
||||
const API_SPEC_PATH = path.join(__dirname, 'overseerr-api.yml');
|
||||
|
||||
@@ -40,9 +41,12 @@ app
|
||||
secret: 'verysecret',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||
},
|
||||
store: new TypeormStore({
|
||||
cleanupLimit: 2,
|
||||
ttl: 86400,
|
||||
ttl: 1000 * 60 * 60 * 24 * 30,
|
||||
}).connect(sessionRespository),
|
||||
})
|
||||
);
|
||||
@@ -87,10 +91,12 @@ app
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
console.log(`Ready to do stuff http://localhost:${port}`);
|
||||
logger.info(`Server ready on port ${port}`, {
|
||||
label: 'SERVER',
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err.stack);
|
||||
logger.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
|
182
server/job/plexsync.ts
Normal file
182
server/job/plexsync.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { getRepository } from 'typeorm';
|
||||
import { User } from '../entity/User';
|
||||
import PlexAPI, { PlexLibraryItem } from '../api/plexapi';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import Media from '../entity/Media';
|
||||
import { MediaStatus, MediaType } from '../constants/media';
|
||||
import logger from '../logger';
|
||||
import { getSettings, Library } from '../lib/settings';
|
||||
import { resolve } from 'dns';
|
||||
|
||||
const BUNDLE_SIZE = 10;
|
||||
|
||||
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
|
||||
const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/);
|
||||
const plexRegex = new RegExp(/plex:\/\//);
|
||||
|
||||
class JobPlexSync {
|
||||
private tmdb: TheMovieDb;
|
||||
private plexClient: PlexAPI;
|
||||
private items: PlexLibraryItem[] = [];
|
||||
private progress = 0;
|
||||
private libraries: Library[];
|
||||
private currentLibrary: Library;
|
||||
private running = false;
|
||||
|
||||
constructor() {
|
||||
this.tmdb = new TheMovieDb();
|
||||
}
|
||||
|
||||
private async getExisting(tmdbId: number) {
|
||||
const mediaRepository = getRepository(Media);
|
||||
|
||||
const existing = await mediaRepository.findOne({
|
||||
where: { tmdbId: tmdbId },
|
||||
});
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
private async processMovie(plexitem: PlexLibraryItem) {
|
||||
const mediaRepository = getRepository(Media);
|
||||
if (plexitem.guid.match(plexRegex)) {
|
||||
const metadata = await this.plexClient.getMetadata(plexitem.ratingKey);
|
||||
const newMedia = new Media();
|
||||
|
||||
metadata.Guid.forEach((ref) => {
|
||||
if (ref.id.match(imdbRegex)) {
|
||||
newMedia.imdbId = ref.id.match(imdbRegex)?.[1] ?? undefined;
|
||||
} else if (ref.id.match(tmdbRegex)) {
|
||||
const tmdbMatch = ref.id.match(tmdbRegex)?.[1];
|
||||
newMedia.tmdbId = Number(tmdbMatch);
|
||||
}
|
||||
});
|
||||
|
||||
const existing = await this.getExisting(newMedia.tmdbId);
|
||||
|
||||
if (existing && existing.status === MediaStatus.AVAILABLE) {
|
||||
this.log(`Title exists and is already available ${metadata.title}`);
|
||||
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
|
||||
existing.status = MediaStatus.AVAILABLE;
|
||||
mediaRepository.save(existing);
|
||||
this.log(
|
||||
`Request for ${metadata.title} exists. Setting status AVAILABLE`
|
||||
);
|
||||
} else {
|
||||
newMedia.status = MediaStatus.AVAILABLE;
|
||||
newMedia.mediaType = MediaType.MOVIE;
|
||||
await mediaRepository.save(newMedia);
|
||||
this.log(`Saved ${plexitem.title}`);
|
||||
}
|
||||
} else {
|
||||
const matchedid = plexitem.guid.match(/imdb:\/\/(tt[0-9]+)/);
|
||||
|
||||
if (matchedid?.[1]) {
|
||||
const tmdbMovie = await this.tmdb.getMovieByImdbId({
|
||||
imdbId: matchedid[1],
|
||||
});
|
||||
|
||||
const existing = await this.getExisting(tmdbMovie.id);
|
||||
if (existing && existing.status === MediaStatus.AVAILABLE) {
|
||||
this.log(`Title exists and is already available ${plexitem.title}`);
|
||||
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
|
||||
existing.status = MediaStatus.AVAILABLE;
|
||||
await mediaRepository.save(existing);
|
||||
this.log(
|
||||
`Request for ${plexitem.title} exists. Setting status AVAILABLE`
|
||||
);
|
||||
} else if (tmdbMovie) {
|
||||
const newMedia = new Media();
|
||||
newMedia.imdbId = tmdbMovie.external_ids.imdb_id;
|
||||
newMedia.tmdbId = tmdbMovie.id;
|
||||
newMedia.status = MediaStatus.AVAILABLE;
|
||||
newMedia.mediaType = MediaType.MOVIE;
|
||||
await mediaRepository.save(newMedia);
|
||||
this.log(`Saved ${tmdbMovie.title}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async processItems(slicedItems: PlexLibraryItem[]) {
|
||||
await Promise.all(
|
||||
slicedItems.map(async (plexitem) => {
|
||||
if (plexitem.type === 'movie') {
|
||||
await this.processMovie(plexitem);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async loop({
|
||||
start = 0,
|
||||
end = BUNDLE_SIZE,
|
||||
}: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
} = {}) {
|
||||
const slicedItems = this.items.slice(start, end);
|
||||
if (start < this.items.length && this.running) {
|
||||
this.progress = start;
|
||||
await this.processItems(slicedItems);
|
||||
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(async () => {
|
||||
await this.loop({
|
||||
start: start + BUNDLE_SIZE,
|
||||
end: end + BUNDLE_SIZE,
|
||||
});
|
||||
resolve();
|
||||
}, 5000)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private log(message: string): void {
|
||||
logger.info(message, { label: 'Plex Sync' });
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
const settings = getSettings();
|
||||
if (!this.running) {
|
||||
this.running = true;
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'plexToken'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
|
||||
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
|
||||
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}`);
|
||||
this.items = await this.plexClient.getLibraryContents(library.id);
|
||||
await this.loop();
|
||||
}
|
||||
this.running = false;
|
||||
this.log('complete');
|
||||
}
|
||||
}
|
||||
|
||||
public status() {
|
||||
return {
|
||||
running: this.running,
|
||||
progress: this.progress,
|
||||
total: this.items.length,
|
||||
currentLibrary: this.currentLibrary,
|
||||
libraries: this.libraries,
|
||||
};
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
const jobPlexSync = new JobPlexSync();
|
||||
|
||||
export default jobPlexSync;
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
interface Library {
|
||||
export interface Library {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
|
32
server/logger.ts
Normal file
32
server/logger.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as winston from 'winston';
|
||||
import path from 'path';
|
||||
|
||||
const hformat = winston.format.printf(
|
||||
({ level, label, message, timestamp, ...metadata }) => {
|
||||
let msg = `${timestamp} [${level}]${
|
||||
label ? `[${label}]` : ''
|
||||
}: ${message} `;
|
||||
if (Object.keys(metadata).length > 0) {
|
||||
msg += JSON.stringify(metadata);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
);
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'debug',
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.splat(),
|
||||
winston.format.timestamp(),
|
||||
hformat
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({
|
||||
filename: path.join(__dirname, '../config/logs/overseerr.log'),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export default logger;
|
@@ -770,6 +770,69 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PlexSettings'
|
||||
/settings/plex/library:
|
||||
get:
|
||||
summary: Get a list of current plex libraries
|
||||
description: Returns a list of plex libraries in a JSON array
|
||||
tags:
|
||||
- settings
|
||||
parameters:
|
||||
- in: query
|
||||
name: sync
|
||||
description: Syncs the current libraries with the current plex server
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- in: query
|
||||
name: enable
|
||||
description: Comma separated list of libraries to enable. Any libraries not passed will be disabled!
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: 'Plex libraries returned'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PlexLibrary'
|
||||
/settings/plex/sync:
|
||||
get:
|
||||
summary: Start a full Plex Library sync
|
||||
description: Runs a full plex library sync and returns the progress in a JSON array
|
||||
tags:
|
||||
- settings
|
||||
parameters:
|
||||
- in: query
|
||||
name: cancel
|
||||
schema:
|
||||
type: boolean
|
||||
example: false
|
||||
responses:
|
||||
'200':
|
||||
description: Status of Plex Sync
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
running:
|
||||
type: boolean
|
||||
example: false
|
||||
progress:
|
||||
type: number
|
||||
example: 0
|
||||
total:
|
||||
type: number
|
||||
example: 100
|
||||
currentLibrary:
|
||||
$ref: '#/components/schemas/PlexLibrary'
|
||||
libraries:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PlexLibrary'
|
||||
/settings/radarr:
|
||||
get:
|
||||
summary: Get all radarr settings
|
||||
|
@@ -4,6 +4,7 @@ import { User } from '../entity/User';
|
||||
import PlexTvAPI from '../api/plextv';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import logger from '../logger';
|
||||
|
||||
const authRoutes = Router();
|
||||
|
||||
@@ -95,7 +96,7 @@ authRoutes.post('/login', async (req, res) => {
|
||||
|
||||
return res.status(200).json(user?.filter() ?? {});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
logger.error(e.message, { label: 'Auth' });
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: 'Something went wrong. Is your auth token valid?' });
|
||||
|
@@ -1,8 +1,14 @@
|
||||
import { Router } from 'express';
|
||||
import { getSettings, RadarrSettings, SonarrSettings } from '../lib/settings';
|
||||
import {
|
||||
getSettings,
|
||||
RadarrSettings,
|
||||
SonarrSettings,
|
||||
Library,
|
||||
} from '../lib/settings';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { User } from '../entity/User';
|
||||
import PlexAPI from '../api/plexapi';
|
||||
import PlexAPI, { PlexLibrary } from '../api/plexapi';
|
||||
import jobPlexSync from '../job/plexsync';
|
||||
|
||||
const settingsRoutes = Router();
|
||||
|
||||
@@ -58,6 +64,55 @@ settingsRoutes.post('/plex', async (req, res, next) => {
|
||||
return res.status(200).json(settings.plex);
|
||||
});
|
||||
|
||||
settingsRoutes.get('/plex/library', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
if (req.query.sync) {
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'plexToken'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
|
||||
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
settings.plex.libraries = newLibraries;
|
||||
}
|
||||
|
||||
const enabledLibraries = req.query.enable
|
||||
? (req.query.enable as string).split(',')
|
||||
: [];
|
||||
settings.plex.libraries = settings.plex.libraries.map((library) => ({
|
||||
...library,
|
||||
enabled: enabledLibraries.includes(library.id),
|
||||
}));
|
||||
settings.save();
|
||||
return res.status(200).json(settings.plex.libraries);
|
||||
});
|
||||
|
||||
settingsRoutes.get('/plex/sync', (req, res) => {
|
||||
if (req.query.cancel) {
|
||||
jobPlexSync.cancel();
|
||||
} else {
|
||||
jobPlexSync.run();
|
||||
}
|
||||
|
||||
return res.status(200).json(jobPlexSync.status());
|
||||
});
|
||||
|
||||
settingsRoutes.get('/radarr', (req, res) => {
|
||||
const settings = getSettings();
|
||||
|
||||
|
24
server/types/plex-api.d.ts
vendored
24
server/types/plex-api.d.ts
vendored
@@ -1 +1,23 @@
|
||||
declare module 'plex-api';
|
||||
declare module 'plex-api' {
|
||||
export default class PlexAPI {
|
||||
constructor(intiialOptions: {
|
||||
hostname: string;
|
||||
post: number;
|
||||
token?: string;
|
||||
authenticator: {
|
||||
authenticate: (
|
||||
_plexApi: PlexAPI,
|
||||
cb: (err?: string, token?: string) => void
|
||||
) => void;
|
||||
};
|
||||
options: {
|
||||
identifier: string;
|
||||
product: string;
|
||||
deviceName: string;
|
||||
platform: string;
|
||||
};
|
||||
});
|
||||
|
||||
query: <T extends Record<string, any>>(endpoint: string) => Promise<T>;
|
||||
}
|
||||
}
|
||||
|
@@ -269,7 +269,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{currentStatus === MediaStatus.AVAILABLE && (
|
||||
{currentStatus === MediaStatus.PROCESSING && (
|
||||
<button className="w-full h-7 text-center text-white bg-red-500 rounded-sm ml-1">
|
||||
<svg
|
||||
className="w-4 mx-auto"
|
||||
|
@@ -10,6 +10,7 @@ import axios from 'axios';
|
||||
import { User } from '../hooks/useUser';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { LanguageContext, AvailableLocales } from '../context/LanguageContext';
|
||||
import Head from 'next/head';
|
||||
|
||||
const loadLocaleData = (locale: string) => {
|
||||
switch (locale) {
|
||||
@@ -76,6 +77,9 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
|
||||
messages={loadedMessages}
|
||||
>
|
||||
<ToastProvider>
|
||||
<Head>
|
||||
<title>Overseerr</title>
|
||||
</Head>
|
||||
<UserContext initialUser={user}>{component}</UserContext>
|
||||
</ToastProvider>
|
||||
</IntlProvider>
|
||||
|
120
yarn.lock
120
yarn.lock
@@ -1257,6 +1257,15 @@
|
||||
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
|
||||
integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
|
||||
|
||||
"@dabh/diagnostics@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31"
|
||||
integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==
|
||||
dependencies:
|
||||
colorspace "1.1.x"
|
||||
enabled "2.0.x"
|
||||
kuler "^2.0.0"
|
||||
|
||||
"@emotion/cache@^10.0.27":
|
||||
version "10.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
|
||||
@@ -2494,6 +2503,11 @@ async-each@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
|
||||
integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
|
||||
|
||||
async@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
@@ -3313,6 +3327,14 @@ color-string@^1.5.2:
|
||||
color-name "^1.0.0"
|
||||
simple-swizzle "^0.2.2"
|
||||
|
||||
color@3.0.x:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a"
|
||||
integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==
|
||||
dependencies:
|
||||
color-convert "^1.9.1"
|
||||
color-string "^1.5.2"
|
||||
|
||||
color@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10"
|
||||
@@ -3326,6 +3348,19 @@ colorette@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
|
||||
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
|
||||
|
||||
colors@^1.2.1:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
|
||||
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
|
||||
|
||||
colorspace@1.1.x:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5"
|
||||
integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==
|
||||
dependencies:
|
||||
color "3.0.x"
|
||||
text-hex "1.0.x"
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
@@ -4263,6 +4298,11 @@ emojis-list@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||
|
||||
enabled@2.0.x:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
|
||||
integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
|
||||
|
||||
encodeurl@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
@@ -4817,6 +4857,11 @@ fast-memoize@^2.5.2:
|
||||
resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e"
|
||||
integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==
|
||||
|
||||
fast-safe-stringify@^2.0.4:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
|
||||
integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481"
|
||||
@@ -4824,6 +4869,11 @@ fastq@^1.6.0:
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
fecha@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41"
|
||||
integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==
|
||||
|
||||
figgy-pudding@^3.5.1:
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
|
||||
@@ -4985,6 +5035,11 @@ flush-write-stream@^1.0.0:
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^2.3.6"
|
||||
|
||||
fn.name@1.x.x:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
|
||||
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
||||
|
||||
follow-redirects@^1.10.0:
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
|
||||
@@ -6193,6 +6248,11 @@ kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
||||
|
||||
kuler@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
|
||||
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
|
||||
|
||||
language-subtag-registry@~0.3.2:
|
||||
version "0.3.20"
|
||||
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz#a00a37121894f224f763268e431c55556b0c0755"
|
||||
@@ -6421,6 +6481,17 @@ log-update@^4.0.0:
|
||||
slice-ansi "^4.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
logform@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2"
|
||||
integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==
|
||||
dependencies:
|
||||
colors "^1.2.1"
|
||||
fast-safe-stringify "^2.0.4"
|
||||
fecha "^4.2.0"
|
||||
ms "^2.1.1"
|
||||
triple-beam "^1.3.0"
|
||||
|
||||
longest@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/longest/-/longest-2.0.1.tgz#781e183296aa94f6d4d916dc335d0d17aefa23f8"
|
||||
@@ -7361,6 +7432,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
one-time@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
|
||||
integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==
|
||||
dependencies:
|
||||
fn.name "1.x.x"
|
||||
|
||||
onetime@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
|
||||
@@ -8420,7 +8498,7 @@ read-pkg@^5.2.0:
|
||||
parse-json "^5.0.0"
|
||||
type-fest "^0.6.0"
|
||||
|
||||
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
|
||||
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6:
|
||||
version "2.3.7"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||
@@ -8443,7 +8521,7 @@ readable-stream@1.1.x:
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
"readable-stream@2 || 3", readable-stream@^3.5.0, readable-stream@^3.6.0:
|
||||
"readable-stream@2 || 3", readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||
@@ -9266,6 +9344,11 @@ stable@^0.1.8:
|
||||
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
|
||||
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
|
||||
|
||||
stack-trace@0.0.x:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
|
||||
integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
|
||||
|
||||
stacktrace-parser@0.1.10:
|
||||
version "0.1.10"
|
||||
resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a"
|
||||
@@ -9697,6 +9780,11 @@ text-extensions@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"
|
||||
integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==
|
||||
|
||||
text-hex@1.0.x:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
|
||||
integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
|
||||
|
||||
text-table@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
@@ -9840,6 +9928,11 @@ trim-off-newlines@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
|
||||
integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM=
|
||||
|
||||
triple-beam@^1.2.0, triple-beam@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
|
||||
integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
|
||||
|
||||
ts-node@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3"
|
||||
@@ -10338,6 +10431,29 @@ widest-line@^3.1.0:
|
||||
dependencies:
|
||||
string-width "^4.0.0"
|
||||
|
||||
winston-transport@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59"
|
||||
integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==
|
||||
dependencies:
|
||||
readable-stream "^2.3.7"
|
||||
triple-beam "^1.2.0"
|
||||
|
||||
winston@^3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170"
|
||||
integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==
|
||||
dependencies:
|
||||
"@dabh/diagnostics" "^2.0.2"
|
||||
async "^3.1.0"
|
||||
is-stream "^2.0.0"
|
||||
logform "^2.2.0"
|
||||
one-time "^1.0.0"
|
||||
readable-stream "^3.4.0"
|
||||
stack-trace "0.0.x"
|
||||
triple-beam "^1.3.0"
|
||||
winston-transport "^4.4.0"
|
||||
|
||||
word-wrap@^1.0.3, word-wrap@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
|
Reference in New Issue
Block a user