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/db/db.sqlite3
|
||||||
config/settings.json
|
config/settings.json
|
||||||
|
|
||||||
|
# logs
|
||||||
|
config/logs/*.log
|
||||||
|
|
||||||
# dist files
|
# dist files
|
||||||
dist
|
dist
|
||||||
|
0
config/logs/.gitkeep
Normal file
0
config/logs/.gitkeep
Normal file
@@ -39,6 +39,7 @@
|
|||||||
"swr": "^0.3.2",
|
"swr": "^0.3.2",
|
||||||
"typeorm": "^0.2.26",
|
"typeorm": "^0.2.26",
|
||||||
"uuid": "^8.3.0",
|
"uuid": "^8.3.0",
|
||||||
|
"winston": "^3.3.3",
|
||||||
"xml2js": "^0.4.23",
|
"xml2js": "^0.4.23",
|
||||||
"yamljs": "^0.3.0"
|
"yamljs": "^0.3.0"
|
||||||
},
|
},
|
||||||
|
@@ -1,8 +1,49 @@
|
|||||||
import NodePlexAPI from 'plex-api';
|
import NodePlexAPI from 'plex-api';
|
||||||
import { getSettings } from '../lib/settings';
|
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 {
|
class PlexAPI {
|
||||||
private plexClient: typeof NodePlexAPI;
|
private plexClient: NodePlexAPI;
|
||||||
|
|
||||||
constructor({ plexToken }: { plexToken?: string }) {
|
constructor({ plexToken }: { plexToken?: string }) {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
@@ -13,7 +54,7 @@ class PlexAPI {
|
|||||||
token: plexToken,
|
token: plexToken,
|
||||||
authenticator: {
|
authenticator: {
|
||||||
authenticate: (
|
authenticate: (
|
||||||
_plexApi: typeof PlexAPI,
|
_plexApi,
|
||||||
cb: (err?: string, token?: string) => void
|
cb: (err?: string, token?: string) => void
|
||||||
) => {
|
) => {
|
||||||
if (!plexToken) {
|
if (!plexToken) {
|
||||||
@@ -34,6 +75,36 @@ class PlexAPI {
|
|||||||
public async getStatus() {
|
public async getStatus() {
|
||||||
return await this.plexClient.query('/');
|
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;
|
export default PlexAPI;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
import xml2js from 'xml2js';
|
import xml2js from 'xml2js';
|
||||||
import { getSettings } from '../lib/settings';
|
import { getSettings } from '../lib/settings';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
interface PlexAccountResponse {
|
interface PlexAccountResponse {
|
||||||
user: PlexUser;
|
user: PlexUser;
|
||||||
@@ -79,9 +80,9 @@ class PlexTvAPI {
|
|||||||
|
|
||||||
return account.data.user;
|
return account.data.user;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
logger.error(
|
||||||
'Something broke when getting account from plex.tv',
|
`Something went wrong getting the account from plex.tv: ${e.message}`,
|
||||||
e.message
|
{ label: 'Plex.tv API' }
|
||||||
);
|
);
|
||||||
throw new Error('Invalid auth token');
|
throw new Error('Invalid auth token');
|
||||||
}
|
}
|
||||||
@@ -124,7 +125,7 @@ class PlexTvAPI {
|
|||||||
(server) => server.$.machineIdentifier === settings.plex.machineId
|
(server) => server.$.machineIdentifier === settings.plex.machineId
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`Error checking user access: ${e.message}`);
|
logger.error(`Error checking user access: ${e.message}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -100,6 +100,11 @@ interface TmdbSearchTvResponse extends TmdbPaginatedResponse {
|
|||||||
results: TmdbTvResult[];
|
results: TmdbTvResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TmdbExternalIdResponse {
|
||||||
|
movie_results: TmdbMovieResult[];
|
||||||
|
tv_results: TmdbTvResult[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface TmdbCreditCast {
|
export interface TmdbCreditCast {
|
||||||
cast_id: number;
|
cast_id: number;
|
||||||
character: string;
|
character: string;
|
||||||
@@ -549,6 +554,70 @@ class TheMovieDb {
|
|||||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
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;
|
export default TheMovieDb;
|
||||||
|
@@ -11,6 +11,7 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { MediaRequest } from './MediaRequest';
|
import { MediaRequest } from './MediaRequest';
|
||||||
import { MediaStatus, MediaType } from '../constants/media';
|
import { MediaStatus, MediaType } from '../constants/media';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
class Media {
|
class Media {
|
||||||
@@ -33,7 +34,7 @@ class Media {
|
|||||||
|
|
||||||
return media;
|
return media;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e.messaage);
|
logger.error(e.message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,7 @@ class Media {
|
|||||||
|
|
||||||
return media;
|
return media;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e.messaage);
|
logger.error(e.messaage);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +66,11 @@ class Media {
|
|||||||
|
|
||||||
@Column({ unique: true, nullable: true })
|
@Column({ unique: true, nullable: true })
|
||||||
@Index()
|
@Index()
|
||||||
public tvdbId: number;
|
public tvdbId?: number;
|
||||||
|
|
||||||
|
@Column({ unique: true, nullable: true })
|
||||||
|
@Index()
|
||||||
|
public imdbId?: string;
|
||||||
|
|
||||||
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
||||||
public status: MediaStatus;
|
public status: MediaStatus;
|
||||||
|
@@ -4,6 +4,7 @@ import TheMovieDb from '../api/themoviedb';
|
|||||||
import RadarrAPI from '../api/radarr';
|
import RadarrAPI from '../api/radarr';
|
||||||
import { getSettings } from '../lib/settings';
|
import { getSettings } from '../lib/settings';
|
||||||
import { MediaType, MediaRequestStatus } from '../constants/media';
|
import { MediaType, MediaRequestStatus } from '../constants/media';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
@ChildEntity(MediaType.MOVIE)
|
@ChildEntity(MediaType.MOVIE)
|
||||||
class MovieRequest extends MediaRequest {
|
class MovieRequest extends MediaRequest {
|
||||||
@@ -18,8 +19,9 @@ class MovieRequest extends MediaRequest {
|
|||||||
try {
|
try {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
if (settings.radarr.length === 0 && !settings.radarr[0]) {
|
if (settings.radarr.length === 0 && !settings.radarr[0]) {
|
||||||
console.log(
|
logger.info(
|
||||||
'[MediaRequest] Skipped radarr request as there is no radarr configured'
|
'Skipped radarr request as there is no radarr configured',
|
||||||
|
{ label: 'Media Request' }
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -44,7 +46,7 @@ class MovieRequest extends MediaRequest {
|
|||||||
monitored: true,
|
monitored: true,
|
||||||
searchNow: true,
|
searchNow: true,
|
||||||
});
|
});
|
||||||
console.log('[MediaRequest] Sent request to Radarr');
|
logger.info('Sent request to Radarr', { label: 'Media Request' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[MediaRequest] Request failed to send to radarr: ${e.message}`
|
`[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 { OpenApiValidator } from 'express-openapi-validator';
|
||||||
import { Session } from './entity/Session';
|
import { Session } from './entity/Session';
|
||||||
import { getSettings } from './lib/settings';
|
import { getSettings } from './lib/settings';
|
||||||
|
import logger from './logger';
|
||||||
|
|
||||||
const API_SPEC_PATH = path.join(__dirname, 'overseerr-api.yml');
|
const API_SPEC_PATH = path.join(__dirname, 'overseerr-api.yml');
|
||||||
|
|
||||||
@@ -40,9 +41,12 @@ app
|
|||||||
secret: 'verysecret',
|
secret: 'verysecret',
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
|
cookie: {
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||||
|
},
|
||||||
store: new TypeormStore({
|
store: new TypeormStore({
|
||||||
cleanupLimit: 2,
|
cleanupLimit: 2,
|
||||||
ttl: 86400,
|
ttl: 1000 * 60 * 60 * 24 * 30,
|
||||||
}).connect(sessionRespository),
|
}).connect(sessionRespository),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -87,10 +91,12 @@ app
|
|||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
console.log(`Ready to do stuff http://localhost:${port}`);
|
logger.info(`Server ready on port ${port}`, {
|
||||||
|
label: 'SERVER',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err.stack);
|
logger.error(err.stack);
|
||||||
process.exit(1);
|
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 path from 'path';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
interface Library {
|
export interface Library {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
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:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/PlexSettings'
|
$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:
|
/settings/radarr:
|
||||||
get:
|
get:
|
||||||
summary: Get all radarr settings
|
summary: Get all radarr settings
|
||||||
|
@@ -4,6 +4,7 @@ import { User } from '../entity/User';
|
|||||||
import PlexTvAPI from '../api/plextv';
|
import PlexTvAPI from '../api/plextv';
|
||||||
import { isAuthenticated } from '../middleware/auth';
|
import { isAuthenticated } from '../middleware/auth';
|
||||||
import { Permission } from '../lib/permissions';
|
import { Permission } from '../lib/permissions';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
const authRoutes = Router();
|
const authRoutes = Router();
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ authRoutes.post('/login', async (req, res) => {
|
|||||||
|
|
||||||
return res.status(200).json(user?.filter() ?? {});
|
return res.status(200).json(user?.filter() ?? {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
logger.error(e.message, { label: 'Auth' });
|
||||||
res
|
res
|
||||||
.status(500)
|
.status(500)
|
||||||
.json({ error: 'Something went wrong. Is your auth token valid?' });
|
.json({ error: 'Something went wrong. Is your auth token valid?' });
|
||||||
|
@@ -1,8 +1,14 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { getSettings, RadarrSettings, SonarrSettings } from '../lib/settings';
|
import {
|
||||||
|
getSettings,
|
||||||
|
RadarrSettings,
|
||||||
|
SonarrSettings,
|
||||||
|
Library,
|
||||||
|
} from '../lib/settings';
|
||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import { User } from '../entity/User';
|
import { User } from '../entity/User';
|
||||||
import PlexAPI from '../api/plexapi';
|
import PlexAPI, { PlexLibrary } from '../api/plexapi';
|
||||||
|
import jobPlexSync from '../job/plexsync';
|
||||||
|
|
||||||
const settingsRoutes = Router();
|
const settingsRoutes = Router();
|
||||||
|
|
||||||
@@ -58,6 +64,55 @@ settingsRoutes.post('/plex', async (req, res, next) => {
|
|||||||
return res.status(200).json(settings.plex);
|
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) => {
|
settingsRoutes.get('/radarr', (req, res) => {
|
||||||
const settings = getSettings();
|
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{currentStatus === MediaStatus.AVAILABLE && (
|
{currentStatus === MediaStatus.PROCESSING && (
|
||||||
<button className="w-full h-7 text-center text-white bg-red-500 rounded-sm ml-1">
|
<button className="w-full h-7 text-center text-white bg-red-500 rounded-sm ml-1">
|
||||||
<svg
|
<svg
|
||||||
className="w-4 mx-auto"
|
className="w-4 mx-auto"
|
||||||
|
@@ -10,6 +10,7 @@ import axios from 'axios';
|
|||||||
import { User } from '../hooks/useUser';
|
import { User } from '../hooks/useUser';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { LanguageContext, AvailableLocales } from '../context/LanguageContext';
|
import { LanguageContext, AvailableLocales } from '../context/LanguageContext';
|
||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
const loadLocaleData = (locale: string) => {
|
const loadLocaleData = (locale: string) => {
|
||||||
switch (locale) {
|
switch (locale) {
|
||||||
@@ -76,6 +77,9 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
|
|||||||
messages={loadedMessages}
|
messages={loadedMessages}
|
||||||
>
|
>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
|
<Head>
|
||||||
|
<title>Overseerr</title>
|
||||||
|
</Head>
|
||||||
<UserContext initialUser={user}>{component}</UserContext>
|
<UserContext initialUser={user}>{component}</UserContext>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</IntlProvider>
|
</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"
|
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
|
||||||
integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
|
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":
|
"@emotion/cache@^10.0.27":
|
||||||
version "10.0.29"
|
version "10.0.29"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
|
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"
|
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
|
||||||
integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
|
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:
|
asynckit@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
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"
|
color-name "^1.0.0"
|
||||||
simple-swizzle "^0.2.2"
|
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:
|
color@^3.1.2:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10"
|
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"
|
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
|
||||||
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
|
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:
|
combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
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"
|
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
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:
|
encodeurl@~1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
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"
|
resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e"
|
||||||
integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==
|
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:
|
fastq@^1.6.0:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481"
|
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481"
|
||||||
@@ -4824,6 +4869,11 @@ fastq@^1.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
reusify "^1.0.4"
|
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:
|
figgy-pudding@^3.5.1:
|
||||||
version "3.5.2"
|
version "3.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
|
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"
|
inherits "^2.0.3"
|
||||||
readable-stream "^2.3.6"
|
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:
|
follow-redirects@^1.10.0:
|
||||||
version "1.13.0"
|
version "1.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
|
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"
|
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||||
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
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:
|
language-subtag-registry@~0.3.2:
|
||||||
version "0.3.20"
|
version "0.3.20"
|
||||||
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz#a00a37121894f224f763268e431c55556b0c0755"
|
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"
|
slice-ansi "^4.0.0"
|
||||||
wrap-ansi "^6.2.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:
|
longest@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/longest/-/longest-2.0.1.tgz#781e183296aa94f6d4d916dc335d0d17aefa23f8"
|
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:
|
dependencies:
|
||||||
wrappy "1"
|
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:
|
onetime@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
|
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"
|
parse-json "^5.0.0"
|
||||||
type-fest "^0.6.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"
|
version "2.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||||
@@ -8443,7 +8521,7 @@ readable-stream@1.1.x:
|
|||||||
isarray "0.0.1"
|
isarray "0.0.1"
|
||||||
string_decoder "~0.10.x"
|
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"
|
version "3.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
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"
|
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
|
||||||
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
|
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:
|
stacktrace-parser@0.1.10:
|
||||||
version "0.1.10"
|
version "0.1.10"
|
||||||
resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a"
|
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"
|
resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"
|
||||||
integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==
|
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:
|
text-table@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
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"
|
resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
|
||||||
integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM=
|
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:
|
ts-node@^9.0.0:
|
||||||
version "9.0.0"
|
version "9.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3"
|
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3"
|
||||||
@@ -10338,6 +10431,29 @@ widest-line@^3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
string-width "^4.0.0"
|
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:
|
word-wrap@^1.0.3, word-wrap@^1.2.3:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||||
|
Reference in New Issue
Block a user