Compare commits
256 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 138fd4a121 | |||
| 5648b8fffa | |||
| 1b7a754224 | |||
| 5d91f1bae7 | |||
| cdcfae56e7 | |||
| f2c77e092d | |||
| d6ac7e55e9 | |||
| a3543090f2 | |||
| 041e944783 | |||
| bfd31ebd23 | |||
| 5036f4ca36 | |||
| 61b59ae3ea | |||
| 92c49ac523 | |||
| f680642f25 | |||
| f89486ae9e | |||
| 4d853565d1 | |||
| 91c81e5cf6 | |||
| 0ecbde9675 | |||
| d8e951c2ef | |||
| 90f3d86511 | |||
| c6791a7027 | |||
| 5b6a2c2651 | |||
| 4f7a22fff1 | |||
| 31b0c998a8 | |||
| 9ce5b476ef | |||
| 554f292e4c | |||
| d8985aaff7 | |||
| be889b8100 | |||
| b5bd672f44 | |||
| 4501bc5302 | |||
| b384e748af | |||
| c676f182b4 | |||
| 95d2b0095b | |||
| 8165cf8e85 | |||
| 14775744b0 | |||
| 559e32c059 | |||
| f4dbaf4c58 | |||
| 1d25914ae0 | |||
| 4d3d8c874c | |||
| 08433523b7 | |||
| fce8879994 | |||
| 505b126043 | |||
| 589bd7b08d | |||
| f0049ffb4e | |||
| 2b25397253 | |||
| 776f83553a | |||
| 815aaedffb | |||
| 578eff30fb | |||
| 943cbe5cb8 | |||
| f89db46bf2 | |||
| 085fb76e11 | |||
| aa4a1c2a57 | |||
| 74340afd16 | |||
| 2672266908 | |||
| f37786aa76 | |||
| 91f64e5cfb | |||
| a4d3123910 | |||
| bc6fe3ed48 | |||
| b23566509f | |||
| 341a07621d | |||
| 259ed9b06f | |||
| cddf06cbcc | |||
| 318d1e331b | |||
| 5923cbf051 | |||
| 291bdf089c | |||
| 8eacde9ccc | |||
| f8847c62f2 | |||
| ddb7e7379d | |||
| 720fb69648 | |||
| fedacf498e | |||
| 9022853502 | |||
| c1b96e17ca | |||
| a5248f0631 | |||
| e2d85c6242 | |||
| 510c014549 | |||
| 2650497986 | |||
| 639f0ec17a | |||
| 977d05c6f2 | |||
| 601fc1d0de | |||
| acc26a2f09 | |||
| 5d3a5dc8a4 | |||
| 3bb9bd84d9 | |||
| ea5bc36956 | |||
| 002e663be1 | |||
| fd475265c1 | |||
| 495a3b4838 | |||
| b0804f8a08 | |||
| 6b737b8ab4 | |||
| 9e2a0101c9 | |||
| 05b001de2e | |||
| 5623344666 | |||
| f8cc19b510 | |||
| c589457a6c | |||
| b802a7b62b | |||
| 879a02b388 | |||
| bc3d4881bd | |||
| ef8d4d90b2 | |||
| d2d396bb7a | |||
| 500b75eaf6 | |||
| 9308d4ea9b | |||
| 6c2c81a1a1 | |||
| 90aa4d2485 | |||
| 0ca3f81bf8 | |||
| b9831c6b3d | |||
| 4781e9ae65 | |||
|
|
eb0881f19e | ||
| bc4d73821d | |||
| ab6144eb81 | |||
| c3d87e2200 | |||
| e391ce7ef9 | |||
| ca707078d9 | |||
|
|
53228a2662 | ||
| 2a9fa27341 | |||
| 3068281461 | |||
| 81e9fe5b15 | |||
| 5d2e375213 | |||
| 7ede37039a | |||
| 8e23ae5a27 | |||
| 04ba094a14 | |||
| 23f9911237 | |||
| 3b27af1f83 | |||
| afb7af46b8 | |||
| 6ba8ca2add | |||
| 135375cb94 | |||
| e5d5bdefd6 | |||
| 6f9ca9e067 | |||
| c42195d242 | |||
| a5aaf1bfca | |||
| af7b1f2424 | |||
| 6aba9774c6 | |||
| e19cfb5870 | |||
| 144b27f128 | |||
| 12afbf6364 | |||
| 8a5ab204e1 | |||
| 3f04d9bc56 | |||
| de50805d1e | |||
| 3a9131a022 | |||
| 77433e8505 | |||
| 3845000b3f | |||
| 071fd54825 | |||
| 537f237e83 | |||
| d3bc854e03 | |||
| 15826a00ba | |||
| 4019d63f3b | |||
| 91dcfaccb9 | |||
| 270a259cee | |||
| 162d20ae52 | |||
| 9f1badc1b1 | |||
| ac027a97d6 | |||
| 127db88ded | |||
| 4b07434615 | |||
| 5d6f2baa34 | |||
| 1a1a7328a3 | |||
| b9dec2344e | |||
| 476a34fb69 | |||
| e3ed08e8dd | |||
| 70f6497404 | |||
| 99bab3fb73 | |||
| e6796aff8b | |||
| 1f9dc067e6 | |||
|
|
4eaa60b044 | ||
|
|
7db8f752c5 | ||
| 784aa2616a | |||
| 7cb55ce054 | |||
| 87eb6de802 | |||
| 840816c930 | |||
| 91d238de7c | |||
| 0ac17d3d0a | |||
| 87c76e3f1d | |||
| e64c4d5d01 | |||
| 22e57c03de | |||
| d80386da40 | |||
| e7c66af3f6 | |||
| 8ece7b84c4 | |||
| 4250b1bd17 | |||
| 7e46d32e30 | |||
| 5a48158f07 | |||
| 161a466ab7 | |||
| 8f5bd44e4d | |||
| 5d8869e042 | |||
| 90b8ee005e | |||
| 1b0525063f | |||
| 41d6bba743 | |||
| 8977a4b195 | |||
| 7e0da028de | |||
| 2250cf2c4b | |||
| b2bd7b6a1f | |||
| a2ad7f5628 | |||
| f85d31991f | |||
| 08dc2153ae | |||
| bc64e69b3e | |||
| a29bca7361 | |||
| d84aa5f173 | |||
| 48ebd398bc | |||
| 1b95103acd | |||
| 6a1d6687eb | |||
| e849864bc2 | |||
| ecc2a67d48 | |||
| bfe0d55f71 | |||
| 634d4513eb | |||
| 0a1276a474 | |||
| 3a34d8995e | |||
| 918e629a06 | |||
| 7dd016a56e | |||
| c10bbcf518 | |||
| 3402a52633 | |||
| 86e9188a5c | |||
| 8918b7906e | |||
| 7e028a461d | |||
| fe5f0c815e | |||
| d02e79e59e | |||
| 5b49216c9d | |||
| 657ab10034 | |||
| ed07c77b13 | |||
| 4fe85d9fae | |||
| b99b5b32ec | |||
| e8058c5e4c | |||
| 7332b7d474 | |||
| 7980f14426 | |||
| 65540fafbd | |||
| 64ede43dec | |||
| bed12cff72 | |||
| 59e7f96643 | |||
| 71e9a5a46e | |||
| fce6dc7658 | |||
| baff59181c | |||
| 0592cca16b | |||
| e4d5f5085c | |||
| 66a2a06f9b | |||
| e984feeb8d | |||
| 490d015f80 | |||
| f1cc2c4ebe | |||
| 2f4421d9e0 | |||
| 92cc094787 | |||
| f30b46c384 | |||
| d9f679603a | |||
| 64bd9d1e14 | |||
| 721826d454 | |||
| 242fe3515c | |||
| ccf40d2161 | |||
| 832b8ba539 | |||
|
|
0477e49eca | ||
| 451b67630a | |||
| 096bbdf085 | |||
| e914e4ab45 | |||
| c1461e1f41 | |||
| 91bf2c1e2a | |||
| da3df383ed | |||
| 9816b978d3 | |||
| 8e22b0f6ea | |||
| 18359f442c | |||
| 42b8b5ea0e | |||
| 996295b1fe | |||
| 1b08c8d3d1 | |||
| 9e145f7068 | |||
| 7051edb212 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.DS_Store
|
||||
|
||||
development.json
|
||||
env
|
||||
shows.db
|
||||
|
||||
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -1,3 +1,10 @@
|
||||
# Docs : https://git-scm.com/book/en/v2/Git-Tools-Submodules
|
||||
|
||||
[submodule "torrent_search"]
|
||||
path = torrent_search
|
||||
url = git@github.com:KevinMidboe/torrent_search.git
|
||||
url = https://github.com/KevinMidboe/torrent_search.git
|
||||
branch = master
|
||||
|
||||
[submodule "delugeClient"]
|
||||
path = delugeClient
|
||||
url = https://github.com/KevinMidboe/delugeClient.git
|
||||
|
||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
1263
.spectral.json
Normal file
1263
.spectral.json
Normal file
File diff suppressed because it is too large
Load Diff
38
.stoplight/custom-functions/oasDiscriminator.js
Normal file
38
.stoplight/custom-functions/oasDiscriminator.js
Normal file
@@ -0,0 +1,38 @@
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
export const oasDiscriminator = (schema, _opts, { path }) => {
|
||||
/**
|
||||
* This function verifies:
|
||||
*
|
||||
* 1. The discriminator property name is defined at this schema.
|
||||
* 2. The discriminator property is in the required property list.
|
||||
*/
|
||||
|
||||
if (!isObject(schema)) return;
|
||||
|
||||
if (typeof schema.discriminator !== 'string') return;
|
||||
|
||||
const discriminatorName = schema.discriminator;
|
||||
|
||||
const results = [];
|
||||
|
||||
if (!isObject(schema.properties) || !Object.keys(schema.properties).some(k => k === discriminatorName)) {
|
||||
results.push({
|
||||
message: `The discriminator property must be defined in this schema.`,
|
||||
path: [...path, 'properties'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!Array.isArray(schema.required) || !schema.required.some(n => n === discriminatorName)) {
|
||||
results.push({
|
||||
message: `The discriminator property must be in the required property list.`,
|
||||
path: [...path, 'required'],
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export default oasDiscriminator;
|
||||
4274
.stoplight/custom-functions/oasDocumentSchema.js
Normal file
4274
.stoplight/custom-functions/oasDocumentSchema.js
Normal file
File diff suppressed because it is too large
Load Diff
228
.stoplight/custom-functions/oasExample.js
Normal file
228
.stoplight/custom-functions/oasExample.js
Normal file
@@ -0,0 +1,228 @@
|
||||
import { isPlainObject, pointerToPath } from '@stoplight/json';
|
||||
import { createRulesetFunction } from '@stoplight/spectral-core';
|
||||
import { oas2, oas3_1, extractDraftVersion, oas3_0 } from '@stoplight/spectral-formats';
|
||||
import { schema as schemaFn } from '@stoplight/spectral-functions';
|
||||
import traverse from 'json-schema-traverse';
|
||||
|
||||
const MEDIA_VALIDATION_ITEMS = {
|
||||
2: [
|
||||
{
|
||||
field: 'examples',
|
||||
multiple: true,
|
||||
keyed: false,
|
||||
},
|
||||
],
|
||||
3: [
|
||||
{
|
||||
field: 'example',
|
||||
multiple: false,
|
||||
keyed: false,
|
||||
},
|
||||
{
|
||||
field: 'examples',
|
||||
multiple: true,
|
||||
keyed: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const SCHEMA_VALIDATION_ITEMS = {
|
||||
2: ['example', 'x-example', 'default'],
|
||||
3: ['example', 'default'],
|
||||
};
|
||||
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
function rewriteNullable(schema, errors) {
|
||||
for (const error of errors) {
|
||||
if (error.keyword !== 'type') continue;
|
||||
const value = getSchemaProperty(schema, error.schemaPath);
|
||||
if (isPlainObject(value) && value.nullable === true) {
|
||||
error.message += ',null';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const visitOAS2 = schema => {
|
||||
if (schema['x-nullable'] === true) {
|
||||
schema.nullable = true;
|
||||
delete schema['x-nullable'];
|
||||
}
|
||||
};
|
||||
|
||||
function getSchemaProperty(schema, schemaPath) {
|
||||
const path = pointerToPath(schemaPath);
|
||||
let value = schema;
|
||||
|
||||
for (const fragment of path.slice(0, -1)) {
|
||||
if (!isPlainObject(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = value[fragment];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
const oasSchema = createRulesetFunction(
|
||||
{
|
||||
input: null,
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
function oasSchema(targetVal, opts, context) {
|
||||
const formats = context.document.formats;
|
||||
|
||||
let { schema } = opts;
|
||||
|
||||
let dialect = 'draft4';
|
||||
let prepareResults;
|
||||
|
||||
if (!formats) {
|
||||
dialect = 'auto';
|
||||
} else if (formats.has(oas3_1)) {
|
||||
if (isPlainObject(context.document.data) && typeof context.document.data.jsonSchemaDialect === 'string') {
|
||||
dialect = extractDraftVersion(context.document.data.jsonSchemaDialect) ?? 'draft2020-12';
|
||||
} else {
|
||||
dialect = 'draft2020-12';
|
||||
}
|
||||
} else if (formats.has(oas3_0)) {
|
||||
prepareResults = rewriteNullable.bind(null, schema);
|
||||
} else if (formats.has(oas2)) {
|
||||
const clonedSchema = JSON.parse(JSON.stringify(schema));
|
||||
traverse(clonedSchema, visitOAS2);
|
||||
schema = clonedSchema;
|
||||
prepareResults = rewriteNullable.bind(null, clonedSchema);
|
||||
}
|
||||
|
||||
return schemaFn(
|
||||
targetVal,
|
||||
{
|
||||
...opts,
|
||||
schema,
|
||||
prepareResults,
|
||||
dialect,
|
||||
},
|
||||
context,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function* getMediaValidationItems(items, targetVal, givenPath, oasVersion) {
|
||||
for (const { field, keyed, multiple } of items) {
|
||||
if (!(field in targetVal)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = targetVal[field];
|
||||
|
||||
if (multiple) {
|
||||
if (!isObject(value)) continue;
|
||||
|
||||
for (const exampleKey of Object.keys(value)) {
|
||||
const exampleValue = value[exampleKey];
|
||||
if (oasVersion === 3 && keyed && (!isObject(exampleValue) || 'externalValue' in exampleValue)) {
|
||||
// should be covered by oas3-examples-value-or-externalValue
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetPath = [...givenPath, field, exampleKey];
|
||||
|
||||
if (keyed) {
|
||||
targetPath.push('value');
|
||||
}
|
||||
|
||||
yield {
|
||||
value: keyed && isObject(exampleValue) ? exampleValue.value : exampleValue,
|
||||
path: targetPath,
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
return yield {
|
||||
value,
|
||||
path: [...givenPath, field],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* getSchemaValidationItems(fields, targetVal, givenPath) {
|
||||
for (const field of fields) {
|
||||
if (!(field in targetVal)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield {
|
||||
value: targetVal[field],
|
||||
path: [...givenPath, field],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default createRulesetFunction(
|
||||
{
|
||||
input: {
|
||||
type: 'object',
|
||||
},
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
oasVersion: {
|
||||
enum: ['2', '3'],
|
||||
},
|
||||
schemaField: {
|
||||
type: 'string',
|
||||
},
|
||||
type: {
|
||||
enum: ['media', 'schema'],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
function oasExample(targetVal, opts, context) {
|
||||
const formats = context.document.formats;
|
||||
const schemaOpts = {
|
||||
schema: opts.schemaField === '$' ? targetVal : targetVal[opts.schemaField],
|
||||
};
|
||||
|
||||
let results = void 0;
|
||||
let oasVersion = parseInt(opts.oasVersion);
|
||||
|
||||
const validationItems =
|
||||
opts.type === 'schema'
|
||||
? getSchemaValidationItems(SCHEMA_VALIDATION_ITEMS[oasVersion], targetVal, context.path)
|
||||
: getMediaValidationItems(MEDIA_VALIDATION_ITEMS[oasVersion], targetVal, context.path, oasVersion);
|
||||
|
||||
if (formats?.has(oas2) && 'required' in schemaOpts.schema && typeof schemaOpts.schema.required === 'boolean') {
|
||||
schemaOpts.schema = { ...schemaOpts.schema };
|
||||
delete schemaOpts.schema.required;
|
||||
}
|
||||
|
||||
for (const validationItem of validationItems) {
|
||||
const result = oasSchema(validationItem.value, schemaOpts, {
|
||||
...context,
|
||||
path: validationItem.path,
|
||||
});
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
if (results === void 0) results = [];
|
||||
results.push(...result);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
);
|
||||
28
.stoplight/custom-functions/oasOpFormDataConsumeCheck.js
Normal file
28
.stoplight/custom-functions/oasOpFormDataConsumeCheck.js
Normal file
@@ -0,0 +1,28 @@
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
const validConsumeValue = /(application\/x-www-form-urlencoded|multipart\/form-data)/;
|
||||
|
||||
export const oasOpFormDataConsumeCheck = targetVal => {
|
||||
if (!isObject(targetVal)) return;
|
||||
|
||||
const parameters = targetVal.parameters;
|
||||
const consumes = targetVal.consumes;
|
||||
|
||||
if (!Array.isArray(parameters) || !Array.isArray(consumes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parameters.some(p => isObject(p) && p.in === 'formData') && !validConsumeValue.test(consumes?.join(','))) {
|
||||
return [
|
||||
{
|
||||
message: 'Consumes must include urlencoded, multipart, or form-data media type when using formData parameter.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export default oasOpFormDataConsumeCheck;
|
||||
76
.stoplight/custom-functions/oasOpIdUnique.js
Normal file
76
.stoplight/custom-functions/oasOpIdUnique.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { isPlainObject } from '@stoplight/json';
|
||||
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
const validOperationKeys = ['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'trace'];
|
||||
|
||||
function* getAllOperations(paths) {
|
||||
if (!isPlainObject(paths)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = {
|
||||
path: '',
|
||||
operation: '',
|
||||
value: null,
|
||||
};
|
||||
|
||||
for (const path of Object.keys(paths)) {
|
||||
const operations = paths[path];
|
||||
if (!isPlainObject(operations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
item.path = path;
|
||||
|
||||
for (const operation of Object.keys(operations)) {
|
||||
if (!isPlainObject(operations[operation]) || !validOperationKeys.includes(operation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
item.operation = operation;
|
||||
item.value = operations[operation];
|
||||
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const oasOpIdUnique = targetVal => {
|
||||
if (!isObject(targetVal) || !isObject(targetVal.paths)) return;
|
||||
|
||||
const results = [];
|
||||
|
||||
const { paths } = targetVal;
|
||||
|
||||
const seenIds = [];
|
||||
|
||||
for (const { path, operation } of getAllOperations(paths)) {
|
||||
const pathValue = paths[path];
|
||||
|
||||
if (!isObject(pathValue)) continue;
|
||||
|
||||
const operationValue = pathValue[operation];
|
||||
|
||||
if (!isObject(operationValue) || !('operationId' in operationValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { operationId } = operationValue;
|
||||
|
||||
if (seenIds.includes(operationId)) {
|
||||
results.push({
|
||||
message: 'operationId must be unique.',
|
||||
path: ['paths', path, operation, 'operationId'],
|
||||
});
|
||||
} else {
|
||||
seenIds.push(operationId);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export default oasOpIdUnique;
|
||||
81
.stoplight/custom-functions/oasOpParams.js
Normal file
81
.stoplight/custom-functions/oasOpParams.js
Normal file
@@ -0,0 +1,81 @@
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
function computeFingerprint(param) {
|
||||
return `${String(param.in)}-${String(param.name)}`;
|
||||
}
|
||||
|
||||
export const oasOpParams = (params, _opts, { path }) => {
|
||||
/**
|
||||
* This function verifies:
|
||||
*
|
||||
* 1. Operations must have unique `name` + `in` parameters.
|
||||
* 2. Operation cannot have both `in:body` and `in:formData` parameters
|
||||
* 3. Operation must have only one `in:body` parameter.
|
||||
*/
|
||||
|
||||
if (!Array.isArray(params)) return;
|
||||
|
||||
if (params.length < 2) return;
|
||||
|
||||
const results = [];
|
||||
|
||||
const count = {
|
||||
body: [],
|
||||
formData: [],
|
||||
};
|
||||
const list = [];
|
||||
const duplicates = [];
|
||||
|
||||
let index = -1;
|
||||
|
||||
for (const param of params) {
|
||||
index++;
|
||||
|
||||
if (!isObject(param)) continue;
|
||||
|
||||
// skip params that are refs
|
||||
if ('$ref' in param) continue;
|
||||
|
||||
// Operations must have unique `name` + `in` parameters.
|
||||
const fingerprint = computeFingerprint(param);
|
||||
if (list.includes(fingerprint)) {
|
||||
duplicates.push(index);
|
||||
} else {
|
||||
list.push(fingerprint);
|
||||
}
|
||||
|
||||
if (typeof param.in === 'string' && param.in in count) {
|
||||
count[param.in].push(index);
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
for (const i of duplicates) {
|
||||
results.push({
|
||||
message: 'A parameter in this operation already exposes the same combination of "name" and "in" values.',
|
||||
path: [...path, i],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (count.body.length > 0 && count.formData.length > 0) {
|
||||
results.push({
|
||||
message: 'Operation must not have both "in:body" and "in:formData" parameters.',
|
||||
});
|
||||
}
|
||||
|
||||
if (count.body.length > 1) {
|
||||
for (let i = 1; i < count.body.length; i++) {
|
||||
results.push({
|
||||
message: 'Operation must not have more than a single instance of the "in:body" parameter.',
|
||||
path: [...path, count.body[i]],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export default oasOpParams;
|
||||
141
.stoplight/custom-functions/oasOpSecurityDefined.js
Normal file
141
.stoplight/custom-functions/oasOpSecurityDefined.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import { isPlainObject } from '@stoplight/json';
|
||||
import { createRulesetFunction } from '@stoplight/spectral-core';
|
||||
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
const validOperationKeys = ['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'trace'];
|
||||
|
||||
function* getAllOperations(paths) {
|
||||
if (!isPlainObject(paths)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = {
|
||||
path: '',
|
||||
operation: '',
|
||||
value: null,
|
||||
};
|
||||
|
||||
for (const path of Object.keys(paths)) {
|
||||
const operations = paths[path];
|
||||
if (!isPlainObject(operations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
item.path = path;
|
||||
|
||||
for (const operation of Object.keys(operations)) {
|
||||
if (!isPlainObject(operations[operation]) || !validOperationKeys.includes(operation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
item.operation = operation;
|
||||
item.value = operations[operation];
|
||||
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _get(value, path) {
|
||||
for (const segment of path) {
|
||||
if (!isObject(value)) {
|
||||
break;
|
||||
}
|
||||
|
||||
value = value[segment];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export default createRulesetFunction(
|
||||
{
|
||||
input: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
paths: {
|
||||
type: 'object',
|
||||
},
|
||||
security: {
|
||||
type: 'array',
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
schemesPath: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: ['string', 'number'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
function oasOpSecurityDefined(targetVal, { schemesPath }) {
|
||||
const { paths } = targetVal;
|
||||
|
||||
const results = [];
|
||||
|
||||
const schemes = _get(targetVal, schemesPath);
|
||||
const allDefs = isObject(schemes) ? Object.keys(schemes) : [];
|
||||
|
||||
// Check global security requirements
|
||||
|
||||
const { security } = targetVal;
|
||||
|
||||
if (Array.isArray(security)) {
|
||||
for (const [index, value] of security.entries()) {
|
||||
if (!isObject(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const securityKeys = Object.keys(value);
|
||||
|
||||
for (const securityKey of securityKeys) {
|
||||
if (!allDefs.includes(securityKey)) {
|
||||
results.push({
|
||||
message: `API "security" values must match a scheme defined in the "${schemesPath.join('.')}" object.`,
|
||||
path: ['security', index, securityKey],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const { path, operation, value } of getAllOperations(paths)) {
|
||||
if (!isObject(value)) continue;
|
||||
|
||||
const { security } = value;
|
||||
|
||||
if (!Array.isArray(security)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [index, value] of security.entries()) {
|
||||
if (!isObject(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const securityKeys = Object.keys(value);
|
||||
|
||||
for (const securityKey of securityKeys) {
|
||||
if (!allDefs.includes(securityKey)) {
|
||||
results.push({
|
||||
message: `Operation "security" values must match a scheme defined in the "${schemesPath.join(
|
||||
'.',
|
||||
)}" object.`,
|
||||
path: ['paths', path, operation, 'security', index, securityKey],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
);
|
||||
32
.stoplight/custom-functions/oasOpSuccessResponse.js
Normal file
32
.stoplight/custom-functions/oasOpSuccessResponse.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createRulesetFunction } from '@stoplight/spectral-core';
|
||||
import { oas3 } from '@stoplight/spectral-formats';
|
||||
|
||||
export const oasOpSuccessResponse = createRulesetFunction(
|
||||
{
|
||||
input: {
|
||||
type: 'object',
|
||||
},
|
||||
options: null,
|
||||
},
|
||||
(input, opts, context) => {
|
||||
const isOAS3X = context.document.formats?.has(oas3) === true;
|
||||
|
||||
for (const response of Object.keys(input)) {
|
||||
if (isOAS3X && (response === '2XX' || response === '3XX')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number(response) >= 200 && Number(response) < 400) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
message: 'Operation must define at least a single 2xx or 3xx response',
|
||||
},
|
||||
];
|
||||
},
|
||||
);
|
||||
|
||||
export default oasOpSuccessResponse;
|
||||
162
.stoplight/custom-functions/oasPathParam.js
Normal file
162
.stoplight/custom-functions/oasPathParam.js
Normal file
@@ -0,0 +1,162 @@
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
const pathRegex = /(\{;?\??[a-zA-Z0-9_-]+\*?\})/g;
|
||||
|
||||
const isNamedPathParam = p => {
|
||||
return p.in !== void 0 && p.in === 'path' && p.name !== void 0;
|
||||
};
|
||||
|
||||
const isUnknownNamedPathParam = (p, path, results, seen) => {
|
||||
if (!isNamedPathParam(p)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (p.required !== true) {
|
||||
results.push(generateResult(requiredMessage(p.name), path));
|
||||
}
|
||||
|
||||
if (p.name in seen) {
|
||||
results.push(generateResult(uniqueDefinitionMessage(p.name), path));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const ensureAllDefinedPathParamsAreUsedInPath = (path, params, expected, results) => {
|
||||
for (const p of Object.keys(params)) {
|
||||
if (!params[p]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!expected.includes(p)) {
|
||||
const resPath = params[p];
|
||||
results.push(generateResult(`Parameter "${p}" must be used in path "${path}".`, resPath));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ensureAllExpectedParamsInPathAreDefined = (path, params, expected, operationPath, results) => {
|
||||
for (const p of expected) {
|
||||
if (!(p in params)) {
|
||||
results.push(
|
||||
generateResult(`Operation must define parameter "{${p}}" as expected by path "${path}".`, operationPath),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const oasPathParam = targetVal => {
|
||||
/**
|
||||
* This rule verifies:
|
||||
*
|
||||
* 1. for every param referenced in the path string ie /users/{userId}, var must be defined in either
|
||||
* path.parameters, or operation.parameters object
|
||||
* 2. every path.parameters + operation.parameters property must be used in the path string
|
||||
*/
|
||||
|
||||
if (!isObject(targetVal) || !isObject(targetVal.paths)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
// keep track of normalized paths for verifying paths are unique
|
||||
const uniquePaths = {};
|
||||
const validOperationKeys = ['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'trace'];
|
||||
|
||||
for (const path of Object.keys(targetVal.paths)) {
|
||||
const pathValue = targetVal.paths[path];
|
||||
if (!isObject(pathValue)) continue;
|
||||
|
||||
// verify normalized paths are functionally unique (ie `/path/{one}` vs `/path/{two}` are
|
||||
// different but equivalent within the context of OAS)
|
||||
const normalized = path.replace(pathRegex, '%'); // '%' is used here since its invalid in paths
|
||||
if (normalized in uniquePaths) {
|
||||
results.push(
|
||||
generateResult(`Paths "${String(uniquePaths[normalized])}" and "${path}" must not be equivalent.`, [
|
||||
'paths',
|
||||
path,
|
||||
]),
|
||||
);
|
||||
} else {
|
||||
uniquePaths[normalized] = path;
|
||||
}
|
||||
|
||||
// find all templated path parameters
|
||||
const pathElements = [];
|
||||
let match;
|
||||
|
||||
while ((match = pathRegex.exec(path))) {
|
||||
const p = match[0].replace(/[{}?*;]/g, '');
|
||||
if (pathElements.includes(p)) {
|
||||
results.push(generateResult(`Path "${path}" must not use parameter "{${p}}" multiple times.`, ['paths', path]));
|
||||
} else {
|
||||
pathElements.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
// find parameters set within the top-level 'parameters' object
|
||||
const topParams = {};
|
||||
if (Array.isArray(pathValue.parameters)) {
|
||||
for (const [i, value] of pathValue.parameters.entries()) {
|
||||
if (!isObject(value)) continue;
|
||||
|
||||
const fullParameterPath = ['paths', path, 'parameters', i];
|
||||
|
||||
if (isUnknownNamedPathParam(value, fullParameterPath, results, topParams)) {
|
||||
topParams[value.name] = fullParameterPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(targetVal.paths[path])) {
|
||||
// find parameters set within the operation's 'parameters' object
|
||||
for (const op of Object.keys(pathValue)) {
|
||||
const operationValue = pathValue[op];
|
||||
if (!isObject(operationValue)) continue;
|
||||
|
||||
if (op === 'parameters' || !validOperationKeys.includes(op)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const operationParams = {};
|
||||
const { parameters } = operationValue;
|
||||
const operationPath = ['paths', path, op];
|
||||
|
||||
if (Array.isArray(parameters)) {
|
||||
for (const [i, p] of parameters.entries()) {
|
||||
if (!isObject(p)) continue;
|
||||
|
||||
const fullParameterPath = [...operationPath, 'parameters', i];
|
||||
|
||||
if (isUnknownNamedPathParam(p, fullParameterPath, results, operationParams)) {
|
||||
operationParams[p.name] = fullParameterPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const definedParams = { ...topParams, ...operationParams };
|
||||
ensureAllDefinedPathParamsAreUsedInPath(path, definedParams, pathElements, results);
|
||||
ensureAllExpectedParamsInPathAreDefined(path, definedParams, pathElements, operationPath, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
function generateResult(message, path) {
|
||||
return {
|
||||
message,
|
||||
path,
|
||||
};
|
||||
}
|
||||
|
||||
const requiredMessage = name => `Path parameter "${name}" must have "required" property that is set to "true".`;
|
||||
|
||||
const uniqueDefinitionMessage = name => `Path parameter "${name}" must not be defined multiple times.`;
|
||||
|
||||
export default oasPathParam;
|
||||
88
.stoplight/custom-functions/oasSchema.js
Normal file
88
.stoplight/custom-functions/oasSchema.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import traverse from 'json-schema-traverse';
|
||||
import { schema as schemaFn } from '@stoplight/spectral-functions';
|
||||
import { createRulesetFunction } from '@stoplight/spectral-core';
|
||||
import { oas2, oas3_1, extractDraftVersion, oas3_0 } from '@stoplight/spectral-formats';
|
||||
import { isPlainObject, pointerToPath } from '@stoplight/json';
|
||||
|
||||
function rewriteNullable(schema, errors) {
|
||||
for (const error of errors) {
|
||||
if (error.keyword !== 'type') continue;
|
||||
const value = getSchemaProperty(schema, error.schemaPath);
|
||||
if (isPlainObject(value) && value.nullable === true) {
|
||||
error.message += ',null';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default createRulesetFunction(
|
||||
{
|
||||
input: null,
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
function oasSchema(targetVal, opts, context) {
|
||||
const formats = context.document.formats;
|
||||
|
||||
let { schema } = opts;
|
||||
|
||||
let dialect = 'draft4';
|
||||
let prepareResults;
|
||||
|
||||
if (!formats) {
|
||||
dialect = 'auto';
|
||||
} else if (formats.has(oas3_1)) {
|
||||
if (isPlainObject(context.document.data) && typeof context.document.data.jsonSchemaDialect === 'string') {
|
||||
dialect = extractDraftVersion(context.document.data.jsonSchemaDialect) ?? 'draft2020-12';
|
||||
} else {
|
||||
dialect = 'draft2020-12';
|
||||
}
|
||||
} else if (formats.has(oas3_0)) {
|
||||
prepareResults = rewriteNullable.bind(null, schema);
|
||||
} else if (formats.has(oas2)) {
|
||||
const clonedSchema = JSON.parse(JSON.stringify(schema));
|
||||
traverse(clonedSchema, visitOAS2);
|
||||
schema = clonedSchema;
|
||||
prepareResults = rewriteNullable.bind(null, clonedSchema);
|
||||
}
|
||||
|
||||
return schemaFn(
|
||||
targetVal,
|
||||
{
|
||||
...opts,
|
||||
schema,
|
||||
prepareResults,
|
||||
dialect,
|
||||
},
|
||||
context,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const visitOAS2 = schema => {
|
||||
if (schema['x-nullable'] === true) {
|
||||
schema.nullable = true;
|
||||
delete schema['x-nullable'];
|
||||
}
|
||||
};
|
||||
|
||||
function getSchemaProperty(schema, schemaPath) {
|
||||
const path = pointerToPath(schemaPath);
|
||||
let value = schema;
|
||||
|
||||
for (const fragment of path.slice(0, -1)) {
|
||||
if (!isPlainObject(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = value[fragment];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
81
.stoplight/custom-functions/oasTagDefined.js
Normal file
81
.stoplight/custom-functions/oasTagDefined.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// This function will check an API doc to verify that any tag that appears on
|
||||
// an operation is also present in the global tags array.
|
||||
import { isPlainObject } from '@stoplight/json';
|
||||
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
const validOperationKeys = ['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'trace'];
|
||||
|
||||
function* getAllOperations(paths) {
|
||||
if (!isPlainObject(paths)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = {
|
||||
path: '',
|
||||
operation: '',
|
||||
value: null,
|
||||
};
|
||||
|
||||
for (const path of Object.keys(paths)) {
|
||||
const operations = paths[path];
|
||||
if (!isPlainObject(operations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
item.path = path;
|
||||
|
||||
for (const operation of Object.keys(operations)) {
|
||||
if (!isPlainObject(operations[operation]) || !validOperationKeys.includes(operation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
item.operation = operation;
|
||||
item.value = operations[operation];
|
||||
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const oasTagDefined = targetVal => {
|
||||
if (!isObject(targetVal)) return;
|
||||
const results = [];
|
||||
|
||||
const globalTags = [];
|
||||
|
||||
if (Array.isArray(targetVal.tags)) {
|
||||
for (const tag of targetVal.tags) {
|
||||
if (isObject(tag) && typeof tag.name === 'string') {
|
||||
globalTags.push(tag.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { paths } = targetVal;
|
||||
|
||||
for (const { path, operation, value } of getAllOperations(paths)) {
|
||||
if (!isObject(value)) continue;
|
||||
|
||||
const { tags } = value;
|
||||
|
||||
if (!Array.isArray(tags)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [i, tag] of tags.entries()) {
|
||||
if (!globalTags.includes(tag)) {
|
||||
results.push({
|
||||
message: 'Operation tags must be defined in global tags.',
|
||||
path: ['paths', path, operation, 'tags', i],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export default oasTagDefined;
|
||||
50
.stoplight/custom-functions/oasUnusedComponent.js
Normal file
50
.stoplight/custom-functions/oasUnusedComponent.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { unreferencedReusableObject } from '@stoplight/spectral-functions';
|
||||
import { createRulesetFunction } from '@stoplight/spectral-core';
|
||||
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
export default createRulesetFunction(
|
||||
{
|
||||
input: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
components: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
required: ['components'],
|
||||
},
|
||||
options: null,
|
||||
},
|
||||
function oasUnusedComponent(targetVal, opts, context) {
|
||||
const results = [];
|
||||
const componentTypes = [
|
||||
'schemas',
|
||||
'responses',
|
||||
'parameters',
|
||||
'examples',
|
||||
'requestBodies',
|
||||
'headers',
|
||||
'links',
|
||||
'callbacks',
|
||||
];
|
||||
|
||||
for (const type of componentTypes) {
|
||||
const value = targetVal.components[type];
|
||||
if (!isObject(value)) continue;
|
||||
|
||||
const resultsForType = unreferencedReusableObject(
|
||||
value,
|
||||
{ reusableObjectsLocation: `#/components/${type}` },
|
||||
context,
|
||||
);
|
||||
if (resultsForType !== void 0 && Array.isArray(resultsForType)) {
|
||||
results.push(...resultsForType);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
);
|
||||
51
.stoplight/custom-functions/refSiblings.js
Normal file
51
.stoplight/custom-functions/refSiblings.js
Normal file
@@ -0,0 +1,51 @@
|
||||
function isObject(value) {
|
||||
return value !== null && typeof value === 'object';
|
||||
}
|
||||
|
||||
function getParentValue(document, path) {
|
||||
if (path.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let piece = document;
|
||||
|
||||
for (let i = 0; i < path.length - 1; i += 1) {
|
||||
if (!isObject(piece)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
piece = piece[path[i]];
|
||||
}
|
||||
|
||||
return piece;
|
||||
}
|
||||
|
||||
const refSiblings = (targetVal, opts, { document, path }) => {
|
||||
const value = getParentValue(document.data, path);
|
||||
|
||||
if (!isObject(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = Object.keys(value);
|
||||
if (keys.length === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const actualObjPath = path.slice(0, -1);
|
||||
|
||||
for (const key of keys) {
|
||||
if (key === '$ref') {
|
||||
continue;
|
||||
}
|
||||
results.push({
|
||||
message: '$ref must not be placed next to any other properties',
|
||||
path: [...actualObjPath, key],
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export default refSiblings;
|
||||
92
.stoplight/custom-functions/typedEnum.js
Normal file
92
.stoplight/custom-functions/typedEnum.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import { oas2, oas3_0 } from '@stoplight/spectral-formats';
|
||||
import { printValue } from '@stoplight/spectral-runtime';
|
||||
import { createRulesetFunction } from '@stoplight/spectral-core';
|
||||
|
||||
function getDataType(input, checkForInteger) {
|
||||
const type = typeof input;
|
||||
switch (type) {
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
return type;
|
||||
case 'number':
|
||||
if (checkForInteger && Number.isInteger(input)) {
|
||||
return 'integer';
|
||||
}
|
||||
|
||||
return 'number';
|
||||
case 'object':
|
||||
if (input === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
return Array.isArray(input) ? 'array' : 'object';
|
||||
default:
|
||||
throw TypeError('Unknown input type');
|
||||
}
|
||||
}
|
||||
|
||||
function getTypes(input, formats) {
|
||||
const { type } = input;
|
||||
|
||||
if (
|
||||
(input.nullable === true && formats?.has(oas3_0) === true) ||
|
||||
(input['x-nullable'] === true && formats?.has(oas2) === true)
|
||||
) {
|
||||
return Array.isArray(type) ? [...type, 'null'] : [type, 'null'];
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
export const typedEnum = createRulesetFunction(
|
||||
{
|
||||
input: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
enum: {
|
||||
type: 'array',
|
||||
},
|
||||
type: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['enum', 'type'],
|
||||
},
|
||||
options: null,
|
||||
},
|
||||
function (input, opts, context) {
|
||||
const { enum: enumValues } = input;
|
||||
const type = getTypes(input, context.document.formats);
|
||||
const checkForInteger = type === 'integer' || (Array.isArray(type) && type.includes('integer'));
|
||||
|
||||
let results;
|
||||
|
||||
enumValues.forEach((value, i) => {
|
||||
const valueType = getDataType(value, checkForInteger);
|
||||
|
||||
if (valueType === type || (Array.isArray(type) && type.includes(valueType))) {
|
||||
return;
|
||||
}
|
||||
|
||||
results ??= [];
|
||||
results.push({
|
||||
message: `Enum value ${printValue(enumValues[i])} must be "${String(type)}".`,
|
||||
path: [...context.path, 'enum', i],
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
},
|
||||
);
|
||||
|
||||
export default typedEnum;
|
||||
2868
.stoplight/styleguide.json
Normal file
2868
.stoplight/styleguide.json
Normal file
File diff suppressed because one or more lines are too long
16
.travis.yml
16
.travis.yml
@@ -1,11 +1,19 @@
|
||||
language: node_js
|
||||
node_js: '8.7.0'
|
||||
node_js: '11.9.0'
|
||||
git:
|
||||
submodules: false
|
||||
submodules: true
|
||||
script:
|
||||
yarn test
|
||||
- yarn test
|
||||
- yarn coverage
|
||||
before_install:
|
||||
- cd seasoned_api
|
||||
before_script: yarn
|
||||
- cp conf/development.json.example conf/development.json
|
||||
before_script:
|
||||
- yarn
|
||||
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
||||
- chmod +x ./cc-test-reporter
|
||||
- ./cc-test-reporter before-build
|
||||
after-script:
|
||||
- ./cc-test-resporter after-build --exit-code $TRAVIS_TEST_RESULT
|
||||
cache: false
|
||||
os: linux
|
||||
|
||||
149
README.md
149
README.md
@@ -1,11 +1,130 @@
|
||||
# 🌶 seasonedShows
|
||||
[](https://travis-ci.org/KevinMidboe/seasonedShows)
|
||||
[](https://snyk.io/test/github/KevinMidboe/seasonedShows?targetFile=seasoned_api/package.json)
|
||||
[]()
|
||||
|
||||
Your customly *seasoned* movie and show requester, downloader and organizer. Demo page can be viewed [here](https://kevinmidboe.com/request)
|
||||
<h1 align="center">
|
||||
🌶 seasonedShows
|
||||
</h1>
|
||||
|
||||
<h4 align="center"> Season your media library with the shows and movies that you and your friends want.</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/KevinMidboe/seasonedShows">
|
||||
<img src="https://travis-ci.org/KevinMidboe/seasonedShows.svg?branch=master"
|
||||
alt="Travis CI">
|
||||
</a>
|
||||
<a href="https://coveralls.io/github/KevinMidboe/seasonedShows?branch=api/v2">
|
||||
<img src="https://coveralls.io/repos/github/KevinMidboe/seasonedShows/badge.svg?branch=api/v2" alt="">
|
||||
</a>
|
||||
<a href="https://snyk.io/test/github/KevinMidboe/seasonedShows?targetFile=seasoned_api/package.json">
|
||||
<img src="https://snyk.io/test/github/KevinMidboe/seasonedShows/badge.svg?targetFile=seasoned_api/package.json" alt="">
|
||||
</a>
|
||||
<a href="https://opensource.org/licenses/MIT">
|
||||
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#demo-documentation">D & D</a> •
|
||||
<a href="#about">About</a> •
|
||||
<a href="#key-features">Key features</a> •
|
||||
<a href="#installation">Installation</a> •
|
||||
<a href="#setup">Setup</a> •
|
||||
<a href="#running">Running</a> •
|
||||
<a href="#daemon">Setup daemon</a> •
|
||||
<a href="#contributing">Contributing</a>
|
||||
</p>
|
||||
|
||||
## <a name="demo-documentation"></a> Demo & Documentation
|
||||
📺 [DEMO](https://kevinmidboe.com/request)
|
||||
📝 Documentation of the api.
|
||||
💖 Checkout my [fancy vue.js page](https://github.com/KevinMidboe/seasonedRequest) for interfacing the api.
|
||||
|
||||
## <a name="about"></a> About
|
||||
This is the backend api for [seasoned request] that allows for uesrs to request movies and shows by fetching movies from themoviedb api and checks them with your plex library to identify if a movie is already present or not. This api allows to search my query, get themoviedb movie lists like popular and now playing, all while checking if the item is already in your plex library. Your friends can create users to see what movies or shows they have requested and searched for.
|
||||
|
||||
The api also uses torrent_search to search for matching torrents and returns results from any site or service available from torrent_search. As a admin of the site you can query torrent_search and return a magnet link that can be added to a autoadd folder of your favorite torrent client.
|
||||
|
||||
## <a name="key-features"></a> Key features
|
||||
### Code
|
||||
- Uses [tmdb api](https://www.themoviedb.org/documentation/api) with over 350k movies and 70k tv shows
|
||||
- Written asynchronously
|
||||
- Uses caching for external requests
|
||||
- Test coverage
|
||||
- CI and dependency integrated
|
||||
- Use either config file or env_variables
|
||||
### Functionality
|
||||
- Queries plex library to check if items exists
|
||||
- Create admin and normal user accounts
|
||||
- [torrent_search](https://github.com/KevinMidboe/torrent_search) to search for torrents
|
||||
- Fetch curated lists from tmdb
|
||||
|
||||
## <a name="installation"></a> Installation
|
||||
Before we can use seasonedShows we need to download node and a package manager. For instructions on how to install [yarn](https://yarnpkg.com/en/) or [npm](https://www.npmjs.com) package managers refer to [wiki: install package manager](https://github.com/KevinMidboe/seasonedShows/wiki/Install-package-manager). This api is written with express using node.js as the JavaScript runtime engine. To download node.js head over the the official [node.js download page](https://nodejs.org/en/download/).
|
||||
|
||||
### Install seasonedShows
|
||||
After you have downloaded a package manager and node.js javascript engine, the following will guide you through how to download, install and run seasonedShows.
|
||||
|
||||
### macOS
|
||||
- Open terminal
|
||||
- Install git. This can be done by running `xcode-select --install` in your favorite terminal.
|
||||
- Install a package manager, refer to this [wiki page] for yarn or [wiki page] for npm
|
||||
- Type: `git clone --recurse-submodules git@github.com:KevinMidboe/seasonedShows.git`
|
||||
- Type: `cd seasonedShows/`
|
||||
- Install required packages
|
||||
* yarn: `yarn install`
|
||||
* npm: `npm install`
|
||||
- Start server:
|
||||
* yarn: `yarn start`
|
||||
* npm: `npm run start`
|
||||
- seasonedShows will now be running at http://localhost:31459
|
||||
- To have seasonedShows run headless on startup, check out this wiki page to [install as a daemon].
|
||||
|
||||
### Linux
|
||||
- Open terminal
|
||||
- Install git
|
||||
* Ubuntu/Debian: `sudo apt-get install git-core`
|
||||
* Fedora: `sudo yum install git`
|
||||
- Type: `git clone --recurse-submodules git@github.com:KevinMidboe/seasonedShows.git`
|
||||
- Type: `cd seasonedShows/`
|
||||
- Install required packages
|
||||
* yarn: `yarn install`
|
||||
* npm: `npm install`
|
||||
- Start server:
|
||||
* yarn: `yarn start`
|
||||
* npm: `npm run start`
|
||||
- seasonedShows will now be running at http://localhost:31459
|
||||
- To have seasonedShows run headless on startup, check out this wiki page to [install as a daemon].
|
||||
|
||||
-- same --
|
||||
(install yarn or npm in a different way)
|
||||
After you have installed the required packages you will have a node_modules directory with all the packages required in packages.json.
|
||||
|
||||
### Requirements
|
||||
- Node 7.6 < [wiki page]
|
||||
- Plex library
|
||||
|
||||
## <a name="setup"></a> Setup and/ configuration
|
||||
There is a config file template, what the values mean and how to change them.
|
||||
Also show how to hide file from git if not want to show up as uncommitted file.
|
||||
Also set variables in environment.
|
||||
|
||||
## <a name="running"></a> Running/using
|
||||
yarn/npm start. (can also say this above)
|
||||
How to create service on linux. This means that
|
||||
|
||||
## <a name="daemon"></a> Setup a daemon
|
||||
The next step is to setup seasonedShows api to run in the background as a daemon. I have written a [wiki page](https://github.com/KevinMidboe/seasonedShows/wiki/Install-as-a-daemon) on how to create a daemon on several unix distors and macOS.
|
||||
*Please don't hesitate to add your own system if you get it running on something that is not yet lists on the formentioned wiki page.*
|
||||
|
||||
## <a name="contributing"></a> Contributing
|
||||
- Fork it!
|
||||
- Create your feature branch: git checkout -b my-new-feature
|
||||
- Commit your changes: git commit -am 'Add some feature'
|
||||
- Push to the branch: git push origin my-new-feature
|
||||
- Submit a pull request
|
||||
|
||||
|
||||
## Api documentation
|
||||
|
||||
|
||||
## About
|
||||
The goal of this project is to create a full custom stack that can to everything surround downloading, organizing and notifiyng of new media. From the top down we have a website using [tmdb](https://www.themoviedb.com) api to search for from over 350k movies and 70k tv shows. Using [hjone72](https://github.com/hjone72/PlexAuth) great PHP reverse proxy we can have a secure way of allowing users to login with their plex credentials which limits request capabilites to only users that are authenticated to use your plex library.
|
||||
seasonedShows is a intelligent organizer for your tv show episodes. It is made to automate and simplify to process of renaming and moving newly downloaded tv show episodes following Plex file naming and placement.
|
||||
|
||||
@@ -20,21 +139,3 @@ The flow of the system will first check for new folders in your tv shows directo
|
||||
Then there is a script for looking for replies on twitter by user_admin, if caanges are needed, it handles the changes specified and updates dtabbase.
|
||||
|
||||
After approval by user the files are modified and moved to folders in resptected area. If error occours, pasteee link if log is sent to user.
|
||||
|
||||
#### External
|
||||
+ Seasoned: request, discover and manage.
|
||||
+ Stray: Overview of downloaded episodes before they are organized.
|
||||
+ (+) Admin Panel: Overview of all stray episodes/movies.
|
||||
|
||||
#### Api
|
||||
+ All communication between public website to server.
|
||||
+ Plex: All querying to what is localy available in your plex library.
|
||||
+ Stray (seasoned) -> also calls services (moveStray) through api.
|
||||
+ Tmdb: Requesting information from tmdb.
|
||||
+ (+) Admin Panel: Use secure login and session tokens to handle logged in viewer.
|
||||
|
||||
#### Services
|
||||
+ Parse directories for new content.
|
||||
+ Extract and save in db information about stray item.
|
||||
+ Move a confirmed stray item.
|
||||
+ (+) Search for torrents matching new content.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-04-05 18:40:11
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-06-18 21:49:33
|
||||
# @Last Modified time: 2018-04-03 22:58:20
|
||||
import os.path, hashlib, time, glob, sqlite3, re, json, tweepy
|
||||
import logging
|
||||
from functools import reduce
|
||||
@@ -61,7 +61,7 @@ class strayEpisode(object):
|
||||
return hashlib.md5("b'{}'".format(self.parent).encode()).hexdigest()[:8]
|
||||
|
||||
def findSeriesName(self):
|
||||
find = re.compile("^[a-zA-Z. ]*")
|
||||
find = re.compile("^[a-zA-Z0-9. ]*")
|
||||
m = re.match(find, self.parent)
|
||||
if m:
|
||||
name, hit = process.extractOne(m.group(0), getShowNames().keys())
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-04-12 23:27:51
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-06-27 15:58:09
|
||||
# @Last Modified time: 2018-05-13 19:17:17
|
||||
|
||||
import sys, sqlite3, json, os.path
|
||||
import logging
|
||||
import env_variables as env
|
||||
import shutil
|
||||
|
||||
import delugeClient.deluge_cli as delugeCli
|
||||
|
||||
class episode(object):
|
||||
def __init__(self, id):
|
||||
self.id = id
|
||||
@@ -91,8 +93,18 @@ def moveStray(strayId):
|
||||
except FileNotFoundError:
|
||||
logging.warning('Cannot remove ' + ep.typeDir('parent_input') + ', file no longer exists.')
|
||||
|
||||
# Remove from deluge client
|
||||
logging.info('Removing {} for deluge'.format(ep.parent))
|
||||
deluge = delugeCli.Deluge()
|
||||
response = deluge.remove(ep.parent)
|
||||
logging.info('Deluge response after delete: {}'.format(response))
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if (os.path.exists(env.logfile)):
|
||||
abspath = os.path.abspath(__file__)
|
||||
dname = os.path.dirname(abspath)
|
||||
if (os.path.exists(os.path.join(dname, env.logfile))):
|
||||
logging.basicConfig(filename=env.logfile, level=logging.INFO)
|
||||
else:
|
||||
print('Logfile could not be found at ' + env.logfile + '. Verifiy presence or disable logging in config.')
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"clean-webpack-plugin": "^0.1.17",
|
||||
"css-loader": "^0.28.4",
|
||||
"css-loader": "^1.0.0",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"path": "^0.12.7",
|
||||
"react": "^15.6.1",
|
||||
@@ -30,8 +30,8 @@
|
||||
"redux-thunk": "^2.2.0",
|
||||
"urijs": "^1.18.12",
|
||||
"webfontloader": "^1.6.28",
|
||||
"webpack": "^3.5.5",
|
||||
"webpack-dev-server": "^2.4.5",
|
||||
"webpack": "^4.0.0",
|
||||
"webpack-dev-server": "^3.1.11",
|
||||
"webpack-merge": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
439
reference/seasoned-api.yaml
Normal file
439
reference/seasoned-api.yaml
Normal file
@@ -0,0 +1,439 @@
|
||||
openapi: 3.1.0
|
||||
x-stoplight:
|
||||
id: lu1x37qqzll6m
|
||||
info:
|
||||
title: seasoned api
|
||||
version: '1.0'
|
||||
summary: Season your media library with the shows and movies that you and your friends want.
|
||||
description: |
|
||||
This is the backend api for [seasoned request] that allows for uesrs to request movies and shows by fetching movies from themoviedb api and checks them with your plex library to identify if a movie is already present or not. This api allows to search my query, get themoviedb movie lists like popular and now playing, all while checking if the item is already in your plex library. Your friends can create users to see what movies or shows they have requested and searched for.
|
||||
|
||||
The api also uses torrent_search to search for matching torrents and returns results from any site or service available from torrent_search. As a admin of the site you can query torrent_search and return a magnet link that can be added to a autoadd folder of your favorite torrent client.
|
||||
servers:
|
||||
- url: 'https://request.movie/api'
|
||||
description: poduction
|
||||
- url: 'https://localhost:31459'
|
||||
description: localhost
|
||||
paths:
|
||||
/v2/movie/now_playing:
|
||||
get:
|
||||
summary: Your GET endpoint
|
||||
tags: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
title:
|
||||
type: string
|
||||
year:
|
||||
type: integer
|
||||
overview:
|
||||
type: string
|
||||
poster:
|
||||
type: string
|
||||
backdrop:
|
||||
type: string
|
||||
release_date:
|
||||
type: string
|
||||
rating:
|
||||
type: number
|
||||
type:
|
||||
type: string
|
||||
page:
|
||||
type: integer
|
||||
total_results:
|
||||
type: integer
|
||||
total_pages:
|
||||
type: integer
|
||||
x-examples:
|
||||
example-1:
|
||||
results:
|
||||
- id: 616037
|
||||
title: 'Thor: Love and Thunder'
|
||||
year: 2022
|
||||
overview: 'After his retirement is interrupted by Gorr the God Butcher, a galactic killer who seeks the extinction of the gods, Thor enlists the help of King Valkyrie, Korg, and ex-girlfriend Jane Foster, who now inexplicably wields Mjolnir as the Mighty Thor. Together they embark upon a harrowing cosmic adventure to uncover the mystery of the God Butcher’s vengeance and stop him before it’s too late.'
|
||||
poster: /pIkRyD18kl4FhoCNQuWxWu5cBLM.jpg
|
||||
backdrop: /p1F51Lvj3sMopG948F5HsBbl43C.jpg
|
||||
release_date: '2022-07-06T00:00:00.000Z'
|
||||
rating: 6.8
|
||||
type: movie
|
||||
- id: 507086
|
||||
title: Jurassic World Dominion
|
||||
year: 2022
|
||||
overview: 'Four years after Isla Nublar was destroyed, dinosaurs now live—and hunt—alongside humans all over the world. This fragile balance will reshape the future and determine, once and for all, whether human beings are to remain the apex predators on a planet they now share with history’s most fearsome creatures.'
|
||||
poster: /kAVRgw7GgK1CfYEJq8ME6EvRIgU.jpg
|
||||
backdrop: /9eAn20y26wtB3aet7w9lHjuSgZ3.jpg
|
||||
release_date: '2022-06-01T00:00:00.000Z'
|
||||
rating: 7.1
|
||||
type: movie
|
||||
- id: 438148
|
||||
title: 'Minions: The Rise of Gru'
|
||||
year: 2022
|
||||
overview: 'A fanboy of a supervillain supergroup known as the Vicious 6, Gru hatches a plan to become evil enough to join them, with the backup of his followers, the Minions.'
|
||||
poster: /wKiOkZTN9lUUUNZLmtnwubZYONg.jpg
|
||||
backdrop: /nmGWzTLMXy9x7mKd8NKPLmHtWGa.jpg
|
||||
release_date: '2022-06-29T00:00:00.000Z'
|
||||
rating: 7.8
|
||||
type: movie
|
||||
- id: 585511
|
||||
title: Luck
|
||||
year: 2022
|
||||
overview: 'Suddenly finding herself in the never-before-seen Land of Luck, the unluckiest person in the world must unite with the magical creatures there to turn her luck around.'
|
||||
poster: /1HOYvwGFioUFL58UVvDRG6beEDm.jpg
|
||||
backdrop: /3VQj6m0I6gkuRaljmhNZl0XR3by.jpg
|
||||
release_date: '2022-08-05T00:00:00.000Z'
|
||||
rating: 8.1
|
||||
type: movie
|
||||
- id: 756999
|
||||
title: The Black Phone
|
||||
year: 2022
|
||||
overview: 'Finney Blake, a shy but clever 13-year-old boy, is abducted by a sadistic killer and trapped in a soundproof basement where screaming is of little use. When a disconnected phone on the wall begins to ring, Finney discovers that he can hear the voices of the killer’s previous victims. And they are dead set on making sure that what happened to them doesn’t happen to Finney.'
|
||||
poster: /lr11mCT85T1JanlgjMuhs9nMht4.jpg
|
||||
backdrop: /jqVyOIz8jxH0NUlc0QUHmV0uOcn.jpg
|
||||
release_date: '2022-06-22T00:00:00.000Z'
|
||||
rating: 8
|
||||
type: movie
|
||||
- id: 610150
|
||||
title: 'Dragon Ball Super: Super Hero'
|
||||
year: 2022
|
||||
overview: 'The Red Ribbon Army, an evil organization that was once destroyed by Goku in the past, has been reformed by a group of people who have created new and mightier Androids, Gamma 1 and Gamma 2, and seek vengeance against Goku and his family.'
|
||||
poster: /rugyJdeoJm7cSJL1q4jBpTNbxyU.jpg
|
||||
backdrop: /uR0FopHrAjDlG5q6PZB07a1JOva.jpg
|
||||
release_date: '2022-06-11T00:00:00.000Z'
|
||||
rating: 7.5
|
||||
type: movie
|
||||
- id: 760104
|
||||
title: X
|
||||
year: 2022
|
||||
overview: 'In 1979, a group of young filmmakers set out to make an adult film in rural Texas, but when their reclusive, elderly hosts catch them in the act, the cast find themselves fighting for their lives.'
|
||||
poster: /woTQx9Q4b8aO13jR9dsj8C9JESy.jpg
|
||||
backdrop: /2oXQpm0wfOkIL0jBJABbL5AfMs6.jpg
|
||||
release_date: '2022-03-17T00:00:00.000Z'
|
||||
rating: 6.7
|
||||
type: movie
|
||||
- id: 718789
|
||||
title: Lightyear
|
||||
year: 2022
|
||||
overview: Legendary Space Ranger Buzz Lightyear embarks on an intergalactic adventure alongside a group of ambitious recruits and his robot companion Sox.
|
||||
poster: /ox4goZd956BxqJH6iLwhWPL9ct4.jpg
|
||||
backdrop: /nW5fUbldp1DYf2uQ3zJTUdachOu.jpg
|
||||
release_date: '2022-06-15T00:00:00.000Z'
|
||||
rating: 7.3
|
||||
type: movie
|
||||
- id: 725201
|
||||
title: The Gray Man
|
||||
year: 2022
|
||||
overview: 'When a shadowy CIA agent uncovers damning agency secrets, he''s hunted across the globe by a sociopathic rogue operative who''s put a bounty on his head.'
|
||||
poster: /8cXbitsS6dWQ5gfMTZdorpAAzEH.jpg
|
||||
backdrop: /27Mj3rFYP3xqFy7lnz17vEd8Ms.jpg
|
||||
release_date: '2022-07-13T00:00:00.000Z'
|
||||
rating: 7
|
||||
type: movie
|
||||
- id: 758724
|
||||
title: The Cellar
|
||||
year: 2022
|
||||
overview: 'When Keira Woods'' daughter mysteriously vanishes in the cellar of their new house in the country, she soon discovers there is an ancient and powerful entity controlling their home that she will have to face or risk losing her family''s souls forever.'
|
||||
poster: /rtfGeS5WMXA6PtikIYUmYTSbVdg.jpg
|
||||
backdrop: /qViFGWCHaSmW4gP00RGh3xjMjsP.jpg
|
||||
release_date: '2022-03-25T00:00:00.000Z'
|
||||
rating: 6.6
|
||||
type: movie
|
||||
- id: 961484
|
||||
title: Last Seen Alive
|
||||
year: 2022
|
||||
overview: 'After Will Spann''s wife suddenly vanishes at a gas station, his desperate search to find her leads him down a dark path that forces him to run from authorities and take the law into his own hands.'
|
||||
poster: /qvqyDj34Uivokf4qIvK4bH0m0qF.jpg
|
||||
backdrop: /ftGzl2GCyko61Qp161bQElN2Uzd.jpg
|
||||
release_date: '2022-05-19T00:00:00.000Z'
|
||||
rating: 6.6
|
||||
type: movie
|
||||
- id: 675353
|
||||
title: Sonic the Hedgehog 2
|
||||
year: 2022
|
||||
overview: 'After settling in Green Hills, Sonic is eager to prove he has what it takes to be a true hero. His test comes when Dr. Robotnik returns, this time with a new partner, Knuckles, in search for an emerald that has the power to destroy civilizations. Sonic teams up with his own sidekick, Tails, and together they embark on a globe-trotting journey to find the emerald before it falls into the wrong hands.'
|
||||
poster: /6DrHO1jr3qVrViUO6s6kFiAGM7.jpg
|
||||
backdrop: /8wwXPG22aNMpPGuXnfm3galoxbI.jpg
|
||||
release_date: '2022-03-30T00:00:00.000Z'
|
||||
rating: 7.7
|
||||
type: movie
|
||||
- id: 924482
|
||||
title: The Ledge
|
||||
year: 2022
|
||||
overview: 'A rock climbing adventure between two friends turns into a terrifying nightmare. After Kelly captures the murder of her best friend on camera, she becomes the next target of a tight-knit group of friends who will stop at nothing to destroy the evidence and anyone in their way. Desperate for her safety, she begins a treacherous climb up a mountain cliff and her survival instincts are put to the test when she becomes trapped with the killers just 20 feet away.'
|
||||
poster: /dHKfsdNcEPw7YIWFPIhqiuWrSAb.jpg
|
||||
backdrop: /jazlkwXfw4KdF6fVTRsolOvRCmu.jpg
|
||||
release_date: '2022-02-18T00:00:00.000Z'
|
||||
rating: 6.3
|
||||
type: movie
|
||||
- id: 698948
|
||||
title: Thirteen Lives
|
||||
year: 2022
|
||||
overview: 'Based on the true nail-biting mission that captivated the world. Twelve boys and the coach of a Thai soccer team explore the Tham Luang cave when an unexpected rainstorm traps them in a chamber inside the mountain. Entombed behind a maze of flooded cave tunnels, they face impossible odds. A team of world-class divers navigate through miles of dangerous cave networks to discover that finding the boys is only the beginning.'
|
||||
poster: /yi5KcJqFxy0D6yP8nCfcF8gJGg5.jpg
|
||||
backdrop: /tHR34A5n0my4maACNdLpWGd6QYq.jpg
|
||||
release_date: '2022-07-18T00:00:00.000Z'
|
||||
rating: 8
|
||||
type: movie
|
||||
- id: 614934
|
||||
title: Elvis
|
||||
year: 2022
|
||||
overview: 'The life story of Elvis Presley as seen through the complicated relationship with his enigmatic manager, Colonel Tom Parker.'
|
||||
poster: /qBOKWqAFbveZ4ryjJJwbie6tXkQ.jpg
|
||||
backdrop: /rLo9T9jEg67UZPq3midjLnTUYYi.jpg
|
||||
release_date: '2022-06-22T00:00:00.000Z'
|
||||
rating: 7.9
|
||||
type: movie
|
||||
- id: 639933
|
||||
title: The Northman
|
||||
year: 2022
|
||||
overview: 'Prince Amleth is on the verge of becoming a man when his father is brutally murdered by his uncle, who kidnaps the boy''s mother. Two decades later, Amleth is now a Viking who''s on a mission to save his mother, kill his uncle and avenge his father.'
|
||||
poster: /8p9zXB7M78nZpm215zHfqpknMeM.jpg
|
||||
backdrop: /k2G4WqGiT60K9yJnPh4K6VLnl3A.jpg
|
||||
release_date: '2022-04-07T00:00:00.000Z'
|
||||
rating: 7.2
|
||||
type: movie
|
||||
- id: 894169
|
||||
title: Vendetta
|
||||
year: 2022
|
||||
overview: 'When his daughter is murdered, William Duncan takes the law into his own hands, setting out on a quest for retribution. After killing the street thug responsible for her death, he finds himself in the middle of a war with the thug''s brother, father, and their gang, who are equally hell-bent on getting even. What ensues is a tense back-and-forth game of vengeance. By the end, William comes to find that the quest for revenge never has a winner.'
|
||||
poster: /7InGE2Sux0o9WGbbn0bl7nZzqEc.jpg
|
||||
backdrop: /33qGtN2GpGEb94pn25PDPeWQZLk.jpg
|
||||
release_date: '2022-05-17T00:00:00.000Z'
|
||||
rating: 6.5
|
||||
type: movie
|
||||
- id: 718930
|
||||
title: Bullet Train
|
||||
year: 2022
|
||||
overview: 'Unlucky assassin Ladybug is determined to do his job peacefully after one too many gigs gone off the rails. Fate, however, may have other plans, as Ladybug''s latest mission puts him on a collision course with lethal adversaries from around the globe—all with connected, yet conflicting, objectives—on the world''s fastest train.'
|
||||
poster: /rTgfp0ZuikSUK8HK8Jgn3PUqteH.jpg
|
||||
backdrop: /C8FpZfTPEZDjngPlatiFsaDB4A.jpg
|
||||
release_date: '2022-07-03T00:00:00.000Z'
|
||||
rating: 7.4
|
||||
type: movie
|
||||
- id: 697799
|
||||
title: WarHunt
|
||||
year: 2022
|
||||
overview: '1945. A U.S. military cargo plane loses control and violently crashes behind enemy lines in the middle of the German black forest. Major Johnson sends a squad of his bravest soldiers on a rescue mission to retrieve the top-secret material the plane was carrying, led by Sergeants Brewer and Walsh. They soon discover hanged Nazi soldiers and other dead bodies bearing ancient, magical symbols. Suddenly their compasses fail, their perceptions twist and straying from the group leads to profound horrors as they are attacked by a powerful, supernatural force.'
|
||||
poster: /9HFFwZOTBB7IPFmn9E0MXdWave3.jpg
|
||||
backdrop: /mTupUmnuwwAyA0CNqpwaZn5mqjk.jpg
|
||||
release_date: '2022-01-21T00:00:00.000Z'
|
||||
rating: 5.1
|
||||
type: movie
|
||||
- id: 818397
|
||||
title: Memory
|
||||
year: 2022
|
||||
overview: 'Alex, an assassin-for-hire, finds that he''s become a target after he refuses to complete a job for a dangerous criminal organization. With the crime syndicate and FBI in hot pursuit, Alex has the skills to stay ahead, except for one thing: he is struggling with severe memory loss, affecting his every move. Alex must question his every action and whom he can ultimately trust.'
|
||||
poster: /4Q1n3TwieoULnuaztu9aFjqHDTI.jpg
|
||||
backdrop: /vjnLXptqdxnpNJer5fWgj2OIGhL.jpg
|
||||
release_date: '2022-04-28T00:00:00.000Z'
|
||||
rating: 7.3
|
||||
type: movie
|
||||
page: 1
|
||||
total_results: 1241
|
||||
total_pages: 63
|
||||
operationId: get-v2-movie-now_playing
|
||||
description: ''
|
||||
security:
|
||||
- authorization: []
|
||||
'/users/{userId}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: integer
|
||||
name: userId
|
||||
in: path
|
||||
required: true
|
||||
description: Id of an existing user.
|
||||
get:
|
||||
summary: Get User Info by User ID
|
||||
tags: []
|
||||
responses:
|
||||
'200':
|
||||
description: User Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
examples:
|
||||
Get User Alice Smith:
|
||||
value:
|
||||
id: 142
|
||||
firstName: Alice
|
||||
lastName: Smith
|
||||
email: alice.smith@gmail.com
|
||||
dateOfBirth: '1997-10-31'
|
||||
emailVerified: true
|
||||
signUpDate: '2019-08-24'
|
||||
'404':
|
||||
description: User Not Found
|
||||
operationId: get-users-userId
|
||||
description: Retrieve the information of the user with the matching user ID.
|
||||
patch:
|
||||
summary: Update User Information
|
||||
operationId: patch-users-userId
|
||||
responses:
|
||||
'200':
|
||||
description: User Updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
examples:
|
||||
Updated User Rebecca Baker:
|
||||
value:
|
||||
id: 13
|
||||
firstName: Rebecca
|
||||
lastName: Baker
|
||||
email: rebecca@gmail.com
|
||||
dateOfBirth: '1985-10-02'
|
||||
emailVerified: false
|
||||
createDate: '2019-08-24'
|
||||
'404':
|
||||
description: User Not Found
|
||||
'409':
|
||||
description: Email Already Taken
|
||||
description: Update the information of an existing user.
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
firstName:
|
||||
type: string
|
||||
lastName:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
description: 'If a new email is given, the user''s email verified property will be set to false.'
|
||||
dateOfBirth:
|
||||
type: string
|
||||
examples:
|
||||
Update First Name:
|
||||
value:
|
||||
firstName: Rebecca
|
||||
Update Email:
|
||||
value:
|
||||
email: rebecca@gmail.com
|
||||
Update Last Name & Date of Birth:
|
||||
value:
|
||||
lastName: Baker
|
||||
dateOfBirth: '1985-10-02'
|
||||
description: Patch user properties to update.
|
||||
/user:
|
||||
post:
|
||||
summary: Create New User
|
||||
operationId: post-user
|
||||
responses:
|
||||
'200':
|
||||
description: User Created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
examples:
|
||||
New User Bob Fellow:
|
||||
value:
|
||||
id: 12
|
||||
firstName: Bob
|
||||
lastName: Fellow
|
||||
email: bob.fellow@gmail.com
|
||||
dateOfBirth: '1996-08-24'
|
||||
emailVerified: false
|
||||
createDate: '2020-11-18'
|
||||
'400':
|
||||
description: Missing Required Information
|
||||
'409':
|
||||
description: Email Already Taken
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
firstName:
|
||||
type: string
|
||||
lastName:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
dateOfBirth:
|
||||
type: string
|
||||
format: date
|
||||
required:
|
||||
- firstName
|
||||
- lastName
|
||||
- email
|
||||
- dateOfBirth
|
||||
examples:
|
||||
Create User Bob Fellow:
|
||||
value:
|
||||
firstName: Bob
|
||||
lastName: Fellow
|
||||
email: bob.fellow@gmail.com
|
||||
dateOfBirth: '1996-08-24'
|
||||
description: Post the necessary fields for the API to create a new user.
|
||||
description: Create a new user.
|
||||
components:
|
||||
schemas:
|
||||
User:
|
||||
title: User
|
||||
type: object
|
||||
description: ''
|
||||
examples:
|
||||
- id: 142
|
||||
firstName: Alice
|
||||
lastName: Smith
|
||||
email: alice.smith@gmail.com
|
||||
dateOfBirth: '1997-10-31'
|
||||
emailVerified: true
|
||||
signUpDate: '2019-08-24'
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: Unique identifier for the given user.
|
||||
firstName:
|
||||
type: string
|
||||
lastName:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
dateOfBirth:
|
||||
type: string
|
||||
format: date
|
||||
example: '1997-10-31'
|
||||
emailVerified:
|
||||
type: boolean
|
||||
description: Set to true if the user's email has been verified.
|
||||
createDate:
|
||||
type: string
|
||||
format: date
|
||||
description: The date that the user was created.
|
||||
required:
|
||||
- id
|
||||
- firstName
|
||||
- lastName
|
||||
- email
|
||||
- emailVerified
|
||||
securitySchemes:
|
||||
authorization:
|
||||
name: Authorization token
|
||||
type: apiKey
|
||||
in: header
|
||||
description: |-
|
||||
An authorization token is a token that you provide when making API calls. Include the token in a header parameter called Authorization.
|
||||
|
||||
Example: `Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI`
|
||||
security:
|
||||
- authorization: []
|
||||
@@ -7,6 +7,8 @@
|
||||
"prefer-destructuring": 0,
|
||||
"camelcase": 0,
|
||||
"import/no-unresolved": 0,
|
||||
"import/no-extraneous-dependencies": 0
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"object-shorthand": 0,
|
||||
"comma-dangle": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"database": {
|
||||
"host": "../shows.db"
|
||||
},
|
||||
"webserver": {
|
||||
"port": 31459
|
||||
},
|
||||
"tmdb": {
|
||||
"apiKey": ""
|
||||
},
|
||||
"raven": {
|
||||
"DSN": ""
|
||||
},
|
||||
"mail": {
|
||||
"host": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"user_pi": "",
|
||||
"password_pi": ""
|
||||
},
|
||||
"authentication": {
|
||||
"secret": "secret"
|
||||
}
|
||||
}
|
||||
26
seasoned_api/conf/development.json.example
Normal file
26
seasoned_api/conf/development.json.example
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"database": {
|
||||
"host": "../shows.db"
|
||||
},
|
||||
"webserver": {
|
||||
"port": 31459,
|
||||
"origins": []
|
||||
},
|
||||
"tmdb": {
|
||||
"apiKey": ""
|
||||
},
|
||||
"plex": {
|
||||
"ip": ""
|
||||
},
|
||||
"tautulli": {
|
||||
"apiKey": "",
|
||||
"ip": "",
|
||||
"port": ""
|
||||
},
|
||||
"raven": {
|
||||
"DSN": ""
|
||||
},
|
||||
"authentication": {
|
||||
"secret": "secret"
|
||||
}
|
||||
}
|
||||
20
seasoned_api/conf/test.json
Normal file
20
seasoned_api/conf/test.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"database": {
|
||||
"host": ":memory:"
|
||||
},
|
||||
"webserver": {
|
||||
"port": 31400
|
||||
},
|
||||
"tmdb": {
|
||||
"apiKey": "bogus-api-key"
|
||||
},
|
||||
"plex": {
|
||||
"ip": "0.0.0.0"
|
||||
},
|
||||
"raven": {
|
||||
"DSN": ""
|
||||
},
|
||||
"authentication": {
|
||||
"secret": "secret"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,57 @@
|
||||
{
|
||||
"name": "seasoned-api",
|
||||
"main": "webserver/server.js",
|
||||
"scripts": {
|
||||
"start": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/webserver/server.js",
|
||||
"test": "cross-env SEASONED_CONFIG=conf/development.json TESTING=true NODE_PATH=. mocha --recursive test/system",
|
||||
"coverage": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. istanbul cover -x script/autogenerate-documentation.js --include-all-sources --dir test/.coverage node_modules/mocha/bin/_mocha --recursive test/**/* -- --report lcovonly && cat test/.coverage/lcov.info | coveralls && rm -rf test/.coverage",
|
||||
"lint": "./node_modules/.bin/eslint src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt-nodejs": "^0.0.3",
|
||||
"body-parser": "~1.0.1",
|
||||
"cross-env": "^3.1.3",
|
||||
"express": "~4.11.0",
|
||||
"jsonwebtoken": "^8.0.1",
|
||||
"mongoose": "^3.6.13",
|
||||
"moviedb": "^0.2.10",
|
||||
"node-cache": "^4.1.1",
|
||||
"nodemailer": "^4.0.1",
|
||||
"python-shell": "^0.4.0",
|
||||
"raven": "^2.2.1",
|
||||
"request": "^2.81.0",
|
||||
"request-promise": "^4.2",
|
||||
"sqlite3": "3.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^4.9.0",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"mocha": "^3.1.0",
|
||||
"supertest": "^2.0.1",
|
||||
"supertest-as-promised": "^4.0.1"
|
||||
}
|
||||
"name": "seasoned-api",
|
||||
"description": "Packages needed to build and commands to run seasoned api node server.",
|
||||
"license": {
|
||||
"type": "MIT",
|
||||
"url": "https://www.opensource.org/licenses/mit-license.php"
|
||||
},
|
||||
"main": "webserver/server.js",
|
||||
"scripts": {
|
||||
"start": "cross-env SEASONED_CONFIG=conf/development.json NODE_ENV=production NODE_PATH=. babel-node src/webserver/server.js",
|
||||
"test": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. mocha --require @babel/register --recursive test/unit test/system",
|
||||
"coverage": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. nyc mocha --require @babel/register --recursive test && nyc report --reporter=text-lcov | coveralls",
|
||||
"lint": "./node_modules/.bin/eslint src/",
|
||||
"update": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node scripts/updateRequestsInPlex.js",
|
||||
"docs": "yarn apiDocs; yarn classDocs",
|
||||
"apiDocs": "",
|
||||
"classDocs": "./script/generate-class-docs.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"bcrypt": "^5.0.1",
|
||||
"body-parser": "~1.18.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cross-env": "~5.1.4",
|
||||
"express": "~4.16.0",
|
||||
"form-data": "^2.5.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"km-moviedb": "^0.2.12",
|
||||
"node-cache": "^4.1.1",
|
||||
"node-fetch": "^2.6.0",
|
||||
"python-shell": "^0.5.0",
|
||||
"raven": "^2.4.2",
|
||||
"redis": "^3.0.2",
|
||||
"request": "^2.87.0",
|
||||
"request-promise": "^4.2",
|
||||
"sqlite3": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/node": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@babel/register": "^7.5.5",
|
||||
"@types/node": "^12.6.8",
|
||||
"coveralls": "^3.0.5",
|
||||
"documentation": "^12.0.3",
|
||||
"eslint": "^4.9.0",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"mocha": "^6.2.0",
|
||||
"mocha-lcov-reporter": "^1.3.0",
|
||||
"nyc": "^11.6.0",
|
||||
"supertest": "^3.0.0",
|
||||
"supertest-as-promised": "^4.0.1",
|
||||
"typescript": "^3.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
44
seasoned_api/scripts/updateRequestsInPlex.js
Normal file
44
seasoned_api/scripts/updateRequestsInPlex.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const Plex = require("src/plex/plex");
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
const plex = new Plex(configuration.get("plex", "ip"));
|
||||
const establishedDatabase = require("src/database/database");
|
||||
|
||||
const queries = {
|
||||
getRequestsNotYetInPlex: `SELECT * FROM requests WHERE status = 'requested' OR status = 'downloading'`,
|
||||
saveNewStatus: `UPDATE requests SET status = ? WHERE id IS ? and type IS ?`
|
||||
};
|
||||
|
||||
const getByStatus = () =>
|
||||
establishedDatabase.all(queries.getRequestsNotYetInPlex);
|
||||
|
||||
const checkIfRequestExistInPlex = async request => {
|
||||
request.existsInPlex = await plex.existsInPlex(request);
|
||||
return request;
|
||||
};
|
||||
|
||||
const commitNewStatus = (status, id, type, title) => {
|
||||
console.log(type, title, "updated to:", status);
|
||||
return establishedDatabase.run(queries.saveNewStatus, [status, id, type]);
|
||||
};
|
||||
|
||||
const getNewRequestMatchesInPlex = async () => {
|
||||
const requests = await getByStatus();
|
||||
|
||||
return Promise.all(requests.map(checkIfRequestExistInPlex))
|
||||
.catch(error =>
|
||||
console.log("error from checking plex for existance:", error)
|
||||
)
|
||||
.then(matchedRequests =>
|
||||
matchedRequests.filter(request => request.existsInPlex)
|
||||
);
|
||||
};
|
||||
|
||||
const updateMatchInDb = (match, status) => {
|
||||
return commitNewStatus(status, match.id, match.type, match.title);
|
||||
};
|
||||
|
||||
getNewRequestMatchesInPlex()
|
||||
.then(newMatches =>
|
||||
Promise.all(newMatches.map(match => updateMatchInDb(match, "downloaded")))
|
||||
)
|
||||
.then(() => process.exit(0));
|
||||
52
seasoned_api/src/cache/redis.js
vendored
Normal file
52
seasoned_api/src/cache/redis.js
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
const redis = require("redis")
|
||||
const client = redis.createClient()
|
||||
|
||||
class Cache {
|
||||
/**
|
||||
* Retrieve an unexpired cache entry by key.
|
||||
* @param {String} key of the cache entry
|
||||
* @returns {Promise}
|
||||
*/
|
||||
get(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.get(key, (error, reply) => {
|
||||
if (reply == null) {
|
||||
return reject();
|
||||
}
|
||||
|
||||
resolve(JSON.parse(reply));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert cache entry with key and value.
|
||||
* @param {String} key of the cache entry
|
||||
* @param {String} value of the cache entry
|
||||
* @param {Number} timeToLive the number of seconds before entry expires
|
||||
* @returns {Object}
|
||||
*/
|
||||
set(key, value, timeToLive = 10800) {
|
||||
if (value == null || key == null) return null;
|
||||
|
||||
const json = JSON.stringify(value);
|
||||
client.set(key, json, (error, reply) => {
|
||||
if (reply == "OK") {
|
||||
// successfully set value with key, now set TTL for key
|
||||
client.expire(key, timeToLive, e => {
|
||||
if (e)
|
||||
console.error(
|
||||
"Unexpected error while setting expiration for key:",
|
||||
key,
|
||||
". Error:",
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Cache;
|
||||
@@ -22,7 +22,7 @@ class Config {
|
||||
|
||||
get(section, option) {
|
||||
if (this.fields[section] === undefined || this.fields[section][option] === undefined) {
|
||||
throw new Error(`Filed "${section} => ${option}" does not exist.`);
|
||||
throw new Error(`Field "${section} => ${option}" does not exist.`);
|
||||
}
|
||||
|
||||
const field = new Field(this.fields[section][option]);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const SqliteDatabase = require('src/database/sqliteDatabase');
|
||||
|
||||
const host = process.env.TESTING ? ':memory:' : configuration.get('database', 'host');
|
||||
const database = new SqliteDatabase(host);
|
||||
const database = new SqliteDatabase(configuration.get('database', 'host'));
|
||||
/**
|
||||
* This module establishes a connection to the database
|
||||
* specified in the confgiuration file. It tries to setup
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
user_name varchar(127) UNIQUE,
|
||||
password varchar(127),
|
||||
email varchar(127) UNIQUE,
|
||||
admin boolean DEFAULT 0,
|
||||
email varchar(127) UNIQUE,
|
||||
primary key (user_name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
user_name varchar(127) UNIQUE,
|
||||
dark_mode boolean DEFAULT 0,
|
||||
plex_userid varchar(127) DEFAULT NULL,
|
||||
emoji varchar(16) DEFAULT NULL,
|
||||
foreign key(user_name) REFERENCES user(user_name) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cache (
|
||||
key varchar(255),
|
||||
value blob,
|
||||
@@ -23,17 +31,26 @@ CREATE TABLE IF NOT EXISTS search_history (
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS requests(
|
||||
id TEXT,
|
||||
id NUMBER,
|
||||
title TEXT,
|
||||
year NUMBER,
|
||||
poster_path TEXT DEFAULT NULL,
|
||||
background_path TEXT DEFAULT NULL,
|
||||
requested_by TEXT,
|
||||
requested_by varchar(127) DEFAULT NULL,
|
||||
ip TEXT,
|
||||
date DATE DEFAULT CURRENT_TIMESTAMP,
|
||||
status CHAR(25) DEFAULT 'requested' NOT NULL,
|
||||
user_agent CHAR(255) DEFAULT NULL,
|
||||
type CHAR(50) DEFAULT 'movie'
|
||||
type CHAR(50) DEFAULT 'movie',
|
||||
foreign key(requested_by) REFERENCES user(user_name) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS request(
|
||||
id int not null,
|
||||
title text not null,
|
||||
year int not null,
|
||||
type char(10) not null,
|
||||
date timestamp default (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
|
||||
@@ -56,3 +73,23 @@ CREATE TABLE IF NOT EXISTS shows(
|
||||
date_added DATE,
|
||||
date_modified DATE DEFUALT CURRENT_DATE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS requested_torrent (
|
||||
magnet TEXT UNIQUE,
|
||||
torrent_name TEXT,
|
||||
tmdb_id TEXT
|
||||
date_added DATE DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS deluge_torrent (
|
||||
key TEXT UNIQUE,
|
||||
name TEXT,
|
||||
progress TEXT,
|
||||
eta NUMBER,
|
||||
save_path TEXT,
|
||||
state TEXT,
|
||||
paused BOOLEAN,
|
||||
finished BOOLEAN,
|
||||
files TEXT,
|
||||
is_folder BOOLEAN
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
DROP TABLE IF EXISTS user;
|
||||
DROP TABLE IF EXISTS settings;
|
||||
DROP TABLE IF EXISTS search_history;
|
||||
DROP TABLE IF EXISTS requests;
|
||||
DROP TABLE IF EXISTS request;
|
||||
|
||||
@@ -6,6 +6,7 @@ class SqliteDatabase {
|
||||
constructor(host) {
|
||||
this.host = host;
|
||||
this.connection = new sqlite3.Database(this.host);
|
||||
this.execute('pragma foreign_keys = on;');
|
||||
this.schemaDirectory = path.join(__dirname, 'schemas');
|
||||
}
|
||||
|
||||
@@ -25,7 +26,7 @@ class SqliteDatabase {
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async run(sql, parameters) {
|
||||
run(sql, parameters) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connection.run(sql, parameters, (error, result) => {
|
||||
if (error)
|
||||
@@ -41,7 +42,7 @@ class SqliteDatabase {
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async all(sql, parameters) {
|
||||
all(sql, parameters) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connection.all(sql, parameters, (err, rows) => {
|
||||
if (err) {
|
||||
@@ -58,7 +59,7 @@ class SqliteDatabase {
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async get(sql, parameters) {
|
||||
get(sql, parameters) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connection.get(sql, parameters, (err, rows) => {
|
||||
if (err) {
|
||||
@@ -75,7 +76,7 @@ class SqliteDatabase {
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async execute(sql) {
|
||||
execute(sql) {
|
||||
return new Promise(resolve => {
|
||||
this.connection.exec(sql, (err, database) => {
|
||||
if (err) {
|
||||
|
||||
35
seasoned_api/src/notifications/sms.js
Normal file
35
seasoned_api/src/notifications/sms.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const request = require("request");
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
|
||||
const sendSMS = (message) => {
|
||||
const apiKey = configuration.get('sms', 'apikey')
|
||||
|
||||
if (!apiKey) {
|
||||
console.warning("api key for sms not set, cannot send sms.")
|
||||
return null
|
||||
}
|
||||
|
||||
const sender = configuration.get('sms', 'sender')
|
||||
const recipient = configuration.get('sms', 'recipient')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.post(
|
||||
{
|
||||
url: `https://gatewayapi.com/rest/mtsms?token=${apiKey}`,
|
||||
json: true,
|
||||
body: {
|
||||
sender,
|
||||
message,
|
||||
recipients: [{ msisdn: `47${recipient}` }]
|
||||
}
|
||||
},
|
||||
function(err, r, body) {
|
||||
console.log(err ? err : body);
|
||||
console.log("sms provider response:", body)
|
||||
resolve()
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { sendSMS }
|
||||
@@ -1,77 +1,104 @@
|
||||
const assert = require('assert');
|
||||
const http = require('http');
|
||||
const { URL } = require('url');
|
||||
const PythonShell = require('python-shell');
|
||||
const assert = require("assert");
|
||||
const http = require("http");
|
||||
const { URL } = require("url");
|
||||
const PythonShell = require("python-shell");
|
||||
|
||||
const establishedDatabase = require("src/database/database");
|
||||
|
||||
const RedisCache = require("src/cache/redis");
|
||||
const cache = new RedisCache();
|
||||
|
||||
function getMagnetFromURL(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = new URL(url);
|
||||
if (options.protocol.includes('magnet'))
|
||||
resolve(url)
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = new URL(url);
|
||||
if (options.protocol.includes("magnet")) resolve(url);
|
||||
|
||||
http.get(options, (res) => {
|
||||
if (res.statusCode == 301) {
|
||||
resolve(res.headers.location)
|
||||
}
|
||||
});
|
||||
});
|
||||
http.get(options, res => {
|
||||
if (res.statusCode == 301 || res.statusCode == 302) {
|
||||
resolve(res.headers.location);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function find(searchterm, callback) {
|
||||
const options = {
|
||||
pythonPath: '/usr/bin/python3',
|
||||
// pythonPath: '/Library/Frameworks/Python.framework/Versions/3.6/bin/python3',
|
||||
args: [searchterm, '-s', 'jackett', '-f', '--print'],
|
||||
};
|
||||
const options = {
|
||||
pythonPath: "../torrent_search/env/bin/python3",
|
||||
scriptPath: "../torrent_search",
|
||||
args: [searchterm, "-s", "jackett", "--print"]
|
||||
};
|
||||
|
||||
PythonShell.run('../torrent_search/torrentSearch/search.py', options, callback);
|
||||
// PythonShell does not support return
|
||||
PythonShell.run("torrentSearch/search.py", options, callback);
|
||||
// PythonShell does not support return
|
||||
}
|
||||
|
||||
|
||||
async function callPythonAddMagnet(url, callback) {
|
||||
getMagnetFromURL(url)
|
||||
.then((magnet) => {
|
||||
getMagnetFromURL(url)
|
||||
.then(magnet => {
|
||||
const options = {
|
||||
pythonPath: '/usr/bin/python',
|
||||
// pythonPath: '/Library/Frameworks/Python.framework/Versions/3.6/bin/python3',
|
||||
args: [magnet],
|
||||
pythonPath: "../delugeClient/env/bin/python3",
|
||||
scriptPath: "../delugeClient",
|
||||
args: ["add", magnet]
|
||||
};
|
||||
|
||||
PythonShell.run('../app/magnet.py', options, callback);
|
||||
})
|
||||
.catch((err) => {
|
||||
PythonShell.run("deluge_cli.py", options, callback);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
throw new Error(err);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async function SearchPiratebay(query) {
|
||||
return await new Promise((resolve, reject) => find(query, (err, results) => {
|
||||
if (err) {
|
||||
/* eslint-disable no-console */
|
||||
console.log('THERE WAS A FUCKING ERROR!\n', err);
|
||||
reject(Error('There was a error when searching for torrents'));
|
||||
}
|
||||
if (results) {
|
||||
/* eslint-disable no-console */
|
||||
console.log('result', results);
|
||||
resolve(JSON.parse(results, null, '\t'));
|
||||
}
|
||||
}));
|
||||
if (query && query.includes("+")) {
|
||||
query = query.replace("+", "%20");
|
||||
}
|
||||
|
||||
const cacheKey = `pirate/${query}`;
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
cache
|
||||
.get(cacheKey)
|
||||
.then(resolve)
|
||||
.catch(() =>
|
||||
find(query, (err, results) => {
|
||||
if (err) {
|
||||
console.log("THERE WAS A FUCKING ERROR!\n", err);
|
||||
reject(Error("There was a error when searching for torrents"));
|
||||
}
|
||||
|
||||
if (results) {
|
||||
const jsonData = JSON.parse(results[1], null, "\t");
|
||||
cache.set(cacheKey, jsonData);
|
||||
resolve(jsonData);
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function AddMagnet(magnet) {
|
||||
return await new Promise((resolve, reject) => callPythonAddMagnet(magnet, (err, results) => {
|
||||
async function AddMagnet(magnet, name, tmdb_id) {
|
||||
return await new Promise((resolve, reject) =>
|
||||
callPythonAddMagnet(magnet, (err, results) => {
|
||||
if (err) {
|
||||
/* eslint-disable no-console */
|
||||
console.log(err);
|
||||
reject(Error('Enable to add torrent', err))
|
||||
/* eslint-disable no-console */
|
||||
console.log(err);
|
||||
reject(Error("Enable to add torrent", err));
|
||||
}
|
||||
/* eslint-disable no-console */
|
||||
console.log('result/error:', err, results);
|
||||
console.log("result/error:", err, results);
|
||||
|
||||
database = establishedDatabase;
|
||||
insert_query =
|
||||
"INSERT INTO requested_torrent(magnet,torrent_name,tmdb_id) \
|
||||
VALUES (?,?,?)";
|
||||
|
||||
let response = database.run(insert_query, [magnet, name, tmdb_id]);
|
||||
console.log("Response from requsted_torrent insert: " + response);
|
||||
|
||||
resolve({ success: true });
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { SearchPiratebay, AddMagnet };
|
||||
|
||||
20
seasoned_api/src/plex/convertPlexToEpisode.js
Normal file
20
seasoned_api/src/plex/convertPlexToEpisode.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const Episode = require('src/plex/types/episode');
|
||||
|
||||
function convertPlexToEpisode(plexEpisode) {
|
||||
const episode = new Episode(plexEpisode.title, plexEpisode.grandparentTitle, plexEpisode.year);
|
||||
episode.season = plexEpisode.parentIndex;
|
||||
episode.episode = plexEpisode.index;
|
||||
episode.summary = plexEpisode.summary;
|
||||
episode.rating = plexEpisode.rating;
|
||||
|
||||
if (plexEpisode.viewCount !== undefined) {
|
||||
episode.views = plexEpisode.viewCount;
|
||||
}
|
||||
|
||||
if (plexEpisode.originallyAvailableAt !== undefined) {
|
||||
episode.airdate = new Date(plexEpisode.originallyAvailableAt)
|
||||
}
|
||||
|
||||
return episode;
|
||||
}
|
||||
module.exports = convertPlexToEpisode;
|
||||
15
seasoned_api/src/plex/convertPlexToMovie.js
Normal file
15
seasoned_api/src/plex/convertPlexToMovie.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const Movie = require('src/plex/types/movie');
|
||||
|
||||
function convertPlexToMovie(plexMovie) {
|
||||
const movie = new Movie(plexMovie.title, plexMovie.year);
|
||||
movie.rating = plexMovie.rating;
|
||||
movie.tagline = plexMovie.tagline;
|
||||
|
||||
if (plexMovie.summary !== undefined) {
|
||||
movie.summary = plexMovie.summary;
|
||||
}
|
||||
|
||||
return movie;
|
||||
}
|
||||
|
||||
module.exports = convertPlexToMovie;
|
||||
13
seasoned_api/src/plex/convertPlexToShow.js
Normal file
13
seasoned_api/src/plex/convertPlexToShow.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const Show = require('src/plex/types/show');
|
||||
|
||||
function convertPlexToShow(plexShow) {
|
||||
const show = new Show(plexShow.title, plexShow.year);
|
||||
show.summary = plexShow.summary;
|
||||
show.rating = plexShow.rating;
|
||||
show.seasons = plexShow.childCount;
|
||||
show.episodes = plexShow.leafCount;
|
||||
|
||||
return show;
|
||||
}
|
||||
|
||||
module.exports = convertPlexToShow;
|
||||
241
seasoned_api/src/plex/plex.js
Normal file
241
seasoned_api/src/plex/plex.js
Normal file
@@ -0,0 +1,241 @@
|
||||
const fetch = require("node-fetch");
|
||||
const convertPlexToMovie = require("src/plex/convertPlexToMovie");
|
||||
const convertPlexToShow = require("src/plex/convertPlexToShow");
|
||||
const convertPlexToEpisode = require("src/plex/convertPlexToEpisode");
|
||||
|
||||
const { Movie, Show, Person } = require("src/tmdb/types");
|
||||
|
||||
const RedisCache = require("src/cache/redis");
|
||||
const redisCache = new RedisCache();
|
||||
|
||||
const sanitize = string => string.toLowerCase().replace(/[^\w]/gi, "");
|
||||
|
||||
function fixedEncodeURIComponent(str) {
|
||||
return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
|
||||
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
const matchingTitleAndYear = (plex, tmdb) => {
|
||||
let matchingTitle, matchingYear;
|
||||
|
||||
if (plex["title"] != null && tmdb["title"] != null) {
|
||||
const plexTitle = sanitize(plex.title);
|
||||
const tmdbTitle = sanitize(tmdb.title);
|
||||
matchingTitle = plexTitle == tmdbTitle;
|
||||
matchingTitle = matchingTitle
|
||||
? matchingTitle
|
||||
: plexTitle.startsWith(tmdbTitle);
|
||||
} else matchingTitle = false;
|
||||
|
||||
if (plex["year"] != null && tmdb["year"] != null)
|
||||
matchingYear = plex.year == tmdb.year;
|
||||
else matchingYear = false;
|
||||
|
||||
return matchingTitle && matchingYear;
|
||||
};
|
||||
|
||||
const successfullResponse = response => {
|
||||
if (response && response["MediaContainer"]) return response;
|
||||
|
||||
if (
|
||||
response == null ||
|
||||
response["status"] == null ||
|
||||
response["statusText"] == null
|
||||
) {
|
||||
throw Error("Unable to decode response");
|
||||
}
|
||||
|
||||
const { status, statusText } = response;
|
||||
|
||||
if (status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw { message: statusText, status: status };
|
||||
}
|
||||
};
|
||||
|
||||
class Plex {
|
||||
constructor(ip, port = 32400, cache = null) {
|
||||
this.plexIP = ip;
|
||||
this.plexPort = port;
|
||||
|
||||
this.cache = cache || redisCache;
|
||||
this.cacheTags = {
|
||||
machineInfo: "plex/mi",
|
||||
search: "plex/s"
|
||||
};
|
||||
}
|
||||
|
||||
fetchMachineIdentifier() {
|
||||
const cacheKey = `${this.cacheTags.machineInfo}`;
|
||||
const url = `http://${this.plexIP}:${this.plexPort}/`;
|
||||
const options = {
|
||||
timeout: 20000,
|
||||
headers: { Accept: "application/json" }
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.cache
|
||||
.get(cacheKey)
|
||||
.then(machineInfo => resolve(machineInfo["machineIdentifier"]))
|
||||
.catch(() => fetch(url, options))
|
||||
.then(response => response.json())
|
||||
.then(machineInfo =>
|
||||
this.cache.set(cacheKey, machineInfo["MediaContainer"], 2628000)
|
||||
)
|
||||
.then(machineInfo => resolve(machineInfo["machineIdentifier"]))
|
||||
.catch(error => {
|
||||
if (error != undefined && error.type === "request-timeout") {
|
||||
reject({
|
||||
message: "Plex did not respond",
|
||||
status: 408,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
reject(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
matchTmdbAndPlexMedia(plex, tmdb) {
|
||||
let match;
|
||||
|
||||
if (plex == null || tmdb == null) return false;
|
||||
|
||||
if (plex instanceof Array) {
|
||||
let possibleMatches = plex.map(plexItem =>
|
||||
matchingTitleAndYear(plexItem, tmdb)
|
||||
);
|
||||
match = possibleMatches.includes(true);
|
||||
} else {
|
||||
match = matchingTitleAndYear(plex, tmdb);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
async existsInPlex(tmdb) {
|
||||
const plexMatch = await this.findPlexItemByTitleAndYear(
|
||||
tmdb.title,
|
||||
tmdb.year
|
||||
);
|
||||
return plexMatch ? true : false;
|
||||
}
|
||||
|
||||
findPlexItemByTitleAndYear(title, year) {
|
||||
const query = { title, year };
|
||||
|
||||
return this.search(title).then(plexResults => {
|
||||
const matchesInPlex = plexResults.map(plex =>
|
||||
this.matchTmdbAndPlexMedia(plex, query)
|
||||
);
|
||||
const matchesIndex = matchesInPlex.findIndex(el => el === true);
|
||||
return matchesInPlex != -1 ? plexResults[matchesIndex] : null;
|
||||
});
|
||||
}
|
||||
|
||||
getDirectLinkByTitleAndYear(title, year) {
|
||||
const machineIdentifierPromise = this.fetchMachineIdentifier();
|
||||
const matchingObjectInPlexPromise = this.findPlexItemByTitleAndYear(
|
||||
title,
|
||||
year
|
||||
);
|
||||
|
||||
return Promise.all([
|
||||
machineIdentifierPromise,
|
||||
matchingObjectInPlexPromise
|
||||
]).then(([machineIdentifier, matchingObjectInPlex]) => {
|
||||
if (
|
||||
matchingObjectInPlex == false ||
|
||||
matchingObjectInPlex == null ||
|
||||
matchingObjectInPlex["key"] == null ||
|
||||
machineIdentifier == null
|
||||
)
|
||||
return false;
|
||||
|
||||
const keyUriComponent = fixedEncodeURIComponent(matchingObjectInPlex.key);
|
||||
return `https://app.plex.tv/desktop#!/server/${machineIdentifier}/details?key=${keyUriComponent}`;
|
||||
});
|
||||
}
|
||||
|
||||
search(query) {
|
||||
const cacheKey = `${this.cacheTags.search}:${query}`;
|
||||
|
||||
const url = `http://${this.plexIP}:${
|
||||
this.plexPort
|
||||
}/hubs/search?query=${fixedEncodeURIComponent(query)}`;
|
||||
const options = {
|
||||
timeout: 20000,
|
||||
headers: { Accept: "application/json" }
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
this.cache
|
||||
.get(cacheKey)
|
||||
.catch(() => fetch(url, options)) // else fetch fresh data
|
||||
.then(successfullResponse)
|
||||
.then(results => this.cache.set(cacheKey, results, 21600)) // 6 hours
|
||||
.then(this.mapResults)
|
||||
.then(resolve)
|
||||
.catch(error => {
|
||||
if (error != undefined && error.type === "request-timeout") {
|
||||
reject({
|
||||
message: "Plex did not respond",
|
||||
status: 408,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
reject(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// this is not guarenteed to work, but if we see a movie or
|
||||
// show has been imported, this function can be helpfull to call
|
||||
// in order to try bust the cache preventing movieInfo and
|
||||
// showInfo from seeing updates through existsInPlex.
|
||||
bustSearchCacheWithTitle(title) {
|
||||
const query = title;
|
||||
const cacheKey = `${this.cacheTags.search}/${query}*`;
|
||||
|
||||
this.cache.del(
|
||||
cacheKey,
|
||||
(error,
|
||||
response => {
|
||||
if (response == 1) return true;
|
||||
|
||||
// TODO improve cache key matching by lowercasing it on the backend.
|
||||
// what do we actually need to check for if the key was deleted or not
|
||||
// it might be an error or another response code.
|
||||
console.log("Unable to delete, key might not exists");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
mapResults(response) {
|
||||
if (
|
||||
response == null ||
|
||||
response.MediaContainer == null ||
|
||||
response.MediaContainer.Hub == null
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return response.MediaContainer.Hub.filter(category => category.size > 0)
|
||||
.map(category => {
|
||||
if (category.type === "movie") {
|
||||
return category.Metadata;
|
||||
} else if (category.type === "show") {
|
||||
return category.Metadata.map(convertPlexToShow);
|
||||
} else if (category.type === "episode") {
|
||||
return category.Metadata.map(convertPlexToEpisode);
|
||||
}
|
||||
})
|
||||
.filter(result => result !== undefined);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Plex;
|
||||
@@ -3,6 +3,10 @@ const convertPlexToStream = require('src/plex/convertPlexToStream');
|
||||
const rp = require('request-promise');
|
||||
|
||||
class PlexRepository {
|
||||
constructor(plexIP) {
|
||||
this.plexIP = plexIP;
|
||||
}
|
||||
|
||||
inPlex(tmdbResult) {
|
||||
return Promise.resolve()
|
||||
.then(() => this.search(tmdbResult.title))
|
||||
@@ -15,8 +19,10 @@ class PlexRepository {
|
||||
}
|
||||
|
||||
search(query) {
|
||||
const queryUri = encodeURIComponent(query)
|
||||
const uri = encodeURI(`http://${this.plexIP}:32400/search?query=${queryUri}`)
|
||||
const options = {
|
||||
uri: `http://10.0.0.44:32400/search?query=${query}`,
|
||||
uri: uri,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
@@ -25,6 +31,7 @@ class PlexRepository {
|
||||
|
||||
return rp(options)
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
throw new Error('Unable to search plex.')
|
||||
})
|
||||
.then(result => this.mapResults(result))
|
||||
@@ -38,8 +45,10 @@ class PlexRepository {
|
||||
tmdb.matchedInPlex = false
|
||||
}
|
||||
else {
|
||||
// console.log('plex and tmdb:', plexResult, '\n', tmdb)
|
||||
plexResult.results.map((plexItem) => {
|
||||
if (tmdb.title === plexItem.title && tmdb.year === plexItem.year) { tmdb.matchedInPlex = true; }
|
||||
if (tmdb.title === plexItem.title && tmdb.year === plexItem.year)
|
||||
tmdb.matchedInPlex = true;
|
||||
return tmdb;
|
||||
});
|
||||
}
|
||||
@@ -62,7 +71,7 @@ class PlexRepository {
|
||||
|
||||
nowPlaying() {
|
||||
const options = {
|
||||
uri: 'http://10.0.0.44:32400/status/sessions',
|
||||
uri: `http://${this.plexIP}:32400/status/sessions`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
@@ -72,7 +81,7 @@ class PlexRepository {
|
||||
return rp(options)
|
||||
.then((result) => {
|
||||
if (result.MediaContainer.size > 0) {
|
||||
const playing = result.MediaContainer.Video.map(convertPlexToStream);
|
||||
const playing = result.MediaContainer.Metadata.map(convertPlexToStream);
|
||||
return { size: Object.keys(playing).length, video: playing };
|
||||
}
|
||||
return { size: 0, video: [] };
|
||||
|
||||
@@ -1,97 +1,130 @@
|
||||
const PlexRepository = require('src/plex/plexRepository');
|
||||
const Cache = require('src/tmdb/cache');
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const TMDB = require('src/tmdb/tmdb');
|
||||
const establishedDatabase = require('src/database/database');
|
||||
const PlexRepository = require("src/plex/plexRepository");
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
const TMDB = require("src/tmdb/tmdb");
|
||||
const establishedDatabase = require("src/database/database");
|
||||
|
||||
const plexRepository = new PlexRepository();
|
||||
const cache = new Cache();
|
||||
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
|
||||
const plexRepository = new PlexRepository(configuration.get("plex", "ip"));
|
||||
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
|
||||
|
||||
class RequestRepository {
|
||||
constructor(database) {
|
||||
this.database = database || establishedDatabase;
|
||||
this.queries = {
|
||||
insertRequest: `INSERT INTO requests(id,title,year,poster_path,background_path,requested_by,ip,user_agent,type)
|
||||
constructor(database) {
|
||||
this.database = database || establishedDatabase;
|
||||
this.queries = {
|
||||
insertRequest: `INSERT INTO requests(id,title,year,poster_path,background_path,requested_by,ip,user_agent,type)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
fetchRequestedItems: 'SELECT * FROM requests ORDER BY date DESC',
|
||||
fetchRequestedItemsByStatus: 'SELECT * FROM requests WHERE status IS ? AND type LIKE ? ORDER BY date DESC',
|
||||
updateRequestedById: 'UPDATE requests SET status = ? WHERE id is ? AND type is ?',
|
||||
checkIfIdRequested: 'SELECT * FROM requests WHERE id IS ? AND type IS ?',
|
||||
userRequests: 'SELECT * FROM requests WHERE requested_by IS ?',
|
||||
};
|
||||
this.cacheTags = {
|
||||
search: 'se',
|
||||
lookup: 'i',
|
||||
};
|
||||
}
|
||||
fetchRequestedItems:
|
||||
"SELECT * FROM requests ORDER BY date DESC LIMIT 25 OFFSET ?*25-25",
|
||||
fetchRequestedItemsByStatus:
|
||||
"SELECT * FROM requests WHERE status IS ? AND type LIKE ? ORDER BY date DESC LIMIT 25 OFFSET ?*25-25",
|
||||
updateRequestedById:
|
||||
"UPDATE requests SET status = ? WHERE id is ? AND type is ?",
|
||||
checkIfIdRequested: "SELECT * FROM requests WHERE id IS ? AND type IS ?",
|
||||
userRequests:
|
||||
"SELECT * FROM requests WHERE requested_by IS ? ORDER BY date DESC"
|
||||
};
|
||||
this.cacheTags = {
|
||||
search: "se",
|
||||
lookup: "i"
|
||||
};
|
||||
}
|
||||
|
||||
search(query, type, page) {
|
||||
return Promise.resolve()
|
||||
.then(() => tmdb.search(query, type, page))
|
||||
.catch(error => Error(`error in the house${error}`));
|
||||
}
|
||||
search(query, type, page) {
|
||||
return Promise.resolve()
|
||||
.then(() => tmdb.search(query, type, page))
|
||||
.catch(error => Error(`error in the house${error}`));
|
||||
}
|
||||
|
||||
lookup(identifier, type = 'movie') {
|
||||
return Promise.resolve()
|
||||
.then(() => tmdb.lookup(identifier, type))
|
||||
.then(tmdbMovie => this.checkID(tmdbMovie))
|
||||
.then(tmdbMovie => plexRepository.inPlex(tmdbMovie))
|
||||
.catch((error) => {
|
||||
throw new Error(error);
|
||||
});
|
||||
}
|
||||
lookup(identifier, type = "movie") {
|
||||
return Promise.resolve()
|
||||
.then(() => tmdb.lookup(identifier, type))
|
||||
.then(tmdbMovie => this.checkID(tmdbMovie))
|
||||
.then(tmdbMovie => plexRepository.inPlex(tmdbMovie))
|
||||
.catch(error => {
|
||||
throw new Error(error);
|
||||
});
|
||||
}
|
||||
|
||||
checkID(tmdbMovie) {
|
||||
return Promise.resolve()
|
||||
.then(() => this.database.get(this.queries.checkIfIdRequested, [tmdbMovie.id, tmdbMovie.type]))
|
||||
.then((result, error) => {
|
||||
if (error) { throw new Error(error); }
|
||||
tmdbMovie.requested = result ? true : false;
|
||||
return tmdbMovie;
|
||||
});
|
||||
}
|
||||
checkID(tmdbMovie) {
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
this.database.get(this.queries.checkIfIdRequested, [
|
||||
tmdbMovie.id,
|
||||
tmdbMovie.type
|
||||
])
|
||||
)
|
||||
.then((result, error) => {
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
tmdbMovie.requested = result ? true : false;
|
||||
return tmdbMovie;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Send request for given media id.
|
||||
* @param {identifier, type} the id of the media object and type of media must be defined
|
||||
* @returns {Promise} If nothing has gone wrong.
|
||||
*/
|
||||
sendRequest(identifier, type, ip, user_agent, user) {
|
||||
return Promise.resolve()
|
||||
.then(() => tmdb.lookup(identifier, type))
|
||||
.then((movie) => {
|
||||
const username = user === undefined ? undefined : user.username;
|
||||
// Add request to database
|
||||
return this.database.run(this.queries.insertRequest, [movie.id, movie.title, movie.year, movie.poster_path, movie.background_path, username, ip, user_agent, movie.type]);
|
||||
});
|
||||
}
|
||||
sendRequest(identifier, type, ip, user_agent, user) {
|
||||
return Promise.resolve()
|
||||
.then(() => tmdb.lookup(identifier, type))
|
||||
.then(movie => {
|
||||
const username = user === undefined ? undefined : user.username;
|
||||
// Add request to database
|
||||
return this.database.run(this.queries.insertRequest, [
|
||||
movie.id,
|
||||
movie.title,
|
||||
movie.year,
|
||||
movie.poster_path,
|
||||
movie.background_path,
|
||||
username,
|
||||
ip,
|
||||
user_agent,
|
||||
movie.type
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
fetchRequested(status, type = '%') {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (status === 'requested' || status === 'downloading' || status === 'downloaded') {
|
||||
return this.database.all(this.queries.fetchRequestedItemsByStatus, [status, type]);
|
||||
}
|
||||
return this.database.all(this.queries.fetchRequestedItems);
|
||||
});
|
||||
}
|
||||
fetchRequested(status, page = "1", type = "%") {
|
||||
return Promise.resolve().then(() => {
|
||||
if (
|
||||
status === "requested" ||
|
||||
status === "downloading" ||
|
||||
status === "downloaded"
|
||||
)
|
||||
return this.database.all(this.queries.fetchRequestedItemsByStatus, [
|
||||
status,
|
||||
type,
|
||||
page
|
||||
]);
|
||||
else return this.database.all(this.queries.fetchRequestedItems, page);
|
||||
});
|
||||
}
|
||||
|
||||
userRequests(user) {
|
||||
return Promise.resolve()
|
||||
.then(() => this.database.all(this.queries.userRequests, user.username))
|
||||
.catch((error) => {
|
||||
if (String(error).includes('no such column')) {
|
||||
throw new Error('Username not found');
|
||||
}
|
||||
throw new Error('Unable to fetch your requests');
|
||||
})
|
||||
.then((result) => { return result; });
|
||||
}
|
||||
userRequests(username) {
|
||||
return Promise.resolve()
|
||||
.then(() => this.database.all(this.queries.userRequests, username))
|
||||
.catch(error => {
|
||||
if (String(error).includes("no such column")) {
|
||||
throw new Error("Username not found");
|
||||
}
|
||||
throw new Error("Unable to fetch your requests");
|
||||
})
|
||||
.then(result => {
|
||||
// TODO do a correct mapping before sending, not just a dump of the database
|
||||
result.map(item => (item.poster = item.poster_path));
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
updateRequestedById(id, type, status) {
|
||||
return this.database.run(this.queries.updateRequestedById, [status, id, type]);
|
||||
}
|
||||
updateRequestedById(id, type, status) {
|
||||
return this.database.run(this.queries.updateRequestedById, [
|
||||
status,
|
||||
id,
|
||||
type
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RequestRepository;
|
||||
|
||||
16
seasoned_api/src/plex/types/episode.js
Normal file
16
seasoned_api/src/plex/types/episode.js
Normal file
@@ -0,0 +1,16 @@
|
||||
class Episode {
|
||||
constructor(title, show, year) {
|
||||
this.title = title;
|
||||
this.show = show;
|
||||
this.year = year;
|
||||
this.season = null;
|
||||
this.episode = null;
|
||||
this.summary = null;
|
||||
this.rating = null;
|
||||
this.views = null;
|
||||
this.aired = null;
|
||||
this.type = 'episode';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Episode;
|
||||
12
seasoned_api/src/plex/types/movie.js
Normal file
12
seasoned_api/src/plex/types/movie.js
Normal file
@@ -0,0 +1,12 @@
|
||||
class Movie {
|
||||
constructor(title, year) {
|
||||
this.title = title;
|
||||
this.year = year;
|
||||
this.summary = null;
|
||||
this.rating = null;
|
||||
this.tagline = null;
|
||||
this.type = 'movie';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Movie;
|
||||
12
seasoned_api/src/plex/types/show.js
Normal file
12
seasoned_api/src/plex/types/show.js
Normal file
@@ -0,0 +1,12 @@
|
||||
class Show {
|
||||
constructor(title, year) {
|
||||
this.title = title;
|
||||
this.year = year;
|
||||
this.summary = null;
|
||||
this.rating = null;
|
||||
this.seasons = null;
|
||||
this.episodes = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Show;
|
||||
167
seasoned_api/src/request/request.js
Normal file
167
seasoned_api/src/request/request.js
Normal file
@@ -0,0 +1,167 @@
|
||||
const assert = require('assert')
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const TMDB = require('src/tmdb/tmdb');
|
||||
const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
|
||||
const establishedDatabase = require('src/database/database');
|
||||
const utils = require('./utils');
|
||||
|
||||
class RequestRepository {
|
||||
constructor(database) {
|
||||
this.database = database || establishedDatabase;
|
||||
this.queries = {
|
||||
add: 'insert into requests (id,title,year,poster_path,background_path,requested_by,ip,user_agent,type) values(?,?,?,?,?,?,?,?,?)',
|
||||
fetchAll: 'select * from requests where status != "downloaded" order by date desc LIMIT 25 OFFSET ?*25-25',
|
||||
fetchAllFilteredStatus: 'select * from requests where status = ? order by date desc LIMIT 25 offset ?*25-25',
|
||||
totalRequests: 'select count(*) as totalRequests from requests where status != "downloaded"',
|
||||
totalRequestsFilteredStatus: 'select count(*) as totalRequests from requests where status = ?',
|
||||
fetchAllSort: `select id, type from request order by ? ?`,
|
||||
fetchAllFilter: `select id, type from request where ? is "?"`,
|
||||
fetchAllQuery: `select id, type from request where title like "%?%" or year like "%?%"`,
|
||||
fetchAllFilterAndSort: `select id, type from request where ? is "?" order by ? ?`,
|
||||
downloaded: '(select status from requests where id is request.id and type is request.type limit 1)',
|
||||
// deluge: '(select status from deluge_torrent where id is request.id and type is request.type limit 1)',
|
||||
// fetchAllFilterStatus: 'select * from request where '
|
||||
readWithoutUserData: 'select id, title, year, type, status, date from requests where id is ? and type is ?',
|
||||
read: 'select id, title, year, type, status, requested_by, ip, date, user_agent from requests where id is ? and type is ?'
|
||||
};
|
||||
}
|
||||
|
||||
sortAndFilterToDbQuery(by, direction, filter, query) {
|
||||
let dbQuery = undefined;
|
||||
|
||||
if (query !== undefined) {
|
||||
const dbParams = [query, query];
|
||||
const dbquery = this.queries.fetchAllQuery
|
||||
|
||||
dbQuery = dbquery.split('').map((char) => char === '?' ? dbParams.shift() : char).join('')
|
||||
}
|
||||
else if (by !== undefined && filter !== undefined) {
|
||||
const paramToColumnAndValue = {
|
||||
movie: ['type', 'movie'],
|
||||
show: ['type', 'show']
|
||||
}
|
||||
const dbParams = paramToColumnAndValue[filter].concat([by, direction]);
|
||||
const query = this.queries.fetchAllFilterAndSort;
|
||||
|
||||
dbQuery = query.split('').map((char) => char === '?' ? dbParams.shift() : char).join('')
|
||||
}
|
||||
else if (by !== undefined) {
|
||||
const dbParams = [by, direction];
|
||||
const query = this.queries.fetchAllSort;
|
||||
|
||||
dbQuery = query.split('').map((char) => char === '?' ? dbParams.shift() : char).join('')
|
||||
}
|
||||
else if (filter !== undefined) {
|
||||
const paramToColumnAndValue = {
|
||||
movie: ['type', 'movie'],
|
||||
show: ['type', 'show'],
|
||||
downloaded: [this.queries.downloaded, 'downloaded']
|
||||
// downloading: [this.database.delugeStatus, 'downloading']
|
||||
}
|
||||
const dbParams = paramToColumnAndValue[filter]
|
||||
const query = this.queries.fetchAllFilter;
|
||||
|
||||
dbQuery = query.split('').map((char) => char === '?' ? dbParams.shift() : char).join('')
|
||||
}
|
||||
else {
|
||||
dbQuery = this.queries.fetchAll;
|
||||
}
|
||||
|
||||
return dbQuery
|
||||
}
|
||||
|
||||
mapToTmdbByType(rows) {
|
||||
return rows.map((row) => {
|
||||
if (row.type === 'movie')
|
||||
return tmdb.movieInfo(row.id)
|
||||
else if (row.type === 'show')
|
||||
return tmdb.showInfo(row.id)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tmdb movie|show to requests
|
||||
* @param {tmdb} tmdb class of movie|show to add
|
||||
* @returns {Promise}
|
||||
*/
|
||||
requestFromTmdb(tmdb, ip, user_agent, username) {
|
||||
return Promise.resolve()
|
||||
.then(() => this.database.get(this.queries.read, [tmdb.id, tmdb.type]))
|
||||
.then(row => assert.equal(row, undefined, 'Id has already been requested'))
|
||||
.then(() => this.database.run(this.queries.add, [tmdb.id, tmdb.title, tmdb.year, tmdb.poster, tmdb.backdrop, username, ip, user_agent, tmdb.type]))
|
||||
.catch((error) => {
|
||||
if (error.name === 'AssertionError' || error.message.endsWith('been requested')) {
|
||||
throw new Error('This id is already requested', error.message);
|
||||
}
|
||||
console.log('Error @ request.addTmdb:', error);
|
||||
throw new Error('Could not add request');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request item by id
|
||||
* @param {String} id
|
||||
* @param {String} type
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getRequestByIdAndType(id, type) {
|
||||
return this.database.get(this.queries.readWithoutUserData, [id, type])
|
||||
.then(row => {
|
||||
assert(row, 'Could not find request item with that id and type')
|
||||
return {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
year: row.year,
|
||||
type: row.type,
|
||||
status: row.status,
|
||||
requested_date: new Date(row.date)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all requests with optional sort and filter params
|
||||
* @param {String} what we are sorting by
|
||||
* @param {String} direction that can be either 'asc' or 'desc', default 'asc'.
|
||||
* @param {String} params to filter by
|
||||
* @param {String} query param to filter result on. Filters on title and year
|
||||
* @returns {Promise}
|
||||
*/
|
||||
fetchAll(page=1, sort_by=undefined, sort_direction='asc', filter=undefined, query=undefined) {
|
||||
// TODO implemented sort and filter
|
||||
page = parseInt(page)
|
||||
let fetchQuery = this.queries.fetchAll
|
||||
let fetchTotalResults = this.queries.totalRequests
|
||||
let fetchParams = [page]
|
||||
|
||||
if (filter && (filter === 'downloading' || filter === 'downloaded' || filter === 'requested')) {
|
||||
console.log('tes')
|
||||
fetchQuery = this.queries.fetchAllFilteredStatus
|
||||
fetchTotalResults = this.queries.totalRequestsFilteredStatus
|
||||
fetchParams = [filter, page]
|
||||
} else {
|
||||
filter = undefined
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
.then((dbQuery) => this.database.all(fetchQuery, fetchParams))
|
||||
.then(async (rows) => {
|
||||
const sqliteResponse = await this.database.get(fetchTotalResults, filter ? filter : undefined)
|
||||
const totalRequests = sqliteResponse['totalRequests']
|
||||
const totalPages = Math.ceil(totalRequests / 26)
|
||||
|
||||
return [ rows.map(item => {
|
||||
item.poster = item.poster_path; delete item.poster_path;
|
||||
item.backdrop = item.background_path; delete item.background_path;
|
||||
return item
|
||||
}), totalPages, totalRequests ]
|
||||
return Promise.all(this.mapToTmdbByType(rows))
|
||||
})
|
||||
.then(([result, totalPages, totalRequests]) => Promise.resolve({
|
||||
results: result, total_results: totalRequests, page: page, total_pages: totalPages
|
||||
}))
|
||||
.catch(error => { console.log(error);throw error })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RequestRepository;
|
||||
34
seasoned_api/src/request/utils.js
Normal file
34
seasoned_api/src/request/utils.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// TODO : test title and date are valid matches to columns in the database
|
||||
const validSortParams = ['title', 'date']
|
||||
const validSortDirs = ['asc', 'desc']
|
||||
const validFilterParams = ['movie', 'show', 'seeding', 'downloading', 'paused', 'finished', 'downloaded']
|
||||
|
||||
function validSort(by, direction) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (by === undefined) {
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (validSortParams.includes(by) && validSortDirs.includes(direction)) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`invalid sort parameter, must be of: ${validSortParams} with optional sort directions: ${validSortDirs} appended with ':'`))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function validFilter(filter_param) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (filter_param === undefined) {
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (filter_param && validFilterParams.includes(filter_param)) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`filter parameteres must be of type: ${validFilterParams}`))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { validSort, validFilter }
|
||||
@@ -28,17 +28,23 @@ class SearchHistory {
|
||||
|
||||
/**
|
||||
* Creates a new search entry in the database.
|
||||
* @param {User} user a new user
|
||||
* @param {String} username logged in user doing the search
|
||||
* @param {String} searchQuery the query the user searched for
|
||||
* @returns {Promise}
|
||||
*/
|
||||
create(user, searchQuery) {
|
||||
return Promise.resolve()
|
||||
.then(() => this.database.run(this.queries.create, [searchQuery, user]))
|
||||
.catch((error) => {
|
||||
create(username, searchQuery) {
|
||||
return this.database.run(this.queries.create, [searchQuery, username])
|
||||
.catch(error => {
|
||||
if (error.message.includes('FOREIGN')) {
|
||||
throw new Error('Could not create search history.');
|
||||
}
|
||||
|
||||
throw {
|
||||
success: false,
|
||||
status: 500,
|
||||
message: 'An unexpected error occured',
|
||||
source: 'database'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class StrayRepository {
|
||||
assert.notEqual(row, undefined, `Stray '${strayId}' already verified.`);
|
||||
|
||||
const options = {
|
||||
pythonPath: '/usr/bin/python3',
|
||||
pythonPath: '../app/env/bin/python3',
|
||||
args: [strayId],
|
||||
};
|
||||
|
||||
|
||||
74
seasoned_api/src/tautulli/tautulli.js
Normal file
74
seasoned_api/src/tautulli/tautulli.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const fetch = require("node-fetch");
|
||||
|
||||
class Tautulli {
|
||||
constructor(apiKey, ip, port) {
|
||||
this.apiKey = apiKey;
|
||||
this.ip = ip;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
buildUrlWithCmdAndUserid(cmd, user_id) {
|
||||
const url = new URL("api/v2", `http://${this.ip}:${this.port}`);
|
||||
url.searchParams.append("apikey", this.apiKey);
|
||||
url.searchParams.append("cmd", cmd);
|
||||
url.searchParams.append("user_id", user_id);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
logTautulliError(error) {
|
||||
console.error("error fetching from tautulli");
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
getPlaysByDayOfWeek(plex_userid, days, y_axis) {
|
||||
const url = this.buildUrlWithCmdAndUserid(
|
||||
"get_plays_by_dayofweek",
|
||||
plex_userid
|
||||
);
|
||||
url.searchParams.append("time_range", days);
|
||||
url.searchParams.append("y_axis", y_axis);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => this.logTautulliError(error));
|
||||
}
|
||||
|
||||
getPlaysByDays(plex_userid, days, y_axis) {
|
||||
const url = this.buildUrlWithCmdAndUserid("get_plays_by_date", plex_userid);
|
||||
url.searchParams.append("time_range", days);
|
||||
url.searchParams.append("y_axis", y_axis);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => this.logTautulliError(error));
|
||||
}
|
||||
|
||||
watchTimeStats(plex_userid) {
|
||||
const url = this.buildUrlWithCmdAndUserid(
|
||||
"get_user_watch_time_stats",
|
||||
plex_userid
|
||||
);
|
||||
url.searchParams.append("grouping", 0);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => this.logTautulliError(error));
|
||||
}
|
||||
|
||||
viewHistory(plex_userid) {
|
||||
const url = this.buildUrlWithCmdAndUserid("get_history", plex_userid);
|
||||
|
||||
url.searchParams.append("start", 0);
|
||||
url.searchParams.append("length", 50);
|
||||
|
||||
console.log("fetching url", url.href);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => this.logTautulliError(error));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Tautulli;
|
||||
3
seasoned_api/src/tmdb/.babelrc
Normal file
3
seasoned_api/src/tmdb/.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"]
|
||||
}
|
||||
@@ -18,12 +18,12 @@ class Cache {
|
||||
* @returns {Object}
|
||||
*/
|
||||
get(key) {
|
||||
return Promise.resolve()
|
||||
.then(() => this.database.get(this.queries.read, [key]))
|
||||
.then((row) => {
|
||||
assert(row, 'Could not find cache enrty with that key.');
|
||||
return JSON.parse(row.value);
|
||||
});
|
||||
return Promise.resolve()
|
||||
.then(() => this.database.get(this.queries.read, [key]))
|
||||
.then(row => {
|
||||
assert(row, 'Could not find cache entry with that key.');
|
||||
return JSON.parse(row.value);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
const TMDB = require('src/media_classes/tmdb');
|
||||
|
||||
function translateYear(tmdbReleaseDate) {
|
||||
return new Date(tmdbReleaseDate).getFullYear();
|
||||
}
|
||||
|
||||
function translateGenre(tmdbGenres) {
|
||||
return tmdbGenres.map(genre => genre.name);
|
||||
}
|
||||
|
||||
function convertType(tmdbType) {
|
||||
if (tmdbType === 'tv') return 'show';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function convertTmdbToSeasoned(tmdb, manualType = undefined) {
|
||||
const title = tmdb.title || tmdb.name;
|
||||
const year = translateYear(tmdb.release_date || tmdb.first_air_date);
|
||||
const type = manualType || convertType(tmdb.media_type) || 'movie';
|
||||
|
||||
const id = tmdb.id;
|
||||
const summary = tmdb.overview;
|
||||
const poster_path = tmdb.poster_path;
|
||||
const background_path = tmdb.backdrop_path;
|
||||
const popularity = tmdb.popularity;
|
||||
const score = tmdb.vote_average;
|
||||
// const genres = translateGenre(tmdb.genres);
|
||||
const release_status = tmdb.status;
|
||||
const tagline = tmdb.tagline;
|
||||
|
||||
const seasons = tmdb.number_of_seasons;
|
||||
const episodes = tmdb.episodes;
|
||||
|
||||
const seasoned = new TMDB(
|
||||
title, year, type, id, summary, poster_path, background_path,
|
||||
popularity, score, release_status, tagline, seasons, episodes
|
||||
);
|
||||
|
||||
// seasoned.print()
|
||||
return seasoned;
|
||||
}
|
||||
|
||||
module.exports = convertTmdbToSeasoned;
|
||||
@@ -1,135 +1,294 @@
|
||||
const moviedb = require('moviedb');
|
||||
const convertTmdbToSeasoned = require('src/tmdb/convertTmdbToSeasoned');
|
||||
const moviedb = require("km-moviedb");
|
||||
const RedisCache = require("src/cache/redis");
|
||||
const redisCache = new RedisCache();
|
||||
|
||||
const TMDB_METHODS = {
|
||||
upcoming: { movie: 'miscUpcomingMovies' },
|
||||
discover: { movie: 'discoverMovie', show: 'discoverTv' },
|
||||
popular: { movie: 'miscPopularMovies', show: 'miscPopularTvs' },
|
||||
nowplaying: { movie: 'miscNowPlayingMovies', show: 'tvOnTheAir' },
|
||||
similar: { movie: 'movieSimilar', show: 'tvSimilar' },
|
||||
search: { movie: 'searchMovie', show: 'searchTv', multi: 'searchMulti' },
|
||||
info: { movie: 'movieInfo', show: 'tvInfo' },
|
||||
const {
|
||||
Movie,
|
||||
Show,
|
||||
Person,
|
||||
Credits,
|
||||
ReleaseDates
|
||||
} = require("src/tmdb/types");
|
||||
|
||||
const tmdbErrorResponse = (error, typeString = undefined) => {
|
||||
if (error.status === 404) {
|
||||
let message = error.response.body.status_message;
|
||||
|
||||
throw {
|
||||
status: 404,
|
||||
message: message.slice(0, -1) + " in tmdb."
|
||||
};
|
||||
} else if (error.status === 401) {
|
||||
throw {
|
||||
status: 401,
|
||||
message: error.response.body.status_message
|
||||
};
|
||||
}
|
||||
|
||||
throw {
|
||||
status: 500,
|
||||
message: `An unexpected error occured while fetching ${typeString} from tmdb`
|
||||
};
|
||||
};
|
||||
|
||||
class TMDB {
|
||||
constructor(cache, apiKey, tmdbLibrary) {
|
||||
this.cache = cache;
|
||||
this.tmdbLibrary = tmdbLibrary || moviedb(apiKey);
|
||||
this.cacheTags = {
|
||||
search: 'se',
|
||||
info: 'i',
|
||||
upcoming: 'u',
|
||||
discover: 'd',
|
||||
popular: 'p',
|
||||
nowplaying: 'n',
|
||||
similar: 'si',
|
||||
};
|
||||
}
|
||||
constructor(apiKey, cache, tmdbLibrary) {
|
||||
this.tmdbLibrary = tmdbLibrary || moviedb(apiKey);
|
||||
|
||||
/**
|
||||
this.cache = cache || redisCache;
|
||||
this.cacheTags = {
|
||||
multiSearch: "mus",
|
||||
movieSearch: "mos",
|
||||
showSearch: "ss",
|
||||
personSearch: "ps",
|
||||
movieInfo: "mi",
|
||||
movieCredits: "mc",
|
||||
movieReleaseDates: "mrd",
|
||||
movieImages: "mimg",
|
||||
showInfo: "si",
|
||||
showCredits: "sc",
|
||||
personInfo: "pi",
|
||||
personCredits: "pc",
|
||||
miscNowPlayingMovies: "npm",
|
||||
miscPopularMovies: "pm",
|
||||
miscTopRatedMovies: "tpm",
|
||||
miscUpcomingMovies: "um",
|
||||
tvOnTheAir: "toa",
|
||||
miscPopularTvs: "pt",
|
||||
miscTopRatedTvs: "trt"
|
||||
};
|
||||
this.defaultTTL = 86400;
|
||||
}
|
||||
|
||||
getFromCacheOrFetchFromTmdb(cacheKey, tmdbMethod, query) {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.cache
|
||||
.get(cacheKey)
|
||||
.then(resolve)
|
||||
.catch(() => this.tmdb(tmdbMethod, query))
|
||||
.then(resolve)
|
||||
.catch(error => reject(tmdbErrorResponse(error, tmdbMethod)))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a specific movie by id from TMDB.
|
||||
* @param {Number} identifier of the movie you want to retrieve
|
||||
* @param {Boolean} add credits (cast & crew) for movie
|
||||
* @param {Boolean} add release dates for every country
|
||||
* @returns {Promise} succeeds if movie was found
|
||||
*/
|
||||
lookup(identifier, type = 'movie') {
|
||||
const query = { id: identifier };
|
||||
const cacheKey = `${this.cacheTags.info}:${type}:${identifier}`;
|
||||
return Promise.resolve()
|
||||
.then(() => this.cache.get(cacheKey))
|
||||
.catch(() => this.tmdb(this.tmdbMethod('info', type), query))
|
||||
.catch(() => { throw new Error('Could not find a movie with that id.'); })
|
||||
.then(response => this.cache.set(cacheKey, response))
|
||||
.then((response) => {
|
||||
try {
|
||||
return convertTmdbToSeasoned(response, type);
|
||||
} catch (parseError) {
|
||||
console.error(parseError);
|
||||
throw new Error('Could not parse movie.');
|
||||
}
|
||||
});
|
||||
}
|
||||
movieInfo(identifier) {
|
||||
const query = { id: identifier };
|
||||
const cacheKey = `tmdb/${this.cacheTags.movieInfo}:${identifier}`;
|
||||
|
||||
/**
|
||||
* Retrive list of of items from TMDB matching the query and/or type given.
|
||||
* @param {queryText, page, type} the page number to specify in the request for discover,
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, "movieInfo", query)
|
||||
.then(movie => this.cache.set(cacheKey, movie, this.defaultTTL))
|
||||
.then(movie => Movie.convertFromTmdbResponse(movie));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve credits for a movie
|
||||
* @param {Number} identifier of the movie to get credits for
|
||||
* @returns {Promise} movie cast object
|
||||
*/
|
||||
movieCredits(identifier) {
|
||||
const query = { id: identifier };
|
||||
const cacheKey = `tmdb/${this.cacheTags.movieCredits}:${identifier}`;
|
||||
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, "movieCredits", query)
|
||||
.then(credits => this.cache.set(cacheKey, credits, this.defaultTTL))
|
||||
.then(credits => Credits.convertFromTmdbResponse(credits));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve release dates for a movie
|
||||
* @param {Number} identifier of the movie to get release dates for
|
||||
* @returns {Promise} movie release dates object
|
||||
*/
|
||||
movieReleaseDates(identifier) {
|
||||
const query = { id: identifier }
|
||||
const cacheKey = `tmdb/${this.cacheTags.movieReleaseDates}:${identifier}`
|
||||
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, 'movieReleaseDates', query)
|
||||
.then(releaseDates => this.cache.set(cacheKey, releaseDates, this.defaultTTL))
|
||||
.then(releaseDates => ReleaseDates.convertFromTmdbResponse(releaseDates))
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a specific show by id from TMDB.
|
||||
* @param {Number} identifier of the show you want to retrieve
|
||||
* @param {String} type filter results by type (default show).
|
||||
* @returns {Promise} succeeds if show was found
|
||||
*/
|
||||
showInfo(identifier) {
|
||||
const query = { id: identifier };
|
||||
const cacheKey = `tmdb/${this.cacheTags.showInfo}:${identifier}`;
|
||||
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, "tvInfo", query)
|
||||
.then(show => this.cache.set(cacheKey, show, this.defaultTTL))
|
||||
.then(show => Show.convertFromTmdbResponse(show));
|
||||
}
|
||||
|
||||
showCredits(identifier) {
|
||||
const query = { id: identifier };
|
||||
const cacheKey = `tmdb/${this.cacheTags.showCredits}:${identifier}`;
|
||||
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, "tvCredits", query)
|
||||
.then(credits => this.cache.set(cacheKey, credits, this.defaultTTL))
|
||||
.then(credits => Credits.convertFromTmdbResponse(credits));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a specific person id from TMDB.
|
||||
* @param {Number} identifier of the person you want to retrieve
|
||||
* @param {String} type filter results by type (default person).
|
||||
* @returns {Promise} succeeds if person was found
|
||||
*/
|
||||
personInfo(identifier) {
|
||||
const query = { id: identifier };
|
||||
const cacheKey = `tmdb/${this.cacheTags.personInfo}:${identifier}`;
|
||||
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, "personInfo", query)
|
||||
.then(person => this.cache.set(cacheKey, person, this.defaultTTL))
|
||||
.then(person => Person.convertFromTmdbResponse(person));
|
||||
}
|
||||
|
||||
personCredits(identifier) {
|
||||
const query = { id: identifier };
|
||||
const cacheKey = `tmdb/${this.cacheTags.personCredits}:${identifier}`;
|
||||
|
||||
return this.getFromCacheOrFetchFromTmdb(
|
||||
cacheKey,
|
||||
"personCombinedCredits",
|
||||
query
|
||||
)
|
||||
.then(credits => this.cache.set(cacheKey, credits, this.defaultTTL))
|
||||
.then(credits => Credits.convertFromTmdbResponse(credits));
|
||||
}
|
||||
|
||||
multiSearch(search_query, page = 1, include_adult = true) {
|
||||
const query = { query: search_query, page, include_adult };
|
||||
const cacheKey = `tmdb/${this.cacheTags.multiSearch}:${page}:${search_query}:${include_adult}`;
|
||||
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchMulti", query)
|
||||
.then(response => this.cache.set(cacheKey, response, this.defaultTTL))
|
||||
.then(response => this.mapResults(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive movie search results from TMDB.
|
||||
* @param {String} text query you want to search for
|
||||
* @param {Number} page representing pagination of results
|
||||
* @returns {Promise} dict with query results, current page and total_pages
|
||||
*/
|
||||
search(text, page = 1, type = 'multi') {
|
||||
const query = { query: text, page };
|
||||
const cacheKey = `${this.cacheTags.search}:${page}:${type}:${text}`;
|
||||
return Promise.resolve()
|
||||
.then(() => this.cache.get(cacheKey))
|
||||
.catch(() => this.tmdb(this.tmdbMethod('search', type), query))
|
||||
.catch(() => { throw new Error('Could not search for movies/shows at tmdb.'); })
|
||||
.then(response => this.cache.set(cacheKey, response))
|
||||
.then(response => this.mapResults(response))
|
||||
.catch((error) => { throw new Error(error); })
|
||||
.then(([mappedResults, pagenumber, totalpages, total_results]) => ({
|
||||
results: mappedResults, page: pagenumber, total_results, total_pages: totalpages,
|
||||
}));
|
||||
}
|
||||
movieSearch(search_query, page = 1, include_adult = true) {
|
||||
const tmdbquery = { query: search_query, page, include_adult };
|
||||
const cacheKey = `tmdb/${this.cacheTags.movieSearch}:${page}:${search_query}:${include_adult}`;
|
||||
|
||||
/**
|
||||
* Fetches a given list from tmdb.
|
||||
* @param {listName} List we want to fetch.
|
||||
* @param {type} The to specify in the request for discover (default 'movie').
|
||||
* @param {id} When finding similar a id can be added to query
|
||||
* @param {page} Page number we want to fetch.
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchMovie", tmdbquery)
|
||||
.then(response => this.cache.set(cacheKey, response, this.defaultTTL))
|
||||
.then(response => this.mapResults(response, "movie"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive show search results from TMDB.
|
||||
* @param {String} text query you want to search for
|
||||
* @param {Number} page representing pagination of results
|
||||
* @returns {Promise} dict with query results, current page and total_pages
|
||||
*/
|
||||
listSearch(listName, type = 'movie', id, page = '1') {
|
||||
const params = { id, page };
|
||||
const cacheKey = `${this.cacheTags[listName]}:${type}:${id}:${page}`;
|
||||
return Promise.resolve()
|
||||
.then(() => this.cache.get(cacheKey))
|
||||
.catch(() => this.tmdb(this.tmdbMethod(listName, type), params))
|
||||
.then(response => this.cache.set(cacheKey, response))
|
||||
.then(response => this.mapResults(response, type))
|
||||
.catch((error) => { throw new Error(error); })
|
||||
.then(([mappedResults, pagenumber, totalpages, total_results]) => ({
|
||||
results: mappedResults, page: pagenumber, total_pages: totalpages, total_results,
|
||||
}));
|
||||
}
|
||||
showSearch(search_query, page = 1, include_adult = true) {
|
||||
const tmdbquery = { query: search_query, page, include_adult };
|
||||
const cacheKey = `tmdb/${this.cacheTags.showSearch}:${page}:${search_query}:${include_adult}`;
|
||||
|
||||
tmdbMethod(apiMethod, type) {
|
||||
const method = TMDB_METHODS[apiMethod][type];
|
||||
if (method !== undefined) return method;
|
||||
throw new Error('Could not find tmdb api method.');
|
||||
}
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchTv", tmdbquery)
|
||||
.then(response => this.cache.set(cacheKey, response, this.defaultTTL))
|
||||
.then(response => this.mapResults(response, "show"));
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrive person search results from TMDB.
|
||||
* @param {String} text query you want to search for
|
||||
* @param {Number} page representing pagination of results
|
||||
* @returns {Promise} dict with query results, current page and total_pages
|
||||
*/
|
||||
personSearch(search_query, page = 1, include_adult = true) {
|
||||
const tmdbquery = { query: search_query, page, include_adult };
|
||||
const cacheKey = `tmdb/${this.cacheTags.personSearch}:${page}:${search_query}:${include_adult}`;
|
||||
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchPerson", tmdbquery)
|
||||
.then(response => this.cache.set(cacheKey, response, this.defaultTTL))
|
||||
.then(response => this.mapResults(response, "person"));
|
||||
}
|
||||
|
||||
movieList(listname, page = 1) {
|
||||
const query = { page: page };
|
||||
const cacheKey = `tmdb/${this.cacheTags[listname]}:${page}`;
|
||||
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, listname, query)
|
||||
.then(response => this.cache.set(cacheKey, response, this.defaultTTL))
|
||||
.then(response => this.mapResults(response, "movie"));
|
||||
}
|
||||
|
||||
showList(listname, page = 1) {
|
||||
const query = { page: page };
|
||||
const cacheKey = `tmdb/${this.cacheTags[listname]}:${page}`;
|
||||
|
||||
return this.getFromCacheOrFetchFromTmdb(cacheKey, listName, query)
|
||||
.then(response => this.cache.set(cacheKey, response, this.defaultTTL))
|
||||
.then(response => this.mapResults(response, "show"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps our response from tmdb api to a movie/show object.
|
||||
* @param {response} JSON response from tmdb.
|
||||
* @param {type} The type declared in listSearch.
|
||||
* @param {String} response from tmdb.
|
||||
* @param {String} The type declared in listSearch.
|
||||
* @returns {Promise} dict with tmdb results, mapped as movie/show objects.
|
||||
*/
|
||||
mapResults(response, type) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const mappedResults = response.results.filter((element) => {
|
||||
return (element.media_type === 'movie' || element.media_type === 'tv' || element.media_type === undefined);
|
||||
}).map((element) => convertTmdbToSeasoned(element, type));
|
||||
return [mappedResults, response.page, response.total_pages, response.total_results];
|
||||
})
|
||||
.catch((error) => { throw new Error(error); });
|
||||
}
|
||||
mapResults(response, type = undefined) {
|
||||
let results = response.results.map(result => {
|
||||
if (type === "movie" || result.media_type === "movie") {
|
||||
const movie = Movie.convertFromTmdbResponse(result);
|
||||
return movie.createJsonResponse();
|
||||
} else if (type === "show" || result.media_type === "tv") {
|
||||
const show = Show.convertFromTmdbResponse(result);
|
||||
return show.createJsonResponse();
|
||||
} else if (type === "person" || result.media_type === "person") {
|
||||
const person = Person.convertFromTmdbResponse(result);
|
||||
return person.createJsonResponse();
|
||||
}
|
||||
});
|
||||
|
||||
tmdb(method, argument) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = (error, reponse) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve(reponse);
|
||||
};
|
||||
return {
|
||||
results: results,
|
||||
page: response.page,
|
||||
total_results: response.total_results,
|
||||
total_pages: response.total_pages
|
||||
};
|
||||
}
|
||||
|
||||
if (!argument) {
|
||||
this.tmdbLibrary[method](callback);
|
||||
} else {
|
||||
this.tmdbLibrary[method](argument, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Wraps moviedb library to support Promises.
|
||||
* @param {String} method function name in the library
|
||||
* @param {Object} argument argument to function being called
|
||||
* @returns {Promise} succeeds if callback succeeds
|
||||
*/
|
||||
tmdb(method, argument) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = (error, reponse) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
resolve(reponse);
|
||||
};
|
||||
|
||||
if (!argument) {
|
||||
this.tmdbLibrary[method](callback);
|
||||
} else {
|
||||
this.tmdbLibrary[method](argument, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TMDB;
|
||||
|
||||
7
seasoned_api/src/tmdb/tmdb.ts
Normal file
7
seasoned_api/src/tmdb/tmdb.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Movie } from './types'
|
||||
|
||||
Movie('str', 123)
|
||||
|
||||
|
||||
|
||||
module.exports = TMDB;
|
||||
7
seasoned_api/src/tmdb/types.js
Normal file
7
seasoned_api/src/tmdb/types.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const Movie = require('./types/movie.js')
|
||||
const Show = require('./types/show.js')
|
||||
const Person = require('./types/person.js')
|
||||
const Credits = require('./types/credits.js')
|
||||
const ReleaseDates = require('./types/releaseDates.js')
|
||||
|
||||
module.exports = { Movie, Show, Person, Credits, ReleaseDates }
|
||||
64
seasoned_api/src/tmdb/types.ts
Normal file
64
seasoned_api/src/tmdb/types.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
interface Movie {
|
||||
adult: boolean;
|
||||
backdrop: string;
|
||||
genres: Genre[];
|
||||
id: number;
|
||||
imdb_id: number;
|
||||
overview: string;
|
||||
popularity: number;
|
||||
poster: string;
|
||||
release_date: Date;
|
||||
rank: number;
|
||||
runtime: number;
|
||||
status: string;
|
||||
tagline: string;
|
||||
title: string;
|
||||
vote_count: number;
|
||||
}
|
||||
|
||||
interface Show {
|
||||
adult: boolean;
|
||||
backdrop: string;
|
||||
episodes: number;
|
||||
genres: Genre[];
|
||||
id: number;
|
||||
imdb_id: number;
|
||||
overview: string;
|
||||
popularity: number;
|
||||
poster: string;
|
||||
rank: number;
|
||||
runtime: number;
|
||||
seasons: number;
|
||||
status: string;
|
||||
tagline: string;
|
||||
title: string;
|
||||
vote_count: number;
|
||||
}
|
||||
|
||||
interface Person {
|
||||
birthday: Date;
|
||||
deathday: Date;
|
||||
id: number;
|
||||
known_for: string;
|
||||
name: string;
|
||||
poster: string;
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
adult: boolean;
|
||||
backdrop_path: string;
|
||||
id: number;
|
||||
original_title: string;
|
||||
release_date: Date;
|
||||
poster_path: string;
|
||||
popularity: number;
|
||||
vote_average: number;
|
||||
vote_counte: number;
|
||||
}
|
||||
|
||||
interface Genre {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export { Movie, Show, Person, Genre }
|
||||
113
seasoned_api/src/tmdb/types/credits.js
Normal file
113
seasoned_api/src/tmdb/types/credits.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import Movie from "./movie";
|
||||
import Show from "./show";
|
||||
|
||||
class Credits {
|
||||
constructor(id, cast = [], crew = []) {
|
||||
this.id = id;
|
||||
this.cast = cast;
|
||||
this.crew = crew;
|
||||
this.type = "credits";
|
||||
}
|
||||
|
||||
static convertFromTmdbResponse(response) {
|
||||
const { id, cast, crew } = response;
|
||||
|
||||
const allCast = cast.map(cast => {
|
||||
if (cast["media_type"]) {
|
||||
if (cast.media_type === "movie") {
|
||||
return CreditedMovie.convertFromTmdbResponse(cast);
|
||||
} else if (cast.media_type === "tv") {
|
||||
return CreditedShow.convertFromTmdbResponse(cast);
|
||||
}
|
||||
}
|
||||
|
||||
return new CastMember(
|
||||
cast.character,
|
||||
cast.gender,
|
||||
cast.id,
|
||||
cast.name,
|
||||
cast.profile_path
|
||||
);
|
||||
});
|
||||
|
||||
const allCrew = crew.map(crew => {
|
||||
if (cast["media_type"]) {
|
||||
if (cast.media_type === "movie") {
|
||||
return CreditedMovie.convertFromTmdbResponse(cast);
|
||||
} else if (cast.media_type === "tv") {
|
||||
return CreditedShow.convertFromTmdbResponse(cast);
|
||||
}
|
||||
}
|
||||
|
||||
return new CrewMember(
|
||||
crew.department,
|
||||
crew.gender,
|
||||
crew.id,
|
||||
crew.job,
|
||||
crew.name,
|
||||
crew.profile_path
|
||||
);
|
||||
});
|
||||
|
||||
return new Credits(id, allCast, allCrew);
|
||||
}
|
||||
|
||||
createJsonResponse() {
|
||||
return {
|
||||
id: this.id,
|
||||
cast: this.cast.map(cast => cast.createJsonResponse()),
|
||||
crew: this.crew.map(crew => crew.createJsonResponse())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CastMember {
|
||||
constructor(character, gender, id, name, profile_path) {
|
||||
this.character = character;
|
||||
this.gender = gender;
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.profile_path = profile_path;
|
||||
this.type = "person";
|
||||
}
|
||||
|
||||
createJsonResponse() {
|
||||
return {
|
||||
character: this.character,
|
||||
gender: this.gender,
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
profile_path: this.profile_path,
|
||||
type: this.type
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CrewMember {
|
||||
constructor(department, gender, id, job, name, profile_path) {
|
||||
this.department = department;
|
||||
this.gender = gender;
|
||||
this.id = id;
|
||||
this.job = job;
|
||||
this.name = name;
|
||||
this.profile_path = profile_path;
|
||||
this.type = "person";
|
||||
}
|
||||
|
||||
createJsonResponse() {
|
||||
return {
|
||||
department: this.department,
|
||||
gender: this.gender,
|
||||
id: this.id,
|
||||
job: this.job,
|
||||
name: this.name,
|
||||
profile_path: this.profile_path,
|
||||
type: this.type
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CreditedMovie extends Movie {}
|
||||
class CreditedShow extends Show {}
|
||||
|
||||
module.exports = Credits;
|
||||
62
seasoned_api/src/tmdb/types/movie.js
Normal file
62
seasoned_api/src/tmdb/types/movie.js
Normal file
@@ -0,0 +1,62 @@
|
||||
class Movie {
|
||||
constructor(id, title, year=undefined, overview=undefined, poster=undefined, backdrop=undefined,
|
||||
releaseDate=undefined, rating=undefined, genres=undefined, productionStatus=undefined,
|
||||
tagline=undefined, runtime=undefined, imdb_id=undefined, popularity=undefined) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.year = year;
|
||||
this.overview = overview;
|
||||
this.poster = poster;
|
||||
this.backdrop = backdrop;
|
||||
this.releaseDate = releaseDate;
|
||||
this.rating = rating;
|
||||
this.genres = genres;
|
||||
this.productionStatus = productionStatus;
|
||||
this.tagline = tagline;
|
||||
this.runtime = runtime;
|
||||
this.imdb_id = imdb_id;
|
||||
this.popularity = popularity;
|
||||
this.type = 'movie';
|
||||
}
|
||||
|
||||
static convertFromTmdbResponse(response) {
|
||||
const { id, title, release_date, overview, poster_path, backdrop_path, vote_average, genres, status,
|
||||
tagline, runtime, imdb_id, popularity } = response;
|
||||
|
||||
const releaseDate = new Date(release_date);
|
||||
const year = releaseDate.getFullYear();
|
||||
const genreNames = genres ? genres.map(g => g.name) : undefined
|
||||
|
||||
return new Movie(id, title, year, overview, poster_path, backdrop_path, releaseDate, vote_average, genreNames, status,
|
||||
tagline, runtime, imdb_id, popularity)
|
||||
}
|
||||
|
||||
static convertFromPlexResponse(response) {
|
||||
// console.log('response', response)
|
||||
const { title, year, rating, tagline, summary } = response;
|
||||
const _ = undefined
|
||||
|
||||
return new Movie(null, title, year, summary, _, _, _, rating, _, _, tagline)
|
||||
}
|
||||
|
||||
createJsonResponse() {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
year: this.year,
|
||||
overview: this.overview,
|
||||
poster: this.poster,
|
||||
backdrop: this.backdrop,
|
||||
release_date: this.releaseDate,
|
||||
rating: this.rating,
|
||||
genres: this.genres,
|
||||
production_status: this.productionStatus,
|
||||
tagline: this.tagline,
|
||||
runtime: this.runtime,
|
||||
imdb_id: this.imdb_id,
|
||||
type: this.type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Movie;
|
||||
70
seasoned_api/src/tmdb/types/person.js
Normal file
70
seasoned_api/src/tmdb/types/person.js
Normal file
@@ -0,0 +1,70 @@
|
||||
class Person {
|
||||
constructor(
|
||||
id,
|
||||
name,
|
||||
poster = undefined,
|
||||
birthday = undefined,
|
||||
deathday = undefined,
|
||||
adult = undefined,
|
||||
placeOfBirth = undefined,
|
||||
biography = undefined,
|
||||
knownForDepartment = undefined
|
||||
) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.poster = poster;
|
||||
this.birthday = birthday;
|
||||
this.deathday = deathday;
|
||||
this.adult = adult;
|
||||
this.placeOfBirth = placeOfBirth;
|
||||
this.biography = biography;
|
||||
this.knownForDepartment = knownForDepartment;
|
||||
this.type = "person";
|
||||
}
|
||||
|
||||
static convertFromTmdbResponse(response) {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
profile_path,
|
||||
birthday,
|
||||
deathday,
|
||||
adult,
|
||||
place_of_birth,
|
||||
biography,
|
||||
known_for_department
|
||||
} = response;
|
||||
|
||||
const birthDay = new Date(birthday);
|
||||
const deathDay = deathday ? new Date(deathday) : null;
|
||||
|
||||
return new Person(
|
||||
id,
|
||||
name,
|
||||
profile_path,
|
||||
birthDay,
|
||||
deathDay,
|
||||
adult,
|
||||
place_of_birth,
|
||||
biography,
|
||||
known_for_department
|
||||
);
|
||||
}
|
||||
|
||||
createJsonResponse() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
poster: this.poster,
|
||||
birthday: this.birthday,
|
||||
deathday: this.deathday,
|
||||
place_of_birth: this.placeOfBirth,
|
||||
biography: this.biography,
|
||||
known_for_department: this.knownForDepartment,
|
||||
adult: this.adult,
|
||||
type: this.type
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Person;
|
||||
78
seasoned_api/src/tmdb/types/releaseDates.js
Normal file
78
seasoned_api/src/tmdb/types/releaseDates.js
Normal file
@@ -0,0 +1,78 @@
|
||||
class ReleaseDates {
|
||||
constructor(id, releases) {
|
||||
this.id = id;
|
||||
this.releases = releases;
|
||||
}
|
||||
|
||||
static convertFromTmdbResponse(response) {
|
||||
const { id, results } = response;
|
||||
|
||||
const releases = results.map(countryRelease =>
|
||||
new Release(
|
||||
countryRelease.iso_3166_1,
|
||||
countryRelease.release_dates.map(rd => new ReleaseDate(rd.certification, rd.iso_639_1, rd.release_date, rd.type, rd.note))
|
||||
))
|
||||
|
||||
return new ReleaseDates(id, releases)
|
||||
}
|
||||
|
||||
createJsonResponse() {
|
||||
return {
|
||||
id: this.id,
|
||||
results: this.releases.map(release => release.createJsonResponse())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Release {
|
||||
constructor(country, releaseDates) {
|
||||
this.country = country;
|
||||
this.releaseDates = releaseDates;
|
||||
}
|
||||
|
||||
createJsonResponse() {
|
||||
return {
|
||||
country: this.country,
|
||||
release_dates: this.releaseDates.map(releaseDate => releaseDate.createJsonResponse())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ReleaseDate {
|
||||
constructor(certification, language, releaseDate, type, note) {
|
||||
this.certification = certification;
|
||||
this.language = language;
|
||||
this.releaseDate = releaseDate;
|
||||
this.type = this.releaseTypeLookup(type);
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
releaseTypeLookup(releaseTypeKey) {
|
||||
const releaseTypeEnum = {
|
||||
1: 'Premier',
|
||||
2: 'Limited theatrical',
|
||||
3: 'Theatrical',
|
||||
4: 'Digital',
|
||||
5: 'Physical',
|
||||
6: 'TV'
|
||||
}
|
||||
if (releaseTypeKey <= Object.keys(releaseTypeEnum).length) {
|
||||
return releaseTypeEnum[releaseTypeKey]
|
||||
} else {
|
||||
// TODO log | Release type not defined, does this need updating?
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
createJsonResponse() {
|
||||
return {
|
||||
certification: this.certification,
|
||||
language: this.language,
|
||||
release_date: this.releaseDate,
|
||||
type: this.type,
|
||||
note: this.note
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ReleaseDates;
|
||||
50
seasoned_api/src/tmdb/types/show.js
Normal file
50
seasoned_api/src/tmdb/types/show.js
Normal file
@@ -0,0 +1,50 @@
|
||||
class Show {
|
||||
constructor(id, title, year=undefined, overview=undefined, poster=undefined, backdrop=undefined,
|
||||
seasons=undefined, episodes=undefined, rank=undefined, genres=undefined, status=undefined,
|
||||
runtime=undefined) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.year = year;
|
||||
this.overview = overview;
|
||||
this.poster = poster;
|
||||
this.backdrop = backdrop;
|
||||
this.seasons = seasons;
|
||||
this.episodes = episodes;
|
||||
this.rank = rank;
|
||||
this.genres = genres;
|
||||
this.productionStatus = status;
|
||||
this.runtime = runtime;
|
||||
this.type = 'show';
|
||||
}
|
||||
|
||||
static convertFromTmdbResponse(response) {
|
||||
const { id, name, first_air_date, overview, poster_path, backdrop_path, number_of_seasons, number_of_episodes,
|
||||
rank, genres, status, episode_run_time, popularity } = response;
|
||||
|
||||
const year = new Date(first_air_date).getFullYear()
|
||||
const genreNames = genres ? genres.map(g => g.name) : undefined
|
||||
|
||||
return new Show(id, name, year, overview, poster_path, backdrop_path, number_of_seasons, number_of_episodes,
|
||||
rank, genreNames, status, episode_run_time, popularity)
|
||||
}
|
||||
|
||||
createJsonResponse() {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
year: this.year,
|
||||
overview: this.overview,
|
||||
poster: this.poster,
|
||||
backdrop: this.backdrop,
|
||||
seasons: this.seasons,
|
||||
episodes: this.episodes,
|
||||
rank: this.rank,
|
||||
genres: this.genres,
|
||||
production_status: this.productionStatus,
|
||||
runtime: this.runtime,
|
||||
type: this.type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Show;
|
||||
@@ -1,37 +1,41 @@
|
||||
const User = require('src/user/user');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const User = require("src/user/user");
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
class Token {
|
||||
constructor(user) {
|
||||
this.user = user;
|
||||
}
|
||||
constructor(user, admin = false, settings = null) {
|
||||
this.user = user;
|
||||
this.admin = admin;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new token.
|
||||
* @param {String} secret a cipher of the token
|
||||
* @returns {String}
|
||||
*/
|
||||
toString(secret) {
|
||||
return jwt.sign({ username: this.user.username }, secret);
|
||||
}
|
||||
/**
|
||||
* Generate a new token.
|
||||
* @param {String} secret a cipher of the token
|
||||
* @returns {String}
|
||||
*/
|
||||
toString(secret) {
|
||||
const { user, admin, settings } = this;
|
||||
|
||||
/**
|
||||
let data = { username: user.username, settings };
|
||||
if (admin) data["admin"] = admin;
|
||||
|
||||
return jwt.sign(data, secret, { expiresIn: "90d" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a token.
|
||||
* @param {Token} jwtToken an encrypted token
|
||||
* @param {String} secret a cipher of the token
|
||||
* @returns {Token}
|
||||
*/
|
||||
static fromString(jwtToken, secret) {
|
||||
let username = null;
|
||||
static fromString(jwtToken, secret) {
|
||||
const token = jwt.verify(jwtToken, secret, { clockTolerance: 10000 });
|
||||
if (token.username == null) throw new Error("Malformed token");
|
||||
|
||||
try {
|
||||
username = jwt.verify(jwtToken, secret).username;
|
||||
} catch (error) {
|
||||
throw new Error('The token is invalid.');
|
||||
}
|
||||
const user = new User(username);
|
||||
return new Token(user);
|
||||
}
|
||||
const { username, admin, settings } = token;
|
||||
const user = new User(username);
|
||||
return new Token(user, admin, settings);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Token;
|
||||
|
||||
@@ -1,64 +1,256 @@
|
||||
const assert = require('assert');
|
||||
const establishedDatabase = require('src/database/database');
|
||||
const assert = require("assert");
|
||||
const establishedDatabase = require("src/database/database");
|
||||
|
||||
class UserRepository {
|
||||
constructor(database) {
|
||||
this.database = database || establishedDatabase;
|
||||
this.queries = {
|
||||
read: 'select * from user where lower(user_name) = lower(?)',
|
||||
create: 'insert into user (user_name) values (?)',
|
||||
change: 'update user set password = ? where user_name = ?',
|
||||
retrieveHash: 'select * from user where user_name = ?',
|
||||
getAdminStateByUser: 'select admin from user where user_name = ?'
|
||||
};
|
||||
}
|
||||
constructor(database) {
|
||||
this.database = database || establishedDatabase;
|
||||
this.queries = {
|
||||
read: "select * from user where lower(user_name) = lower(?)",
|
||||
create: "insert into user (user_name) values (?)",
|
||||
change: "update user set password = ? where user_name = ?",
|
||||
retrieveHash: "select * from user where user_name = ?",
|
||||
getAdminStateByUser: "select admin from user where user_name = ?",
|
||||
link: "update settings set plex_userid = ? where user_name = ?",
|
||||
unlink: "update settings set plex_userid = null where user_name = ?",
|
||||
createSettings: "insert into settings (user_name) values (?)",
|
||||
updateSettings:
|
||||
"update settings set user_name = ?, dark_mode = ?, emoji = ?",
|
||||
getSettings: "select * from settings where user_name = ?"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Create a user in a database.
|
||||
* @param {User} user the user you want to create
|
||||
* @returns {Promise}
|
||||
*/
|
||||
create(user) {
|
||||
return Promise.resolve()
|
||||
.then(() => this.database.get(this.queries.read, user.username))
|
||||
.then(() => this.database.run(this.queries.create, user.username))
|
||||
.catch((error) => {
|
||||
if (error.name === 'AssertionError' || error.message.endsWith('user_name')) {
|
||||
throw new Error('That username is already registered');
|
||||
}
|
||||
});
|
||||
}
|
||||
create(user) {
|
||||
return this.database
|
||||
.get(this.queries.read, user.username)
|
||||
.then(() => this.database.run(this.queries.create, user.username))
|
||||
.catch(error => {
|
||||
if (
|
||||
error.name === "AssertionError" ||
|
||||
error.message.endsWith("user_name")
|
||||
) {
|
||||
throw new Error("That username is already registered");
|
||||
}
|
||||
throw Error(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Retrieve a password from a database.
|
||||
* @param {User} user the user you want to retrieve the password
|
||||
* @returns {Promise}
|
||||
*/
|
||||
retrieveHash(user) {
|
||||
return Promise.resolve()
|
||||
.then(() => this.database.get(this.queries.retrieveHash, user.username))
|
||||
.then((row) => {
|
||||
assert(row, 'The user does not exist.');
|
||||
return row.password;
|
||||
})
|
||||
.catch((err) => console.log('there was a error when getting hash', err));
|
||||
}
|
||||
retrieveHash(user) {
|
||||
return this.database
|
||||
.get(this.queries.retrieveHash, user.username)
|
||||
.then(row => {
|
||||
assert(row, "The user does not exist.");
|
||||
return row.password;
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(error);
|
||||
throw new Error("Unable to find your user.");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Change a user's password in a database.
|
||||
* @param {User} user the user you want to create
|
||||
* @param {String} password the new password you want to change
|
||||
* @returns {Promise}
|
||||
*/
|
||||
changePassword(user, password) {
|
||||
return Promise.resolve(this.database.run(this.queries.change, [password, user.username]));
|
||||
}
|
||||
changePassword(user, password) {
|
||||
return this.database.run(this.queries.change, [password, user.username]);
|
||||
}
|
||||
|
||||
checkAdmin(user) {
|
||||
return this.database.get(this.queries.getAdminStateByUser, user.username).then((row) => {
|
||||
return row.admin;
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Link plex userid with seasoned user
|
||||
* @param {String} username the user you want to lunk plex userid with
|
||||
* @param {Number} plexUserID plex unique id
|
||||
* @returns {Promsie}
|
||||
*/
|
||||
linkPlexUserId(username, plexUserID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.database
|
||||
.run(this.queries.link, [plexUserID, username])
|
||||
.then(row => resolve(row))
|
||||
.catch(error => {
|
||||
// TODO log this unknown db error
|
||||
console.error("db error", error);
|
||||
|
||||
reject({
|
||||
status: 500,
|
||||
message:
|
||||
"An unexpected error occured while linking plex and seasoned accounts",
|
||||
source: "seasoned database"
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink plex userid with seasoned user
|
||||
* @param {User} user the user you want to lunk plex userid with
|
||||
* @returns {Promsie}
|
||||
*/
|
||||
unlinkPlexUserId(username) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.database
|
||||
.run(this.queries.unlink, username)
|
||||
.then(row => resolve(row))
|
||||
.catch(error => {
|
||||
// TODO log this unknown db error
|
||||
console.log("db error", error);
|
||||
|
||||
reject({
|
||||
status: 500,
|
||||
message:
|
||||
"An unexpected error occured while unlinking plex and seasoned accounts",
|
||||
source: "seasoned database"
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has boolean flag set for admin in database
|
||||
* @param {User} user object
|
||||
* @returns {Promsie}
|
||||
*/
|
||||
checkAdmin(user) {
|
||||
return this.database
|
||||
.get(this.queries.getAdminStateByUser, user.username)
|
||||
.then(row => row.admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings for user matching string username
|
||||
* @param {String} username
|
||||
* @returns {Promsie}
|
||||
*/
|
||||
getSettings(username) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.database
|
||||
.get(this.queries.getSettings, username)
|
||||
.then(async row => {
|
||||
if (row == null) {
|
||||
console.debug(
|
||||
`settings do not exist for user: ${username}. Creating settings entry.`
|
||||
);
|
||||
|
||||
const userExistsWithUsername = await this.database.get(
|
||||
"select * from user where user_name is ?",
|
||||
username
|
||||
);
|
||||
if (userExistsWithUsername !== undefined) {
|
||||
try {
|
||||
resolve(this.dbCreateSettings(username));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
} else {
|
||||
reject({
|
||||
status: 404,
|
||||
message: "User not found, no settings to get"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resolve(row);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(
|
||||
"Unexpected error occured while fetching settings for your account. Error:",
|
||||
error
|
||||
);
|
||||
reject({
|
||||
status: 500,
|
||||
message:
|
||||
"An unexpected error occured while fetching settings for your account",
|
||||
source: "seasoned database"
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings values for user matching string username
|
||||
* @param {String} username
|
||||
* @param {String} dark_mode
|
||||
* @param {String} emoji
|
||||
* @returns {Promsie}
|
||||
*/
|
||||
updateSettings(username, dark_mode = undefined, emoji = undefined) {
|
||||
const settings = this.getSettings(username);
|
||||
dark_mode = dark_mode !== undefined ? dark_mode : settings.dark_mode;
|
||||
emoji = emoji !== undefined ? emoji : settings.emoji;
|
||||
|
||||
return this.dbUpdateSettings(username, dark_mode, emoji).catch(error => {
|
||||
if (error.status && error.message) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 500,
|
||||
message:
|
||||
"An unexpected error occured while updating settings for your account"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for creating settings in the database
|
||||
* @param {String} username
|
||||
* @returns {Promsie}
|
||||
*/
|
||||
dbCreateSettings(username) {
|
||||
return this.database
|
||||
.run(this.queries.createSettings, username)
|
||||
.then(() => this.database.get(this.queries.getSettings, username))
|
||||
.catch(error =>
|
||||
rejectUnexpectedDatabaseError(
|
||||
"Unexpected error occured while creating settings",
|
||||
503,
|
||||
error
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for updating settings in the database
|
||||
* @param {String} username
|
||||
* @returns {Promsie}
|
||||
*/
|
||||
dbUpdateSettings(username, dark_mode, emoji) {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.database
|
||||
.run(this.queries.updateSettings, [username, dark_mode, emoji])
|
||||
.then(row => resolve(row))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const rejectUnexpectedDatabaseError = (
|
||||
message,
|
||||
status,
|
||||
error,
|
||||
reject = null
|
||||
) => {
|
||||
console.error(error);
|
||||
const body = {
|
||||
status,
|
||||
message,
|
||||
source: "seasoned database"
|
||||
};
|
||||
|
||||
if (reject == null) {
|
||||
return new Promise((resolve, reject) => reject(body));
|
||||
}
|
||||
reject(body);
|
||||
};
|
||||
|
||||
module.exports = UserRepository;
|
||||
|
||||
@@ -1,73 +1,75 @@
|
||||
const bcrypt = require('bcrypt-nodejs');
|
||||
const UserRepository = require('src/user/userRepository');
|
||||
const bcrypt = require("bcrypt");
|
||||
const UserRepository = require("src/user/userRepository");
|
||||
|
||||
class UserSecurity {
|
||||
constructor(database) {
|
||||
this.userRepository = new UserRepository(database);
|
||||
}
|
||||
constructor(database) {
|
||||
this.userRepository = new UserRepository(database);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Create a new user in PlanFlix.
|
||||
* @param {User} user the new user you want to create
|
||||
* @param {String} clearPassword a password of the user
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createNewUser(user, clearPassword) {
|
||||
if (user.username.trim() === '') {
|
||||
throw new Error('The username is empty.');
|
||||
} else if (clearPassword.trim() === '') {
|
||||
throw new Error('The password is empty.');
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
.then(() => this.userRepository.create(user))
|
||||
.then(() => UserSecurity.hashPassword(clearPassword))
|
||||
.then(hash => this.userRepository.changePassword(user, hash));
|
||||
}
|
||||
}
|
||||
createNewUser(user, clearPassword) {
|
||||
if (user.username.trim() === "") {
|
||||
throw new Error("The username is empty.");
|
||||
} else if (clearPassword.trim() === "") {
|
||||
throw new Error("The password is empty.");
|
||||
} else {
|
||||
return this.userRepository
|
||||
.create(user)
|
||||
.then(() => UserSecurity.hashPassword(clearPassword))
|
||||
.then(hash => this.userRepository.changePassword(user, hash));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Login into PlanFlix.
|
||||
* @param {User} user the user you want to login
|
||||
* @param {String} clearPassword the user's password
|
||||
* @returns {Promise}
|
||||
*/
|
||||
login(user, clearPassword) {
|
||||
return Promise.resolve()
|
||||
.then(() => this.userRepository.retrieveHash(user))
|
||||
.then(hash => UserSecurity.compareHashes(hash, clearPassword))
|
||||
.catch(() => { throw new Error('Wrong username or password.'); });
|
||||
}
|
||||
login(user, clearPassword) {
|
||||
return this.userRepository
|
||||
.retrieveHash(user)
|
||||
.then(hash => UserSecurity.compareHashes(hash, clearPassword))
|
||||
.catch(() => {
|
||||
throw new Error("Incorrect username or password.");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Compare between a password and a hash password from database.
|
||||
* @param {String} hash the hash password from database
|
||||
* @param {String} clearPassword the user's password
|
||||
* @returns {Promise}
|
||||
*/
|
||||
static compareHashes(hash, clearPassword) {
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.compare(clearPassword, hash, (error, matches) => {
|
||||
if (matches === true) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
static compareHashes(hash, clearPassword) {
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.compare(clearPassword, hash, (error, match) => {
|
||||
if (match) resolve(true);
|
||||
reject(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Hashes a password.
|
||||
* @param {String} clearPassword the user's password
|
||||
* @returns {Promise}
|
||||
*/
|
||||
static hashPassword(clearPassword) {
|
||||
return new Promise((resolve) => {
|
||||
bcrypt.hash(clearPassword, null, null, (error, hash) => {
|
||||
resolve(hash);
|
||||
});
|
||||
static hashPassword(clearPassword) {
|
||||
return new Promise(resolve => {
|
||||
const saltRounds = 10;
|
||||
bcrypt.hash(clearPassword, saltRounds, (error, hash) => {
|
||||
if (error) reject(error);
|
||||
|
||||
resolve(hash);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserSecurity;
|
||||
|
||||
@@ -1,118 +1,239 @@
|
||||
const express = require('express');
|
||||
const Raven = require('raven');
|
||||
const bodyParser = require('body-parser');
|
||||
const tokenToUser = require('./middleware/tokenToUser');
|
||||
const mustBeAuthenticated = require('./middleware/mustBeAuthenticated');
|
||||
const mustBeAdmin = require('./middleware/mustBeAdmin');
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const express = require("express");
|
||||
const Raven = require("raven");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const bodyParser = require("body-parser");
|
||||
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
|
||||
const reqTokenToUser = require("./middleware/reqTokenToUser");
|
||||
const mustBeAuthenticated = require("./middleware/mustBeAuthenticated");
|
||||
const mustBeAdmin = require("./middleware/mustBeAdmin");
|
||||
const mustHaveAccountLinkedToPlex = require("./middleware/mustHaveAccountLinkedToPlex");
|
||||
|
||||
const listController = require("./controllers/list/listController");
|
||||
const tautulli = require("./controllers/user/viewHistory.js");
|
||||
const SettingsController = require("./controllers/user/settings");
|
||||
const AuthenticatePlexAccountController = require("./controllers/user/authenticatePlexAccount");
|
||||
|
||||
// TODO: Have our raven router check if there is a value, if not don't enable raven.
|
||||
Raven.config(configuration.get('raven', 'DSN')).install();
|
||||
Raven.config(configuration.get("raven", "DSN")).install();
|
||||
|
||||
const app = express(); // define our app using express
|
||||
app.use(Raven.requestHandler());
|
||||
// this will let us get the data from a POST
|
||||
// configure app to use bodyParser()
|
||||
app.use(bodyParser.json());
|
||||
// router.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
|
||||
/* Decode the Authorization header if provided */
|
||||
// router.use(tokenToUser);
|
||||
app.use(cookieParser());
|
||||
|
||||
const router = express.Router();
|
||||
const allowedOrigins = ['https://kevinmidboe.com', 'http://localhost:8080'];
|
||||
const allowedOrigins = configuration.get("webserver", "origins");
|
||||
|
||||
// TODO: All JSON handling in a single router
|
||||
// router.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
|
||||
// This is probably a correct middleware/router setup
|
||||
/* Translate the user token to a user name */
|
||||
router.use(tokenToUser);
|
||||
/* Check header and cookie for authentication and set req.loggedInUser */
|
||||
router.use(reqTokenToUser);
|
||||
|
||||
// TODO: Should have a separate middleware/router for handling headers.
|
||||
router.use((req, res, next) => {
|
||||
// TODO add logging of all incoming
|
||||
console.log('Request: ', req.originalUrl);
|
||||
const origin = req.headers.origin;
|
||||
if (allowedOrigins.indexOf(origin) > -1) {
|
||||
console.log('allowed');
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, loggedinuser');
|
||||
res.header('Access-Control-Allow-Methods', 'POST, GET, PUT');
|
||||
// TODO add logging of all incoming
|
||||
// const origin = req.headers.origin;
|
||||
// if (allowedOrigins.indexOf(origin) > -1) {
|
||||
// res.setHeader("Access-Control-Allow-Origin", origin);
|
||||
// }
|
||||
|
||||
next();
|
||||
res.header(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Content-Type, Authorization, loggedinuser, set-cookie"
|
||||
);
|
||||
|
||||
res.header("Access-Control-Allow-Credentials", "true");
|
||||
res.header("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS");
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/', function mainHandler(req, res) {
|
||||
throw new Error('Broke!');
|
||||
router.get("/", (req, res) => {
|
||||
res.send("welcome to seasoned api");
|
||||
});
|
||||
|
||||
app.use(Raven.errorHandler());
|
||||
app.use(function onError(err, req, res, next) {
|
||||
res.statusCode = 500;
|
||||
res.end(res.sentry + '\n');
|
||||
app.use((err, req, res, next) => {
|
||||
res.statusCode = 500;
|
||||
res.end(res.sentry + "\n");
|
||||
});
|
||||
|
||||
/**
|
||||
* User
|
||||
*/
|
||||
router.post('/v1/user', require('./controllers/user/register.js'));
|
||||
router.post('/v1/user/login', require('./controllers/user/login.js'));
|
||||
router.get('/v1/user/history', mustBeAuthenticated, require('./controllers/user/history.js'));
|
||||
router.get('/v1/user/requests', mustBeAuthenticated, require('./controllers/user/requests.js'));
|
||||
router.post("/v1/user", require("./controllers/user/register.js"));
|
||||
router.post("/v1/user/login", require("./controllers/user/login.js"));
|
||||
router.post("/v1/user/logout", require("./controllers/user/logout.js"));
|
||||
router.get(
|
||||
"/v1/user/settings",
|
||||
mustBeAuthenticated,
|
||||
SettingsController.getSettingsController
|
||||
);
|
||||
router.put(
|
||||
"/v1/user/settings",
|
||||
mustBeAuthenticated,
|
||||
SettingsController.updateSettingsController
|
||||
);
|
||||
router.get(
|
||||
"/v1/user/search_history",
|
||||
mustBeAuthenticated,
|
||||
require("./controllers/user/searchHistory.js")
|
||||
);
|
||||
router.get(
|
||||
"/v1/user/requests",
|
||||
mustBeAuthenticated,
|
||||
require("./controllers/user/requests.js")
|
||||
);
|
||||
router.post(
|
||||
"/v1/user/link_plex",
|
||||
mustBeAuthenticated,
|
||||
AuthenticatePlexAccountController.link
|
||||
);
|
||||
router.post(
|
||||
"/v1/user/unlink_plex",
|
||||
mustBeAuthenticated,
|
||||
AuthenticatePlexAccountController.unlink
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/v1/user/view_history",
|
||||
mustHaveAccountLinkedToPlex,
|
||||
tautulli.userViewHistoryController
|
||||
);
|
||||
router.get(
|
||||
"/v1/user/watch_time",
|
||||
mustHaveAccountLinkedToPlex,
|
||||
tautulli.watchTimeStatsController
|
||||
);
|
||||
router.get(
|
||||
"/v1/user/plays_by_day",
|
||||
mustHaveAccountLinkedToPlex,
|
||||
tautulli.getPlaysByDaysController
|
||||
);
|
||||
router.get(
|
||||
"/v1/user/plays_by_dayofweek",
|
||||
mustHaveAccountLinkedToPlex,
|
||||
tautulli.getPlaysByDayOfWeekController
|
||||
);
|
||||
|
||||
/**
|
||||
* Seasoned
|
||||
*/
|
||||
router.get('/v1/seasoned/all', require('./controllers/seasoned/readStrays.js'));
|
||||
router.get('/v1/seasoned/:strayId', require('./controllers/seasoned/strayById.js'));
|
||||
router.post('/v1/seasoned/verify/:strayId', require('./controllers/seasoned/verifyStray.js'));
|
||||
router.get("/v1/seasoned/all", require("./controllers/seasoned/readStrays.js"));
|
||||
router.get(
|
||||
"/v1/seasoned/:strayId",
|
||||
require("./controllers/seasoned/strayById.js")
|
||||
);
|
||||
router.post(
|
||||
"/v1/seasoned/verify/:strayId",
|
||||
require("./controllers/seasoned/verifyStray.js")
|
||||
);
|
||||
|
||||
router.get("/v2/search/", require("./controllers/search/multiSearch.js"));
|
||||
router.get("/v2/search/movie", require("./controllers/search/movieSearch.js"));
|
||||
router.get("/v2/search/show", require("./controllers/search/showSearch.js"));
|
||||
router.get(
|
||||
"/v2/search/person",
|
||||
require("./controllers/search/personSearch.js")
|
||||
);
|
||||
|
||||
router.get("/v2/movie/now_playing", listController.nowPlayingMovies);
|
||||
router.get("/v2/movie/popular", listController.popularMovies);
|
||||
router.get("/v2/movie/top_rated", listController.topRatedMovies);
|
||||
router.get("/v2/movie/upcoming", listController.upcomingMovies);
|
||||
router.get("/v2/movie/:id/credits", require("./controllers/movie/credits.js"));
|
||||
router.get(
|
||||
"/v2/movie/:id/release_dates",
|
||||
require("./controllers/movie/releaseDates.js")
|
||||
);
|
||||
router.get("/v2/movie/:id", require("./controllers/movie/info.js"));
|
||||
|
||||
router.get("/v2/show/now_playing", listController.nowPlayingShows);
|
||||
router.get("/v2/show/popular", listController.popularShows);
|
||||
router.get("/v2/show/top_rated", listController.topRatedShows);
|
||||
router.get("/v2/show/:id/credits", require("./controllers/show/credits.js"));
|
||||
router.get("/v2/show/:id", require("./controllers/show/info.js"));
|
||||
|
||||
router.get(
|
||||
"/v2/person/:id/credits",
|
||||
require("./controllers/person/credits.js")
|
||||
);
|
||||
router.get("/v2/person/:id", require("./controllers/person/info.js"));
|
||||
|
||||
/**
|
||||
* Plex
|
||||
*/
|
||||
router.get('/v1/plex/search', require('./controllers/plex/searchMedia.js'));
|
||||
router.get('/v1/plex/playing', require('./controllers/plex/plexPlaying.js'));
|
||||
router.get('/v1/plex/request', require('./controllers/plex/searchRequest.js'));
|
||||
router.get('/v1/plex/request/:mediaId', require('./controllers/plex/readRequest.js'));
|
||||
router.post('/v1/plex/request/:mediaId', require('./controllers/plex/submitRequest.js'));
|
||||
router.get('/v1/plex/hook', require('./controllers/plex/hookDump.js'));
|
||||
router.get("/v2/plex/search", require("./controllers/plex/search"));
|
||||
|
||||
/**
|
||||
* List
|
||||
*/
|
||||
router.get("/v1/plex/search", require("./controllers/plex/searchMedia.js"));
|
||||
router.get("/v1/plex/playing", require("./controllers/plex/plexPlaying.js"));
|
||||
router.get("/v1/plex/request", require("./controllers/plex/searchRequest.js"));
|
||||
router.get(
|
||||
"/v1/plex/request/:mediaId",
|
||||
require("./controllers/plex/readRequest.js")
|
||||
);
|
||||
router.post(
|
||||
"/v1/plex/request/:mediaId",
|
||||
require("./controllers/plex/submitRequest.js")
|
||||
);
|
||||
router.post("/v1/plex/hook", require("./controllers/plex/hookDump.js"));
|
||||
|
||||
router.get(
|
||||
"/v1/plex/watch-link",
|
||||
mustBeAuthenticated,
|
||||
require("./controllers/plex/watchDirectLink.js")
|
||||
);
|
||||
|
||||
/**
|
||||
* Requests
|
||||
*/
|
||||
router.get('/v1/plex/requests/all', require('./controllers/plex/fetchRequested.js'));
|
||||
router.put('/v1/plex/request/:requestId', mustBeAuthenticated, require('./controllers/plex/updateRequested.js'));
|
||||
|
||||
router.get("/v2/request", require("./controllers/request/fetchAllRequests.js"));
|
||||
router.get("/v2/request/:id", require("./controllers/request/getRequest.js"));
|
||||
router.post("/v2/request", require("./controllers/request/requestTmdbId.js"));
|
||||
router.get(
|
||||
"/v1/plex/requests/all",
|
||||
require("./controllers/plex/fetchRequested.js")
|
||||
);
|
||||
router.put(
|
||||
"/v1/plex/request/:requestId",
|
||||
mustBeAuthenticated,
|
||||
require("./controllers/plex/updateRequested.js")
|
||||
);
|
||||
|
||||
/**
|
||||
* Pirate
|
||||
*/
|
||||
router.get('/v1/pirate/search', mustBeAuthenticated, require('./controllers/pirate/searchTheBay.js'));
|
||||
router.post('/v1/pirate/add', mustBeAuthenticated, require('./controllers/pirate/addMagnet.js'));
|
||||
|
||||
/**
|
||||
* TMDB
|
||||
*/
|
||||
router.get('/v1/tmdb/search', require('./controllers/tmdb/searchMedia.js'));
|
||||
router.get('/v1/tmdb/list/:listname', require('./controllers/tmdb/listSearch.js'));
|
||||
router.get('/v1/tmdb/:mediaId', require('./controllers/tmdb/readMedia.js'));
|
||||
router.get(
|
||||
"/v1/pirate/search",
|
||||
mustBeAuthenticated,
|
||||
require("./controllers/pirate/searchTheBay.js")
|
||||
);
|
||||
router.post(
|
||||
"/v1/pirate/add",
|
||||
mustBeAuthenticated,
|
||||
require("./controllers/pirate/addMagnet.js")
|
||||
);
|
||||
|
||||
/**
|
||||
* git
|
||||
*/
|
||||
router.post('/v1/git/dump', require('./controllers/git/dumpHook.js'));
|
||||
router.post("/v1/git/dump", require("./controllers/git/dumpHook.js"));
|
||||
|
||||
/**
|
||||
* misc
|
||||
*/
|
||||
router.get('/v1/emoji', require('./controllers/misc/emoji.js'));
|
||||
router.get("/v1/emoji", require("./controllers/misc/emoji.js"));
|
||||
|
||||
// REGISTER OUR ROUTES -------------------------------
|
||||
// all of our routes will be prefixed with /api
|
||||
app.use('/api', router);
|
||||
app.use("/api", router);
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const TMDB = require('src/tmdb/tmdb');
|
||||
const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
|
||||
|
||||
// there should be a translate function from query params to
|
||||
// tmdb list that is valid. Should it be a helper function or does it
|
||||
// belong in tmdb.
|
||||
// + could also have default value that are sent to the client.
|
||||
// * have the same class create a getListNames() and a fetchList()
|
||||
// * dicover list might be overkill_https://tinyurl.com/y7f8ragw
|
||||
// + trending! https://tinyurl.com/ydywrqox
|
||||
// by all, mediatype, or person. Can also define time periode to
|
||||
// get more trending view of what people are checking out.
|
||||
// + newly created (tv/latest).
|
||||
// + movie/latest
|
||||
//
|
||||
function handleError(error, res) {
|
||||
const { status, message } = error;
|
||||
|
||||
if (status && message) {
|
||||
res.status(status).send({ success: false, message })
|
||||
} else {
|
||||
console.log('caught list controller error', error)
|
||||
res.status(500).send({ message: 'An unexpected error occured while requesting list'})
|
||||
}
|
||||
}
|
||||
|
||||
function handleListResponse(response, res) {
|
||||
return res.send(response)
|
||||
.catch(error => handleError(error, res))
|
||||
}
|
||||
|
||||
function fetchTmdbList(req, res, listname, type) {
|
||||
const { page } = req.query;
|
||||
|
||||
if (type === 'movie') {
|
||||
return tmdb.movieList(listname, page)
|
||||
.then(listResponse => res.send(listResponse))
|
||||
.catch(error => handleError(error, res))
|
||||
} else if (type === 'show') {
|
||||
return tmdb.showList(listname, page)
|
||||
.then(listResponse => res.send(listResponse))
|
||||
.catch(error => handleError(error, res))
|
||||
}
|
||||
|
||||
handleError({
|
||||
status: 400,
|
||||
message: `'${type}' is not a valid list type.`
|
||||
}, res)
|
||||
}
|
||||
|
||||
const nowPlayingMovies = (req, res) => fetchTmdbList(req, res, 'miscNowPlayingMovies', 'movie')
|
||||
const popularMovies = (req, res) => fetchTmdbList(req, res, 'miscPopularMovies', 'movie')
|
||||
const topRatedMovies = (req, res) => fetchTmdbList(req, res, 'miscTopRatedMovies', 'movie')
|
||||
const upcomingMovies = (req, res) => fetchTmdbList(req, res, 'miscUpcomingMovies', 'movie')
|
||||
const nowPlayingShows = (req, res) => fetchTmdbList(req, res, 'tvOnTheAir', 'show')
|
||||
const popularShows = (req, res) => fetchTmdbList(req, res, 'miscPopularTvs', 'show')
|
||||
const topRatedShows = (req, res) => fetchTmdbList(req, res, 'miscTopRatedTvs', 'show')
|
||||
|
||||
module.exports = {
|
||||
nowPlayingMovies,
|
||||
popularMovies,
|
||||
topRatedMovies,
|
||||
upcomingMovies,
|
||||
nowPlayingShows,
|
||||
popularShows,
|
||||
topRatedShows
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
24
seasoned_api/src/webserver/controllers/movie/credits.js
Normal file
24
seasoned_api/src/webserver/controllers/movie/credits.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const TMDB = require('src/tmdb/tmdb');
|
||||
|
||||
const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
|
||||
|
||||
const movieCreditsController = (req, res) => {
|
||||
const movieId = req.params.id;
|
||||
|
||||
tmdb.movieCredits(movieId)
|
||||
.then(credits => res.send(credits.createJsonResponse()))
|
||||
.catch(error => {
|
||||
const { status, message } = error;
|
||||
|
||||
if (status && message) {
|
||||
res.status(status).send({ success: false, message })
|
||||
} else {
|
||||
// TODO log unhandled errors
|
||||
console.log('caugth movie credits controller error', error)
|
||||
res.status(500).send({ message: 'An unexpected error occured while requesting movie credits' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = movieCreditsController;
|
||||
71
seasoned_api/src/webserver/controllers/movie/info.js
Normal file
71
seasoned_api/src/webserver/controllers/movie/info.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
const TMDB = require("src/tmdb/tmdb");
|
||||
const Plex = require("src/plex/plex");
|
||||
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
|
||||
const plex = new Plex(configuration.get("plex", "ip"));
|
||||
|
||||
function handleError(error, res) {
|
||||
const { status, message } = error;
|
||||
|
||||
if (status && message) {
|
||||
res.status(status).send({ success: false, message });
|
||||
} else {
|
||||
console.log("caught movieinfo controller error", error);
|
||||
res.status(500).send({
|
||||
message: "An unexpected error occured while requesting movie info"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller: Retrieve information for a movie
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
async function movieInfoController(req, res) {
|
||||
const movieId = req.params.id;
|
||||
let { credits, release_dates, check_existance } = req.query;
|
||||
|
||||
credits && credits.toLowerCase() === "true"
|
||||
? (credits = true)
|
||||
: (credits = false);
|
||||
release_dates && release_dates.toLowerCase() === "true"
|
||||
? (release_dates = true)
|
||||
: (release_dates = false);
|
||||
check_existance && check_existance.toLowerCase() === "true"
|
||||
? (check_existance = true)
|
||||
: (check_existance = false);
|
||||
|
||||
let tmdbQueue = [tmdb.movieInfo(movieId)];
|
||||
if (credits) tmdbQueue.push(tmdb.movieCredits(movieId));
|
||||
if (release_dates) tmdbQueue.push(tmdb.movieReleaseDates(movieId));
|
||||
|
||||
try {
|
||||
const [Movie, Credits, ReleaseDates] = await Promise.all(tmdbQueue);
|
||||
|
||||
const movie = Movie.createJsonResponse();
|
||||
if (Credits) movie.credits = Credits.createJsonResponse();
|
||||
if (ReleaseDates)
|
||||
movie.release_dates = ReleaseDates.createJsonResponse().results;
|
||||
|
||||
if (check_existance) {
|
||||
try {
|
||||
movie.exists_in_plex = await plex.existsInPlex(movie);
|
||||
} catch (error) {
|
||||
if (error.status === 401) {
|
||||
console.log("Unathorized request, check plex server LAN settings");
|
||||
} else {
|
||||
console.log("Unkown error from plex!");
|
||||
}
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
res.send(movie);
|
||||
} catch (error) {
|
||||
handleError(error, res);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = movieInfoController;
|
||||
24
seasoned_api/src/webserver/controllers/movie/releaseDates.js
Normal file
24
seasoned_api/src/webserver/controllers/movie/releaseDates.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const TMDB = require('src/tmdb/tmdb');
|
||||
|
||||
const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
|
||||
|
||||
const movieReleaseDatesController = (req, res) => {
|
||||
const movieId = req.params.id;
|
||||
|
||||
tmdb.movieReleaseDates(movieId)
|
||||
.then(releaseDates => res.send(releaseDates.createJsonResponse()))
|
||||
.catch(error => {
|
||||
const { status, message } = error;
|
||||
|
||||
if (status && message) {
|
||||
res.status(status).send({ success: false, message })
|
||||
} else {
|
||||
// TODO log unhandled errors : here our at tmdbReleaseError ?
|
||||
console.log('caugth release dates controller error', error)
|
||||
res.status(500).send({ message: 'An unexpected error occured while requesting movie credits' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = movieReleaseDatesController;
|
||||
26
seasoned_api/src/webserver/controllers/person/credits.js
Normal file
26
seasoned_api/src/webserver/controllers/person/credits.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
const TMDB = require("src/tmdb/tmdb");
|
||||
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
|
||||
|
||||
const personCreditsController = (req, res) => {
|
||||
const personId = req.params.id;
|
||||
|
||||
return tmdb
|
||||
.personCredits(personId)
|
||||
.then(credits => res.send(credits))
|
||||
.catch(error => {
|
||||
const { status, message } = error;
|
||||
|
||||
if (status && message) {
|
||||
res.status(status).send({ success: false, message });
|
||||
} else {
|
||||
// TODO log unhandled errors
|
||||
console.log("caugth show credits controller error", error);
|
||||
res.status(500).send({
|
||||
message: "An unexpected error occured while requesting person credits"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = personCreditsController;
|
||||
49
seasoned_api/src/webserver/controllers/person/info.js
Normal file
49
seasoned_api/src/webserver/controllers/person/info.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
const TMDB = require("src/tmdb/tmdb");
|
||||
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
|
||||
|
||||
function handleError(error, res) {
|
||||
const { status, message } = error;
|
||||
|
||||
if (status && message) {
|
||||
res.status(status).send({ success: false, message });
|
||||
} else {
|
||||
console.log("caught personinfo controller error", error);
|
||||
res.status(500).send({
|
||||
message: "An unexpected error occured while requesting person info."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller: Retrieve information for a person
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
|
||||
async function personInfoController(req, res) {
|
||||
const personId = req.params.id;
|
||||
let { credits } = req.query;
|
||||
arguments;
|
||||
|
||||
credits && credits.toLowerCase() === "true"
|
||||
? (credits = true)
|
||||
: (credits = false);
|
||||
|
||||
let tmdbQueue = [tmdb.personInfo(personId)];
|
||||
if (credits) tmdbQueue.push(tmdb.personCredits(personId));
|
||||
|
||||
try {
|
||||
const [Person, Credits] = await Promise.all(tmdbQueue);
|
||||
|
||||
const person = Person.createJsonResponse();
|
||||
if (credits) person.credits = Credits.createJsonResponse();
|
||||
|
||||
return res.send(person);
|
||||
} catch (error) {
|
||||
handleError(error, res);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = personInfoController;
|
||||
@@ -10,13 +10,15 @@ const PirateRepository = require('src/pirate/pirateRepository');
|
||||
|
||||
function addMagnet(req, res) {
|
||||
const magnet = req.body.magnet;
|
||||
const name = req.body.name;
|
||||
const tmdb_id = req.body.tmdb_id;
|
||||
|
||||
PirateRepository.AddMagnet(magnet)
|
||||
PirateRepository.AddMagnet(magnet, name, tmdb_id)
|
||||
.then((result) => {
|
||||
res.send(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(500).send({ success: false, error: error.message });
|
||||
.catch(error => {
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-10-21 09:54:31
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2018-02-26 19:56:32
|
||||
*/
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-10-21 09:54:31
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2018-02-26 19:56:32
|
||||
*/
|
||||
|
||||
const PirateRepository = require('src/pirate/pirateRepository');
|
||||
const PirateRepository = require("src/pirate/pirateRepository");
|
||||
// const pirateRepository = new PirateRepository();
|
||||
|
||||
/**
|
||||
@@ -15,15 +15,15 @@ const PirateRepository = require('src/pirate/pirateRepository');
|
||||
* @returns {Callback}
|
||||
*/
|
||||
function updateRequested(req, res) {
|
||||
const { query, page, type } = req.query;
|
||||
const { query, page, type } = req.query;
|
||||
|
||||
PirateRepository.SearchPiratebay(query, page, type)
|
||||
.then((result) => {
|
||||
res.send({ success: true, results: result });
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(401).send({ success: false, error: error.message });
|
||||
});
|
||||
PirateRepository.SearchPiratebay(query, page, type)
|
||||
.then(result => {
|
||||
res.send({ success: true, results: result });
|
||||
})
|
||||
.catch(error => {
|
||||
res.status(401).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = updateRequested;
|
||||
|
||||
@@ -10,14 +10,14 @@ const requestRepository = new RequestRepository();
|
||||
*/
|
||||
function fetchRequestedController(req, res) {
|
||||
// const user = req.loggedInUser;
|
||||
const { status } = req.query;
|
||||
const { status, page } = req.query;
|
||||
|
||||
requestRepository.fetchRequested(status)
|
||||
requestRepository.fetchRequested(status, page)
|
||||
.then((requestedItems) => {
|
||||
res.send({ success: true, results: requestedItems, total_results: requestedItems.length });
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(401).send({ success: false, error: error.message });
|
||||
res.status(401).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
const PlexRepository = require('src/plex/plexRepository');
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
|
||||
const plexRepository = new PlexRepository();
|
||||
const plexRepository = new PlexRepository(configuration.get('plex', 'ip'));
|
||||
|
||||
function playingController(req, res) {
|
||||
plexRepository.nowPlaying()
|
||||
.then((movies) => {
|
||||
.then(movies => {
|
||||
res.send(movies);
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(500).send({ success: false, error: error.message });
|
||||
.catch(error => {
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ function readRequestController(req, res) {
|
||||
const mediaId = req.params.mediaId;
|
||||
const { type } = req.query;
|
||||
requestRepository.lookup(mediaId, type)
|
||||
.then((movies) => {
|
||||
.then(movies => {
|
||||
res.send(movies);
|
||||
}).catch((error) => {
|
||||
res.status(404).send({ success: false, error: error.message });
|
||||
}).catch(error => {
|
||||
res.status(404).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
25
seasoned_api/src/webserver/controllers/plex/search.js
Normal file
25
seasoned_api/src/webserver/controllers/plex/search.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const Plex = require('src/plex/plex');
|
||||
const plex = new Plex(configuration.get('plex', 'ip'));
|
||||
|
||||
/**
|
||||
* Controller: Search plex for movies, shows and episodes by query
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
function searchPlexController(req, res) {
|
||||
const { query, type } = req.query;
|
||||
plex.search(query, type)
|
||||
.then(movies => {
|
||||
if (movies.length > 0) {
|
||||
res.send(movies);
|
||||
} else {
|
||||
res.status(404).send({ success: false, message: 'Search query did not give any results from plex.'})
|
||||
}
|
||||
}).catch(error => {
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = searchPlexController;
|
||||
@@ -1,6 +1,7 @@
|
||||
const PlexRepository = require('src/plex/plexRepository');
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
|
||||
const plexRepository = new PlexRepository();
|
||||
const plexRepository = new PlexRepository(configuration.get('plex', 'ip'));
|
||||
|
||||
/**
|
||||
* Controller: Search for media and check existence
|
||||
@@ -13,15 +14,15 @@ function searchMediaController(req, res) {
|
||||
const { query } = req.query;
|
||||
|
||||
plexRepository.search(query)
|
||||
.then((media) => {
|
||||
.then(media => {
|
||||
if (media !== undefined || media.length > 0) {
|
||||
res.send(media);
|
||||
} else {
|
||||
res.status(404).send({ success: false, error: 'Search query did not return any results.' });
|
||||
res.status(404).send({ success: false, message: 'Search query did not return any results.' });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(500).send({ success: false, error: error.message });
|
||||
.catch(error => {
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
const SearchHistory = require('src/searchHistory/searchHistory');
|
||||
const Cache = require('src/tmdb/cache');
|
||||
const RequestRepository = require('src/plex/requestRepository.js');
|
||||
const SearchHistory = require("src/searchHistory/searchHistory");
|
||||
const Cache = require("src/tmdb/cache");
|
||||
const RequestRepository = require("src/plex/requestRepository.js");
|
||||
|
||||
const cache = new Cache();
|
||||
const requestRepository = new RequestRepository(cache);
|
||||
const searchHistory = new SearchHistory();
|
||||
|
||||
|
||||
function searchRequestController(req, res) {
|
||||
const user = req.loggedInUser;
|
||||
const { query, page, type } = req.query;
|
||||
const username = user == undefined ? undefined : user.username;
|
||||
const { query, page, type } = req.query;
|
||||
const username = req.loggedInUser ? req.loggedInUser.username : null;
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => searchHistory.create(username, query))
|
||||
.then(() => requestRepository.search(query, page, type))
|
||||
.then((searchResult) => {
|
||||
res.send(searchResult);
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(500).send({ success: false, error: error });
|
||||
});
|
||||
Promise.resolve()
|
||||
.then(() => searchHistory.create(username, query))
|
||||
.then(() => requestRepository.search(query, page, type))
|
||||
.then(searchResult => {
|
||||
res.send(searchResult);
|
||||
})
|
||||
.catch(error => {
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = searchRequestController;
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
const RequestRepository = require('src/plex/requestRepository.js');
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
const RequestRepository = require("src/request/request");
|
||||
const TMDB = require("src/tmdb/tmdb");
|
||||
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
|
||||
const request = new RequestRepository();
|
||||
|
||||
const requestRepository = new RequestRepository();
|
||||
const tmdbMovieInfo = id => {
|
||||
return tmdb.movieInfo(id);
|
||||
};
|
||||
|
||||
const tmdbShowInfo = id => {
|
||||
return tmdb.showInfo(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Controller: POST a media id to be donwloaded
|
||||
@@ -8,22 +18,46 @@ const requestRepository = new RequestRepository();
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
|
||||
function submitRequestController(req, res) {
|
||||
// This is the id that is the param of the url
|
||||
const id = req.params.mediaId;
|
||||
const type = req.query.type;
|
||||
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
const user_agent = req.headers['user-agent'];
|
||||
const user = req.loggedInUser;
|
||||
// This is the id that is the param of the url
|
||||
const id = req.params.mediaId;
|
||||
const type = req.query.type ? req.query.type.toLowerCase() : undefined;
|
||||
const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress;
|
||||
const user_agent = req.headers["user-agent"];
|
||||
const username = req.loggedInUser ? req.loggedInUser.username : null;
|
||||
|
||||
requestRepository.sendRequest(id, type, ip, user_agent, user)
|
||||
.then(() => {
|
||||
res.send({ success: true, message: 'Media item sucessfully requested!' });
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(500).send({ success: false, error: error.message });
|
||||
let mediaFunction = undefined;
|
||||
|
||||
if (type === "movie") {
|
||||
console.log("movie");
|
||||
mediaFunction = tmdbMovieInfo;
|
||||
} else if (type === "show") {
|
||||
console.log("show");
|
||||
mediaFunction = tmdbShowInfo;
|
||||
} else {
|
||||
res
|
||||
.status(422)
|
||||
.send({
|
||||
success: false,
|
||||
message: 'Incorrect type. Allowed types: "movie" or "show"'
|
||||
});
|
||||
}
|
||||
|
||||
if (mediaFunction === undefined) {
|
||||
res.status(200);
|
||||
return;
|
||||
}
|
||||
|
||||
mediaFunction(id)
|
||||
.then(tmdbMedia =>
|
||||
request.requestFromTmdb(tmdbMedia, ip, user_agent, username)
|
||||
)
|
||||
.then(() =>
|
||||
res.send({ success: true, message: "Media item successfully requested" })
|
||||
)
|
||||
.catch(err =>
|
||||
res.status(500).send({ success: false, message: err.message })
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = submitRequestController;
|
||||
|
||||
@@ -18,7 +18,7 @@ function updateRequested(req, res) {
|
||||
res.send({ success: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(401).send({ success: false, error: error.message });
|
||||
res.status(401).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const Plex = require('src/plex/plex');
|
||||
const plex = new Plex(configuration.get('plex', 'ip'));
|
||||
|
||||
/**
|
||||
* Controller: Search plex for movies, shows and episodes by query
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
|
||||
function watchDirectLink (req, res) {
|
||||
const { title, year } = req.query;
|
||||
|
||||
plex.getDirectLinkByTitleAndYear(title, year)
|
||||
.then(plexDirectLink => {
|
||||
if (plexDirectLink == false)
|
||||
res.status(404).send({ success: true, link: null })
|
||||
else
|
||||
res.status(200).send({ success: true, link: plexDirectLink })
|
||||
})
|
||||
.catch(error => {
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = watchDirectLink;
|
||||
@@ -0,0 +1,27 @@
|
||||
const RequestRepository = require('src/request/request');
|
||||
const request = new RequestRepository();
|
||||
|
||||
/**
|
||||
* Controller: Fetch all requested items
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
function fetchAllRequests(req, res) {
|
||||
let { page, filter, sort, query } = req.query;
|
||||
let sort_by = sort;
|
||||
let sort_direction = undefined;
|
||||
|
||||
if (sort !== undefined && sort.includes(':')) {
|
||||
[sort_by, sort_direction] = sort.split(':')
|
||||
}
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => request.fetchAll(page, sort_by, sort_direction, filter, query))
|
||||
.then(result => res.send(result))
|
||||
.catch(error => {
|
||||
res.status(404).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = fetchAllRequests;
|
||||
21
seasoned_api/src/webserver/controllers/request/getRequest.js
Normal file
21
seasoned_api/src/webserver/controllers/request/getRequest.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const RequestRepository = require('src/request/request');
|
||||
const request = new RequestRepository();
|
||||
|
||||
/**
|
||||
* Controller: Get requested item by tmdb id and type
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
function fetchAllRequests(req, res) {
|
||||
const id = req.params.id;
|
||||
const { type } = req.query;
|
||||
|
||||
request.getRequestByIdAndType(id, type)
|
||||
.then(result => res.send(result))
|
||||
.catch(error => {
|
||||
res.status(404).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = fetchAllRequests;
|
||||
@@ -0,0 +1,67 @@
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
const TMDB = require("src/tmdb/tmdb");
|
||||
const RequestRepository = require("src/request/request");
|
||||
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
|
||||
const request = new RequestRepository();
|
||||
// const { sendSMS } = require("src/notifications/sms");
|
||||
|
||||
const tmdbMovieInfo = id => {
|
||||
return tmdb.movieInfo(id);
|
||||
};
|
||||
|
||||
const tmdbShowInfo = id => {
|
||||
return tmdb.showInfo(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Controller: Request by id with type param
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
function requestTmdbIdController(req, res) {
|
||||
const { id, type } = req.body;
|
||||
|
||||
const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress;
|
||||
const user_agent = req.headers["user-agent"];
|
||||
const username = req.loggedInUser ? req.loggedInUser.username : null;
|
||||
|
||||
let mediaFunction = undefined;
|
||||
|
||||
if (id === undefined || type === undefined) {
|
||||
res.status(422).send({
|
||||
success: false,
|
||||
message: "'Missing parameteres: 'id' and/or 'type'"
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "movie") {
|
||||
mediaFunction = tmdbMovieInfo;
|
||||
} else if (type === "show") {
|
||||
mediaFunction = tmdbShowInfo;
|
||||
} else {
|
||||
res.status(422).send({
|
||||
success: false,
|
||||
message: 'Incorrect type. Allowed types: "movie" or "show"'
|
||||
});
|
||||
}
|
||||
|
||||
mediaFunction(id)
|
||||
// .catch((error) => { console.error(error); res.status(404).send({ success: false, error: 'Id not found' }) })
|
||||
.then(tmdbMedia => {
|
||||
request.requestFromTmdb(tmdbMedia, ip, user_agent, username);
|
||||
|
||||
// TODO enable SMS
|
||||
// const url = `https://request.movie?${tmdbMedia.type}=${tmdbMedia.id}`;
|
||||
// const message = `${tmdbMedia.title} (${tmdbMedia.year}) requested!\n${url}`;
|
||||
// sendSMS(message);
|
||||
})
|
||||
.then(() =>
|
||||
res.send({ success: true, message: "Request has been submitted." })
|
||||
)
|
||||
.catch(error => {
|
||||
res.send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = requestTmdbIdController;
|
||||
40
seasoned_api/src/webserver/controllers/search/movieSearch.js
Normal file
40
seasoned_api/src/webserver/controllers/search/movieSearch.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
const TMDB = require("src/tmdb/tmdb");
|
||||
const SearchHistory = require("src/searchHistory/searchHistory");
|
||||
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
|
||||
const searchHistory = new SearchHistory();
|
||||
|
||||
/**
|
||||
* Controller: Search for movies by query and pagey
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
function movieSearchController(req, res) {
|
||||
const { query, page, adult } = req.query;
|
||||
const username = req.loggedInUser ? req.loggedInUser.username : null;
|
||||
const includeAdult = adult == "true" ? true : false;
|
||||
|
||||
if (username) {
|
||||
searchHistory.create(username, query);
|
||||
}
|
||||
|
||||
return tmdb
|
||||
.movieSearch(query, page, includeAdult)
|
||||
.then(movieSearchResults => res.send(movieSearchResults))
|
||||
.catch(error => {
|
||||
const { status, message } = error;
|
||||
|
||||
if (status && message) {
|
||||
res.status(status).send({ success: false, message });
|
||||
} else {
|
||||
// TODO log unhandled errors
|
||||
console.log("caugth movie search controller error", error);
|
||||
res.status(500).send({
|
||||
message: `An unexpected error occured while searching movies with query: ${query}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = movieSearchController;
|
||||
48
seasoned_api/src/webserver/controllers/search/multiSearch.js
Normal file
48
seasoned_api/src/webserver/controllers/search/multiSearch.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
const TMDB = require("src/tmdb/tmdb");
|
||||
const SearchHistory = require("src/searchHistory/searchHistory");
|
||||
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
|
||||
const searchHistory = new SearchHistory();
|
||||
|
||||
function checkAndCreateJsonResponse(result) {
|
||||
if (typeof result["createJsonResponse"] === "function") {
|
||||
return result.createJsonResponse();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller: Search for multi (movies, shows and people by query and pagey
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
function multiSearchController(req, res) {
|
||||
const { query, page, adult } = req.query;
|
||||
const username = req.loggedInUser ? req.loggedInUser.username : null;
|
||||
|
||||
if (username) {
|
||||
searchHistory.create(username, query);
|
||||
}
|
||||
|
||||
return tmdb
|
||||
.multiSearch(query, page, adult)
|
||||
.then(multiSearchResults => res.send(multiSearchResults))
|
||||
.catch(error => {
|
||||
const { status, message } = error;
|
||||
|
||||
if (status && message) {
|
||||
res.status(status).send({ success: false, message });
|
||||
} else {
|
||||
// TODO log unhandled errors
|
||||
console.log("caugth multi search controller error", error);
|
||||
res
|
||||
.status(500)
|
||||
.send({
|
||||
message: `An unexpected error occured while searching with query: ${query}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = multiSearchController;
|
||||
@@ -0,0 +1,40 @@
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
const TMDB = require("src/tmdb/tmdb");
|
||||
const SearchHistory = require("src/searchHistory/searchHistory");
|
||||
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
|
||||
const searchHistory = new SearchHistory();
|
||||
|
||||
/**
|
||||
* Controller: Search for person by query and pagey
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
function personSearchController(req, res) {
|
||||
const { query, page, adult } = req.query;
|
||||
const username = req.loggedInUser ? req.loggedInUser.username : null;
|
||||
const includeAdult = adult == "true" ? true : false;
|
||||
|
||||
if (username) {
|
||||
searchHistory.create(username, query);
|
||||
}
|
||||
|
||||
return tmdb
|
||||
.personSearch(query, page, includeAdult)
|
||||
.then(persons => res.send(persons))
|
||||
.catch(error => {
|
||||
const { status, message } = error;
|
||||
|
||||
if (status && message) {
|
||||
res.status(status).send({ success: false, message });
|
||||
} else {
|
||||
// TODO log unhandled errors
|
||||
console.log("caugth person search controller error", error);
|
||||
res.status(500).send({
|
||||
message: `An unexpected error occured while searching people with query: ${query}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = personSearchController;
|
||||
32
seasoned_api/src/webserver/controllers/search/showSearch.js
Normal file
32
seasoned_api/src/webserver/controllers/search/showSearch.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const SearchHistory = require("src/searchHistory/searchHistory");
|
||||
const configuration = require("src/config/configuration").getInstance();
|
||||
const TMDB = require("src/tmdb/tmdb");
|
||||
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
|
||||
const searchHistory = new SearchHistory();
|
||||
|
||||
/**
|
||||
* Controller: Search for shows by query and pagey
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
function showSearchController(req, res) {
|
||||
const { query, page, adult } = req.query;
|
||||
const username = req.loggedInUser ? req.loggedInUser.username : null;
|
||||
const includeAdult = adult == "true" ? true : false;
|
||||
|
||||
if (username) {
|
||||
searchHistory.create(username, query);
|
||||
}
|
||||
|
||||
return tmdb
|
||||
.showSearch(query, page, includeAdult)
|
||||
.then(shows => {
|
||||
res.send(shows);
|
||||
})
|
||||
.catch(error => {
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = showSearchController;
|
||||
@@ -10,7 +10,7 @@ function readStraysController(req, res) {
|
||||
res.send(strays);
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(500).send({ success: false, error: error.message });
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ function strayByIdController(req, res) {
|
||||
res.send(stray);
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(500).send({ success: false, error: error.message });
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ function verifyStrayController(req, res) {
|
||||
res.send({ success: true, message: 'Episode verified' });
|
||||
})
|
||||
.catch((error) => {
|
||||
res.status(500).send({ success: false, error: error.message });
|
||||
res.status(500).send({ success: false, message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
23
seasoned_api/src/webserver/controllers/show/credits.js
Normal file
23
seasoned_api/src/webserver/controllers/show/credits.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const TMDB = require('src/tmdb/tmdb');
|
||||
const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
|
||||
|
||||
const showCreditsController = (req, res) => {
|
||||
const showId = req.params.id;
|
||||
|
||||
tmdb.showCredits(showId)
|
||||
.then(credits => res.send(credits.createJsonResponse()))
|
||||
.catch(error => {
|
||||
const { status, message } = error;
|
||||
|
||||
if (status && message) {
|
||||
res.status(status).send({ success: false, message })
|
||||
} else {
|
||||
// TODO log unhandled errors
|
||||
console.log('caugth show credits controller error', error)
|
||||
res.status(500).send({ message: 'An unexpected error occured while requesting show credits' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = showCreditsController;
|
||||
54
seasoned_api/src/webserver/controllers/show/info.js
Normal file
54
seasoned_api/src/webserver/controllers/show/info.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const TMDB = require('src/tmdb/tmdb');
|
||||
const Plex = require('src/plex/plex');
|
||||
const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
|
||||
const plex = new Plex(configuration.get('plex', 'ip'));
|
||||
|
||||
function handleError(error, res) {
|
||||
const { status, message } = error;
|
||||
|
||||
if (status && message) {
|
||||
res.status(status).send({ success: false, message })
|
||||
} else {
|
||||
console.log('caught showinfo controller error', error)
|
||||
res.status(500).send({
|
||||
message: 'An unexpected error occured while requesting show info.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller: Retrieve information for a show
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
|
||||
async function showInfoController(req, res) {
|
||||
const showId = req.params.id;
|
||||
let { credits, check_existance } = req.query;
|
||||
|
||||
credits && credits.toLowerCase() === 'true' ? credits = true : credits = false
|
||||
check_existance && check_existance.toLowerCase() === 'true' ? check_existance = true : check_existance = false
|
||||
|
||||
let tmdbQueue = [tmdb.showInfo(showId)]
|
||||
if (credits)
|
||||
tmdbQueue.push(tmdb.showCredits(showId))
|
||||
|
||||
try {
|
||||
const [Show, Credits] = await Promise.all(tmdbQueue)
|
||||
|
||||
const show = Show.createJsonResponse()
|
||||
if (credits)
|
||||
show.credits = Credits.createJsonResponse()
|
||||
|
||||
if (check_existance)
|
||||
show.exists_in_plex = await plex.existsInPlex(show)
|
||||
|
||||
res.send(show)
|
||||
} catch(error) {
|
||||
handleError(error, res)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = showInfoController;
|
||||
@@ -1,25 +0,0 @@
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const Cache = require('src/tmdb/cache');
|
||||
const TMDB = require('src/tmdb/tmdb');
|
||||
|
||||
const cache = new Cache();
|
||||
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
|
||||
|
||||
/**
|
||||
* Controller: Retrieve nowplaying movies / now airing shows
|
||||
* @param {Request} req http request variable
|
||||
* @param {Response} res
|
||||
* @returns {Callback}
|
||||
*/
|
||||
function listSearchController(req, res) {
|
||||
const listname = req.params.listname;
|
||||
const { type, id, page } = req.query;
|
||||
tmdb.listSearch(listname, type, id, page)
|
||||
.then((results) => {
|
||||
res.send(results);
|
||||
}).catch((error) => {
|
||||
res.status(404).send({ success: false, error: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = listSearchController;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user