1 Commits

Author SHA1 Message Date
afe0a698f0 Added morgan 1.8.2 to packages 2017-06-02 12:15:14 +02:00
265 changed files with 4386 additions and 27823 deletions

15
.gitignore vendored
View File

@@ -1,7 +1,10 @@
.DS_Store
development.json
env
__pycache__
nohup.out
shows.db
*/package-lock.json
.DS_Store
env_variables.py
conf/classedOutput.log
node_modules
*.pyc
npm-debug.log
webpage/js/env_variables.js

10
.gitmodules vendored
View File

@@ -1,10 +0,0 @@
# 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

@@ -1,10 +0,0 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"vueIndentScriptAndStyle": false,
"trailingComma": "none"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
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

@@ -1,228 +0,0 @@
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

@@ -1,28 +0,0 @@
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

@@ -1,76 +0,0 @@
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

@@ -1,81 +0,0 @@
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

@@ -1,141 +0,0 @@
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

@@ -1,32 +0,0 @@
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

@@ -1,162 +0,0 @@
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

@@ -1,88 +0,0 @@
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

@@ -1,81 +0,0 @@
// 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

@@ -1,50 +0,0 @@
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

@@ -1,51 +0,0 @@
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

@@ -1,92 +0,0 @@
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;

File diff suppressed because one or more lines are too long

View File

@@ -1,19 +0,0 @@
language: node_js
node_js: '11.9.0'
git:
submodules: true
script:
- yarn test
- yarn coverage
before_install:
- cd seasoned_api
- 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

View File

@@ -1,18 +0,0 @@
## What kind of an issue is this?
- [ ] Bug report
- [ ] Feature request
## Expected behaviour?
## Current behaviour?
*if this is a bug report*
## Steps to reproduce behaviour?
*if this is a bug report*
## Screenshot? 📷
*A image tells a thousands words*

135
README.md
View File

@@ -1,137 +1,6 @@
# *Seasoned*: an intelligent organizer for your shows
<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
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
*Seasoned* 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.
## 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.

View File

@@ -1 +0,0 @@
theme: jekyll-theme-cayman

108
app/.gitignore vendored
View File

@@ -1,108 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
.static_storage/
.media/
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# - - - - -
# My own gitignore files and folders
env_variables.py

View File

@@ -1,279 +0,0 @@
#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-08-25 23:22:27
# @Last Modified by: KevinMidboe
# @Last Modified time: 2017-10-12 22:44:27
from guessit import guessit
import os, errno
import logging
import tvdb_api
from pprint import pprint
import env_variables as env
from video import VIDEO_EXTENSIONS, Episode, Movie, Video
from subtitle import SUBTITLE_EXTENSIONS, Subtitle, get_subtitle_path
from utils import sanitize
logging.basicConfig(filename=os.path.dirname(__file__) + '/' + env.logfile, level=logging.INFO)
from datetime import datetime
#: Supported archive extensions
ARCHIVE_EXTENSIONS = ('.rar',)
def scan_video(path):
"""Scan a video from a `path`.
:param str path: existing path to the video.
:return: the scanned video.
:rtype: :class:`~subliminal.video.Video`
"""
# check for non-existing path
if not os.path.exists(path):
raise ValueError('Path does not exist')
# check video extension
# if not path.endswith(VIDEO_EXTENSIONS):
# raise ValueError('%r is not a valid video extension' % os.path.splitext(path)[1])
dirpath, filename = os.path.split(path)
logging.info('Scanning video %r in %r', filename, dirpath)
# guess
parent_path = path.strip(filename)
# video = Video.fromguess(filename, parent_path, guessit(path))
video = Video(filename)
# guessit(path)
return video
def scan_subtitle(path):
if not os.path.exists(path):
raise ValueError('Path does not exist')
dirpath, filename = os.path.split(path)
logging.info('Scanning subtitle %r in %r', filename, dirpath)
# guess
parent_path = path.strip(filename)
subtitle = Subtitle.fromguess(filename, parent_path, guessit(path))
return subtitle
def scan_files(path, age=None, archives=True):
"""Scan `path` for videos and their subtitles.
See :func:`refine` to find additional information for the video.
:param str path: existing directory path to scan.
:param datetime.timedelta age: maximum age of the video or archive.
:param bool archives: scan videos in archives.
:return: the scanned videos.
:rtype: list of :class:`~subliminal.video.Video`
"""
# check for non-existing path
if not os.path.exists(path):
raise ValueError('Path does not exist')
# check for non-directory path
if not os.path.isdir(path):
raise ValueError('Path is not a directory')
name_dict = {}
# walk the path
mediafiles = []
for dirpath, dirnames, filenames in os.walk(path):
logging.debug('Walking directory %r', dirpath)
# remove badly encoded and hidden dirnames
for dirname in list(dirnames):
if dirname.startswith('.'):
logging.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
dirnames.remove(dirname)
# scan for videos
for filename in filenames:
# filter on videos and archives
if not (filename.endswith(VIDEO_EXTENSIONS) or filename.endswith(SUBTITLE_EXTENSIONS) or archives and filename.endswith(ARCHIVE_EXTENSIONS)):
continue
# skip hidden files
if filename.startswith('.'):
logging.debug('Skipping hidden filename %r in %r', filename, dirpath)
continue
# reconstruct the file path
filepath = os.path.join(dirpath, filename)
# skip links
if os.path.islink(filepath):
logging.debug('Skipping link %r in %r', filename, dirpath)
continue
# skip old files
if age and datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(filepath)) > age:
logging.debug('Skipping old file %r in %r', filename, dirpath)
continue
# scan
if filename.endswith(VIDEO_EXTENSIONS): # video
try:
video = scan_video(filepath)
try:
name_dict[video.series] += 1
except KeyError:
name_dict[video.series] = 0
mediafiles.append(video)
except ValueError: # pragma: no cover
logging.exception('Error scanning video')
continue
elif archives and filename.endswith(ARCHIVE_EXTENSIONS): # archive
print('archive')
pass
# try:
# video = scan_archive(filepath)
# mediafiles.append(video)
# except (NotRarFile, RarCannotExec, ValueError): # pragma: no cover
# logging.exception('Error scanning archive')
# continue
elif filename.endswith(SUBTITLE_EXTENSIONS): # subtitle
try:
subtitle = scan_subtitle(filepath)
mediafiles.append(subtitle)
except ValueError:
logging.exception('Error scanning subtitle')
continue
else: # pragma: no cover
raise ValueError('Unsupported file %r' % filename)
pprint(name_dict)
return mediafiles
def organize_files(path):
hashList = {}
mediafiles = scan_files(path)
# print(mediafiles)
for file in mediafiles:
hashList.setdefault(file.__hash__(),[]).append(file)
# hashList[file.__hash__()] = file
return hashList
def save_subtitles(files, single=False, directory=None, encoding=None):
t = tvdb_api.Tvdb()
if not isinstance(files, list):
files = [files]
for file in files:
# TODO this should not be done in the loop
dirname = "%s S%sE%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode))
createParentfolder = not dirname in file.parent_path
if createParentfolder:
dirname = os.path.join(file.parent_path, dirname)
print('Created: %s' % dirname)
try:
os.makedirs(dirname)
except OSError as e:
if e.errno != errno.EEXIST:
raise
# TODO Clean this !
try:
tvdb_episode = t[file.series][file.season][file.episode]
episode_title = tvdb_episode['episodename']
except:
episode_title = ''
old = os.path.join(file.parent_path, file.name)
if file.name.endswith(SUBTITLE_EXTENSIONS):
lang = file.getLanguage()
sdh = '.sdh' if file.sdh else ''
filename = "%s S%sE%s %s%s.%s.%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode), episode_title, sdh, lang, file.container)
else:
filename = "%s S%sE%s %s.%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode), episode_title, file.container)
if createParentfolder:
newname = os.path.join(dirname, filename)
else:
newname = os.path.join(file.parent_path, filename)
print('Moved: %s ---> %s' % (old, newname))
os.rename(old, newname)
print()
# for hash in files:
# hashIndex = [files[hash]]
# for hashItems in hashIndex:
# for file in hashItems:
# print(file.series)
# saved_subtitles = []
# for subtitle in files:
# # check content
# if subtitle.name is None:
# logging.error('Skipping subtitle %r: no content', subtitle)
# continue
# # check language
# if subtitle.language in set(s.language for s in saved_subtitles):
# logging.debug('Skipping subtitle %r: language already saved', subtitle)
# continue
# # create subtitle path
# subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
# if directory is not None:
# subtitle_path = os.path.join(directory, os.path.split(subtitle_path)[1])
# # save content as is or in the specified encoding
# logging.info('Saving %r to %r', subtitle, subtitle_path)
# if encoding is None:
# with io.open(subtitle_path, 'wb') as f:
# f.write(subtitle.content)
# else:
# with io.open(subtitle_path, 'w', encoding=encoding) as f:
# f.write(subtitle.text)
# saved_subtitles.append(subtitle)
# # check single
# if single:
# break
# return saved_subtitles
def stringTime():
return str(datetime.now().strftime("%Y-%m-%d %H:%M:%S:%f"))
def main():
# episodePath = '/Volumes/media/tv/Black Mirror/Black Mirror Season 01/'
episodePath = '/Volumes/mainframe/shows/Black Mirror/Black Mirror Season 01/'
t = tvdb_api.Tvdb()
hashList = organize_files(episodePath)
pprint(hashList)
if __name__ == '__main__':
main()

View File

@@ -1,99 +0,0 @@
#!/usr/bin/env python
'''
Created on Apr 19, 2012
@author: dan, Faless
GNU GENERAL PUBLIC LICENSE - Version 3
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
http://www.gnu.org/licenses/gpl-3.0.txt
'''
import shutil
import tempfile
import os.path as pt
import sys, logging
import libtorrent as lt
from time import sleep
import env_variables as env
logging.basicConfig(filename=pt.dirname(__file__) + '/' + env.logfile)
def magnet2torrent(magnet, output_name=None):
if output_name and \
not pt.isdir(output_name) and \
not pt.isdir(pt.dirname(pt.abspath(output_name))):
logging.info("Invalid output folder: " + pt.dirname(pt.abspath(output_name)))
logging.info("")
sys.exit(0)
tempdir = tempfile.mkdtemp()
ses = lt.session()
params = {
'save_path': tempdir,
'storage_mode': lt.storage_mode_t(2),
'paused': False,
'auto_managed': True,
'duplicate_is_error': True
}
handle = lt.add_magnet_uri(ses, magnet, params)
logging.info("Downloading Metadata (this may take a while)")
while (not handle.has_metadata()):
try:
sleep(1)
except KeyboardInterrupt:
logging.info("Aborting...")
ses.pause()
logging.info("Cleanup dir " + tempdir)
shutil.rmtree(tempdir)
sys.exit(0)
ses.pause()
logging.info("Done")
torinfo = handle.get_torrent_info()
torfile = lt.create_torrent(torinfo)
output = pt.abspath(torinfo.name() + ".torrent")
if output_name:
if pt.isdir(output_name):
output = pt.abspath(pt.join(
output_name, torinfo.name() + ".torrent"))
elif pt.isdir(pt.dirname(pt.abspath(output_name))):
output = pt.abspath(output_name)
logging.info("Saving torrent file here : " + output + " ...")
torcontent = lt.bencode(torfile.generate())
f = open(output, "wb")
f.write(lt.bencode(torfile.generate()))
f.close()
logging.info("Saved! Cleaning up dir: " + tempdir)
ses.remove_torrent(handle)
shutil.rmtree(tempdir)
return output
def main():
magnet = sys.argv[1]
logging.info('INPUT: {}'.format(magnet))
magnet2torrent(magnet, env.torrent_dumpsite)
if __name__ == "__main__":
main()

View File

@@ -1,112 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-04-12 23:27:51
# @Last Modified by: KevinMidboe
# @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
self.getVarsFromDB()
def getVarsFromDB(self):
c = sqlite3.connect(env.db_path).cursor()
c.execute('SELECT parent, name, season, episode, video_files, subtitles, trash FROM stray_eps WHERE id = ?', (self.id,))
returnMsg = c.fetchone()
self.parent = returnMsg[0]
self.name = returnMsg[1]
self.season = returnMsg[2]
self.episode = returnMsg[3]
self.video_files = json.loads(returnMsg[4])
self.subtitles = json.loads(returnMsg[5])
self.trash = json.loads(returnMsg[6])
c.close()
self.queries = {
'parent_input': [env.input_dir, self.parent],
'season': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season],
'episode': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season, \
self.name + ' S' + "%02d" % self.season + 'E' + "%02d" % self.episode],
}
def typeDir(self, dType, create=False, mergeItem=None):
url = '/'.join(self.queries[dType])
print(url)
if create and not os.path.isdir(url):
os.makedirs(url)
fix_ownership(url)
if mergeItem:
return '/'.join([url, str(mergeItem)])
return url
def fix_ownership(path):
pass
# TODO find this from username from config
# uid = 1000
# gid = 112
# os.chown(path, uid, gid)
def moveStray(strayId):
ep = episode(strayId)
for item in ep.video_files:
try:
old_dir = ep.typeDir('parent_input', mergeItem=item[0])
new_dir = ep.typeDir('episode', mergeItem=item[1], create=True)
shutil.move(old_dir, new_dir)
except FileNotFoundError:
logging.warning(old_dir + ' does not exits, cannot be moved.')
for item in ep.subtitles:
try:
old_dir = ep.typeDir('parent_input', mergeItem=item[0])
new_dir = ep.typeDir('episode', mergeItem=item[1], create=True)
shutil.move(old_dir, new_dir)
except FileNotFoundError:
logging.warning(old_dir + ' does not exits, cannot be moved.')
for item in ep.trash:
try:
os.remove(ep.typeDir('parent_input', mergeItem=item))
except FileNotFoundError:
logging.warning(ep.typeDir('parent_input', mergeItem=item) + 'does not exist, cannot be removed.')
fix_ownership(ep.typeDir('episode'))
for root, dirs, files in os.walk(ep.typeDir('episode')):
for item in files:
fix_ownership(os.path.join(ep.typeDir('episode'), item))
# TODO because we might jump over same files, the dir might no longer
# be empty and cannot remove dir like this.
try:
os.rmdir(ep.typeDir('parent_input'))
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__':
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.')
moveStray(sys.argv[-1])

