1 Commits

Author SHA1 Message Date
138fd4a121 Setup spotlight project 2022-08-18 20:08:26 +02:00
325 changed files with 21964 additions and 12916 deletions

View File

@@ -1 +0,0 @@
**/node_modules

View File

@@ -1,111 +0,0 @@
---
kind: pipeline
type: docker
name: seasoned api build
platform:
os: linux
arch: amd64
volumes:
- name: cache
host:
path: /tmp/cache
steps:
- name: Load cached packages
image: sinlead/drone-cache:1.0.0
settings:
action: load
key: yarn.lock
mount: node_modules
prefix: yarn-modules-seasoned_api
volumes:
- name: cache
path: /cache
- name: Install dependencies
image: node:18.2.0
commands:
- node -v
- yarn --version
- yarn
- name: Cache packages
image: sinlead/drone-cache:1.0.0
settings:
action: save
key: yarn.lock
mount: node_modules
prefix: yarn-modules-seasoned_api
volumes:
- name: cache
path: /cache
# - name: Compile typescript
# image: node:18.2.0
# commands:
# - yarn build:ts
- name: Run test suite
image: node:18.2.0
commands:
- yarn test
failure: ignore
- name: Lint project using eslint
image: node:18.2.0
commands:
- yarn lint
failure: ignore
- name: Build and publish docker image
image: plugins/docker
settings:
registry: ghcr.io
repo: ghcr.io/kevinmidboe/seasoned_shows
dockerfile: Dockerfile
username:
from_secret: GITHUB_USERNAME
password:
from_secret: GITHUB_PASSWORD
tags: latest
environment:
TMDB_APIKEY:
from_secret: TMDB_APIKEY
PLEX_IP:
from_secret: PLEX_IP
PLEX_TOKEN:
from_secret: PLEX_TOKEN
when:
event:
- push
branch:
- master
# - name: deploy
# image: appleboy/drone-ssh
# pull: true
# secrets:
# - ssh_key
# when:
# event:
# - push
# branch:
# - master
# - drone-test
# status: success
# settings:
# host: 10.0.0.54
# username: root
# key:
# from_secret: ssh_key
# command_timeout: 600s
# script:
# - /home/kevin/deploy/seasoned.sh
trigger:
event:
include:
- push
# - pull_request

View File

@@ -1,25 +0,0 @@
{
"root": true,
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"extends": ["eslint-config-airbnb-base", "plugin:prettier/recommended"],
"plugins": ["mocha"],
"rules": {
"import/extensions": 0,
"max-classes-per-file": 1,
"no-empty": [
2,
{
"allowEmptyCatch": true
}
],
"no-promise-executor-return": 1,
"no-shadow": "off",
"no-underscore-dangle": "off"
},
"env": {
"mocha": true
}
}

View File

@@ -1,18 +0,0 @@
{
"root": true,
"parserOptions": {
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"extends": [
"eslint-config-airbnb-base",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"rules": {
"no-underscore-dangle": "off",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"]
}
}

8
.gitignore vendored
View File

@@ -1,11 +1,7 @@
.DS_Store
configurations/development.json
configurations/production.json
.env
development.json
env
shows.db
node_modules
*/package-lock.json
.nyc_output
yarn-error.log

10
.gitmodules vendored Normal file
View File

@@ -0,0 +1,10 @@
# Docs : https://git-scm.com/book/en/v2/Git-Tools-Submodules
[submodule "torrent_search"]
path = torrent_search
url = https://github.com/KevinMidboe/torrent_search.git
branch = master
[submodule "delugeClient"]
path = delugeClient
url = https://github.com/KevinMidboe/delugeClient.git

View File

@@ -5,5 +5,6 @@
"singleQuote": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"vueIndentScriptAndStyle": false,
"trailingComma": "none"
}

1263
.spectral.json Normal file

File diff suppressed because it is too large Load Diff

View 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;

File diff suppressed because it is too large Load Diff

View 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;
},
);

View 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;

View 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;

View 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;

View 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;
},
);

View 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;

View 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;

View 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;
}

View 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;

View 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;
},
);

View 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;

View 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

File diff suppressed because one or more lines are too long

View File