View File

@@ -1,318 +0,0 @@
#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-10-12 11:55:03
# @Last Modified by: KevinMidboe
# @Last Modified time: 2017-10-22 18:54:18
import sys, logging, re, json
from urllib import parse, request
from urllib.error import URLError
from bs4 import BeautifulSoup
from os import path
import datetime
from pprint import pprint
from core import stringTime
import env_variables as env
logging.basicConfig(filename=path.dirname(__file__) + '/' + env.logfile, level=logging.INFO)
RELEASE_TYPES = ('bdremux', 'brremux', 'remux',
'bdrip', 'brrip', 'blu-ray', 'bluray', 'bdmv', 'bdr', 'bd5',
'web-cap', 'webcap', 'web cap',
'webrip', 'web rip', 'web-rip', 'web',
'webdl', 'web dl', 'web-dl', 'hdrip',
'dsr', 'dsrip', 'satrip', 'dthrip', 'dvbrip', 'hdtv', 'pdtv', 'tvrip', 'hdtvrip',
'dvdr', 'dvd-full', 'full-rip', 'iso',
'ts', 'hdts', 'hdts', 'telesync', 'pdvd', 'predvdrip',
'camrip', 'cam')
def sanitize(string, ignore_characters=None, replace_characters=None):
"""Sanitize a string to strip special characters.
:param str string: the string to sanitize.
:param set ignore_characters: characters to ignore.
:return: the sanitized string.
:rtype: str
"""
# only deal with strings
if string is None:
return
replace_characters = replace_characters or ''
ignore_characters = ignore_characters or set()
characters = ignore_characters
if characters:
string = re.sub(r'[%s]' % re.escape(''.join(characters)), replace_characters, string)
return string
def return_re_match(string, re_statement):
if string is None:
return
m = re.search(re_statement, string)
if 'Y-day' in m.group():
return datetime.timedelta(days=1).strftime('%m-%d %Y')
if 'Today' in m.group():
return datetime.datetime.now().strftime('%m-%d %Y')
return sanitize(m.group(), '\xa0', ' ')
# Can maybe be moved away from this class
# returns a number that is either the value of multiple_pages
# or if it exceeds total_pages, return total_pages.
def pagesToCount(multiple, total):
if (multiple > total):
return total
return multiple
# Should maybe not be able to set values without checking if they are valid?
class piratebay(object):
def __init__(self, query=None, page=0, sort=None, category=None):
# This should be moved to a config file
self.url = 'https://thepiratebay.org/search'
self.sortTypes = {
'size': 5,
'seed_count': 99
}
self.categoryTypes = {
'movies': 207,
'porn_movies': 505,
}
# - - -
# Req params
self.query = query
self.page = page
self.sort = sort
self.category = category
self.total_pages = 0
self.headers = {'User-Agent': 'Mozilla/5.0'}
# self.headers = {}
def build_URL_request(self):
url = '/'.join([self.url, parse.quote(self.query), str(self.page), str(self.sort), str(self.category)])
return request.Request(url, headers=self.headers)
def next_page(self):
# If page exceeds the max_page, return None
# Can either save the last query/url in the object or have it passed
# again on call to next_page
# Throw a error if it is not possible (overflow)
self.page += 1
raw_page = self.callPirateBaT()
return self.parse_raw_page_for_torrents(raw_page)
def set_total_pages(self, raw_page):
# body-id:searchResults-id:content-align:center
soup = BeautifulSoup(raw_page, 'html.parser')
content_searchResult = soup.body.find(id='SearchResults')
page_div = content_searchResult.find_next(attrs={"align": "center"})
last_page = 0
for page in page_div.find_all('a'):
last_page += 1
self.total_pages = last_page
def callPirateBaT(self):
req = self.build_URL_request()
raw_page = self.fetchURL(req).read()
logging.info('Finished searching piratebay for query | %s' % stringTime())
if raw_page is None:
raise ValueError('Search result returned no content. Please check log for error reason.')
if self.total_pages is 0:
self.set_total_pages(raw_page)
return raw_page
# Sets the search
def search(self, query, multiple_pages=1, page=0, sort=None, category=None):
# This should not be logged here, but in loop. Something else here maybe?
logging.info('Searching piratebay with query: %r, sort: %s and category: %s | %s' %
(query, sort, category, stringTime()))
if sort is not None and sort in self.sortTypes:
self.sort = self.sortTypes[sort]
else:
raise ValueError('Invalid sort category for piratebay search')
# Verify input? and reset total_pages
self.query = query
self.total_pages = 0
if str(page).isnumeric() and type(page) == int and page >= 0:
self.page = page
# TODO add category list
if category is not None and category in self.categoryTypes:
self.category = self.categoryTypes[category]
# TODO Pull most of this logic out bc it needs to also be done in next_page
raw_page = self.callPirateBaT()
torrents_found = self.parse_raw_page_for_torrents(raw_page)
# Fetch in parallel
n = pagesToCount(multiple_pages, self.total_pages)
while n > 1:
torrents_found.extend(self.next_page())
n -= 1
return torrents_found
def removeHeader(self, bs4_element):
if ('header' in bs4_element['class']):
return bs4_element.find_next('tr')
return bs4_element
def has_magnet(self, href):
return href and re.compile('magnet').search(href)
def parse_raw_page_for_torrents(self, content):
soup = BeautifulSoup(content, 'html.parser')
content_searchResult = soup.body.find(id='searchResult')
if content_searchResult is None:
logging.info('No torrents found for the search criteria.')
return None
listElements = content_searchResult.tr
torrentWrapper = self.removeHeader(listElements)
torrents_found = []
for torrentElement in torrentWrapper.find_all_next('td'):
if torrentElement.find_all("div", class_='detName'):
name = torrentElement.find('a', class_='detLink').get_text()
url = torrentElement.find('a', class_='detLink')['href']
magnet = torrentElement.find(href=self.has_magnet)
uploader = torrentElement.find('a', class_='detDesc')
if uploader is None:
uploader = torrentElement.find('i')
uploader = uploader.get_text()
info_text = torrentElement.find('font', class_='detDesc').get_text()
date = return_re_match(info_text, r"(\d+\-\d+(\s\d{4})?)|(Y\-day|Today)")
size = return_re_match(info_text, r"(\d+(\.\d+)?\s[a-zA-Z]+)")
# COULD NOT FIND HREF!
if (magnet is None):
continue
seed_and_leech = torrentElement.find_all_next(attrs={"align": "right"})
seed = seed_and_leech[0].get_text()
leech = seed_and_leech[1].get_text()
torrent = Torrent(name, magnet['href'], size, uploader, date, seed, leech, url)
torrents_found.append(torrent)
else:
# print(torrentElement)
continue
logging.info('Found %s torrents for given search criteria.' % len(torrents_found))
return torrents_found
def fetchURL(self, req):
try:
response = request.urlopen(req)
except URLError as e:
if hasattr(e, 'reason'):
logging.error('We failed to reach a server with request: %s' % req.full_url)
logging.error('Reason: %s' % e.reason)
elif hasattr(e, 'code'):
logging.error('The server couldn\'t fulfill the request.')
logging.error('Error code: ', e.code)
else:
return response
class Torrent(object):
def __init__(self, name, magnet=None, size=None, uploader=None, date=None,
seed_count=None, leech_count=None, url=None):
self.name = name
self.magnet = magnet
self.size = size
self.uploader = uploader
self.date = date
self.seed_count = seed_count
self.leech_count = leech_count
self.url = url
def find_release_type(self):
name = self.name.casefold()
return [r_type for r_type in RELEASE_TYPES if r_type in name]
def get_all_attr(self):
return ({'name': self.name, 'magnet': self.magnet,'uploader': self.uploader,'size': self.size,'date': self.date,'seed': self.seed_count,'leech': self.leech_count,'url': self.url})
def __repr__(self):
return '<%s [%r]>' % (self.__class__.__name__, self.name)
# This should be done front_end!
# I.E. filtering like this should be done in another script
# and should be done with the shared standard for types.
# PS: Is it the right move to use a shared standard? What
# happens if it is no longer public?
def chooseCandidate(torrent_list):
interesting_torrents = []
match_release_type = ['bdremux', 'brremux', 'remux', 'bdrip', 'brrip', 'blu-ray', 'bluray', 'bdmv', 'bdr', 'bd5']
for torrent in torrent_list:
intersecting_release_types = set(torrent.find_release_type()) & set(match_release_type)
size, _, size_id = torrent.size.partition(' ')
if intersecting_release_types and int(torrent.seed_count) > 0 and float(size) > 4 and size_id == 'GiB':
# print('{} : {} : {} {}'.format(torrent.name, torrent.size, torrent.seed_count, torrent.magnet))
interesting_torrents.append(torrent.get_all_attr())
# else:
# print('Denied match! %s : %s : %s' % (torrent.name, torrent.size, torrent.seed_count))
return interesting_torrents
def searchTorrentSite(query, site='piratebay'):
pirate = piratebay()
torrents_found = pirate.search(query, page=0, multiple_pages=3, sort='size')
candidates = {}
if (torrents_found):
candidates = chooseCandidate(torrents_found)
print(json.dumps(candidates))
# torrents_found = pirate.next_page()
# pprint(torrents_found)
# candidates = chooseCandidate(torrents_found)
# Can autocall to next_page in a looped way to get more if nothing is found
# and there is more pages to be looked at
def main():
query = sys.argv[1]
searchTorrentSite(query)
if __name__ == '__main__':
main()

View File

@@ -1,57 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-07-11 19:16:23
# @Last Modified by: KevinMidboe
# @Last Modified time: 2017-07-11 19:16:23
import fire, re, os
class seasonMover(object):
''' Moving multiple files to multiple folders with
identifer '''
workingDir = os.getcwd()
def create(self, name, interval):
pass
def move(self, fileSyntax, folderName):
episodeRange = self.findInterval(fileSyntax)
self.motherMover(fileSyntax, folderName, episodeRange)
def findInterval(self, item):
if (re.search(r'\((.*)\)', item) is None):
raise ValueError('Need to declare an identifier e.g. (1..3) in: \n\t' + item)
start = int(re.search('\((\d+)\.\.', item).group(1))
end = int(re.search('\.\.(\d+)\)', item).group(1))
return list(range(start, end+1))
def removeUploadSign(self, file):
match = re.search('-[a-zA-Z\[\]\-]*.[a-z]{3}', file)
if match:
uploader = match.group(0)[:-4]
return re.sub(uploader, '', file)
return file
def motherMover(self, fileSyntax, folderName, episodeRange):
# Call for sub of fileList
# TODO check if range is same as folderContent
for episode in episodeRange:
leadingZeroNumber = "%02d" % episode
fileName = re.sub(r'\((.*)\)', leadingZeroNumber, fileSyntax)
oldPath = os.path.join(self.workingDir,fileName)
newFolder = os.path.join(self.workingDir, folderName + leadingZeroNumber)
newPath = os.path.join(newFolder, self.removeUploadSign(fileName))
os.makedirs(newFolder)
os.rename(oldPath, newPath)
# print(newFolder)
# print(oldPath + ' --> ' + newPath)
if __name__ == '__main__':
fire.Fire(seasonMover)

View File

@@ -1,111 +0,0 @@
# -*- coding: utf-8 -*-
import codecs
import logging
import os
import chardet
import hashlib
from video import Episode, Movie
from utils import sanitize
from langdetect import detect
logger = logging.getLogger(__name__)
#: Subtitle extensions
SUBTITLE_EXTENSIONS = ('.srt', '.sub')
class Subtitle(object):
"""Base class for subtitle.
:param language: language of the subtitle.
:type language: :class:`~babelfish.language.Language`
:param bool hearing_impaired: whether or not the subtitle is hearing impaired.
:param page_link: URL of the web page from which the subtitle can be downloaded.
:type page_link: str
:param encoding: Text encoding of the subtitle.
:type encoding: str
"""
#: Name of the provider that returns that class of subtitle
provider_name = ''
def __init__(self, name, parent_path, series, season, episode, language=None, hash=None, container=None, format=None, sdh=False):
#: Language of the subtitle
self.name = name
self.parent_path = parent_path
self.series = series
self.season = season
self.episode = episode
self.language=language
self.hash = hash
self.container = container
self.format = format
self.sdh = sdh
@classmethod
def fromguess(cls, name, parent_path, guess):
if not (guess['type'] == 'movie' or guess['type'] == 'episode'):
raise ValueError('The guess must be an episode guess')
if 'title' not in guess:
raise ValueError('Insufficient data to process the guess')
sdh = 'sdh' in name.lower()
if guess['type'] is 'episode':
return cls(name, parent_path, guess.get('title', 1), guess.get('season'), guess['episode'],
container=guess.get('container'), format=guess.get('format'), sdh=sdh)
elif guess['type'] is 'movie':
return cls(name, parent_path, guess.get('title', 1), container=guess.get('container'),
format=guess.get('format'), sdh=sdh)
def getLanguage(self):
f = open(os.path.join(self.parent_path, self.name), 'r', encoding='ISO-8859-15')
language = detect(f.read())
f.close()
return language
def __hash__(self):
return hashlib.md5("b'{}'".format(str(self.series) + str(self.season) + str(self.episode)).encode()).hexdigest()
def __repr__(self):
return '<%s %s [%sx%s]>' % (self.__class__.__name__, self.series, self.season, str(self.episode))
def get_subtitle_path(subtitles_path, language=None, extension='.srt'):
"""Get the subtitle path using the `subtitles_path` and `language`.
:param str subtitles_path: path to the subtitle.
:param language: language of the subtitle to put in the path.
:type language: :class:`~babelfish.language.Language`
:param str extension: extension of the subtitle.
:return: path of the subtitle.
:rtype: str
"""
subtitle_root = os.path.splitext(subtitles_path)[0]
if language:
subtitle_root += '.' + str(language)
return subtitle_root + extension

View File

@@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
from datetime import datetime
import hashlib
import os
import re
import struct
def sanitize(string, ignore_characters=None):
"""Sanitize a string to strip special characters.
:param str string: the string to sanitize.
:param set ignore_characters: characters to ignore.
:return: the sanitized string.
:rtype: str
"""
# only deal with strings
if string is None:
return
ignore_characters = ignore_characters or set()
# replace some characters with one space
# characters = {'-', ':', '(', ')', '.'} - ignore_characters
# if characters:
# string = re.sub(r'[%s]' % re.escape(''.join(characters)), ' ', string)
# remove some characters
characters = {'\''} - ignore_characters
if characters:
string = re.sub(r'[%s]' % re.escape(''.join(characters)), '', string)
# replace multiple spaces with one
string = re.sub(r'\s+', ' ', string)
# strip and lower case
return string.strip().lower()

View File

@@ -1,233 +0,0 @@
#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-08-26 08:23:18
# @Last Modified by: KevinMidboe
# @Last Modified time: 2017-09-29 13:56:21
from guessit import guessit
import os
import hashlib, tvdb_api
#: Video extensions
VIDEO_EXTENSIONS = ('.3g2', '.3gp', '.3gp2', '.3gpp', '.60d', '.ajp', '.asf', '.asx', '.avchd', '.avi', '.bik',
'.bix', '.box', '.cam', '.dat', '.divx', '.dmf', '.dv', '.dvr-ms', '.evo', '.flc', '.fli',
'.flic', '.flv', '.flx', '.gvi', '.gvp', '.h264', '.m1v', '.m2p', '.m2ts', '.m2v', '.m4e',
'.m4v', '.mjp', '.mjpeg', '.mjpg', '.mkv', '.moov', '.mov', '.movhd', '.movie', '.movx', '.mp4',
'.mpe', '.mpeg', '.mpg', '.mpv', '.mpv2', '.mxf', '.nsv', '.nut', '.ogg', '.ogm' '.ogv', '.omf',
'.ps', '.qt', '.ram', '.rm', '.rmvb', '.swf', '.ts', '.vfw', '.vid', '.video', '.viv', '.vivo',
'.vob', '.vro', '.wm', '.wmv', '.wmx', '.wrap', '.wvx', '.wx', '.x264', '.xvid')
class Video(object):
"""Base class for videos.
Represent a video, existing or not.
:param str name: name or path of the video.
:param str format: format of the video (HDTV, WEB-DL, BluRay, ...).
:param str release_group: release group of the video.
:param str resolution: resolution of the video stream (480p, 720p, 1080p or 1080i).
:param str video_codec: codec of the video stream.
:param str audio_codec: codec of the main audio stream.
:param str imdb_id: IMDb id of the video.
:param dict hashes: hashes of the video file by provider names.
:param int size: size of the video file in bytes.
:param set subtitle_languages: existing subtitle languages.
"""
def __init__(self, name, format=None, release_group=None, resolution=None, video_codec=None, audio_codec=None,
imdb_id=None, hashes=None, size=None, subtitle_languages=None):
#: Name or path of the video
self.name = name
#: Format of the video (HDTV, WEB-DL, BluRay, ...)
self.format = format
#: Release group of the video
self.release_group = release_group
#: Resolution of the video stream (480p, 720p, 1080p or 1080i)
self.resolution = resolution
#: Codec of the video stream
self.video_codec = video_codec
#: Codec of the main audio stream
self.audio_codec = audio_codec
#: IMDb id of the video
self.imdb_id = imdb_id
#: Hashes of the video file by provider names
self.hashes = hashes or {}
#: Size of the video file in bytes
self.size = size
#: Existing subtitle languages
self.subtitle_languages = subtitle_languages or set()
@property
def exists(self):
"""Test whether the video exists"""
return os.path.exists(self.name)
@property
def age(self):
"""Age of the video"""
if self.exists:
return datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(self.name))
return timedelta()
@classmethod
def fromguess(cls, name, parent_path, guess):
"""Create an :class:`Episode` or a :class:`Movie` with the given `name` based on the `guess`.
:param str name: name of the video.
:param dict guess: guessed data.
:raise: :class:`ValueError` if the `type` of the `guess` is invalid
"""
if guess['type'] == 'episode':
return Episode.fromguess(name, parent_path, guess)
if guess['type'] == 'movie':
return Movie.fromguess(name, guess)
raise ValueError('The guess must be an episode or a movie guess')
@classmethod
def fromname(cls, name):
"""Shortcut for :meth:`fromguess` with a `guess` guessed from the `name`.
:param str name: name of the video.
"""
return cls.fromguess(name, guessit(name))
def __repr__(self):
return '<%s [%r]>' % (self.__class__.__name__, self.name)
def __hash__(self):
return hash(self.name)
class Episode():
"""Episode :class:`Video`.
:param str series: series of the episode.
:param int season: season number of the episode.
:param int episode: episode number of the episode.
:param str title: title of the episode.
:param int year: year of the series.
:param bool original_series: whether the series is the first with this name.
:param int tvdb_id: TVDB id of the episode.
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
"""
def __init__(self, name, parent_path, series, season, episode, year=None, original_series=True, tvdb_id=None,
series_tvdb_id=None, series_imdb_id=None, release_group=None, video_codec=None, container=None,
format=None, screen_size=None, **kwargs):
super(Episode, self).__init__()
self.name = name
self.parent_path = parent_path
#: Series of the episode
self.series = series
#: Season number of the episode
self.season = season
#: Episode number of the episode
self.episode = episode
#: Year of series
self.year = year
#: The series is the first with this name
self.original_series = original_series
#: TVDB id of the episode
self.tvdb_id = tvdb_id
#: TVDB id of the series
self.series_tvdb_id = series_tvdb_id
#: IMDb id of the series
self.series_imdb_id = series_imdb_id
# The release group of the episode
self.release_group = release_group
# The video vodec of the series
self.video_codec = video_codec
# The Video container of the episode
self.container = container
# The Video format of the episode
self.format = format
# The Video screen_size of the episode
self.screen_size = screen_size
@classmethod
def fromguess(cls, name, parent_path, guess):
if guess['type'] != 'episode':
raise ValueError('The guess must be an episode guess')
if 'title' not in guess or 'episode' not in guess:
raise ValueError('Insufficient data to process the guess')
return cls(name, parent_path, guess['title'], guess.get('season', 1), guess['episode'],
year=guess.get('year'), original_series='year' not in guess, release_group=guess.get('release_group'),
video_codec=guess.get('video_codec'), audio_codec=guess.get('audio_codec'), container=guess.get('container'),
format=guess.get('format'), screen_size=guess.get('screen_size'))
@classmethod
def fromname(cls, name):
return cls.fromguess(name, guessit(name, {'type': 'episode'}))
def __hash__(self):
return hashlib.md5("b'{}'".format(str(self.series) + str(self.season) + str(self.episode)).encode()).hexdigest()
# THE EP NUMBER IS CONVERTED TO STRING AS A QUICK FIX FOR MULTIPLE NUMBERS IN ONE
def __repr__(self):
if self.year is None:
return '<%s [%r, %sx%s]>' % (self.__class__.__name__, self.series, self.season, str(self.episode))
return '<%s [%r, %d, %sx%s]>' % (self.__class__.__name__, self.series, self.year, self.season, str(self.episode))
class Movie():
"""Movie :class:`Video`.
:param str title: title of the movie.
:param int year: year of the movie.
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
"""
def __init__(self, name, title, year=None, format=None, **kwargs):
super(Movie, self).__init__()
#: Title of the movie
self.title = title
#: Year of the movie
self.year = year
self.format = format
@classmethod
def fromguess(cls, name, guess):
if guess['type'] != 'movie':
raise ValueError('The guess must be a movie guess')
if 'title' not in guess:
raise ValueError('Insufficient data to process the guess')
return cls(name, guess['title'], format=guess.get('format'), release_group=guess.get('release_group'),
resolution=guess.get('screen_size'), video_codec=guess.get('video_codec'),
audio_codec=guess.get('audio_codec'), year=guess.get('year'))
@classmethod
def fromname(cls, name):
return cls.fromguess(name, guessit(name, {'type': 'movie'}))
def __repr__(self):
if self.year is None:
return '<%s [%r]>' % (self.__class__.__name__, self.title)
return '<%s [%r, %d]>' % (self.__class__.__name__, self.title, self.year)

View File

@@ -3,7 +3,7 @@
# @Author: KevinMidboe
# @Date: 2017-04-05 18:40:11
# @Last Modified by: KevinMidboe
# @Last Modified time: 2018-04-03 22:58:20
# @Last Modified time: 2017-06-01 19:02:04
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-Z0-9. ]*")
find = re.compile("^[a-zA-Z. ]*")
m = re.match(find, self.parent)
if m:
name, hit = process.extractOne(m.group(0), getShowNames().keys())
@@ -91,12 +91,7 @@ class strayEpisode(object):
def analyseSubtitles(self, subFile):
# TODO verify that it is a file
try:
subtitlePath = os.path.join([env.input_dir, self.parent, subFile])
except TypeError:
# TODO don't get a list in subtitlePath
return self.removeUploadSign(subFile)
f = open(subtitlesPath, 'r', encoding='ISO-8859-15')
f = open(os.path.join([env.show_dir, self.parent, subFile]), 'r', encoding='ISO-8859-15')
language = detect(f.read())
f.close()
@@ -131,7 +126,7 @@ class strayEpisode(object):
conn = sqlite3.connect(env.db_path)
c = conn.cursor()
path = '/'.join([env.input_dir, self.parent])
path = '/'.join([env.show_dir, self.parent])
video_files = json.dumps(self.videoFiles)
subtitles = json.dumps(self.subtitles)
trash = json.dumps(self.trash)
@@ -149,13 +144,14 @@ class strayEpisode(object):
def getDirContent(dir=env.input_dir):
def getDirContent(dir=env.show_dir):
# TODO What if item in db is not in this list?
try:
return [d for d in os.listdir(dir) if d[0] != '.']
except FileNotFoundError:
# TODO Log to error file
logging.info('Error: "' + dir + '" is not a directory.')
# TODO Remove this exit(0)
# Hashes the contents of media folder to easily check for changes.
def directoryChecksum():
@@ -189,12 +185,12 @@ def XOR(list1, list2):
def filterChildItems(parent):
try:
children = getDirContent('/'.join([env.input_dir, parent]))
children = getDirContent('/'.join([env.show_dir, parent]))
if children:
strayEpisode(parent, children)
except FileNotFoundError:
# TODO Log to error file
logging.info('Error: "' + '/'.join([env.input_dir, parent]) + '" is not a valid directory.')
logging.info('Error: "' + '/'.join([env.show_dir, parent]) + '" is not a valid directory.')
def getNewItems():
newItems = XOR(getDirContent(), getShowNames())
@@ -213,7 +209,7 @@ def main():
if __name__ == '__main__':
if (os.path.exists(env.logfile)):
logging.basicConfig(filename=env.logfile, level=logging.DEBUG)
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.')
exit(0)
@@ -221,4 +217,3 @@ if __name__ == '__main__':
while True:
main()
sleep(30)

58
client/.gitignore vendored
View File

@@ -1,58 +0,0 @@
# 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

Binary file not shown.

View File

@@ -1,25 +0,0 @@
import React, { Component } from 'react';
import { HashRouter as Router, Route, Switch, IndexRoute } from 'react-router-dom';
import SearchRequest from './components/SearchRequest.jsx';
import AdminComponent from './components/admin/Admin.jsx';
class Root extends Component {
// We need to provide a list of routes
// for our app, and in this case we are
// doing so from a Root component
render() {
return (
<Router>
<Switch>
<Route exact path='/' component={SearchRequest} />
<Route path='/admin/:request' component={AdminComponent} />
<Route path='/admin' component={AdminComponent} />
</Switch>
</Router>
);
}
}
export default Root;

View File

@@ -1,41 +0,0 @@
@font-face {
font-family: "din";
src: url('/app/DIN-Regular-webfont.woff')
}
html {
font-family: 'din', 'Open Sans', sans-serif;
display: inline-block;
color:red;
}
#requestMovieList {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.movie_wrapper {
color:red;
display: flex;
align-content: center;
width: 30%;
background-color: #ffffff;
height: 231px;
margin: 20px;
-webkit-box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
}
.movie_content {
margin-left: 15px;
}
.movie_header {
font-size: 1.6em;
}

View File

@@ -0,0 +1,23 @@
/*
./app/components/App.jsx
<FetchData url={"https://apollo.kevinmidboe.com/api/v1/plex/playing"} />
*/
import React from 'react';
import FetchData from './FetchData.js';
import ListStrays from './ListStrays.jsx'
export default class App extends React.Component {
render() {
return (
<div>
<div style={{textAlign: 'center'}}>
<h1>Welcome to Seasoned</h1>
</div>
<ListStrays />
<FetchData />
</div>
);
}
}

View File