@@ -1,20 +0,0 @@
FROM node:18
LABEL org.opencontainers.image.source https://github.com/kevinmidboe/seasonedShows
RUN mkdir -p /opt/seasonedShows/src
WORKDIR /opt/seasonedShows
COPY src/ src
COPY configurations/ configurations
COPY package.json .
COPY yarn.lock .
RUN apt update
RUN apt install node-pre-gyp -y
RUN yarn
RUN cp configurations/development.json.example configurations/production.json
EXPOSE 31459
CMD ["yarn", "start"]

133
README.md
View File

@@ -1,3 +1,4 @@
<h1 align="center">
🌶 seasonedShows
</h1>
@@ -5,19 +6,16 @@
<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://drone.schleppe.cloud/KevinMidboe/seasonedShows">
<img src="https://drone.schleppe.cloud/api/badges/KevinMidboe/seasonedShows/status.svg"
alt="Drone CI">
<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=package.json">
<img src="https://snyk.io/test/github/KevinMidboe/seasonedShows/badge.svg?targetFile=package.json" alt="">
<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>
@@ -35,73 +33,63 @@
</p>
## <a name="demo-documentation"></a> Demo & Documentation
📺 [DEMO](https://request.movie)
📺 [DEMO](https://kevinmidboe.com/request)
📝 Documentation of the api.
💖 Checkout my [fancy vue.js page](https://github.com/KevinMidboe/seasoned) for interfacing 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.
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.
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
- 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
- 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 git@github.com:KevinMidboe/seasonedShows.git`
- Type: `git clone --recurse-submodules git@github.com:KevinMidboe/seasonedShows.git`
- Type: `cd seasonedShows/`
- Install required packages
- yarn: `yarn install`
- npm: `npm install`
* yarn: `yarn install`
* npm: `npm install`
- Start server:
- yarn: `yarn start`
- npm: `npm run start`
* 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 git@github.com:KevinMidboe/seasonedShows.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`
* yarn: `yarn install`
* npm: `npm install`
- Start server:
- yarn: `yarn start`
- npm: `npm run start`
* 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].
@@ -110,73 +98,42 @@ After you have downloaded a package manager and node.js javascript engine, the f
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
- Node 18 < [wiki page]
- Plex library
- Optional:
- redis
- deluge
- jackett
## <a name="setup"></a> Setup and configuration
Make a copy of configuration file in `configurations/` folder.
For use during development and with `yarn dev` command:
```bash
cp configurations/development.json.example configurations/development.json
```
For use during production and with `yarn start` command:
```bash
cp configurations/development.json.example configurations/production.json
```
Most important values to change here is adding [TMDB api key](https://developers.themoviedb.org/3/getting-started/introduction) and plex server IP.
### Optional setup
To allow authenticated or admin users add items [delugeClient](https://github.com/kevinmidboe/delugeClient) & [torrentSearch](https://github.com/KevinMidboe/torrent_search) can be setup to fetch and add magnet files. Both require python version >= 3.8 and can be downloaded using following pip command:
```bash
pip3 install delugeClient_kevin torrent_search
```
Both of these need to be configured, view their separate README's or find configuration files under `$HOME/.config/`.
## <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
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._
*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
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.
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.
So this is a multipart system that lets your plex users request movies, and then from the admin page the owner can.
## Installation
There are two main ways of
There are two main ways of
## Architecture
The flow of the system will first check for new folders in your tv shows directory, if a new file is found it's contents are analyzed, stored and tweets suggested changes to it's contents to use_admin.
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.

View File

@@ -1,31 +0,0 @@
{
"database": {
"host": ":memory:"
},
"redis": {
"host": "localhost",
"port": 6379
},
"webserver": {
"port": 31400,
"origins": []
},
"tmdb": {
"apiKey": "bogus-api-key"
},
"plex": {
"ip": "localhost",
"token": ""
},
"tautulli": {
"apiKey": "",
"ip": "",
"port": ""
},
"raven": {
"DSN": ""
},
"authentication": {
"secret": "secret"
}
}

View File

@@ -1,55 +0,0 @@
{
"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",
"type": "module",
"scripts": {
"start": "SEASONED_CONFIG=configurations/production.json NODE_ENV=production node src/webserver/server.js",
"dev": "SEASONED_CONFIG=configurations/development.json NODE_ENV=development node src/webserver/server.js",
"test": "SEASONED_CONFIG=configurations/test.json mocha --recursive tests/unit tests/system",
"coverage:upload": "SEASONED_CONFIG=configurations/test.json mocha --recursive tests/unit && nyc report --reporter=text-lcov | coveralls",
"coverage": "SEASONED_CONFIG=configurations/test.json mocha --recursive tests/unit && nyc report",
"lint": "eslint src tests",
"update": "SEASONED_CONFIG=configurations/development.json node scripts/updateRequestsInPlex.js",
"docs": "yarn apiDocs; yarn classDocs",
"apiDocs": "",
"classDocs": "scripts/generate-class-docs.sh"
},
"dependencies": {
"bcrypt": "^5.0.1",
"body-parser": "~1.18.2",
"cookie-parser": "^1.4.6",
"express": "~4.16.0",
"form-data": "^2.5.1",
"jsonwebtoken": "^8.5.1",
"km-moviedb": "^0.2.12",
"raven": "^2.4.2",
"redis": "^3.0.2",
"sqlite3": "^5.0.1"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/register": "^7.5.5",
"@types/node": "^12.6.8",
"chai": "^4.3.6",
"chai-http": "^4.3.0",
"coveralls": "^3.0.5",
"documentation": "^12.0.3",
"eslint": "^8.22.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-mocha": "10.1.0",
"eslint-plugin-prettier": "^4.2.1",
"istanbul": "^0.4.5",
"mocha": "8.4.0",
"mocha-lcov-reporter": "^1.3.0",
"nyc": "15.1.0",
"prettier": "^2.7.1"
}
}

439
reference/seasoned-api.yaml Normal file
View 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 Butchers vengeance and stop him before its 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 historys 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 killers previous victims. And they are dead set on making sure that what happened to them doesnt 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: []

View File

@@ -1,41 +0,0 @@
import Plex from "../src/plex/plex.js";
import establishedDatabase from "../src/database/database.js";
import Configuration from "../src/config/configuration.js";
const configuration = Configuration.getInstance();
const plex = new Plex(
configuration.get("plex", "ip"),
configuration.get("plex", "token")
);
const queries = {
getRequestsNotYetInPlex: `SELECT * FROM requests WHERE status = 'requested' OR status = 'downloading'`,
saveNewStatus: `UPDATE requests SET status = ? WHERE id IS ? and type IS ?`
};
const getRequestsNotYetInPlex = () =>
establishedDatabase.all(queries.getRequestsNotYetInPlex);
async function getNewRequestMatchesInPlex() {
const requests = await getRequestsNotYetInPlex();
const exists = await Promise.all(
requests.map(request => plex.existsInPlex(request))
);
return requests.filter(() => exists.shift());
}
function commitNewStatus(status, id, type, title) {
console.log(`${type} ${title} updated to: ${status}`);
return establishedDatabase.run(queries.saveNewStatus, [status, id, type]);
}
function updateMatchInDb(match, status) {
return commitNewStatus(status, match.id, match.type, match.title);
}
getNewRequestMatchesInPlex()
.then(newMatches =>
Promise.all(newMatches.map(match => updateMatchInDb(match, "downloaded")))
)
.finally(() => process.exit(0));

View File

@@ -0,0 +1,14 @@
{
"extends": [
"airbnb-base"
],
"rules": {
"indent": ["error", 3],
"prefer-destructuring": 0,
"camelcase": 0,
"import/no-unresolved": 0,
"import/no-extraneous-dependencies": 0,
"object-shorthand": 0,
"comma-dangle": 0
}
}

66
seasoned_api/.gitignore vendored Normal file
View File

@@ -0,0 +1,66 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# - - - - -
# My own gitignore files and folders
shows.db
conf/development.json
# conf/development-prod.json

View File

@@ -1,10 +1,6 @@
{
"database": {
"host": "./shows.db"
},
"redis": {
"host": "localhost",
"port": 6379
"host": "../shows.db"
},
"webserver": {
"port": 31459,
@@ -14,8 +10,7 @@
"apiKey": ""
},
"plex": {
"ip": "localhost",
"token": ""
"ip": ""
},
"tautulli": {
"apiKey": "",

View 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"
}
}

57
seasoned_api/package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"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"
}
}

View 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
View 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;

Some files were not shown because too many files have changed in this diff Show More