@@ -1,26 +0,0 @@
import React from 'react';
export function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return false;
}
export function setCookie(cname, cvalue, exdays) {
var d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
var expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

View File

@@ -4,7 +4,7 @@ class FetchData extends React.Component {
constructor(props){
super(props)
this.state = {
playing: [],
imgUrls: [],
hei: '1',
intervalId: null,
url: ''
@@ -16,9 +16,11 @@ class FetchData extends React.Component {
fetch("https://apollo.kevinmidboe.com/api/v1/plex/playing").then(
function(response){
response.json().then(function(data){
console.log(data.size);
that.setState({
playing: that.state.playing.concat(data.video)
imgUrls: that.state.imgUrls.concat(data.video)
})
console.log(data.video.title);
})
}
)
@@ -30,30 +32,23 @@ class FetchData extends React.Component {
}
getPlaying() {
if (this.state.playing.length != 0) {
return this.state.playing.map((playingObj) => {
if (playingObj.type === 'episode') {
console.log('episode')
return ([
<span>{playingObj.title}</span>,
<span>{playingObj.season}</span>,
<span>{playingObj.episode}</span>
])
} else if (playingObj.type === 'movie') {
console.log('movie')
return ([
<span>{playingObj.title}</span>
])
}
})
} else {
return (<span>Nothing playing</span>)
}
console.log('Should not reach')
// Need callback to work
// Should try to clear out old requests to limit mem use
}
render(){
return(
<div className="FetchData">{this.getPlaying()}</div>
<div className="FetchData">
{this.state.imgUrls.map((imgObj) => {
return ([
<span>{imgObj.title}</span>,
<span>{imgObj.season}</span>,
<span>{imgObj.episode}</span>,
]);
})}
</div>
);
}

View File

@@ -1,266 +0,0 @@
import React from 'react';
import requestElement from './styles/requestElementStyle.jsx'
import { getCookie } from './Cookie.jsx';
class DropdownList extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: ['all', 'requested', 'downloading', 'downloaded'],
sort: ['requested_date', 'name', 'status', 'requested_by', 'ip', 'user_agent'],
status: ['requested', 'downloading', 'downloaded'],
}
}
render() {
const {elementType, elementId, elementStatus, elementCallback, props} = this.props;
console.log(elementCallback('downloaded'))
switch (elementType) {
case 'status':
return (
<div>HERE</div>
)
}
return (
<div {...props}>
</div>
);
}
}
class RequestElement extends React.Component {
constructor(props) {
super(props);
this.state = {
dropDownState: undefined,
}
}
filterRequestList(requestList, filter) {
if (filter === 'all')
return requestList
if (filter === 'movie' || filter === 'show')
return requestList.filter(item => item.type === filter)
return requestList.filter(item => item.status === filter)
}
sortRequestList(requestList, sort_type, reversed) {
requestList.sort(function(a,b) {
if(a[sort_type] < b[sort_type]) return -1;
if(a[sort_type] > b[sort_type]) return 1;
return 0;
});
if (reversed)
requestList.reverse();
}
userAgent(agent) {
if (agent) {
try {
return agent.split(" ")[1].replace(/[\(\;]/g, '');
}
catch(e) {
return agent;
}
}
return '';
}
updateDropDownState(status) {
if (status !== this.dropDownState) {
this.dropDownState = status;
}
}
ItemsStatusDropdown(id, type, status) {
return (
<div>
<select id="lang"
defaultValue={status}
onChange={event => this.updateDropDownState(event.target.value)}
>
<option value='requested'>Requested</option>
<option value='downloading'>Downloading</option>
<option value='downloaded'>Downloaded</option>
</select>
<button onClick={() => { this.updateRequestedItem(id, type)}}>Update Status</button>
</div>
)
}
updateRequestedItem(id, type) {
console.log(id, type, this.dropDownState);
Promise.resolve()
fetch('https://apollo.kevinmidboe.com/api/v1/plex/request/' + id, {
method: 'PUT',
headers: {
'Content-type': 'application/json',
'authorization': getCookie('token')
},
body: JSON.stringify({
type: type,
status: this.dropDownState,
})
})
.then(response => {
if (response.status !== 200) {
console.log('error');
}
response.json()
.then(data => {
if (data.success === true) {
console.log('UPDATED :', id, ' with ', this.dropDownState)
}
})
})
.catch(error => {
new Error(error);
})
}
createHTMLElement(data, index) {
var posterPath = 'https://image.tmdb.org/t/p/w300' + data.image_path;
return (
<div style={requestElement.wrappingDiv} key={index}>
<img style={requestElement.requestPoster} src={posterPath}></img>
<div style={requestElement.infoDiv}>
<span><b>Name</b>: {data.name} </span><br></br>
<span><b>Year</b>: {data.year}</span><br></br>
<span><b>Type</b>: {data.type}</span><br></br>
<span><b>Status</b>: {data.status}</span><br></br>
<span><b>Address</b>: {data.ip}</span><br></br>
<span><b>Requested Data:</b> {data.requested_date}</span><br></br>
<span><b>Requested By:</b> {data.requested_by}</span><br></br>
<span><b>Agent</b>: { this.userAgent(data.user_agent) }</span><br></br>
</div>
{ this.ItemsStatusDropdown(data.id, data.type, data.status) }
</div>
)
}
render() {
const {requestedElementsList, requestedElementsFilter, requestedElementsSort, props} = this.props;
var filteredRequestedList = this.filterRequestList(requestedElementsList, requestedElementsFilter)
this.sortRequestList(filteredRequestedList, requestedElementsSort.value, requestedElementsSort.reversed)
return (
<div {...props} style={requestElement.bodyDiv}>
{filteredRequestedList.map((requestItem, index) => this.createHTMLElement(requestItem, index))}
</div>
);
}
}
class FetchRequested extends React.Component {
constructor(props){
super(props)
this.state = {
requested_objects: [],
filter: 'all',
sort: {
value: 'requested_date',
reversed: false
},
}
}
componentDidMount(){
Promise.resolve()
fetch('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', {
method: 'GET',
headers: {
'Content-type': 'application/json',
'authorization': getCookie('token')
}
})
.then(response => {
if (response.status !== 200) {
console.log('error');
}
response.json()
.then(data => {
if (data.success === true) {
this.setState({
requested_objects: data.requestedItems
})
}
})
})
.catch(error => {
new Error(error);
})
}
changeFilter(filter) {
this.setState({
filter: filter
})
}
updateSort(sort=null, reverse=false) {
if (sort) {
this.setState({
sort: { value: sort, reversed: reverse }
})
}
else {
this.setState({
sort: { value: this.state.sort.value, reversed: reverse }
})
}
}
render(){
return(
<div>
<select id="lang" onChange={event => this.changeFilter(event.target.value)} value={this.state.value}>
<option value="all">All</option>
<option value="requested">Requested</option>
<option value="downloading">Downloading</option>
<option value="downloaded">Downloaded</option>
<option value='movie'>Movies</option>
<option value='show'>Shows</option>
</select>
<select id="lang" onChange={event => this.updateSort(event.target.value)} value={this.state.value}>
<option value='requested_date'>Date</option>
<option value='name'>Title</option>
<option value='status'>Status</option>
<option value='requested_by'>Requested By</option>
<option value='ip'>Address</option>
<option value='user_agent'>Agent</option>
</select>
<button onClick={() => {this.updateSort(null, !this.state.sort.reversed)}}>Reverse</button>
<RequestElement
requestedElementsList={this.state.requested_objects}
requestedElementsFilter={this.state.filter}
requestedElementsSort={this.state.sort}
/>
</div>
)
}
}
export default FetchRequested;

View File

@@ -1,11 +0,0 @@
import React from 'react'
import { Link } from 'react-router-dom'
// The Header creates links that can be used to navigate
// between routes.
const Header = () => (
<header>
</header>
)
export default Header

View File

@@ -29,6 +29,7 @@ class ListStrays extends React.Component {
{this.state.strays.map((strayObj) => {
if (strayObj.verified == 0) {
var url = "https://kevinmidboe.com/seasoned/verified.html?id=" + strayObj.id;
console.log(url);
return ([
<span key={strayObj.id}>{strayObj.name}</span>,
<a href={url}>{strayObj.id}</a>

View File

@@ -1,10 +0,0 @@
// components/NotFound.js
import React from 'react';
const NotFound = () =>
<div>
<h3>404 page not found</h3>
<p>We are sorry but the page you are looking for does not exist.</p>
</div>
export default NotFound;

View File

@@ -1,126 +0,0 @@
import React from 'react';
import Notifications, {notify} from 'react-notify-toast';
// StyleComponents
import searchObjectCSS from './styles/searchObject.jsx';
import buttonsCSS from './styles/buttons.jsx';
import InfoButton from './buttons/InfoButton.jsx';
var MediaQuery = require('react-responsive');
import { fetchJSON } from './http.jsx';
import Interactive from 'react-interactive';
class SearchObject {
constructor(object) {
this.id = object.id;
this.title = object.title;
this.year = object.year;
this.type = object.type;
this.rating = object.rating;
this.poster = object.poster_path;
this.background = object.background_path;
this.matchedInPlex = object.matchedInPlex;
this.summary = object.summary;
}
requestExisting(movie) {
console.log('Exists', movie);
}
requestMovie() {
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + this.id + '?type='+this.type, 'POST')
.then((response) => {
console.log(response);
notify.show(this.title + ' requested!', 'success', 3000);
})
.catch((e) => {
console.error('Request movie fetch went wrong: '+ e);
})
}
getElement(index) {
const element_key = index + this.id;
if (this.poster == null || this.poster == undefined) {
var posterPath = 'https://openclipart.org/image/2400px/svg_to_png/211479/Simple-Image-Not-Found-Icon.png'
} else {
var posterPath = 'https://image.tmdb.org/t/p/w185' + this.poster;
}
var backgroundPath = 'https://image.tmdb.org/t/p/w640_and_h360_bestv2/' + this.background;
var foundInPlex;
if (this.matchedInPlex) {
foundInPlex = <Interactive
as='button'
onClick={() => {this.requestExisting(this)}}
style={buttonsCSS.submit}
focus={buttonsCSS.submit_hover}
hover={buttonsCSS.submit_hover}>
<span>Request Anyway</span>
</Interactive>;
} else {
foundInPlex = <Interactive
as='button'
onClick={() => {this.requestMovie()}}
style={buttonsCSS.submit}
focus={buttonsCSS.submit_hover}
hover={buttonsCSS.submit_hover}>
<span>&#x0002B; Request</span>
</Interactive>;
}
// TODO go away from using mediaQuery, and create custom resizer
return (
<div key={element_key}>
<Notifications />
<div style={searchObjectCSS.container} key={this.id}>
<MediaQuery minWidth={600}>
<div style={searchObjectCSS.posterContainer}>
<img style={searchObjectCSS.posterImage} id='poster' src={posterPath}></img>
</div>
<span style={searchObjectCSS.title_large}>{this.title}</span>
<br></br>
<span style={searchObjectCSS.stats_large}>
Released: { this.year } | Rating: {this.rating} | Type: {this.type}
</span>
<br></br>
<span style={searchObjectCSS.summary}>{this.summary}</span>
<br></br>
</MediaQuery>
<MediaQuery maxWidth={600}>
<img src={ backgroundPath } style={searchObjectCSS.backgroundImage}></img>
<span style={searchObjectCSS.title_small}>{this.title}</span>
<br></br>
<span style={searchObjectCSS.stats_small}>Released: {this.year} | Rating: {this.rating}</span>
</MediaQuery>
<div style={searchObjectCSS.buttons}>
{foundInPlex}
<InfoButton id={this.id} type={this.type} />
</div>
</div>
<MediaQuery maxWidth={600}>
<br />
</MediaQuery>
<div style={searchObjectCSS.dividerRow}>
<div style={searchObjectCSS.itemDivider}></div>
</div>
</div>
)
}
}
export default SearchObject;

View File

@@ -1,464 +0,0 @@
import React from 'react';
import URI from 'urijs';
import InfiniteScroll from 'react-infinite-scroller';
// StyleComponents
import searchRequestCSS from './styles/searchRequestStyle.jsx';
import SearchObject from './SearchObject.jsx';
import Loading from './images/loading.jsx'
import { fetchJSON } from './http.jsx';
import { getCookie } from './Cookie.jsx';
var MediaQuery = require('react-responsive');
// TODO add option for searching multi, movies or tv shows
class SearchRequest extends React.Component {
constructor(props){
super(props)
// Constructor with states holding the search query and the element of reponse.
this.state = {
lastApiCallURI: '',
searchQuery: '',
responseMovieList: null,
movieFilter: false,
showFilter: false,
discoverType: '',
page: 1,
resultHeader: '',
loadResults: false,
scrollHasMore: true,
loading: false,
}
this.allowedListTypes = ['discover', 'popular', 'nowplaying', 'upcoming']
this.baseUrl = 'https://apollo.kevinmidboe.com/api/v1/tmdb/list';
// this.baseUrl = 'http://localhost:31459/api/v1/tmdb/list';
this.searchUrl = 'https://apollo.kevinmidboe.com/api/v1/plex/request';
// this.searchUrl = 'http://localhost:31459/api/v1/plex/request';
}
componentWillMount(){
var that = this;
// this.setState({responseMovieList: null})
this.resetPageNumber();
this.state.loadResults = true;
this.fetchTmdbList(this.allowedListTypes[Math.floor(Math.random()*this.allowedListTypes.length)]);
}
// Handles all errors of the response of a fetch call
handleErrors(response) {
if (!response.ok)
throw Error(response.status);
return response;
}
handleQueryError(response) {
if (!response.ok) {
if (response.status === 404) {
this.setState({
responseMovieList: <h1>Nothing found for search query: { this.findQueryInURI(uri) }</h1>
})
}
console.log('handleQueryError: ', error);
}
return response;
}
// Unpacks the query value of a uri
findQueryValueInURI(uri) {
let uriSearchValues = uri.query(true);
let queryValue = uriSearchValues['query']
return queryValue;
}
// Unpacks the page value of a uri
findPageValueInURI(uri) {
let uriSearchValues = uri.query(true);
let queryValue = uriSearchValues['page']
return queryValue;
}
resetPageNumber() {
this.state.page = 1;
}
setLoading(value) {
this.setState({
loading: value
});
}
// Test this by calling missing endpoint or 404 query and see what code
// and filter the error message based on the code.
// Calls a uri and returns the response as json
callURI(uri, method, data={}) {
return fetch(uri, {
method: method,
headers: new Headers({
'Content-Type': 'application/json',
'authorization': getCookie('token'),
'loggedinuser': getCookie('loggedInUser'),
})
})
.then(response => { return response })
.catch((error) => {
throw Error(error);
});
}
// Saves the input string as a h1 element in responseMovieList state
fillResponseMovieListWithError(msg) {
this.setState({
responseMovieList: <h1>{ msg }</h1>
})
}
// Here we first call api for a search with the input uri, handle any errors
// and fill the reponseData from api into the state of reponseMovieList as movieObjects
callSearchFillMovieList(uri) {
Promise.resolve()
.then(() => this.callURI(uri, 'GET'))
.then(response => {
// If we get a error code for the request
if (!response.ok) {
if (response.status === 404) {
if (this.findPageValueInURI(new URI(response.url)) > 1) {
this.state.scrollHasMore = false;
console.log(this.state.scrollHasMore)
return null
let returnMessage = 'this is the return mesasge than will never be delivered'
let theSecondReturnMsg = 'this is the second return messag ethat will NEVE be delivered'
}
else {
let errorMsg = 'Nothing found for the search query: ' + this.findQueryValueInURI(uri);
this.fillResponseMovieListWithError(errorMsg)
}
}
else {
let errorMsg = 'Error fetching query from server ' + this.response.status;
this.fillResponseMovieListWithError(errorMsg)
}
}
// Convert to json and update the state of responseMovieList with the results of the api call
// mapped as a SearchObject.
response.json()
.then(responseData => {
if (this.state.page === 1) {
this.setState({
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
} else {
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
this.setState({
responseMovieList: growingReponseMovieObjectList,
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
}
})
.catch((error) => {
console.log('CallSearchFillMovieList: ', error)
})
})
.catch((error) => {
console.log('Something went wrong when fetching query.', error)
})
}
callListFillMovieList(uri) {
// Write loading animation
Promise.resolve()
.then(() => this.callURI(uri, 'GET', undefined))
.then(response => {
// If we get a error code for the request
if (!response.ok) {
if (response.status === 404) {
let errorMsg = 'List not found';
this.fillResponseMovieListWithError(errorMsg)
}
else {
let errorMsg = 'Error fetching list from server ' + this.response.status;
this.fillResponseMovieListWithError(errorMsg)
}
}
// Convert to json and update the state of responseMovieList with the results of the api call
// mapped as a SearchObject.
response.json()
.then(responseData => {
if (this.state.page === 1) {
this.setState({
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
} else {
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
this.setState({
responseMovieList: growingReponseMovieObjectList,
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
}
})
})
.catch((error) => {
console.log('Something went wrong when fetching query.', error)
})
}
searchSeasonedRequest() {
this.state.resultHeader = 'Search result for: ' + this.state.searchQuery;
// Build uri with the url for searching requests
var uri = new URI(this.searchUrl);
// Add input of search query and page count to the uri payload
uri = uri.search({ 'query': this.state.searchQuery, 'page': this.state.page });
if (this.state.showFilter)
uri = uri.addSearch('type', 'show');
else
if (this.state.movieFilter)
uri = uri.addSearch('type', 'movie')
// Send uri to call and fill the response list with movie/show objects
this.callSearchFillMovieList(uri);
}
fetchTmdbList(tmdbListType) {
console.log(tmdbListType)
// Check if it is a whitelisted list, this should be replaced with checking if the return call is 500
if (this.allowedListTypes.indexOf(tmdbListType) === -1)
throw Error('Invalid discover type: ' + tmdbListType);
this.state.responseMovieList = []
// Captialize the first letter of and save the discoverQueryType to resultHeader state.
this.state.resultHeader = tmdbListType.toLowerCase().replace(/\b[a-z]/g, function(letter) {
return letter.toUpperCase();
});
// Build uri with the url for searching requests
var uri = new URI(this.baseUrl);
uri.segment(tmdbListType);
// Add input of search query and page count to the uri payload
uri = uri.search({ 'page': this.state.page });
if (this.state.showFilter)
uri = uri.addSearch('type', 'show');
// Send uri to call and fill the response list with movie/show objects
this.callListFillMovieList(uri);
}
// Updates the internal state of the query search field.
updateQueryState(event){
this.setState({
searchQuery: event.target.value
});
}
// For checking if the enter key was pressed in the search field.
_handleQueryKeyPress(e) {
if (e.key === 'Enter') {
// this.fetchQuery();
// Reset page number for a new search
this.resetPageNumber();
this.searchSeasonedRequest();
}
}
// When called passes the variable to SearchObject and calls it's interal function for
// generating the wanted HTML
createMovieObjects(item, index) {
let movie = new SearchObject(item);
return movie.getElement(index);
}
toggleFilter(filterType) {
if (filterType == 'movies') {
this.setState({
movieFilter: !this.state.movieFilter
})
console.log(this.state.movieFilter);
}
else if (filterType == 'shows') {
this.setState({
showFilter: !this.state.showFilter
})
console.log(this.state.showFilter);
}
}
pageBackwards() {
if (this.state.page > 1) {
let pageNumber = this.state.page - 1;
let uri = this.state.lastApiCallURI;
// Augment the page number of the uri with a callback
uri.search(function(data) {
data.page = pageNumber;
});
// Call the api with the new uri
this.callSearchFillMovieList(uri);
// Update state of our page number after the call is done
this.state.page = pageNumber;
}
}
// TODO need to get total page number and save in a state to not overflow
pageForwards() {
// Wrap this in the check
let pageNumber = this.state.page + 1;
let uri = this.state.lastApiCallURI;
// Augment the page number of the uri with a callback
uri.search(function(data) {
data.page = pageNumber;
});
// Call the api with the new uri
this.callSearchFillMovieList(uri);
// Update state of our page number after the call is done
this.state.page = pageNumber;
}
movieToggle() {
if (this.state.movieFilter)
return <span style={searchRequestCSS.searchFilterActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('movies')}}
id="category_active">Movies</span>
else
return <span style={searchRequestCSS.searchFilterNotActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('movies')}}
id="category_active">Movies</span>
}
showToggle() {
if (this.state.showFilter)
return <span style={searchRequestCSS.searchFilterActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('shows')}}
id="category_active">TV Shows</span>
else
return <span style={searchRequestCSS.searchFilterNotActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('shows')}}
id="category_active">TV Shows</span>
}
render(){
const loader = <div className="loader">Loading ...<br></br></div>;
return(
<InfiniteScroll
pageStart={0}
loadMore={this.pageForwards.bind(this)}
hasMore={this.state.scrollHasMore}
loader={<Loading />}
initialLoad={this.state.loadResults}>
<MediaQuery minWidth={600}>
<div style={searchRequestCSS.body}>
<div className='backgroundHeader' style={searchRequestCSS.backgroundLargeHeader}>
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
<span style={searchRequestCSS.pageTitleLargeSpan}>Request new content for plex</span>
</div>
<div style={searchRequestCSS.searchLargeContainer}>
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
<input style={searchRequestCSS.searchLargeBar} type="text" id="search" placeholder="Search for new content..."
onKeyPress={(event) => this._handleQueryKeyPress(event)}
onChange={event => this.updateQueryState(event)}
value={this.state.searchQuery}/>
</div>
</div>
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
<div style={{marginLeft: '30px'}}>
<div style={searchRequestCSS.resultLargeHeader}>{this.state.resultHeader}</div>
<span style={{content: '', display: 'block', width: '2em', borderTop: '2px solid #000,'}}></span>
</div>
<br></br>
{this.state.responseMovieList}
</div>
</div>
</MediaQuery>
<MediaQuery maxWidth={600}>
<div style={searchRequestCSS.body}>
<div className='backgroundHeader' style={searchRequestCSS.backgroundSmallHeader}>
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
<span style={searchRequestCSS.pageTitleSmallSpan}>Request new content</span>
</div>
<div className='box' style={searchRequestCSS.box}>
<div style={searchRequestCSS.searchSmallContainer}>
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
<input style={searchRequestCSS.searchSmallBar} type="text" id="search" placeholder="Search for new content..."
onKeyPress={(event) => this._handleQueryKeyPress(event)}
onChange={event => this.updateQueryState(event)}
value={this.state.searchQuery}/>
</div>
</div>
</div>
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
<span style={searchRequestCSS.resultSmallHeader}>{this.state.resultHeader}</span>
<br></br><br></br>
{this.state.responseMovieList}
</div>
</div>
</MediaQuery>
</InfiniteScroll>
)
}
// <form style={searchRequestCSS.controls}>
// <label style={searchRequestCSS.withData}>
// <div style={searchRequestCSS.sortOptions}>Discover</div>
// </label>
// </form>
// <form style={searchRequestCSS.controls}>
// <label style={searchRequestCSS.withData}>
// <select style={searchRequestCSS.sortOptions}>
// <option value="discover">All</option>
// <option value="nowplaying">Movies</option>
// <option value="nowplaying">TV Shows</option>
// </select>
// </label>
// </form>
}
export default SearchRequest;

View File

@@ -1,92 +0,0 @@
import React from 'react';
import LoginForm from './LoginForm/LoginForm.jsx';
import { Provider } from 'react-redux';
import store from '../redux/store.jsx';
import { getCookie } from '../Cookie.jsx';
import { fetchJSON } from '../http.jsx';
import Sidebar from './Sidebar.jsx';
import AdminRequestInfo from './AdminRequestInfo.jsx';
import adminCSS from '../styles/adminComponent.jsx'
class AdminComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
requested_objects: '',
}
this.updateHandler = this.updateHandler.bind(this)
}
// Fetches all requested elements and updates the state with response
componentWillMount() {
this.fetchRequestedItems()
}
fetchRequestedItems() {
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', 'GET')
.then(result => {
this.setState({
requested_objects: result.results.reverse()
})
})
}
updateHandler() {
this.fetchRequestedItems()
}
// Displays loginform if not logged in and passes response from
// api call to sidebar and infoPanel through props
verifyLoggedIn() {
const logged_in = getCookie('logged_in');
if (!logged_in) {
return <LoginForm />
}
let selectedRequest = undefined;
let listItemSelected = undefined;
const requestParam = this.props.match.params.request;
if (requestParam && this.state.requested_objects !== '') {
selectedRequest = this.state.requested_objects[requestParam]
listItemSelected = requestParam;
}
return (
<div>
<div style={adminCSS.selectedObjectPanel}>
<AdminRequestInfo
selectedRequest={selectedRequest}
listItemSelected={listItemSelected}
updateHandler = {this.updateHandler}
/>
</div>
<div style={adminCSS.sidebar}>
<Sidebar
requested_objects={this.state.requested_objects}
listItemSelected={listItemSelected}
/>
</div>
</div>
)
}
render() {
return (
<Provider store={store}>
{ this.verifyLoggedIn() }
</Provider>
)
}
}
export default AdminComponent;

View File

@@ -1,218 +0,0 @@
import React, { Component } from 'react';
import { fetchJSON } from '../http.jsx';
import PirateSearch from './PirateSearch.jsx'
// No in use!
import InfoButton from '../buttons/InfoButton.jsx';
// Stylesheets
import requestInfoCSS from '../styles/adminRequestInfo.jsx'
import buttonsCSS from '../styles/buttons.jsx';
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
}
class AdminRequestInfo extends Component {
constructor() {
super();
this.state = {
statusValue: '',
movieInfo: undefined,
expandedSummary: false,
}
this.requestInfo = '';
}
componentWillReceiveProps(props) {
this.requestInfo = props.selectedRequest;
this.state.statusValue = this.requestInfo.status;
this.state.expandedSummary = false;
this.fetchIteminfo()
}
userAgent(agent) {
if (agent) {
try {
return agent.split(" ")[1].replace(/[\(\;]/g, '');
}
catch(e) {
return agent;
}
}
return '';
}
generateStatusDropdown() {
return (
<select onChange={ event => this.updateRequestStatus(event) } value={this.state.statusValue}>
<option value='requested'>Requested</option>
<option value='downloading'>Downloading</option>
<option value='downloaded'>Downloaded</option>
</select>
)
}
updateRequestStatus(event) {
const eventValue = event.target.value;
const itemID = this.requestInfo.id;
const apiData = {
type: this.requestInfo.type,
status: eventValue,
}
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + itemID, 'PUT', apiData)
.then((response) => {
console.log('Response, updateRequestStatus: ', response)
this.props.updateHandler()
})
}
generateStatusIndicator(status) {
switch (status) {
case 'requested':
// Yellow
return 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 10px, #fff 4px, #fff 100%) no-repeat'
case 'downloading':
// Blue
return 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 10px, #fff 4px, #fff 100%) no-repeat'
case 'downloaded':
// Green
return 'linear-gradient(to right, #39aa56 0, #39aa56 10px, #fff 4px, #fff 100%) no-repeat'
default:
return 'linear-gradient(to right, grey 0, grey 10px, #fff 4px, #fff 100%) no-repeat'
}
}
generateTypeIcon(type) {
if (type === 'show')
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"></rect><polyline points="17 2 12 7 7 2"></polyline></svg>
)
else if (type === 'movie')
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>
)
else
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12" y2="16"></line></svg>
)
}
toggleSummmaryLength() {
this.setState({
expandedSummary: !this.state.expandedSummary
})
}
generateSummary() {
// { this.state.movieInfo != undefined ? this.state.movieInfo.summary : 'Loading...' }
const info = this.state.movieInfo;
if (info !== undefined) {
const summary = this.state.movieInfo.summary
const summary_short = summary.slice(0, 180);
return (
<div>
<span><b>Matched: </b> {String(info.matchedInPlex)}</span> <br/>
<span><b>Rating: </b> {info.rating}</span> <br/>
<span><b>Popularity: </b> {info.popularity}</span> <br/>
{
(summary.length > 180 && this.state.expandedSummary === false) ?
<span><b>Summary: </b> { summary_short }<span onClick = {() => this.toggleSummmaryLength()}>... <span style={{color: 'blue', cursor: 'pointer'}}>Show more</span></span></span>
:
<span><b>Summary: </b> { summary }<span onClick = {() => this.toggleSummmaryLength()}><span style={{color: 'blue', cursor: 'pointer'}}> Show less</span></span></span>
}
</div>
)
} else {
return <span>Loading...</span>
}
}
requested_by_user(request_user) {
if (request_user === 'NULL')
return undefined
return (
<span><b>Requested by:</b> {request_user}</span>
)
}
fetchIteminfo() {
const itemID = this.requestInfo.id;
const type = this.requestInfo.type;
fetchJSON('https://apollo.kevinmidboe.com/api/v1/tmdb/' + itemID +'&type='+type, 'GET')
.then((response) => {
console.log('Response, getInfo:', response)
this.setState({
movieInfo: response
});
console.log(this.state.movieInfo)
})
}
displayInfo() {
const request = this.props.selectedRequest;
if (request) {
requestInfoCSS.info.background = this.generateStatusIndicator(request.status);
return (
<div style={requestInfoCSS.wrapper}>
<div style={requestInfoCSS.stick}>
<span style={requestInfoCSS.title}> {request.title} {request.year}</span>
<span style={{marginLeft: '2em'}}>
<span style={requestInfoCSS.type_icon}>{this.generateTypeIcon(request.type)}</span>
{/*<span style={style.type_text}>{request.type.capitalize()}</span> <br />*/}
</span>
</div>
<div style={requestInfoCSS.info}>
<div style={requestInfoCSS.info_poster}>
<img src={'https://image.tmdb.org/t/p/w185' + request.poster_path} style={requestInfoCSS.image} alt='Movie poster image'></img>
</div>
<div style={requestInfoCSS.info_request}>
<h3 style={requestInfoCSS.info_request_header}>Request info</h3>
<span><b>status:</b>{ request.status }</span><br />
<span><b>ip:</b>{ request.ip }</span><br />
<span><b>user_agent:</b>{ this.userAgent(request.user_agent) }</span><br />
<span><b>request_date:</b>{ request.requested_date}</span><br />
{ this.requested_by_user(request.requested_by) }<br />
{ this.generateStatusDropdown() }<br />
</div>
<div style={requestInfoCSS.info_movie}>
<h3 style={requestInfoCSS.info_movie_header}>Movie info</h3>
{ this.generateSummary() }
</div>
</div>
<PirateSearch style={requestInfoCSS.search} name={request.title} />
</div>
)
}
}
render() {
return (
<div>{this.displayInfo()}</div>
);
}
}
export default AdminRequestInfo;

View File

@@ -1,66 +0,0 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { login } from '../../redux/reducer.jsx';
class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {};
this.onSubmit = this.onSubmit.bind(this);
}
render() {
let {email, password} = this.state;
let {isLoginPending, isLoginSuccess, loginError} = this.props;
return (
<form name="loginForm" onSubmit={this.onSubmit}>
<div className="form-group-collection">
<div className="form-group">
<label>Email:</label>
<input type="" name="email" onChange={e => this.setState({email: e.target.value})} value={email}/>
</div>
<div className="form-group">
<label>Password:</label>
<input type="password" name="password" onChange={e => this.setState({password: e.target.value})} value={password}/>
</div>
</div>
<input type="submit" value="Login" />
<div className="message">
{ isLoginPending && <div>Please wait...</div> }
{ isLoginSuccess && <div>Success.</div> }
{ loginError && <div>{loginError.message}</div> }
</div>
</form>
)
}
onSubmit(e) {
e.preventDefault();
let { email, password } = this.state;
this.props.login(email, password);
this.setState({
email: '',
password: ''
});
}
}
const mapStateToProps = (state) => {
return {
isLoginPending: state.isLoginPending,
isLoginSuccess: state.isLoginSuccess,
loginError: state.loginError
};
}
const mapDispatchToProps = (dispatch) => {
return {
login: (email, password) => dispatch(login(email, password))
};
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);

View File

@@ -1,95 +0,0 @@
import React, { Component } from 'react';
import { fetchJSON } from '../http.jsx';
// Components
import TorrentTable from './TorrentTable.jsx'
// Stylesheets
import btnStylesheet from '../styles/buttons.jsx';
// Interactive button
import Interactive from 'react-interactive';
import Loading from '../images/loading.jsx'
class PirateSearch extends Component {
constructor() {
super();
this.state = {
torrentResponse: undefined,
name: '',
loading: null,
showButton: true,
}
}
componentWillReceiveProps(props) {
if (props.name != this.state.name) {
this.setState({
torrentResponse: undefined,
showButton: true,
})
}
}
searchTheBay() {
const query = this.props.name;
const type = this.props.type;
this.setState({
showButton: false,
loading: <Loading />,
})
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
// fetchJSON('http://localhost:31459/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
.then((response) => {
console.log('this is the first response: ', response)
if (response.success === true) {
this.setState({
torrentResponse: response.torrents,
loading: null,
})
}
else {
console.error(response.message)
}
})
.catch((error) => {
console.error(error);
this.setState({
showButton: true,
})
})
}
render() {
btnStylesheet.submit.top = '50%'
btnStylesheet.submit.position = 'absolute'
btnStylesheet.submit.marginLeft = '-75px'
return (
<div>
{ this.state.showButton ?
<div style={{textAlign:'center'}}>
<Interactive
as='button'
onClick={() => {this.searchTheBay()}}
style={btnStylesheet.submit}
focus={btnStylesheet.submit_hover}
hover={btnStylesheet.submit_hover}>
<span style={{whiteSpace: 'nowrap'}}>Search for torrents</span>
</Interactive>
</div>
: null }
{ this.state.loading }
<TorrentTable response={this.state.torrentResponse} />
</div>
)
}
}
export default PirateSearch

View File

@@ -1,247 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import Interactive from 'react-interactive';
import sidebarCSS from '../styles/adminSidebar.jsx'
class SidebarComponent extends Component {
constructor(props){
super(props)
// Constructor with states holding the search query and the element of reponse.
this.state = {
filterValue: '',
filterQuery: '',
requestItemsToBeDisplayed: [],
listItemSelected: '',
height: '0',
}
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
}
// Where we wait for api response to be delivered from parent through props
componentWillReceiveProps(props) {
this.state.listItemSelected = props.listItemSelected;
this.displayRequestedElementsInfo(props.requested_objects);
}
componentDidMount() {
this.updateWindowDimensions();
window.addEventListener('resize', this.updateWindowDimensions);
}
componentWillUnmount() {
window.removeEventListener('resize', this.updateWindowDimensions);
}
updateWindowDimensions() {
this.setState({ height: window.innerHeight });
}
// Inputs a date and returns a text string that matches how long it was since
convertDateToDaysSince(date) {
var oneDay = 24*60*60*1000;
var firstDate = new Date(date);
var secondDate = new Date();
var diffDays = Math.round(Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay));
switch (diffDays) {
case 0:
return 'Today';
case 1:
return '1 day ago'
default:
return diffDays + ' days ago'
}
}
// Called from our dropdown, receives a filter string and checks it with status field
// of our request objects.
filterItems(filterValue) {
let filteredRequestElements = this.props.requested_objects.map((item, index) => {
if (item.status === filterValue || filterValue === 'all')
return this.generateListElements(index, item);
})
this.setState({
requestItemsToBeDisplayed: filteredRequestElements,
filterValue: filterValue,
})
}
// Updates the internal state of the query filter and updates the list to only
// display names matching the query. This is real-time filtering.
updateFilterQuery(event) {
const query = event.target.value;
let filteredByQuery = this.props.requested_objects.map((item, index) => {
if (item.title.toLowerCase().indexOf(query.toLowerCase()) != -1)
return this.generateListElements(index, item);
})
this.setState({
requestItemsToBeDisplayed: filteredByQuery,
filterQuery: query,
});
}
generateFilterSearch() {
return (
<div style={sidebarCSS.searchSidebar}>
<div style={sidebarCSS.searchInner}>
<input
type="text"
id="search"
style={sidebarCSS.searchTextField}
placeholder="Search requested items"
onChange={event => this.updateFilterQuery(event)}
value={this.state.filterQuery}/>
<span>
<svg id="icon-search" style={sidebarCSS.searchIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
<g id="search">
<circle style={sidebarCSS.searchSVGIcon} cx="6.055" cy="5.805" r="5.305"></circle>
<path style={sidebarCSS.searchSVGIcon} d="M9.847 9.727l4.166 4.773"></path>
</g>
</svg>
</span>
</div>
</div>
)
}
generateNav() {
let filterValue = this.state.filterValue;
return (
<nav style={sidebarCSS.sidebar_navbar_underline}>
<ul style={sidebarCSS.ulFilterSelectors}>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('all') }>All</span>
{ (filterValue === 'all' || filterValue === '') && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('requested') }>Requested</span>
{ filterValue === 'requested' && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloading') }>Downloading</span>
{ filterValue === 'downloading' && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloaded') }>Downloaded</span>
{ filterValue === 'downloaded' && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
</ul>
</nav>
)
}
generateBody(cards) {
let style = sidebarCSS.ulCard;
style.maxHeight = this.state.height - 160;
return (
<ul style={style}>
{ cards }
</ul>
)
}
generateListElements(index, item) {
let statusBar;
switch (item.status) {
case 'requested':
// Yellow
statusBar = { background: 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 4px, #fff 4px, #fff 100%) no-repeat' }
break;
case 'downloading':
// Blue
statusBar = { background: 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 4px, #fff 4px, #fff 100%) no-repeat' }
break;
case 'downloaded':
// Green
statusBar = { background: 'linear-gradient(to right, #39aa56 0, #39aa56 4px, #fff 4px, #fff 100%) no-repeat' }
break;
default:
statusBar = { background: 'linear-gradient(to right, grey 0, grey 4px, #fff 4px, #fff 100%) no-repeat' }
}
statusBar.listStyleType = 'none';
return (
<Link style={sidebarCSS.link} to={{ pathname: '/admin/'+String(index)}} key={index}>
<li style={statusBar}>
<Interactive
as='div'
style={ (index != this.state.listItemSelected) ? sidebarCSS.card : sidebarCSS.cardSelected }
hover={sidebarCSS.cardSelected}
focus={sidebarCSS.cardSelected}
active={sidebarCSS.cardSelected}>
<h2 style={sidebarCSS.titleCard}>
<span>{ item.title }</span>
</h2>
<p style={sidebarCSS.pCard}>
<span>Requested:
<time>
&nbsp;{ this.convertDateToDaysSince(item.requested_date) }
</time>
</span>
</p>
</Interactive>
</li>
</Link>
)
}
// This is our main loader that gets called when we receive api response through props from parent
displayRequestedElementsInfo(requested_objects) {
let requestedElement = requested_objects.map((item, index) => {
if (['requested', 'downloading', 'downloaded'].indexOf(this.state.filterValue) != -1) {
if (item.status === this.state.filterValue){
return this.generateListElements(index, item);
}
}
else if (this.state.filterQuery !== '') {
if (item.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) != -1)
return this.generateListElements(index, item);
}
else
return this.generateListElements(index, item);
})
this.setState({
requestItemsToBeDisplayed: this.generateBody(requestedElement)
})
}
render() {
// if (typeof InstallTrigger !== 'undefined')
// bodyCSS.width = '-moz-min-content';
return (
<div>
<h1 style={sidebarCSS.header}>Requested items</h1>
{ this.generateFilterSearch() }
{ this.generateNav() }
<div key='requestedTable' style={sidebarCSS.body}>
{ this.state.requestItemsToBeDisplayed }
</div>
</div>
);
}
}
export default SidebarComponent;

View File

@@ -1,209 +0,0 @@
import React, { Component } from 'react';
import { fetchJSON } from '../http.jsx';
import torrentTableCSS from '../styles/adminTorrentTable.jsx';
class TorrentTable extends Component {
constructor() {
super();
this.state = {
torrentResponse: [],
listElements: undefined,
showTable: false,
filterQuery: '',
sortValue: 'name',
sortDesc: true,
}
this.UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
}
componentWillReceiveProps(props) {
if (props.response !== undefined && props.response !== this.state.torrentResponse) {
console.log('not called', props)
this.setState({
torrentResponse: props.response,
showTable: true,
})
} else {
this.setState({
showTable: false,
})
}
}
// BORROWED FROM GITHUB user sindresorhus
// Link to repo: https://github.com/sindresorhus/pretty-bytes
convertSizeToHumanSize(num) {
if (!Number.isFinite(num)) {
return num
// throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
}
const neg = num < 0;
if (neg) {
num = -num;
}
if (num < 1) {
return (neg ? '-' : '') + num + ' B';
}
const exponent = Math.min(Math.floor(Math.log10(num) / 3), this.UNITS.length - 1);
const numStr = Number((num / Math.pow(1000, exponent)).toPrecision(3));
const unit = this.UNITS[exponent];
return (neg ? '-' : '') + numStr + ' ' + unit;
}
convertHumanSizeToBytes(string) {
const [numStr, unit] = string.split(' ');
if (this.UNITS.indexOf(unit) === -1) {
return string
}
const exponent = this.UNITS.indexOf(unit) * 3
return numStr * (Math.pow(10, exponent))
}
sendToDownload(magnet) {
const apiData = {
magnet: magnet,
}
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/add', 'POST', apiData)
// fetchJSON('http://localhost:31459/api/v1/pirate/add', 'POST', apiData)
.then((response) => {
console.log('Response, addMagnet: ', response)
// TODO Display the feedback in a notification component (text, status)
})
}
// Updates the internal state of the query filter and updates the list to only
// display names matching the query. This is real-time filtering.
updateFilterQuery(event) {
const query = event.target.value;
let filteredByQuery = this.props.response.map((item, index) => {
if (item.name.toLowerCase().indexOf(query.toLowerCase()) != -1)
return item
})
this.setState({
torrentResponse: filteredByQuery,
filterQuery: query,
});
}
sortTable(col) {
let direction = this.state.sortDesc;
if (col === this.state.sortValue)
direction = !direction;
else
direction = true
let sortedItems = this.state.torrentResponse.sort((a,b) => {
// This is so we also can sort string that only contain numbers
let valueA = isNaN(a[col]) ? a[col] : parseInt(a[col])
let valueB = isNaN(b[col]) ? b[col] : parseInt(b[col])
valueA = (col == 'size') ? this.convertHumanSizeToBytes(valueA) : valueA
valueB = (col == 'size') ? this.convertHumanSizeToBytes(valueB) : valueB
if (direction)
return valueA<valueB? 1:valueA>valueB?-1:0;
else
return valueA>valueB? 1:valueA<valueB?-1:0;
})
this.setState({
torrentResponse: sortedItems,
sortDesc: direction,
sortValue: col,
})
}
generateFilterSearch() {
return (
<div style={torrentTableCSS.searchSidebar}>
<div style={torrentTableCSS.searchInner}>
<input
type="text"
id="search"
style={torrentTableCSS.searchTextField}
placeholder="Filter torrents by query"
onChange={event => this.updateFilterQuery(event)}
value={this.state.filterQuery}/>
<span>
<svg id="icon-search" style={torrentTableCSS.searchIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
<g id="search">
<circle style={torrentTableCSS.searchSVGIcon} cx="6.055" cy="5.805" r="5.305"></circle>
<path style={torrentTableCSS.searchSVGIcon} d="M9.847 9.727l4.166 4.773"></path>
</g>
</svg>
</span>
</div>
</div>
)
}
generateListElements() {
let listElements = this.state.torrentResponse.map((item, index) => {
if (item !== undefined) {
let title = item.name
let size = this.convertSizeToHumanSize(item.size)
return (
<tr key={index} style={torrentTableCSS.bodyCol}>
<td>{ item.name }</td>
<td>{ item.uploader }</td>
<td>{ size }</td>
<td>{ item.seed }</td>
<td><button onClick = { event => this.sendToDownload(item.magnet) }>Send to download</button></td>
</tr>
)
}
})
return listElements
}
render() {
return (
<div style= { this.state.showTable ? null : {display: 'none'}}>
{ this.generateFilterSearch() }
<table style={torrentTableCSS.table} cellSpacing="0" cellPadding="0">
<thead>
<tr>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('name') }>
Title
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'name' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('uploader') }>
Uploader
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'uploader' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('size') }>
Size
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'size' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('seed') }>
Seeds
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'seed' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col}>Magnet</th>
</tr>
</thead>
<tbody>
{this.generateListElements()}
</tbody>
</table>
</div>
)
}
}
export default TorrentTable;

View File

@@ -1,52 +0,0 @@
import React, { Component } from 'react';
import Interactive from 'react-interactive';
import buttonsCSS from '../styles/buttons.jsx';
class InfoButton extends Component {
constructor(props) {
super(props);
if (props) {
this.state = {
id: props.id,
type: props.type,
}
}
}
componentWillReceiveProps(props) {
this.setState({
id: props.id,
type: props.type,
})
}
getTMDBLink() {
const id = this.state.id;
const type = this.state.type;
if (type === 'movie')
return 'https://www.themoviedb.org/movie/' + id
else if (type === 'show')
return 'https://www.themoviedb.org/tv/' + id
}
render() {
return (
<a href={this.getTMDBLink()}>
<Interactive
as='button'
hover={buttonsCSS.info_hover}
focus={buttonsCSS.info_hover}
style={buttonsCSS.info}>
<span>More info</span>
</Interactive>
</a>
);
}
}
export default InfoButton;

View File

@@ -1,22 +0,0 @@
import React from 'react';
class RequestButton extends React.Component {
constructor() {
super();
this.state = {textColor: 'white'};
}
render() {
return (
<Text
style={{color: this.state.textColor}}
onEnter={() => this.setState({textColor: 'red'})}
onExit={() => this.setState({textColor: 'white'})}>
This text will turn red when you look at it.
</Text>
);
}
}
export default RequestButton;

View File

@@ -1,53 +0,0 @@
import React from 'react';
import { getCookie } from './Cookie.jsx';
// class http {
// dispatch(obj) {
// console.log(obj);
// }
function checkStatus(response) {
const hasError = (response.status < 200 || response.status >= 300)
if (hasError) {
throw response.text();
}
return response;
}
function parseJSON(response) { return response.json(); }
// *
// * Retrieve search results from tmdb with added seasoned information.
// * @param {String} uri query you want to search for
// * @param {Number} page representing pagination of results
// * @returns {Promise} succeeds if results were found
// fetchSearch(uri) {
// fetch(uri, {
// method: 'GET',
// headers: {
// 'authorization': getCookie('token')
// },
// })
// .then(response => {
// });
// }
// }
// export default http;
export function fetchJSON(url, method, data) {
return fetch(url, {
method: method,
headers: new Headers({
'Content-Type': 'application/json',
'authorization': getCookie('token'),
'loggedinuser': getCookie('loggedInUser'),
}),
body: JSON.stringify(data)
}).then(checkStatus).then(parseJSON);
}

View File

@@ -1,34 +0,0 @@
import React from 'react';
function Loading() {
return (
<div style={{textAlign: 'center'}}>
<svg version="1.1"
style={{height: '75px'}}
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 80 80">
<path
fill="#e9a131"
d="M40,72C22.4,72,8,57.6,8,40C8,22.4,
22.4,8,40,8c17.6,0,32,14.4,32,32c0,1.1-0.9,2-2,2
s-2-0.9-2-2c0-15.4-12.6-28-28-28S12,24.6,12,40s12.6,
28,28,28c1.1,0,2,0.9,2,2S41.1,72,40,72z">
<animateTransform
attributeType="xml"
attributeName="transform"
type="rotate"
from="0 40 40"
to="360 40 40"
dur="1.0s"
repeatCount="indefinite"/>
</path>
</svg>
</div>
)
}
export default Loading;

View File

@@ -1,109 +0,0 @@
import { setCookie } from '../Cookie.jsx';
const SET_LOGIN_PENDING = 'SET_LOGIN_PENDING';
const SET_LOGIN_SUCCESS = 'SET_LOGIN_SUCCESS';
const SET_LOGIN_ERROR = 'SET_LOGIN_ERROR';
export function login(email, password) {
return dispatch => {
dispatch(setLoginPending(true));
dispatch(setLoginSuccess(false));
dispatch(setLoginError(null));
callLoginApi(email, password, error => {
dispatch(setLoginPending(false));
if (!error) {
dispatch(setLoginSuccess(true));
} else {
dispatch(setLoginError(error));
}
});
}
}
function setLoginPending(isLoginPending) {
return {
type: SET_LOGIN_PENDING,
isLoginPending
};
}
function setLoginSuccess(isLoginSuccess) {
return {
type: SET_LOGIN_SUCCESS,
isLoginSuccess
};
}
function setLoginError(loginError) {
return {
type: SET_LOGIN_ERROR,
loginError
}
}
function callLoginApi(username, password, callback) {
Promise.resolve()
fetch('https://apollo.kevinmidboe.com/api/v1/user/login', {
method: 'POST',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password,
})
})
.then(response => {
switch (response.status) {
case 200:
response.json()
.then((data) => {
if (data.success === true) {
let token = data.token;
setCookie('token', token, 10);
setCookie('logged_in', true, 10);
setCookie('loggedInUser', username, 10);
window.location.reload();
}
return callback(null);
})
case 401:
return callback(new Error(response.statusText));
}
})
.catch(error => {
return callback(new Error('Invalid username and password'));
});
}
export default function reducer(state = {
isLoginSuccess: false,
isLoginPending: false,
loginError: null
}, action) {
switch (action.type) {
case SET_LOGIN_PENDING:
return Object.assign({}, state, {
isLoginPending: action.isLoginPending
});
case SET_LOGIN_SUCCESS:
return Object.assign({}, state, {
isLoginSuccess: action.isLoginSuccess
});
case SET_LOGIN_ERROR:
return Object.assign({}, state, {
loginError: action.loginError
});
default:
return state;
}
}

View File

@@ -1,7 +0,0 @@
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import reducer from './reducer.jsx';
const store = createStore(reducer, {}, applyMiddleware(thunk, logger));
export default store;

View File

@@ -1,16 +0,0 @@
export default {
sidebar: {
float: 'left',
width: '18%',
minWidth: '250px',
fontFamily: '"Open Sans", sans-serif',
fontSize: '14px',
borderRight: '2px solid #f2f2f2',
},
selectedObjectPanel: {
width: '80%',
float: 'right',
fontFamily: '"Open Sans", sans-serif',
marginTop: '1em',
}
}

View File

@@ -1,58 +0,0 @@
export default {
wrapper: {
width: '100%',
},
stick: {
marginBottom: '1em',
},
title: {
fontSize: '2em',
},
image: {
width: '105px',
borderRadius: '4px',
},
info: {
paddingTop: '1em',
paddingBottom: '0.5em',
marginRight: '2em',
backgroundColor: 'white',
border: '1px solid #d0d0d0',
borderRadius: '2px',
display: 'flex',
},
type_icon: {
marginLeft: '-0.2em',
marginRight: '0.7em',
},
type_text: {
verticalAlign: 'super',
},
info_poster: {
marginLeft: '2em',
flex: '0 1 10%'
},
info_request: {
flex: '0 1 auto'
},
info_request_header: {
margin: '0',
marginBottom: '0.5em',
},
info_movie: {
maxWidth: '70%',
marginLeft: '1em',
flex: '0 1 auto',
},
info_movie_header: {
margin: '0',
marginBottom: '0.5em',
}
}

View File

@@ -1,153 +0,0 @@
export default {
header: {
textAlign: 'center',
},
body: {
backgroundColor: 'white',
},
parentElement: {
display: 'inline-block',
width: '100%',
border: '1px solid grey',
borderRadius: '2px',
padding: '4px',
margin: '4px',
marginLeft: '4px',
backgroundColor: 'white',
},
parentElement_hover: {
backgroundColor: '#f8f8f8',
pointer: 'hand',
},
parentElement_active: {
textDecoration: 'none',
},
parentElement_selected: {
display: 'inline-block',
width: '100%',
border: '1px solid grey',
borderRadius: '2px',
padding: '4px',
margin: '4px 0px 4px 4px',
marginLeft: '10px',
backgroundColor: 'white',
},
title: {
maxWidth: '65%',
display: 'inline-flex',
},
link: {
color: 'black',
textDecoration: 'none',
},
rightContainer: {
float: 'right',
},
searchSidebar: {
height: '4em',
},
searchInner: {
top: '0',
right: '0',
left: '0',
bottom: '0',
margin: 'auto',
width: '90%',
minWidth: '280px',
height: '30px',
border: '1px solid #d0d0d0',
borderRadius: '4px',
overflow: 'hidden'
},
searchTextField: {
display: 'inline-block',
width: '90%',
padding: '.3em',
verticalAlign: 'middle',
border: 'none',
background: '#fff',
fontSize: '14px',
marginTop: '-7px',
},
searchIcon: {
width: '15px',
height: '16px',
marginRight: '4px',
marginTop: '7px',
},
searchSVGIcon: {
fill: 'none',
stroke: '#9d9d9d',
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeMiterlimit: '10',
},
ulFilterSelectors: {
borderBottom: '2px solid #f1f1f1',
display: 'flex',
padding: '0',
margin: '0',
listStyle: 'none',
justifyContent: 'space-evenly',
},
aFilterSelectors: {
color: '#3eaaaf',
fontSize: '16px',
cursor: 'pointer',
},
spanFilterSelectors: {
content: '""',
bottom: '-2px',
display: 'block',
width: '100%',
height: '2px',
backgroundColor: '#3eaaaa',
},
ulCard: {
margin: '1em 0 0 0',
padding: '0',
listStyle: 'none',
borderBottom: '.46rem solid #f1f1f',
backgroundColor: '#f1f1f1',
overflow: 'scroll',
},
card: {
padding: '.1em .5em .8em 1.5em',
marginBottom: '.26rem',
height: 'auto',
cursor: 'pointer',
},
cardSelected: {
padding: '.1em .5em .8em 1.5em',
marginBottom: '.26rem',
height: 'auto',
cursor: 'pointer',
backgroundColor: '#f9f9f9',
},
titleCard: {
fontSize: '15px',
fontWeight: '400',
whiteSpace: 'no-wrap',
textDecoration: 'none',
},
pCard: {
margin: '0',
},
}

View File

@@ -1,59 +0,0 @@
export default {
table: {
width: '80%',
marginRight: 'auto',
marginLeft: 'auto',
},
tableHeader: {
},
col: {
cursor: 'pointer',
borderBottom: '1px solid #e0e0e0',
paddingBottom: '0.5em',
textAlign: 'left',
},
bodyCol: {
marginTop: '0.5em',
},
searchSidebar: {
height: '4em',
marginTop: '1em',
},
searchInner: {
top: '0',
right: '0',
left: '0',
bottom: '0',
margin: 'auto',
width: '50%',
minWidth: '280px',
height: '30px',
border: '1px solid #d0d0d0',
borderRadius: '4px',
overflow: 'hidden'
},
searchTextField: {
display: 'inline-block',
width: '95%',
padding: '.3em',
verticalAlign: 'middle',
border: 'none',
background: '#fff',
fontSize: '14px',
marginTop: '-7px',
},
searchIcon: {
width: '15px',
height: '16px',
marginRight: '4px',
marginTop: '7px',
},
searchSVGIcon: {
fill: 'none',
stroke: '#9d9d9d',
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeMiterlimit: '10',
},
}

View File

@@ -1,80 +0,0 @@
export default {
submit: {
color: '#e9a131',
marginRight: '10px',
backgroundColor: 'white',
border: '#e9a131 2px solid',
borderColor: '#e9a131',
borderRadius: '4px',
textAlign: 'center',
padding: '10px',
minWidth: '100px',
float: 'left',
fontSize: '13px',
fontWeight: '800',
cursor: 'pointer',
},
submit_hover: {
backgroundColor: '#e9a131',
color: 'white',
},
info: {
color: '#00d17c',
marginRight: '10px',
backgroundColor: 'white',
border: '#00d17c 2px solid',
borderRadius: '4px',
textAlign: 'center',
padding: '10px',
minWidth: '100px',
float: 'left',
fontSize: '13px',
fontWeight: '800',
cursor: 'pointer',
},
info_hover: {
backgroundColor: '#00d17c',
color: 'white',
},
edit: {
color: '#4a95da',
marginRight: '10px',
backgroundColor: 'white',
border: '#4a95da 2px solid',
borderRadius: '4px',
textAlign: 'center',
padding: '10px',
minWidth: '100px',
float: 'left',
fontSize: '13px',
fontWeight: '800',
cursor: 'pointer',
},
edit_small: {
color: '#4a95da',
marginRight: '10px',
backgroundColor: 'white',
border: '#4a95da 2px solid',
borderRadius: '4px',
textAlign: 'center',
padding: '4px',
minWidth: '50px',
float: 'left',
fontSize: '13px',
fontWeight: '800',
cursor: 'pointer',
},
edit_hover: {
backgroundColor: '#4a95da',
color: 'white',
},
}

View File

@@ -1,24 +0,0 @@
export default {
bodyDiv: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
flexFlow: 'row wrap',
justifyContent: 'space-around',
},
wrappingDiv: {
},
requestPoster: {
height: '150px',
},
infoDiv: {
marginTop: 0,
marginLeft: '10px',
float: 'right',
},
}

View File

@@ -1,62 +0,0 @@
export default {
container: {
maxWidth: '95%',
margin: '0 auto',
minHeight: '230px'
},
title_large: {
color: 'black',
fontSize: '2em',
},
title_small: {
color: 'black',
fontSize: '22px',
},
stats_large: {
fontSize: '0.8em'
},
stats_small: {
marginTop: '5px',
fontSize: '0.8em'
},
posterContainer: {
float: 'left',
zIndex: '3',
position: 'relative',
marginRight: '30px'
},
posterImage: {
border: '2px none',
borderRadius: '2px',
width: '150px'
},
backgroundImage: {
width: '100%'
},
buttons: {
paddingTop: '20px',
},
summary: {
fontSize: '15px',
},
dividerRow: {
width: '100%'
},
itemDivider: {
width: '90%',
borderBottom: '1px solid grey',
margin: '2rem auto'
}
}

View File

@@ -1,177 +0,0 @@
export default {
body: {
fontFamily: "'Open Sans', sans-serif",
backgroundColor: '#f7f7f7',
margin: 0,
padding: 0,
minHeight: '100%',
},
backgroundLargeHeader: {
width: '100%',
minHeight: '180px',
backgroundColor: 'rgb(1, 28, 35)',
// backgroundImage: 'radial-gradient(circle, #004c67 0, #005771 120%)',
zIndex: 1,
marginBottom: '70px'
},
backgroundSmallHeader: {
width: '100%',
minHeight: '120px',
backgroundColor: '#011c23',
zIndex: 1,
marginBottom: '40px'
},
requestWrapper: {
maxWidth: '1200px',
margin: 'auto',
paddingTop: '10px',
backgroundColor: 'white',
position: 'relative',
zIndex: '10',
boxShadow: '0 1px 2px grey',
},
pageTitle: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
},
pageTitleLargeSpan: {
color: 'white',
fontSize: '3em',
marginTop: '4vh',
marginBottom: '6vh'
},
pageTitleSmallSpan: {
color: 'white',
fontSize: '2em',
marginTop: '3vh',
marginBottom: '3vh'
},
searchLargeContainer: {
height: '52px',
width: '77%',
paddingLeft: '23%',
backgroundColor: 'white',
boxShadow: 'grey 0px 1px 2px',
},
searchSmallContainer: {
},
searchIcon: {
position: 'absolute',
fontSize: '1.6em',
marginTop: '7px',
color: '#4f5b66',
display: 'block',
},
searchLargeBar: {
width: '50%',
height: '50px',
background: '#ffffff',
border: 'none',
fontSize: '12pt',
float: 'left',
color: '#63717f',
paddingLeft: '40px',
},
searchSmallBar: {
width: '100%',
height: '50px',
background: '#ffffff',
border: 'none',
fontSize: '11pt',
float: 'left',
color: '#63717f',
paddingLeft: '65px',
marginLeft: '-25px',
borderRadius: '5px',
},
// Dropdown for selecting tmdb lists
controls: {
textAlign: 'left',
paddingTop: '8px',
width: '33.3333%',
marginLeft: '0',
marginRight: '0',
},
withData: {
boxSizing: 'border-box',
marginBottom: '0',
display: 'block',
padding: '0',
verticalAlign: 'baseline',
font: 'inherit',
textAlign: 'left',
boxSizing: 'border-box',
},
sortOptions: {
border: '1px solid #000',
maxWidth: '100%',
overflow: 'hidden',
lineHeight: 'normal',
textAlign: 'left',
padding: '4px 12px',
paddingRight: '2rem',
backgroundImage: 'url("data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOCAxOCI+CiAgPHRpdGxlPmFycm93LWRvd24tbWljcm88L3RpdGxlPgogIDxwb2x5bGluZSBwb2ludHM9IjE0IDQuNjcgOSAxMy4zMyA0IDQuNjciIHN0eWxlPSJmaWxsOiBub25lO3N0cm9rZTogIzAwMDtzdHJva2UtbWl0ZXJsaW1pdDogMTA7c3Ryb2tlLXdpZHRoOiAycHgiLz4KPC9zdmc+Cg==")',
backgroundSize: '18px 18px',
backgroundPosition: 'right 8px center',
backgroundRepeat: 'no-repeat',
width: 'auto',
display: 'inline-block',
outline: 'none',
boxSizing: 'border-box',
fontSize: '15px',
WebkitAppearance: 'none',
MozAppearance: 'none',
appearance: 'none',
},
searchFilterActive: {
color: '#00d17c',
fontSize: '1em',
marginLeft: '10px',
cursor: 'pointer'
},
searchFilterNotActive: {
color: 'white',
fontSize: '1em',
marginLeft: '10px',
cursor: 'pointer'
},
filter: {
color: 'white',
paddingLeft: '40px',
width: '60%',
},
resultLargeHeader: {
color: 'black',
fontSize: '1.6em',
width: '20%',
},
resultSmallHeader: {
paddingLeft: '12px',
color: 'black',
fontSize: '1.4em',
},
}

View File

@@ -2,12 +2,9 @@
<html>
<head>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0, user-scalable=0">
<title>seasoned Shows</title>
</head>
<body style='margin: 0'>
<body>
<div id="root">
</div>

View File

@@ -2,19 +2,14 @@
* @Author: KevinMidboe
* @Date: 2017-06-01 21:08:55
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-10-20 19:24:52
* @Last Modified time: 2017-06-01 21:34:32
./client/index.js
which is the webpack entry file
*/
import React from 'react';
import { render } from 'react-dom';
import { HashRouter } from 'react-router-dom';
import Root from './Root.jsx';
import ReactDOM from 'react-dom';
import App from './components/App.jsx';
render((
<HashRouter>
<Root />
</HashRouter>
), document.getElementById('root'));
ReactDOM.render(<App />, document.getElementById('root'));

View File

@@ -6,38 +6,20 @@
"author": "Kevin Midboe",
"license": "MIT",
"scripts": {
"start": "webpack-dev-server --open --config webpack.dev.js",
"build": "NODE_ENV=production webpack --config webpack.prod.js",
"build_dev": "webpack --config webpack.dev.js"
"start": "webpack-dev-server"
},
"dependencies": {
"clean-webpack-plugin": "^0.1.17",
"css-loader": "^1.0.0",
"html-webpack-plugin": "^2.28.0",
"path": "^0.12.7",
"react": "^15.6.1",
"react-burger-menu": "^2.1.6",
"react": "^15.5.4",
"react-dom": "^15.5.4",
"react-infinite-scroller": "^1.0.15",
"react-interactive": "^0.8.1",
"react-notify-toast": "^0.3.2",
"react-redux": "^5.0.6",
"react-responsive": "^1.3.4",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"redux": "^3.7.2",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
"urijs": "^1.18.12",
"webfontloader": "^1.6.28",
"webpack": "^4.0.0",
"webpack-dev-server": "^3.1.11",
"webpack-merge": "^4.1.0"
"webpack": "^2.6.1",
"webpack-dev-server": "^2.4.5"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.0",
"babel-core": "^6.24.1",
"babel-loader": "^7.0.0",
"babel-preset-env": "^1.5.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1"
}

View File

@@ -1,33 +0,0 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 19:09:16
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-10-24 21:55:41
*/
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './app/index.js',
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
template: './app/index.html',
})
],
module: {
loaders: [
{ test: /\.(js|jsx)$/, loader: 'babel-loader', exclude: /node_modules/ },
]
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};

33
client/webpack.config.js Normal file
View File

@@ -0,0 +1,33 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 19:09:16
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-06-01 22:11:51
*/
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({
template: './app/index.html',
filename: 'index.html',
inject: 'body'
})
module.exports = {
entry: './app/index.js',
output: {
path: path.resolve('dist'),
filename: 'index_bundle.js'
},
devServer: {
headers: { "Access-Control-Allow-Origin": "*" }
},
module: {
loaders: [
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
{ test: /\.jsx$/, loader: 'babel-loader', exclude: /node_modules/ }
]
},
plugins: [HtmlWebpackPluginConfig]
}

View File

@@ -1,17 +0,0 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 19:09:16
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-10-24 22:12:52
*/
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
headers: {'Access-Control-Allow-Origin': '*'}
}
});;

View File

@@ -1,28 +0,0 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 19:09:16
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-10-24 22:26:29
*/
const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const common = require('./webpack.common.js');
var webpack = require('webpack')
module.exports = merge(common, {
plugins: [
new UglifyJSPlugin(),
new HtmlWebpackPlugin({
template: './app/index.html',
title: 'Caching'
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
}),
],
output: {
filename: '[name].[chunkhash].js',
}
});

3323
client/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

0
conf/classedOutput.log Normal file
View File

11
conf/development.json Normal file
View File

@@ -0,0 +1,11 @@
{
"database": {
"host": "shows.db"
},
"webserver": {
"port": 31459
},
"tmdb": {
"apiKey": "9fa154f5355c37a1b9b57ac06e7d6712"
}
}

60
moveSeasoned.py Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-04-12 23:27:51
# @Last Modified by: KevinMidboe
# @Last Modified time: 2017-04-13 16:22:23
import sys, sqlite3, json, os
import env_variables as env
class episode(object):
def __init__(self, id):
self.id = id
self.getVarsFromDB()
def getVarsFromDB(self):
c = sqlite3.connect(env.db_path).cursor()
c.execute('SELECT parent, name, season, episode, video_files, subtitles, trash FROM stray_eps WHERE id = ?', (self.id,))
returnMsg = c.fetchone()
self.parent = returnMsg[0]
self.name = returnMsg[1]
self.season = returnMsg[2]
self.episode = returnMsg[3]
self.video_files = json.loads(returnMsg[4])
self.subtitles = json.loads(returnMsg[5])
self.trash = json.loads(returnMsg[6])
c.close()
self.queries = {
'parent': [env.show_dir, self.parent],
'season': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season],
'episode': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season, \
self.name + ' S' + "%02d" % self.season + 'E' + "%02d" % self.episode],
}
def typeDir(self, dType, create=False, mergeItem=None):
url = '/'.join(self.queries[dType])
if create and not os.path.isdir(url):
os.makedirs(url)
if mergeItem:
return '/'.join([url, str(mergeItem)])
return url
def moveStray(strayId):
ep = episode(strayId)
for item in ep.video_files:
os.rename(ep.typeDir('parent', mergeItem=item[0]), ep.typeDir('episode', mergeItem=item[1], create=True))
for item in ep.subtitles:
os.rename(ep.typeDir('parent', mergeItem=item[0]), ep.typeDir('episode', mergeItem=item[1], create=True))
for item in ep.trash:
os.remove(ep.typeDir('parent', mergeItem=item))
os.rmdir(ep.typeDir('parent'))
if __name__ == '__main__':
moveStray(sys.argv[-1])

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "node-api",
"main": "src/webserver/server.js",
"scripts": {
"start": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/webserver/server.js"
},
"dependencies": {
"body-parser": "~1.0.1",
"cross-env": "^3.1.3",
"express": "~4.0.0",
"mongoose": "~3.6.13",
"morgan": "^1.8.2",
"moviedb": "^0.2.7",
"node-cache": "^4.1.1",
"python-shell": "^0.4.0",
"request": "^2.81.0",
"request-promise": "^4.2",
"sqlite": "^2.5.0"
}
}

View File

@@ -1,439 +0,0 @@
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: []

1
seasoned.md Normal file
View File

@@ -0,0 +1 @@
So to get people to sign up for a account could be to have them sign up for shows that they can be alerted when are added. They can choose by SMS, twitter, email or maybe newsletter.

View File

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

View File

@@ -1,66 +0,0 @@
# 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,26 +0,0 @@
{
"database": {
"host": "../shows.db"
},
"webserver": {
"port": 31459,
"origins": []
},
"tmdb": {
"apiKey": ""
},
"plex": {
"ip": ""
},
"tautulli": {
"apiKey": "",
"ip": "",
"port": ""
},
"raven": {
"DSN": ""
},
"authentication": {
"secret": "secret"
}
}

View File

@@ -1,20 +0,0 @@
{
"database": {
"host": ":memory:"
},
"webserver": {
"port": 31400
},
"tmdb": {
"apiKey": "bogus-api-key"
},
"plex": {
"ip": "0.0.0.0"
},
"raven": {
"DSN": ""
},
"authentication": {
"secret": "secret"
}
}

View File

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

@@ -1,44 +0,0 @@
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));

View File

@@ -1,52 +0,0 @@
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