1 Commits

Author SHA1 Message Date
c5920d060b first attempts to remember scroll position 2021-03-10 17:45:20 +01:00
98 changed files with 1702 additions and 3923 deletions

View File

@ -1,292 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
module.exports = {
env: {
es6: true
},
parserOptions: {
sourceType: 'module'
},
extends: [
'plugin:prettier/recommended', // https://github.com/prettier/eslint-plugin-prettier#recommended-configuration
'prettier'
],
plugins: ['eslint-comments', 'react', 'react-hooks', 'jest'],
settings: {
react: {
version: 'detect'
}
},
ignorePatterns: ['node_modules/', '**/*.js'],
overrides: [
{
files: ['*.ts', '*.tsx'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint/eslint-plugin'],
rules: {
'@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}],
'no-unused-vars': 'off'
}
}
],
// Map from global var to bool specifying if it can be redefined
globals: {
jest: true,
__DEV__: true,
__dirname: false,
__fbBatchedBridgeConfig: false,
AbortController: false,
alert: false,
cancelAnimationFrame: false,
cancelIdleCallback: false,
clearImmediate: true,
clearInterval: false,
clearTimeout: false,
console: false,
document: false,
ErrorUtils: false,
escape: false,
Event: false,
EventTarget: false,
exports: false,
fetch: false,
FileReader: false,
FormData: false,
global: false,
Headers: false,
Intl: false,
Map: true,
module: false,
navigator: false,
process: false,
Promise: true,
requestAnimationFrame: true,
requestIdleCallback: true,
require: false,
Set: true,
setImmediate: true,
setInterval: false,
setTimeout: false,
URL: false,
URLSearchParams: false,
WebSocket: true,
window: false,
XMLHttpRequest: false,
JSX: true,
KeyboardEvent: true,
MouseEvent: true,
Node: true,
HTMLDivElement: true,
HTMLInputElement: true
},
rules: {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "error",
// General
'comma-dangle': [1, 'never'], // allow or disallow trailing commas
'no-cond-assign': 1, // disallow assignment in conditional expressions
'no-console': 0, // disallow use of console (off by default in the node environment)
'no-const-assign': 2, // disallow assignment to const-declared variables
'no-constant-condition': 0, // disallow use of constant expressions in conditions
'no-control-regex': 1, // disallow control characters in regular expressions
'no-debugger': 1, // disallow use of debugger
'no-dupe-class-members': 2, // Disallow duplicate name in class members
'no-dupe-keys': 2, // disallow duplicate keys when creating object literals
'no-empty': 0, // disallow empty statements
'no-ex-assign': 1, // disallow assigning to the exception in a catch block
'no-extra-boolean-cast': 1, // disallow double-negation boolean casts in a boolean context
'no-extra-parens': 0, // disallow unnecessary parentheses (off by default)
'no-extra-semi': 1, // disallow unnecessary semicolons
'no-func-assign': 1, // disallow overwriting functions written as function declarations
'no-inner-declarations': 0, // disallow function or variable declarations in nested blocks
'no-invalid-regexp': 1, // disallow invalid regular expression strings in the RegExp constructor
'no-negated-in-lhs': 1, // disallow negation of the left operand of an in expression
'no-obj-calls': 1, // disallow the use of object properties of the global object (Math and JSON) as functions
'no-regex-spaces': 1, // disallow multiple spaces in a regular expression literal
'no-reserved-keys': 0, // disallow reserved words being used as object literal keys (off by default)
'no-sparse-arrays': 1, // disallow sparse arrays
'no-unreachable': 2, // disallow unreachable statements after a return, throw, continue, or break statement
'use-isnan': 1, // disallow comparisons with the value NaN
'valid-jsdoc': 0, // Ensure JSDoc comments are valid (off by default)
'valid-typeof': 1, // Ensure that the results of typeof are compared against a valid string
// Best Practices
// These are rules designed to prevent you from making mistakes. They either prescribe a better way of doing something or help you avoid footguns.
'block-scoped-var': 0, // treat var statements as if they were block scoped (off by default)
complexity: 0, // specify the maximum cyclomatic complexity allowed in a program (off by default)
'consistent-return': 0, // require return statements to either always or never specify values
curly: 1, // specify curly brace conventions for all control statements
'default-case': 0, // require default case in switch statements (off by default)
'dot-notation': 1, // encourages use of dot notation whenever possible
eqeqeq: [1, 'allow-null'], // require the use of === and !==
'guard-for-in': 0, // make sure for-in loops have an if statement (off by default)
'no-alert': 1, // disallow the use of alert, confirm, and prompt
'no-caller': 1, // disallow use of arguments.caller or arguments.callee
'no-div-regex': 1, // disallow division operators explicitly at beginning of regular expression (off by default)
'no-else-return': 0, // disallow else after a return in an if (off by default)
'no-eq-null': 0, // disallow comparisons to null without a type-checking operator (off by default)
'no-eval': 2, // disallow use of eval()
'no-extend-native': 1, // disallow adding to native types
'no-extra-bind': 1, // disallow unnecessary function binding
'no-fallthrough': 1, // disallow fallthrough of case statements
'no-floating-decimal': 1, // disallow the use of leading or trailing decimal points in numeric literals (off by default)
'no-implied-eval': 1, // disallow use of eval()-like methods
'no-labels': 1, // disallow use of labeled statements
'no-iterator': 1, // disallow usage of __iterator__ property
'no-lone-blocks': 1, // disallow unnecessary nested blocks
'no-loop-func': 0, // disallow creation of functions within loops
'no-multi-str': 0, // disallow use of multiline strings
'no-native-reassign': 0, // disallow reassignments of native objects
'no-new': 1, // disallow use of new operator when not part of the assignment or comparison
'no-new-func': 2, // disallow use of new operator for Function object
'no-new-wrappers': 1, // disallows creating new instances of String,Number, and Boolean
'no-octal': 1, // disallow use of octal literals
'no-octal-escape': 1, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251";
'no-proto': 1, // disallow usage of __proto__ property
'no-redeclare': 0, // disallow declaring the same variable more then once
'no-return-assign': 1, // disallow use of assignment in return statement
'no-script-url': 1, // disallow use of javascript: urls.
'no-self-compare': 1, // disallow comparisons where both sides are exactly the same (off by default)
'no-sequences': 1, // disallow use of comma operator
'no-unused-expressions': 0, // disallow usage of expressions in statement position
'no-useless-escape': 1, // disallow escapes that don't have any effect in literals
'no-void': 1, // disallow use of void operator (off by default)
'no-warning-comments': 0, // disallow usage of configurable warning terms in comments": 1, // e.g. TODO or FIXME (off by default)
'no-with': 1, // disallow use of the with statement
radix: 1, // require use of the second argument for parseInt() (off by default)
'semi-spacing': 1, // require a space after a semi-colon
'vars-on-top': 0, // requires to declare all vars on top of their containing scope (off by default)
'wrap-iife': 0, // require immediate function invocation to be wrapped in parentheses (off by default)
yoda: 1, // require or disallow Yoda conditions
// Variables
// These rules have to do with variable declarations.
'no-catch-shadow': 1, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment)
'no-delete-var': 1, // disallow deletion of variables
'no-label-var': 1, // disallow labels that share a name with a variable
'no-shadow': 1, // disallow declaration of variables already declared in the outer scope
'no-shadow-restricted-names': 1, // disallow shadowing of names such as arguments
'no-undef': 2, // disallow use of undeclared variables unless mentioned in a /*global */ block
'no-undefined': 0, // disallow use of undefined variable (off by default)
'no-undef-init': 1, // disallow use of undefined when initializing variables
'no-unused-vars': [1, {vars: 'all', args: 'none', ignoreRestSiblings: true}], // disallow declaration of variables that are not used in the code
'no-use-before-define': 0, // disallow use of variables before they are defined
// Node.js
// These rules are specific to JavaScript running on Node.js.
'handle-callback-err': 1, // enforces error handling in callbacks (off by default) (on by default in the node environment)
'no-mixed-requires': 1, // disallow mixing regular variable and require declarations (off by default) (on by default in the node environment)
'no-new-require': 1, // disallow use of new operator with the require function (off by default) (on by default in the node environment)
'no-path-concat': 1, // disallow string concatenation with __dirname and __filename (off by default) (on by default in the node environment)
'no-process-exit': 0, // disallow process.exit() (on by default in the node environment)
'no-restricted-modules': 1, // restrict usage of specified node modules (off by default)
'no-sync': 0, // disallow use of synchronous methods (off by default)
// ESLint Comments Plugin
// The following rules are made available via `eslint-plugin-eslint-comments`
'eslint-comments/no-aggregating-enable': 1, // disallows eslint-enable comments for multiple eslint-disable comments
'eslint-comments/no-unlimited-disable': 1, // disallows eslint-disable comments without rule names
'eslint-comments/no-unused-disable': 1, // disallow disables that don't cover any errors
'eslint-comments/no-unused-enable': 1, // // disallow enables that don't enable anything or enable rules that weren't disabled
// Stylistic Issues
// These rules are purely matters of style and are quite subjective.
'key-spacing': 0,
'keyword-spacing': 1, // enforce spacing before and after keywords
'jsx-quotes': [1, 'prefer-single'], // enforces the usage of double quotes for all JSX attribute values which doesnt contain a double quote
'comma-spacing': 0,
'no-multi-spaces': 0,
'brace-style': 0, // enforce one true brace style (off by default)
camelcase: 1, // require camel case names
'consistent-this': 1, // enforces consistent naming when capturing the current execution context (off by default)
'eol-last': 1, // enforce newline at the end of file, with no multiple empty lines
'func-names': 0, // require function expressions to have a name (off by default)
'func-style': 0, // enforces use of function declarations or expressions (off by default)
'new-cap': 0, // require a capital letter for constructors
'new-parens': 1, // disallow the omission of parentheses when invoking a constructor with no arguments
'no-nested-ternary': 0, // disallow nested ternary expressions (off by default)
'no-array-constructor': 1, // disallow use of the Array constructor
'no-empty-character-class': 1, // disallow the use of empty character classes in regular expressions
'no-lonely-if': 0, // disallow if as the only statement in an else block (off by default)
'no-new-object': 1, // disallow use of the Object constructor
'no-spaced-func': 1, // disallow space between function identifier and application
'no-ternary': 0, // disallow the use of ternary operators (off by default)
'no-trailing-spaces': 1, // disallow trailing whitespace at the end of lines
'no-underscore-dangle': 0, // disallow dangling underscores in identifiers
'no-mixed-spaces-and-tabs': 1, // disallow mixed spaces and tabs for indentation
quotes: [1, 'single', 'avoid-escape'], // specify whether double or single quotes should be used
'quote-props': 0, // require quotes around object literal property names (off by default)
semi: 1, // require or disallow use of semicolons instead of ASI
'sort-vars': 0, // sort variables within the same declaration block (off by default)
'space-in-brackets': 0, // require or disallow spaces inside brackets (off by default)
'space-in-parens': 0, // require or disallow spaces inside parentheses (off by default)
'space-infix-ops': 1, // require spaces around operators
'space-unary-ops': [1, {words: true, nonwords: false}], // require or disallow spaces before/after unary operators (words on by default, nonwords off by default)
'max-nested-callbacks': 0, // specify the maximum depth callbacks can be nested (off by default)
'one-var': 0, // allow just one var statement per function (off by default)
'wrap-regex': 0, // require regex literals to be wrapped in parentheses (off by default)
// Legacy
// The following rules are included for compatibility with JSHint and JSLint. While the names of the rules may not match up with the JSHint/JSLint counterpart, the functionality is the same.
'max-depth': 0, // specify the maximum depth that blocks can be nested (off by default)
'max-len': 0, // specify the maximum length of a line in your program (off by default)
'max-params': 0, // limits the number of parameters that can be used in the function declaration. (off by default)
'max-statements': 0, // specify the maximum number of statement allowed in a function (off by default)
'no-bitwise': 1, // disallow use of bitwise operators (off by default)
'no-plusplus': 0, // disallow use of unary operators, ++ and -- (off by default)
// React Plugin
// The following rules are made available via `eslint-plugin-react`.
'react/display-name': 0,
'react/jsx-boolean-value': 0,
'react/jsx-no-comment-textnodes': 2,
'react/jsx-no-duplicate-props': 2,
'react/jsx-no-undef': 2,
'react/jsx-sort-props': 0,
'react/jsx-uses-react': 1,
'react/jsx-uses-vars': 1,
'react/no-did-mount-set-state': 1,
'react/no-did-update-set-state': 1,
'react/no-multi-comp': 0,
'react/no-string-refs': 1,
'react/no-unknown-property': 0,
'react/prop-types': 0,
'react/react-in-jsx-scope': 1,
'react/self-closing-comp': 1,
'react/wrap-multilines': 0,
// React-Hooks Plugin
// The following rules are made available via `eslint-plugin-react-hooks`
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
// Jest Plugin
// The following rules are made available via `eslint-plugin-jest`.
'jest/no-disabled-tests': 1,
'jest/no-focused-tests': 1,
'jest/no-identical-title': 1,
'jest/valid-expect': 1
}
};

View File

@ -1,19 +1,23 @@
image: node:14 image: node:14
stages: stages:
- build_frontend - build
- build_backend
- test - test
- packaging - packaging
- deploy - deploy
include:
- template: Code-Quality.gitlab-ci.yml
variables:
SAST_DISABLE_DIND: "true"
Minimize_Frontend: Minimize_Frontend:
stage: build_frontend stage: build
before_script: before_script:
- yarn install --cache-folder .yarn - yarn install --cache-folder .yarn
script: script:
- yarn run build - yarn run build
- rm build/*/*/*.map
artifacts: artifacts:
expire_in: 2 days expire_in: 2 days
paths: paths:
@ -26,15 +30,11 @@ Minimize_Frontend:
Build_Backend: Build_Backend:
image: golang:latest image: golang:latest
stage: build_backend stage: build
script: script:
- cd apiGo - cd apiGo
- go build -v -o openmediacenter - go build -v -o openmediacenter
- cp -r ../build/ ./static/ - env GOOS=windows GOARCH=amd64 go build -v -o openmediacenter.exe
- go build -v -tags static -o openmediacenter_full
- env GOOS=windows GOARCH=amd64 go build -v -tags static -o openmediacenter.exe
needs:
- Minimize_Frontend
artifacts: artifacts:
expire_in: 2 days expire_in: 2 days
paths: paths:
@ -46,7 +46,6 @@ Frontend_Tests:
- yarn install --cache-folder .yarn - yarn install --cache-folder .yarn
script: script:
- yarn run test - yarn run test
needs: []
artifacts: artifacts:
reports: reports:
junit: junit:
@ -64,27 +63,14 @@ Backend_Tests:
- cd apiGo - cd apiGo
- go get -u github.com/jstemmer/go-junit-report - go get -u github.com/jstemmer/go-junit-report
- go test -v ./... 2>&1 | go-junit-report -set-exit-code > report.xml - go test -v ./... 2>&1 | go-junit-report -set-exit-code > report.xml
needs: []
artifacts: artifacts:
when: always when: always
reports: reports:
junit: ./apiGo/report.xml junit: ./apiGo/report.xml
lint: code_quality:
stage: test tags:
before_script: - dind
- yarn install --cache-folder .yarn
script:
- yarn run lint
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .yarn/
- ./node_modules/
artifacts:
reports:
codequality: gl-codequality.json
needs: []
Debian_Server: Debian_Server:
stage: packaging stage: packaging
@ -112,7 +98,7 @@ Debian_Server:
Test_Server: Test_Server:
stage: deploy stage: deploy
image: luki42/ssh:latest image: luki42/alpineopenssh:latest
needs: needs:
- Frontend_Tests - Frontend_Tests
- Backend_Tests - Backend_Tests
@ -121,7 +107,7 @@ Test_Server:
- master - master
script: script:
- eval $(ssh-agent -s) - eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add - - ssh-add <(echo "$SSH_PRIVATE_KEY")
- mkdir -p ~/.ssh - mkdir -p ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- scp deb/OpenMediaCenter-*.deb root@192.168.0.42:/tmp/ - scp deb/OpenMediaCenter-*.deb root@192.168.0.42:/tmp/

View File

@ -1,10 +0,0 @@
module.exports = {
bracketSpacing: false,
jsxBracketSameLine: true,
singleQuote: true,
tabWidth: 4,
trailingComma: 'none',
printWidth: 135,
semi: true,
jsxSingleQuote: true
};

View File

@ -22,30 +22,24 @@ and in dark mode:
![](https://i.ibb.co/xzhdsbJ/Screenshot-20200812-172926.png) ![](https://i.ibb.co/xzhdsbJ/Screenshot-20200812-172926.png)
## Installation ## Installation
First of all clone the repository.
Download the latest release .deb file from the Releases page and install it via `apt install ./OpenMediaCenter-0.1.x_amd64.deb` `git clone https://gitlab.heili.eu/lukas/openmediacenter.git`
Now you could optionally check if the service is up and running: `systemctl status OpenMediaCenter` Then build a production build via npm.
`npm run build`
Afterwards you can copy the content of the generated `build` folder as well as the `api` folder to your webserver root.
You need also to setup a Database with the structure described in [SQL Style Reference](https://gitlab.heili.eu/lukas/openmediacenter/-/blob/master/database.sql).
The login data to this database needs to be specified in the `api/Database.php` file.
## Usage ## Usage
Now you can access your MediaCenter via your servers global ip on port 8080 (: Now you can access your MediaCenter via your servers global ip (:
At the settings tab you can set the correct videopath on server and click reindex afterwards. At the settings tab you can set the correct videopath on server and click reindex afterwards.
## Development
Build and start the go backend:
`go build`
Start frontend dev server:
`npm start`
### Environent Variables:
`REACT_APP_CUST_BACK_DOMAIN` :: Set a custom movie domain
## Contact ## Contact
Any contribution is appreciated. Any contribution is appreciated.
Feel free to contact me (lukas.heiligenbrunner@gmail.com), open an issue or request a new feature. Feel free to contact me (lukas.heiligenbrunner@gmail.com), open an issue or request a new feature.

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"net/http" "net/http"
"openmediacenter/apiGo/api/oauth" "openmediacenter/apiGo/api/oauth"
) )
@ -15,7 +16,6 @@ const (
TagNode = iota TagNode = iota
SettingsNode = iota SettingsNode = iota
ActorNode = iota ActorNode = iota
TVShowNode = iota
) )
type actionStruct struct { type actionStruct struct {
@ -36,15 +36,17 @@ func AddHandler(action string, apiNode int, n interface{}, h func() []byte) {
handlers = append(handlers, Handler{action, h, n, apiNode}) handlers = append(handlers, Handler{action, h, n, apiNode})
} }
func ServerInit() { func ServerInit(port uint16) {
http.Handle(APIPREFIX+"/video", oauth.ValidateToken(handlefunc, VideoNode)) http.Handle(APIPREFIX+"/video", oauth.ValidateToken(videoHandler))
http.Handle(APIPREFIX+"/tags", oauth.ValidateToken(handlefunc, TagNode)) http.Handle(APIPREFIX+"/tags", oauth.ValidateToken(tagHandler))
http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(handlefunc, SettingsNode)) http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler))
http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(handlefunc, ActorNode)) http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(actorHandler))
http.Handle(APIPREFIX+"/tvshow", oauth.ValidateToken(handlefunc, TVShowNode))
// initialize oauth service and add corresponding auth routes // initialize oauth service and add corresponding auth routes
oauth.InitOAuth() oauth.InitOAuth()
fmt.Printf("OpenMediacenter server up and running on port %d\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
} }
func handleAPICall(action string, requestBody string, apiNode int) []byte { func handleAPICall(action string, requestBody string, apiNode int) []byte {
@ -67,6 +69,22 @@ func handleAPICall(action string, requestBody string, apiNode int) []byte {
return nil return nil
} }
func actorHandler(rw http.ResponseWriter, req *http.Request) {
handlefunc(rw, req, ActorNode)
}
func videoHandler(rw http.ResponseWriter, req *http.Request) {
handlefunc(rw, req, VideoNode)
}
func tagHandler(rw http.ResponseWriter, req *http.Request) {
handlefunc(rw, req, TagNode)
}
func settingsHandler(rw http.ResponseWriter, req *http.Request) {
handlefunc(rw, req, SettingsNode)
}
func handlefunc(rw http.ResponseWriter, req *http.Request, node int) { func handlefunc(rw http.ResponseWriter, req *http.Request, node int) {
// only allow post requests // only allow post requests
if req.Method != "POST" { if req.Method != "POST" {

View File

@ -61,22 +61,6 @@ func readActorsFromResultset(rows *sql.Rows) []types.Actor {
return result return result
} }
// ID - Name : pay attention to the order!
func readTVshowsFromResultset(rows *sql.Rows) []types.TVShow {
result := []types.TVShow{}
for rows.Next() {
var vid types.TVShow
err := rows.Scan(&vid.Id, &vid.Name)
if err != nil {
panic(err.Error()) // proper error handling instead of panic in your app
}
result = append(result, vid)
}
rows.Close()
return result
}
func jsonify(v interface{}) []byte { func jsonify(v interface{}) []byte {
// jsonify results // jsonify results
str, err := json.Marshal(v) str, err := json.Marshal(v)

View File

@ -2,12 +2,10 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"openmediacenter/apiGo/api/types" "openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"openmediacenter/apiGo/database/settings"
"openmediacenter/apiGo/videoparser" "openmediacenter/apiGo/videoparser"
"regexp"
"strings"
) )
func AddSettingsHandlers() { func AddSettingsHandlers() {
@ -17,38 +15,45 @@ func AddSettingsHandlers() {
} }
func getSettingsFromDB() { func getSettingsFromDB() {
AddHandler("loadGeneralSettings", SettingsNode, nil, func() []byte {
result := database.GetSettings()
return jsonify(result)
})
AddHandler("loadInitialData", SettingsNode, nil, func() []byte { AddHandler("loadInitialData", SettingsNode, nil, func() []byte {
sett := settings.LoadSettings() query := "SELECT DarkMode, password, mediacenter_name, video_path from settings"
type InitialDataTypeResponse struct { type InitialDataType struct {
DarkMode bool DarkMode int
Pasword bool Pasword int
MediacenterName string Mediacenter_name string
VideoPath string VideoPath string
TVShowPath string
} }
regexMatchUrl := regexp.MustCompile("^http(|s)://([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}:[0-9]{1,5}") result := InitialDataType{}
videoUrl := regexMatchUrl.FindString(sett.VideoPath)
tvshowurl := regexMatchUrl.FindString(sett.TVShowPath) err := database.QueryRow(query).Scan(&result.DarkMode, &result.Pasword, &result.Mediacenter_name, &result.VideoPath)
serverVideoPath := strings.TrimPrefix(sett.VideoPath, videoUrl) if err != nil {
serverTVShowPath := strings.TrimPrefix(sett.TVShowPath, tvshowurl) fmt.Println("error while parsing db data: " + err.Error())
}
type InitialDataTypeResponse struct {
DarkMode bool
Pasword bool
Mediacenter_name string
VideoPath string
}
res := InitialDataTypeResponse{ res := InitialDataTypeResponse{
DarkMode: sett.DarkMode, DarkMode: result.DarkMode != 0,
Pasword: sett.Pasword != "-1", Pasword: result.Pasword != -1,
MediacenterName: sett.MediacenterName, Mediacenter_name: result.Mediacenter_name,
VideoPath: serverVideoPath, VideoPath: result.VideoPath,
TVShowPath: serverTVShowPath,
} }
str, _ := json.Marshal(res) str, _ := json.Marshal(res)
return str return str
}) })
AddHandler("loadGeneralSettings", SettingsNode, nil, func() []byte {
result := database.GetSettings()
return jsonify(result)
})
} }
func saveSettingsToDB() { func saveSettingsToDB() {
@ -78,13 +83,12 @@ func reIndexHandling() {
return database.ManualSuccessResponse(nil) return database.ManualSuccessResponse(nil)
}) })
AddHandler("startTVShowReindex", SettingsNode, nil, func() []byte {
videoparser.StartTVShowReindex()
return database.ManualSuccessResponse(nil)
})
AddHandler("cleanupGravity", SettingsNode, nil, func() []byte { AddHandler("cleanupGravity", SettingsNode, nil, func() []byte {
videoparser.StartCleanup() videoparser.StartCleanup()
return nil return nil
}) })
AddHandler("getStatusMessage", SettingsNode, nil, func() []byte {
return jsonify(videoparser.GetStatusMessage())
})
} }

View File

@ -1,92 +0,0 @@
package api
import (
"fmt"
"openmediacenter/apiGo/database"
)
func AddTvshowHandlers() {
AddHandler("getTVShows", TVShowNode, nil, func() []byte {
query := "SELECT id, name FROM tvshow"
rows := database.Query(query)
return jsonify(readTVshowsFromResultset(rows))
})
var ge struct {
ShowID uint32
}
AddHandler("getEpisodes", TVShowNode, &ge, func() []byte {
query := fmt.Sprintf("SELECT id, name, season, episode FROM tvshow_episodes WHERE tvshow_id=%d", ge.ShowID)
rows := database.Query(query)
type Episode struct {
ID uint32
Name string
Season uint8
Episode uint8
}
episodes := []Episode{}
for rows.Next() {
var ep Episode
err := rows.Scan(&ep.ID, &ep.Name, &ep.Season, &ep.Episode)
if err != nil {
fmt.Println(err.Error())
continue
}
episodes = append(episodes, ep)
}
return jsonify(episodes)
})
var le struct {
ID uint32
}
AddHandler("loadEpisode", TVShowNode, &le, func() []byte {
query := fmt.Sprintf(`
SELECT tvshow_episodes.name, season, tvshow_id, episode, filename, t.foldername
FROM tvshow_episodes
JOIN tvshow t on t.id = tvshow_episodes.tvshow_id
WHERE tvshow_episodes.id=%d`, le.ID)
row := database.QueryRow(query)
var ret struct {
Name string
Season uint8
Episode uint8
TVShowID uint32
Path string
}
var filename string
var foldername string
err := row.Scan(&ret.Name, &ret.Season, &ret.TVShowID, &ret.Episode, &filename, &foldername)
if err != nil {
fmt.Println(err.Error())
return nil
}
ret.Path = foldername + "/" + filename
return jsonify(ret)
})
var rtn struct {
Id int
}
AddHandler("readThumbnail", TVShowNode, &rtn, func() []byte {
var pic []byte
query := fmt.Sprintf("SELECT thumbnail FROM tvshow WHERE id=%d", rtn.Id)
err := database.QueryRow(query).Scan(&pic)
if err != nil {
fmt.Printf("the thumbnail of movie id %d couldn't be found", rtn.Id)
return nil
}
return pic
})
}

View File

@ -37,7 +37,7 @@ func deleteFromDB() {
return database.ManualSuccessResponse(err) return database.ManualSuccessResponse(err)
} else { } else {
// check with regex if its the key constraint error // check with regex if its the key constraint error
r := regexp.MustCompile("^.*a foreign key constraint fails.*$") r, _ := regexp.Compile("^.*a foreign key constraint fails.*$")
if r.MatchString(err.Error()) { if r.MatchString(err.Error()) {
return []byte(`{"result":"not empty tag"}`) return []byte(`{"result":"not empty tag"}`)
} else { } else {

View File

@ -44,7 +44,7 @@ func getVideoHandlers() {
AddHandler("readThumbnail", VideoNode, &rtn, func() []byte { AddHandler("readThumbnail", VideoNode, &rtn, func() []byte {
var pic []byte var pic []byte
query := fmt.Sprintf("SELECT thumbnail FROM videos WHERE movie_id=%d", rtn.Movieid) query := fmt.Sprintf("SELECT thumbnail FROM videos WHERE movie_id='%d'", rtn.Movieid)
err := database.QueryRow(query).Scan(&pic) err := database.QueryRow(query).Scan(&pic)
if err != nil { if err != nil {
@ -227,20 +227,7 @@ func addToVideoHandlers() {
MovieId int MovieId int
} }
AddHandler("deleteVideo", VideoNode, &dv, func() []byte { AddHandler("deleteVideo", VideoNode, &dv, func() []byte {
// delete tag constraints query := fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", dv.MovieId)
query := fmt.Sprintf("DELETE FROM video_tags WHERE video_id=%d", dv.MovieId)
err := database.Edit(query)
// delete actor constraints
query = fmt.Sprintf("DELETE FROM actors_videos WHERE video_id=%d", dv.MovieId)
err = database.Edit(query)
// respond only if result not successful
if err != nil {
return database.ManualSuccessResponse(err)
}
query = fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", dv.MovieId)
return database.SuccessQuery(query) return database.SuccessQuery(query)
}) })
} }

View File

@ -1,57 +0,0 @@
package oauth
import (
"gopkg.in/oauth2.v3"
"openmediacenter/apiGo/database/settings"
)
type CustomClientStore struct {
oauth2.ClientStore
}
type CustomClientInfo struct {
oauth2.ClientInfo
ID string
Secret string
Domain string
UserID string
}
func NewCustomStore() oauth2.ClientStore {
s := new(CustomClientStore)
return s
}
func (a *CustomClientStore) GetByID(id string) (oauth2.ClientInfo, error) {
password := settings.GetPassword()
// if password not set assign default password
if password == nil {
defaultpassword := "openmediacenter"
password = &defaultpassword
}
clientinfo := CustomClientInfo{
ID: "openmediacenter",
Secret: *password,
Domain: "http://localhost:8081",
UserID: "openmediacenter",
}
return &clientinfo, nil
}
func (a *CustomClientInfo) GetID() string {
return a.ID
}
func (a *CustomClientInfo) GetSecret() string {
return a.Secret
}
func (a *CustomClientInfo) GetDomain() string {
return a.Domain
}
func (a *CustomClientInfo) GetUserID() string {
return a.UserID
}

View File

@ -3,6 +3,7 @@ package oauth
import ( import (
"gopkg.in/oauth2.v3/errors" "gopkg.in/oauth2.v3/errors"
"gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/manage"
"gopkg.in/oauth2.v3/models"
"gopkg.in/oauth2.v3/server" "gopkg.in/oauth2.v3/server"
"gopkg.in/oauth2.v3/store" "gopkg.in/oauth2.v3/store"
"log" "log"
@ -16,10 +17,15 @@ func InitOAuth() {
// token store // token store
manager.MustTokenStorage(store.NewMemoryTokenStore()) manager.MustTokenStorage(store.NewMemoryTokenStore())
// create new secretstore clientStore := store.NewClientStore()
clientStore := NewCustomStore() // todo we need to check here if a password is enabled in db -- when yes set it here!
manager.MapClientStorage(clientStore) clientStore.Set("openmediacenter", &models.Client{
ID: "openmediacenter",
Secret: "openmediacenter",
Domain: "http://localhost:8081",
})
manager.MapClientStorage(clientStore)
srv = server.NewServer(server.NewConfig(), manager) srv = server.NewServer(server.NewConfig(), manager)
srv.SetClientInfoHandler(server.ClientFormHandler) srv.SetClientInfoHandler(server.ClientFormHandler)
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg) manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)
@ -48,7 +54,7 @@ func InitOAuth() {
}) })
} }
func ValidateToken(f func(rw http.ResponseWriter, req *http.Request, node int), node int) http.HandlerFunc { func ValidateToken(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
_, err := srv.ValidationBearerToken(r) _, err := srv.ValidationBearerToken(r)
if err != nil { if err != nil {
@ -56,6 +62,6 @@ func ValidateToken(f func(rw http.ResponseWriter, req *http.Request, node int),
return return
} }
f(w, r, node) f.ServeHTTP(w, r)
} }
} }

View File

@ -7,12 +7,12 @@ type VideoUnloadedType struct {
type FullVideoType struct { type FullVideoType struct {
MovieName string MovieName string
MovieId uint32 MovieId int
MovieUrl string MovieUrl string
Poster string Poster string
Likes uint64 Likes int
Quality uint16 Quality int
Length uint16 Length int
Tags []Tag Tags []Tag
SuggestedTag []Tag SuggestedTag []Tag
Actors []Actor Actors []Actor
@ -20,22 +20,22 @@ type FullVideoType struct {
type Tag struct { type Tag struct {
TagName string TagName string
TagId uint32 TagId int
} }
type Actor struct { type Actor struct {
ActorId uint32 ActorId int
Name string Name string
Thumbnail string Thumbnail string
} }
type StartData struct { type StartData struct {
VideoNr uint32 VideoNr int
FullHdNr uint32 FullHdNr int
HDNr uint32 HDNr int
SDNr uint32 SDNr int
DifferentTags uint32 DifferentTags int
Tagged uint32 Tagged int
} }
type SettingsType struct { type SettingsType struct {
@ -47,15 +47,10 @@ type SettingsType struct {
TMDBGrabbing bool TMDBGrabbing bool
DarkMode bool DarkMode bool
VideoNr uint32 VideoNr int
DBSize float32 DBSize float32
DifferentTags uint32 DifferentTags int
TagsAdded uint32 TagsAdded int
PathPrefix string PathPrefix string
} }
type TVShow struct {
Id uint32
Name string
}

View File

@ -1,38 +0,0 @@
package settings
import (
"fmt"
"openmediacenter/apiGo/database"
)
func GetPassword() *string {
pwd := LoadSettings().Pasword
if pwd == "-1" {
return nil
} else {
return &pwd
}
}
type SettingsType struct {
DarkMode bool
Pasword string
MediacenterName string
VideoPath string
TVShowPath string
}
func LoadSettings() *SettingsType {
query := "SELECT DarkMode, password, mediacenter_name, video_path, episode_path from settings"
result := SettingsType{}
var darkmode uint8
err := database.QueryRow(query).Scan(&darkmode, &result.Pasword, &result.MediacenterName, &result.VideoPath, &result.TVShowPath)
if err != nil {
fmt.Println("error while parsing db data: " + err.Error())
}
result.DarkMode = darkmode != 0
return &result
}

View File

@ -3,7 +3,7 @@ module openmediacenter/apiGo
go 1.16 go 1.16
require ( require (
github.com/go-session/session v3.1.2+incompatible
github.com/go-sql-driver/mysql v1.5.0 github.com/go-sql-driver/mysql v1.5.0
gopkg.in/oauth2.v3 v3.12.0 gopkg.in/oauth2.v3 v3.12.0
nhooyr.io/websocket v1.8.7
) )

View File

@ -1,58 +1,55 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0 h1:QnyrPZZvPmR0AtJCxxfCtI1qN+fYpKTKJ/5opWmZ34k= github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0 h1:QnyrPZZvPmR0AtJCxxfCtI1qN+fYpKTKJ/5opWmZ34k=
github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
@ -70,16 +67,22 @@ github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2K
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE= github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY=
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -87,6 +90,7 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -94,13 +98,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@ -108,7 +107,5 @@ gopkg.in/oauth2.v3 v3.12.0 h1:yOffAPoolH/i2JxwmC+pgtnY3362iPahsDpLXfDFvNg=
gopkg.in/oauth2.v3 v3.12.0/go.mod h1:XEYgKqWX095YiPT+Aw5y3tCn+7/FMnlTFKrupgSiJ3I= gopkg.in/oauth2.v3 v3.12.0/go.mod h1:XEYgKqWX095YiPT+Aw5y3tCn+7/FMnlTFKrupgSiJ3I=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=

View File

@ -3,17 +3,12 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"log"
"net/http"
"openmediacenter/apiGo/api" "openmediacenter/apiGo/api"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"openmediacenter/apiGo/static"
"openmediacenter/apiGo/videoparser"
) )
func main() { func main() {
fmt.Println("init OpenMediaCenter server") fmt.Println("init OpenMediaCenter server")
port := 8081
db, verbose, pathPrefix := handleCommandLineArguments() db, verbose, pathPrefix := handleCommandLineArguments()
// todo some verbosity logger or sth // todo some verbosity logger or sth
@ -31,17 +26,8 @@ func main() {
api.AddSettingsHandlers() api.AddSettingsHandlers()
api.AddTagHandlers() api.AddTagHandlers()
api.AddActorsHandlers() api.AddActorsHandlers()
api.AddTvshowHandlers()
videoparser.SetupSettingsWebsocket() api.ServerInit(8081)
// add the static files
static.ServeStaticFiles()
api.ServerInit()
fmt.Printf("OpenMediacenter server up and running on port %d\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
} }
func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) { func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) {

View File

@ -1,89 +0,0 @@
// +build static
package static
import (
"embed"
"fmt"
"io/fs"
"net/http"
"net/http/httputil"
"net/url"
"openmediacenter/apiGo/database/settings"
"regexp"
"strings"
)
//go:embed build
var staticFiles embed.FS
func ServeStaticFiles() {
// http.FS can be used to create a http Filesystem
subfs, _ := fs.Sub(staticFiles, "build")
staticFS := http.FS(subfs)
fs := http.FileServer(staticFS)
// Serve static files
http.Handle("/", validatePrefix(fs))
// we need to proxy the videopath to somewhere in a standalone binary
proxyVideoURL()
}
type handler struct {
proxy *httputil.ReverseProxy
}
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.proxy.ServeHTTP(w, r)
}
func proxyVideoURL() {
conf := settings.LoadSettings()
// match base url
regexMatchUrl := regexp.MustCompile("^http(|s):\\/\\/([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}:[0-9]{1,5}")
var videoUrl *url.URL
if regexMatchUrl.MatchString(conf.VideoPath) {
fmt.Println("matches string...")
var err error
videoUrl, err = url.Parse(regexMatchUrl.FindString(conf.VideoPath))
if err != nil {
panic(err)
}
} else {
videoUrl, _ = url.Parse("http://127.0.0.1:8081")
}
director := func(req *http.Request) {
req.URL.Scheme = videoUrl.Scheme
req.URL.Host = videoUrl.Host
}
serverVideoPath := strings.TrimPrefix(conf.VideoPath, regexMatchUrl.FindString(conf.VideoPath))
reverseProxy := &httputil.ReverseProxy{Director: director}
handler := handler{proxy: reverseProxy}
http.Handle(serverVideoPath, handler)
}
// ValidatePrefix check if requested path is a file -- if not proceed with index.html
func validatePrefix(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
regex := regexp.MustCompile("\\..*$")
matchFile := regex.MatchString(r.URL.Path)
if matchFile {
h.ServeHTTP(w, r)
} else {
r2 := new(http.Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
r2.URL.Path = "/"
r2.URL.RawPath = "/"
h.ServeHTTP(w, r2)
}
})
}

View File

@ -1,6 +0,0 @@
// +build !static
package static
// add nothing on no static build
func ServeStaticFiles() {}

View File

@ -1,120 +0,0 @@
package videoparser
import (
"encoding/base64"
"encoding/json"
"fmt"
"os/exec"
"strconv"
)
func AppendMessage(message string) {
msger := TextMessage{
MessageBase: MessageBase{Action: "message"},
Message: message,
}
marshal, err := json.Marshal(msger)
if err != nil {
return
}
IndexSender.Publish(marshal)
}
func SendEvent(message string) {
msger := ReindexEvent{
MessageBase: MessageBase{Action: "reindexAction"},
Event: message,
}
marshal, err := json.Marshal(msger)
if err != nil {
return
}
IndexSender.Publish(marshal)
}
// ext dependency support check
func checkExtDependencySupport() *ExtDependencySupport {
var extDepsAvailable ExtDependencySupport
extDepsAvailable.FFMpeg = commandExists("ffmpeg")
extDepsAvailable.MediaInfo = commandExists("mediainfo")
return &extDepsAvailable
}
// check if a specific system command is available
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// parse the thumbail picture from video file
func parseFFmpegPic(path string) (*string, error) {
app := "ffmpeg"
cmd := exec.Command(app,
"-hide_banner",
"-loglevel", "panic",
"-ss", "00:04:00",
"-i", path,
"-vframes", "1",
"-q:v", "2",
"-f", "singlejpeg",
"pipe:1")
stdout, err := cmd.Output()
if err != nil {
fmt.Println(err.Error())
fmt.Println(string(err.(*exec.ExitError).Stderr))
return nil, err
}
strEncPic := base64.StdEncoding.EncodeToString(stdout)
if strEncPic == "" {
return nil, nil
}
backpic64 := fmt.Sprintf("data:image/jpeg;base64,%s", strEncPic)
return &backpic64, nil
}
func getVideoAttributes(path string) *VideoAttributes {
app := "mediainfo"
arg0 := path
arg1 := "--Output=JSON"
cmd := exec.Command(app, arg1, "-f", arg0)
stdout, err := cmd.Output()
var t struct {
Media struct {
Track []struct {
Duration string
FileSize string
Width string
}
}
}
err = json.Unmarshal(stdout, &t)
if err != nil {
fmt.Println(err.Error())
return nil
}
duration, err := strconv.ParseFloat(t.Media.Track[0].Duration, 32)
filesize, err := strconv.Atoi(t.Media.Track[0].FileSize)
width, err := strconv.Atoi(t.Media.Track[1].Width)
ret := VideoAttributes{
Duration: float32(duration),
FileSize: uint(filesize),
Width: uint(width),
}
return &ret
}

View File

@ -2,13 +2,15 @@ package videoparser
import ( import (
"database/sql" "database/sql"
"encoding/base64"
"encoding/json"
"fmt" "fmt"
"openmediacenter/apiGo/api/types" "openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"openmediacenter/apiGo/videoparser/tmdb" "openmediacenter/apiGo/videoparser/tmdb"
"os/exec"
"regexp" "regexp"
"strconv" "strconv"
"strings"
) )
var mSettings types.SettingsType var mSettings types.SettingsType
@ -39,71 +41,35 @@ func ReIndexVideos(path []string, sett types.SettingsType) {
fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg) fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg)
fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo) fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo)
// filter out those urls which are already existing in db for _, s := range path {
nonExisting := filterExisting(path)
fmt.Printf("There are %d videos not existing in db.\n", len(*nonExisting))
for _, s := range *nonExisting {
processVideo(s) processVideo(s)
} }
AppendMessage("reindex finished successfully!") AppendMessageBuffer("reindex finished successfully!")
SendEvent("stop")
contentAvailable = false
fmt.Println("Reindexing finished!") fmt.Println("Reindexing finished!")
} }
// filter those entries from array which are already existing!
func filterExisting(paths []string) *[]string {
var nameStr string
// build the query string with files on disk
for i, s := range paths {
// escape ' in url name
s = strings.Replace(s, "'", "\\'", -1)
nameStr += "SELECT '" + s + "' "
// if first index add as url
if i == 0 {
nameStr += "AS url "
}
// if not last index add union all
if i != len(paths)-1 {
nameStr += "UNION ALL "
}
}
query := fmt.Sprintf("SELECT * FROM (%s) urls WHERE urls.url NOT IN(SELECT movie_url FROM videos)", nameStr)
rows := database.Query(query)
var resultarr []string
// parse the result rows into a array
for rows.Next() {
var url string
err := rows.Scan(&url)
if err != nil {
continue
}
resultarr = append(resultarr, url)
}
rows.Close()
return &resultarr
}
func processVideo(fileNameOrig string) { func processVideo(fileNameOrig string) {
fmt.Printf("Processing %s video-", fileNameOrig) fmt.Printf("Processing %s video-", fileNameOrig)
// match the file extension // match the file extension
r := regexp.MustCompile(`\.[a-zA-Z0-9]+$`) r, _ := regexp.Compile(`\.[a-zA-Z0-9]+$`)
fileName := r.ReplaceAllString(fileNameOrig, "") fileName := r.ReplaceAllString(fileNameOrig, "")
// match the year and cut year from name
year, fileName := matchYear(fileName) year, fileName := matchYear(fileName)
fmt.Printf("The Video %s doesn't exist! Adding it to database.\n", fileName) // now we should look if this video already exists in db
addVideo(fileName, fileNameOrig, year) query := "SELECT * FROM videos WHERE movie_name = ?"
err := database.QueryRow(query, fileName).Scan()
if err == sql.ErrNoRows {
fmt.Printf("The Video %s does't exist! Adding it to database.\n", fileName)
addVideo(fileName, fileNameOrig, year)
} else {
fmt.Println(" :existing!")
}
} }
// add a video to the database // add a video to the database
@ -121,7 +87,7 @@ func addVideo(videoName string, fileName string, year int) {
} }
if mExtDepsAvailable.FFMpeg { if mExtDepsAvailable.FFMpeg {
ppic, err = parseFFmpegPic(mSettings.VideoPath + fileName) ppic, err = parseFFmpegPic(fileName)
if err != nil { if err != nil {
fmt.Printf("FFmpeg error occured: %s\n", err.Error()) fmt.Printf("FFmpeg error occured: %s\n", err.Error())
} else { } else {
@ -130,7 +96,7 @@ func addVideo(videoName string, fileName string, year int) {
} }
if mExtDepsAvailable.MediaInfo { if mExtDepsAvailable.MediaInfo {
atr := getVideoAttributes(mSettings.VideoPath + fileName) atr := getVideoAttributes(fileName)
if atr != nil { if atr != nil {
vidAtr = atr vidAtr = atr
} }
@ -164,18 +130,17 @@ func addVideo(videoName string, fileName string, year int) {
insertTMDBTags(tmdbData.GenreIds, insertId) insertTMDBTags(tmdbData.GenreIds, insertId)
} }
AppendMessage(fmt.Sprintf("%s - added!", videoName)) AppendMessageBuffer(fmt.Sprintf("%s - added!", videoName))
} }
func matchYear(fileName string) (int, string) { func matchYear(fileName string) (int, string) {
r := regexp.MustCompile(`\([0-9]{4}?\)`) r, _ := regexp.Compile(`\([0-9]{4}?\)`)
years := r.FindAllString(fileName, -1) years := r.FindAllString(fileName, -1)
if len(years) == 0 { if len(years) == 0 {
return -1, fileName return -1, fileName
} }
yearStr := years[len(years)-1]
// get last year occurance and cut first and last char year, err := strconv.Atoi(years[len(years)-1])
year, err := strconv.Atoi(yearStr[1 : len(yearStr)-1])
if err != nil { if err != nil {
return -1, fileName return -1, fileName
@ -185,6 +150,91 @@ func matchYear(fileName string) (int, string) {
return year, r.ReplaceAllString(fileName, "") return year, r.ReplaceAllString(fileName, "")
} }
// parse the thumbail picture from video file
func parseFFmpegPic(fileName string) (*string, error) {
app := "ffmpeg"
cmd := exec.Command(app,
"-hide_banner",
"-loglevel", "panic",
"-ss", "00:04:00",
"-i", mSettings.VideoPath+fileName,
"-vframes", "1",
"-q:v", "2",
"-f", "singlejpeg",
"pipe:1")
stdout, err := cmd.Output()
if err != nil {
fmt.Println(err.Error())
fmt.Println(string(err.(*exec.ExitError).Stderr))
return nil, err
}
backpic64 := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(stdout)
return &backpic64, nil
}
func getVideoAttributes(fileName string) *VideoAttributes {
app := "mediainfo"
arg0 := mSettings.VideoPath + fileName
arg1 := "--Output=JSON"
cmd := exec.Command(app, arg1, "-f", arg0)
stdout, err := cmd.Output()
var t struct {
Media struct {
Track []struct {
Duration string
FileSize string
Width string
}
}
}
err = json.Unmarshal(stdout, &t)
if err != nil {
fmt.Println(err.Error())
return nil
}
duration, err := strconv.ParseFloat(t.Media.Track[0].Duration, 32)
filesize, err := strconv.Atoi(t.Media.Track[0].FileSize)
width, err := strconv.Atoi(t.Media.Track[1].Width)
ret := VideoAttributes{
Duration: float32(duration),
FileSize: uint(filesize),
Width: uint(width),
}
return &ret
}
func AppendMessageBuffer(message string) {
messageBuffer = append(messageBuffer, message)
}
// ext dependency support check
func checkExtDependencySupport() *ExtDependencySupport {
var extDepsAvailable ExtDependencySupport
extDepsAvailable.FFMpeg = commandExists("ffmpeg")
extDepsAvailable.MediaInfo = commandExists("mediainfo")
return &extDepsAvailable
}
// check if a specific system command is available
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// insert the default size tags to corresponding video // insert the default size tags to corresponding video
func insertSizeTag(width uint, videoId uint) { func insertSizeTag(width uint, videoId uint) {
var tagType uint var tagType uint

View File

@ -1,138 +0,0 @@
package videoparser
import (
"fmt"
"openmediacenter/apiGo/database"
"openmediacenter/apiGo/videoparser/tmdb"
"regexp"
"strconv"
"strings"
)
func startTVShowReindex(files []Show) {
allTVshows := getAllTVShows()
for _, file := range files {
// insert new TVShow entry if not existing.
insertShowIfNotExisting(file, allTVshows)
AppendMessage("Processing show: " + file.Name)
insertEpisodesIfNotExisting(file)
}
AppendMessage("reindex finished successfully!")
SendEvent("stop")
}
func insertEpisodesIfNotExisting(show Show) {
query := "SELECT tvshow_episodes.name, season, episode FROM tvshow_episodes JOIN tvshow t on t.id = tvshow_episodes.tvshow_id WHERE t.name=?"
rows := database.Query(query, show.Name)
var dbepisodes []string
for rows.Next() {
var epname string
var season int
var episode int
err := rows.Scan(&epname, &season, &episode)
if err != nil {
fmt.Println(err.Error())
}
dbepisodes = append(dbepisodes, fmt.Sprintf("%s S%02dE%02d.mp4", epname, season, episode))
}
// get those episodes that are missing in db
diff := difference(show.files, dbepisodes)
for _, s := range diff {
AppendMessage("Adding Episode: " + s)
insertEpisode(s, show.Name)
}
}
func insertEpisode(path string, ShowName string) {
seasonRegex := regexp.MustCompile("S[0-9][0-9]")
episodeRegex := regexp.MustCompile("E[0-9][0-9]")
matchENDPattern := regexp.MustCompile(" S[0-9][0-9]E[0-9][0-9].+$")
seasonStr := seasonRegex.FindString(path)
episodeStr := episodeRegex.FindString(path)
extString := matchENDPattern.FindString(path)
// handle invalid matches
if len(seasonStr) != 3 || len(episodeStr) != 3 || len(extString) < 8 {
fmt.Printf("Error inserting episode: %s -- %s/%s/%s\n", path, seasonStr, episodeStr, extString)
return
}
name := strings.TrimSuffix(path, extString)
season, err := strconv.ParseInt(seasonStr[1:], 10, 8)
episode, err := strconv.ParseInt(episodeStr[1:], 10, 8)
if err != nil {
fmt.Println(err.Error())
}
query := `
INSERT INTO tvshow_episodes (name, season, poster, tvshow_id, episode, filename)
VALUES (?, ?, ?, (SELECT tvshow.id FROM tvshow WHERE tvshow.name=?), ?, ?)`
err = database.Edit(query, name, season, "", ShowName, episode, path)
if err != nil {
fmt.Println(err.Error())
}
}
// difference returns the elements in `a` that aren't in `b`.
func difference(a, b []string) []string {
mb := make(map[string]struct{}, len(b))
for _, x := range b {
mb[x] = struct{}{}
}
var diff []string
for _, x := range a {
if _, found := mb[x]; !found {
diff = append(diff, x)
}
}
return diff
}
func insertShowIfNotExisting(show Show, allShows *[]string) {
// if show already exists return
fmt.Println(*allShows)
for _, s := range *allShows {
if s == show.Name {
return
}
}
// insert empty thubnail if tmdb fails
thubnail := ""
// load tmdb infos
tmdbInfo := tmdb.SearchTVShow(show.Name)
if tmdbInfo != nil {
thubnail = tmdbInfo.Thumbnail
}
// currently the foldernamme == name which mustn't necessarily be
query := "INSERT INTO tvshow (name, thumbnail, foldername) VALUES (?, ?, ?)"
err := database.Edit(query, show.Name, thubnail, show.Name)
if err != nil {
fmt.Println(err.Error())
}
}
func getAllTVShows() *[]string {
query := "SELECT name FROM tvshow"
rows := database.Query(query)
var res []string
for rows.Next() {
var show string
rows.Scan(&show)
res = append(res, show)
}
return &res
}

View File

@ -1,13 +0,0 @@
package videoparser
import "testing"
func TestDifference(t *testing.T) {
arr1 := []string{"test1", "test2", "test3"}
arr2 := []string{"test1", "test3"}
res := difference(arr1, arr2)
if len(res) != 1 || res[0] != "test2" {
t.Errorf("wrong difference result.")
}
}

View File

@ -2,22 +2,25 @@ package videoparser
import ( import (
"fmt" "fmt"
"io/ioutil"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
) )
var messageBuffer []string
var contentAvailable = false
type StatusMessage struct { type StatusMessage struct {
Messages []string Messages []string
ContentAvailable bool ContentAvailable bool
} }
func StartReindex() bool { func StartReindex() bool {
messageBuffer = []string{}
contentAvailable = true
fmt.Println("starting reindex..") fmt.Println("starting reindex..")
SendEvent("start")
AppendMessage("starting reindex..")
mSettings := database.GetSettings() mSettings := database.GetSettings()
// add the path prefix to videopath // add the path prefix to videopath
@ -26,8 +29,6 @@ func StartReindex() bool {
// check if path even exists // check if path even exists
if _, err := os.Stat(mSettings.VideoPath); os.IsNotExist(err) { if _, err := os.Stat(mSettings.VideoPath); os.IsNotExist(err) {
fmt.Println("Reindex path doesn't exist!") fmt.Println("Reindex path doesn't exist!")
AppendMessage(fmt.Sprintf("Reindex path doesn't exist! :%s", mSettings.VideoPath))
SendEvent("stop")
return false return false
} }
@ -48,77 +49,20 @@ func StartReindex() bool {
fmt.Println(err.Error()) fmt.Println(err.Error())
} }
// start reindex process // start reindex process
AppendMessage("Starting Reindexing!") AppendMessageBuffer("Starting Reindexing!")
go ReIndexVideos(files, mSettings) go ReIndexVideos(files, mSettings)
return true return true
} }
type Show struct { func GetStatusMessage() *StatusMessage {
Name string msg := StatusMessage{
files []string Messages: messageBuffer,
} ContentAvailable: contentAvailable,
// StartTVShowReindex reindex dir walks for TVShow reindex
func StartTVShowReindex() {
fmt.Println("starting tvshow reindex..")
SendEvent("start")
AppendMessage("starting tvshow reindex...")
mSettings := database.GetSettings()
// add the path prefix to videopath
mSettings.EpisodePath = mSettings.PathPrefix + mSettings.EpisodePath
// add slash suffix if not existing
if !strings.HasSuffix(mSettings.EpisodePath, "/") {
mSettings.EpisodePath += "/"
} }
// check if path even exists messageBuffer = []string{}
if _, err := os.Stat(mSettings.EpisodePath); os.IsNotExist(err) {
msg := fmt.Sprintf("Reindex path doesn't exist! :%s", mSettings.EpisodePath)
fmt.Println(msg)
AppendMessage(msg)
SendEvent("stop")
return
}
var files []Show return &msg
filess, err := ioutil.ReadDir(mSettings.EpisodePath)
if err != nil {
fmt.Println(err.Error())
}
for _, file := range filess {
if file.IsDir() {
elem := Show{
Name: file.Name(),
files: nil,
}
fmt.Println(file.Name())
episodefiles, err := ioutil.ReadDir(mSettings.EpisodePath + file.Name())
if err != nil {
fmt.Println(err.Error())
}
for _, epfile := range episodefiles {
if strings.HasSuffix(epfile.Name(), ".mp4") {
elem.files = append(elem.files, epfile.Name())
}
}
files = append(files, elem)
}
}
if err != nil {
fmt.Println(err.Error())
}
// start reindex process
AppendMessage("Starting Reindexing!")
go startTVShowReindex(files)
} }
func StartCleanup() { func StartCleanup() {

View File

@ -1,135 +0,0 @@
package videoparser
import (
"context"
"errors"
"fmt"
"net/http"
"nhooyr.io/websocket"
"sync"
"time"
)
// subscriber represents a subscriber.
// Messages are sent on the msgs channel and if the client
// cannot keep up with the messages, closeSlow is called.
type subscriber struct {
msgs chan []byte
closeSlow func()
}
type ChatSender struct {
subscribersMu sync.Mutex
subscribers map[*subscriber]struct{}
}
func newChatSender() *ChatSender {
return &ChatSender{
subscribers: make(map[*subscriber]struct{}),
}
}
func (t *ChatSender) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
OriginPatterns: []string{"*"},
})
if err != nil {
fmt.Println(err.Error())
return
}
defer c.Close(websocket.StatusInternalError, "")
err = t.subscribe(r.Context(), c)
if errors.Is(err, context.Canceled) {
return
}
if websocket.CloseStatus(err) == websocket.StatusNormalClosure ||
websocket.CloseStatus(err) == websocket.StatusGoingAway {
return
}
if err != nil {
fmt.Println(err.Error())
return
}
}
func (t *ChatSender) subscribe(ctx context.Context, c *websocket.Conn) error {
ctx = c.CloseRead(ctx)
s := &subscriber{
msgs: make(chan []byte, 16),
closeSlow: func() {
c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages")
},
}
t.addSubscriber(s)
defer t.deleteSubscriber(s)
for {
select {
case msg := <-s.msgs:
err := writeTimeout(ctx, time.Second*5, c, msg)
if err != nil {
return err
}
case <-ctx.Done():
return ctx.Err()
}
}
}
type MessageBase struct {
Action string
}
type TextMessage struct {
MessageBase
Message string
}
type ReindexEvent struct {
MessageBase
Event string
}
func (t *ChatSender) Publish(msg []byte) {
t.subscribersMu.Lock()
defer t.subscribersMu.Unlock()
for s := range t.subscribers {
select {
case s.msgs <- msg:
default:
go s.closeSlow()
}
}
}
var IndexSender = newChatSender()
func SetupSettingsWebsocket() {
http.Handle("/subscribe", IndexSender)
}
// addSubscriber registers a subscriber.
func (t *ChatSender) addSubscriber(s *subscriber) {
t.subscribersMu.Lock()
t.subscribers[s] = struct{}{}
t.subscribersMu.Unlock()
}
// deleteSubscriber deletes the given subscriber.
func (t *ChatSender) deleteSubscriber(s *subscriber) {
t.subscribersMu.Lock()
delete(t.subscribers, s)
t.subscribersMu.Unlock()
}
func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
return c.Write(ctx, websocket.MessageText, msg)
}

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
) )
@ -21,43 +20,21 @@ type VideoTMDB struct {
GenreIds []int GenreIds []int
} }
type TVShowTMDB struct {
Thumbnail string
Overview string
GenreIds []int
}
type tmdbVidResult struct { type tmdbVidResult struct {
PosterPath string `json:"poster_path"` Poster_path string
Adult bool `json:"adult"` Adult bool
Overview string `json:"overview"` Overview string
ReleaseDate string `json:"release_date"` Release_date string
GenreIds []int `json:"genre_ids"` Genre_ids []int
Id int `json:"id"` Id int
OriginalTitle string `json:"original_title"` Original_title string
OriginalLanguage string `json:"original_language"` Original_language string
Title string `json:"title"` Title string
BackdropPath string `json:"backdrop_path"` Backdrop_path string
Popularity int `json:"popularity"` Popularity int
VoteCount int `json:"vote_count"` Vote_count int
Video bool `json:"video"` Video bool
VoteAverage int `json:"vote_average"` Vote_average int
}
type tmdbTvResult struct {
PosterPath string `json:"poster_path"`
Popularity int `json:"popularity"`
Id int `json:"id"`
BackdropPath string `json:"backdrop_path"`
VoteAverage int `json:"vote_average"`
Overview string `json:"overview"`
FirstAirDate string `json:"first_air_date"`
OriginCountry []string `json:"origin_country"`
GenreIds []int `json:"genre_ids"`
OriginalLanguage string `json:"original_language"`
VoteCount int `json:"vote_count"`
Name string `json:"name"`
OriginalName string `json:"original_name"`
} }
type TMDBGenre struct { type TMDBGenre struct {
@ -66,9 +43,8 @@ type TMDBGenre struct {
} }
func SearchVideo(MovieName string, year int) *VideoTMDB { func SearchVideo(MovieName string, year int) *VideoTMDB {
fmt.Printf("Searching TMDB for: Moviename: %s, year:%d \n", MovieName, year) url := fmt.Sprintf("%ssearch/movie?api_key=%s&query=%s", baseUrl, apiKey, MovieName)
queryURL := fmt.Sprintf("%ssearch/movie?api_key=%s&query=%s", baseUrl, apiKey, url.QueryEscape(MovieName)) resp, err := http.Get(url)
resp, err := http.Get(queryURL)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
return nil return nil
@ -87,16 +63,11 @@ func SearchVideo(MovieName string, year int) *VideoTMDB {
fmt.Println(len(t.Results)) fmt.Println(len(t.Results))
// if there was no match with tmdb return 0
if len(t.Results) == 0 {
return nil
}
var tmdbVid tmdbVidResult var tmdbVid tmdbVidResult
if year != -1 { if year != -1 {
for _, result := range t.Results { for _, result := range t.Results {
r, _ := regexp.Compile(fmt.Sprintf(`^%d-[0-9]{2}?-[0-9]{2}?$`, year)) r, _ := regexp.Compile(fmt.Sprintf(`^%d-[0-9]{2}?-[0-9]{2}?$`, year))
if r.MatchString(result.ReleaseDate) { if r.MatchString(result.Release_date) {
tmdbVid = result tmdbVid = result
// continue parsing // continue parsing
goto cont goto cont
@ -111,61 +82,22 @@ func SearchVideo(MovieName string, year int) *VideoTMDB {
// continue label // continue label
cont: cont:
thumbnail := fetchPoster(tmdbVid.PosterPath) thumbnail := fetchPoster(tmdbVid)
result := VideoTMDB{ result := VideoTMDB{
Thumbnail: *thumbnail, Thumbnail: *thumbnail,
Overview: tmdbVid.Overview, Overview: tmdbVid.Overview,
Title: tmdbVid.Title, Title: tmdbVid.Title,
GenreIds: tmdbVid.GenreIds, GenreIds: tmdbVid.Genre_ids,
} }
return &result return &result
} }
func SearchTVShow(Name string) *TVShowTMDB { func fetchPoster(vid tmdbVidResult) *string {
fmt.Printf("Searching TMDB for: TVShow: %s\n", Name) url := fmt.Sprintf("%s%s", pictureBase, vid.Poster_path)
queryURL := fmt.Sprintf("%ssearch/tv?api_key=%s&query=%s", baseUrl, apiKey, url.QueryEscape(Name))
resp, err := http.Get(queryURL)
if err != nil {
fmt.Println(err.Error())
return nil
}
body, err := ioutil.ReadAll(resp.Body) resp, err := http.Get(url)
if err != nil {
fmt.Println(err.Error())
return nil
}
var t struct {
Results []tmdbTvResult `json:"results"`
}
err = json.Unmarshal(body, &t)
fmt.Println(len(t.Results))
if len(t.Results) == 0 {
return nil
}
res := TVShowTMDB{
Thumbnail: "",
Overview: t.Results[0].Overview,
GenreIds: t.Results[0].GenreIds,
}
thumbnail := fetchPoster(t.Results[0].PosterPath)
if thumbnail != nil {
res.Thumbnail = *thumbnail
}
return &res
}
func fetchPoster(posterPath string) *string {
posterURL := fmt.Sprintf("%s%s", pictureBase, posterPath)
resp, err := http.Get(posterURL)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
return nil return nil
@ -184,8 +116,8 @@ func fetchPoster(posterPath string) *string {
var tmdbGenres *[]TMDBGenre var tmdbGenres *[]TMDBGenre
func fetchGenres() *[]TMDBGenre { func fetchGenres() *[]TMDBGenre {
posterURL := fmt.Sprintf("%sgenre/movie/list?api_key=%s", baseUrl, apiKey) url := fmt.Sprintf("%sgenre/movie/list?api_key=%s", baseUrl, apiKey)
resp, err := http.Get(posterURL) resp, err := http.Get(url)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
return nil return nil

View File

@ -9,12 +9,12 @@ create table if not exists actors
create table if not exists settings create table if not exists settings
( (
video_path varchar(255) null, video_path varchar(255) null,
episode_path varchar(255) null, episode_path varchar(255) null,
password varchar(32) default '-1' null, password varchar(32) null,
mediacenter_name varchar(32) default 'OpenMediaCenter' null, mediacenter_name varchar(32) null,
TMDB_grabbing tinyint null, TMDB_grabbing tinyint null,
DarkMode tinyint default 0 null DarkMode tinyint default 0 null
); );
create table if not exists tags create table if not exists tags
@ -24,41 +24,18 @@ create table if not exists tags
tag_name varchar(50) null tag_name varchar(50) null
); );
create table if not exists tvshow
(
name varchar(100) null,
thumbnail mediumblob null,
id int auto_increment
primary key,
foldername varchar(100) null
);
create table if not exists tvshow_episodes
(
id int auto_increment
primary key,
name varchar(100) null,
season int null,
poster mediumblob null,
tvshow_id int null,
episode int null,
filename varchar(100) null,
constraint tvshow_episodes_tvshow_id_fk
foreign key (tvshow_id) references tvshow (id)
);
create table if not exists videos create table if not exists videos
( (
movie_id int auto_increment movie_id int auto_increment
primary key, primary key,
movie_name varchar(200) null, movie_name varchar(200) null,
movie_url varchar(250) null, movie_url varchar(250) null,
thumbnail mediumblob null, thumbnail mediumblob null,
poster mediumblob null, likes int default 0 null,
likes int default 0 null, create_date datetime default CURRENT_TIMESTAMP null,
quality int null, quality int null,
length int null comment 'in seconds', length int null comment 'in seconds',
create_date datetime default current_timestamp() null poster mediumblob null
); );
create table if not exists actors_videos create table if not exists actors_videos
@ -71,10 +48,10 @@ create table if not exists actors_videos
foreign key (video_id) references videos (movie_id) foreign key (video_id) references videos (movie_id)
); );
create index if not exists actors_videos_actor_id_index create index actors_videos_actor_id_index
on actors_videos (actor_id); on actors_videos (actor_id);
create index if not exists actors_videos_video_id_index create index actors_videos_video_id_index
on actors_videos (video_id); on actors_videos (video_id);
create table if not exists video_tags create table if not exists video_tags

View File

@ -18,4 +18,4 @@ chown -R www-data:www-data /var/www/openmediacenter
systemctl restart nginx systemctl restart nginx
systemctl enable OpenMediaCenter.service systemctl enable OpenMediaCenter.service
systemctl restart OpenMediaCenter.service systemctl start OpenMediaCenter.service

View File

@ -16,11 +16,4 @@ server {
location ~* ^/(api/|token) { location ~* ^/(api/|token) {
proxy_pass http://127.0.0.1:8081; proxy_pass http://127.0.0.1:8081;
} }
location /subscribe {
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
} }

View File

@ -1,7 +1,8 @@
{ {
"name": "openmediacenter", "name": "openmediacenter",
"version": "0.1.3", "version": "0.1.2",
"private": true, "private": true,
"main": "public/electron.js",
"author": { "author": {
"email": "lukas.heiligenbrunner@gmail.com", "email": "lukas.heiligenbrunner@gmail.com",
"name": "Lukas Heiligenbrunner", "name": "Lukas Heiligenbrunner",
@ -23,9 +24,8 @@
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "CI=false react-scripts build", "build": "react-scripts build",
"test": "CI=true react-scripts test --reporters=jest-junit --verbose --silent --coverage --reporters=default", "test": "CI=true react-scripts test --reporters=jest-junit --verbose --silent --coverage --reporters=default"
"lint": "eslint --format gitlab src/"
}, },
"jest": { "jest": {
"collectCoverageFrom": [ "collectCoverageFrom": [
@ -39,6 +39,23 @@
}, },
"proxy": "http://127.0.0.1:8081", "proxy": "http://127.0.0.1:8081",
"homepage": "/", "homepage": "/",
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"overrides": [
{
"files": [
"**/*.ts?(x)"
],
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "error"
}
}
]
},
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
@ -61,21 +78,9 @@
"@types/react-dom": "^17.0.1", "@types/react-dom": "^17.0.1",
"@types/react-router": "5.1.12", "@types/react-router": "5.1.12",
"@types/react-router-dom": "^5.1.6", "@types/react-router-dom": "^5.1.6",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5", "enzyme-adapter-react-16": "^1.15.5",
"eslint": "^7.22.0",
"eslint-config-prettier": "^8.1.0",
"eslint-formatter-gitlab": "^2.2.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-jest": "^24.3.1",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"jest-junit": "^12.0.0", "jest-junit": "^12.0.0",
"prettier": "^2.2.1",
"prettier-config": "^1.0.0",
"react-scripts": "4.0.3" "react-scripts": "4.0.3"
} }
} }

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -2,14 +2,14 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/logo_circle.png" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="A Application to run a Mediacenter in your local network" content="A Application to run a Mediacenter in your local network"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo_circle.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -3,9 +3,19 @@
"name": "Create React App Sample", "name": "Create React App Sample",
"icons": [ "icons": [
{ {
"src": "logo_circle.png", "src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
} }
], ],
"start_url": ".", "start_url": ".",

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import App from './App'; import App from './App';
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import GlobalInfos from "./utils/GlobalInfos";
describe('<App/>', function () { describe('<App/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
@ -11,37 +10,34 @@ describe('<App/>', function () {
it('renders title', () => { it('renders title', () => {
const wrapper = shallow(<App/>); const wrapper = shallow(<App/>);
wrapper.setState({password: false});
expect(wrapper.find('.navbrand').text()).toBe('OpenMediaCenter'); expect(wrapper.find('.navbrand').text()).toBe('OpenMediaCenter');
}); });
it('are navlinks correct', function () { it('are navlinks correct', function () {
const wrapper = shallow(<App/>); const wrapper = shallow(<App/>);
wrapper.setState({password: false}); expect(wrapper.find('.navitem')).toHaveLength(4);
expect(wrapper.find('.navitem')).toHaveLength(5);
}); });
it('test initial fetch from api', done => { it('test initial fetch from api', done => {
callAPIMock({ global.fetch = global.prepareFetchApi({
MediacenterName: 'testname' generalSettingsLoaded: true,
}) passwordsupport: true,
mediacentername: 'testname'
GlobalInfos.enableDarkTheme = jest.fn((r) => {}) });
const wrapper = shallow(<App/>); const wrapper = shallow(<App/>);
const func = jest.fn();
wrapper.instance().setState = func;
expect(global.fetch).toBeCalledTimes(1);
process.nextTick(() => { process.nextTick(() => {
expect(document.title).toBe('testname'); expect(func).toBeCalledTimes(1);
global.fetch.mockClear(); global.fetch.mockClear();
done(); done();
}); });
}); });
it('test render of password page', function () {
const wrapper = shallow(<App/>);
wrapper.setState({password: true});
expect(wrapper.find('AuthenticationPage')).toHaveLength(1);
});
}); });

View File

@ -10,21 +10,19 @@ import style from './App.module.css';
import SettingsPage from './pages/SettingsPage/SettingsPage'; import SettingsPage from './pages/SettingsPage/SettingsPage';
import CategoryPage from './pages/CategoryPage/CategoryPage'; import CategoryPage from './pages/CategoryPage/CategoryPage';
import {APINode, callAPI} from './utils/Api'; import {APINode, callAPI} from './utils/Api';
import {NoBackendConnectionPopup} from './elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup';
import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom'; import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom';
import Player from './pages/Player/Player'; import Player from './pages/Player/Player';
import ActorOverviewPage from './pages/ActorOverviewPage/ActorOverviewPage'; import ActorOverviewPage from './pages/ActorOverviewPage/ActorOverviewPage';
import ActorPage from './pages/ActorPage/ActorPage'; import ActorPage from './pages/ActorPage/ActorPage';
import {SettingsTypes} from './types/ApiTypes'; import {SettingsTypes} from './types/ApiTypes';
import AuthenticationPage from './pages/AuthenticationPage/AuthenticationPage';
import TVShowPage from './pages/TVShowPage/TVShowPage';
import TVPlayer from './pages/TVShowPage/TVPlayer';
import {CookieTokenStore} from './utils/TokenStore/CookieTokenStore';
import {token} from './utils/TokenHandler';
interface state { interface state {
password: boolean | null; // null if uninitialized - true if pwd needed false if not needed generalSettingsLoaded: boolean;
passwordsupport: boolean;
mediacentername: string; mediacentername: string;
onapierror: boolean;
} }
/** /**
@ -33,48 +31,11 @@ interface state {
class App extends React.Component<{}, state> { class App extends React.Component<{}, state> {
constructor(props: {}) { constructor(props: {}) {
super(props); super(props);
token.init(new CookieTokenStore());
let pwdneeded: boolean | null = null;
if (token.apiTokenValid()) {
pwdneeded = false;
} else {
token.refreshAPIToken((err) => {
if (err === 'invalid_client') {
this.setState({password: true});
} else if (err === '') {
this.setState({password: false});
} else {
console.log('unimplemented token error: ' + err);
}
});
}
this.state = { this.state = {
generalSettingsLoaded: false,
passwordsupport: false,
mediacentername: 'OpenMediaCenter', mediacentername: 'OpenMediaCenter',
password: pwdneeded onapierror: false
};
// force an update on theme change
GlobalInfos.onThemeChange(() => {
this.forceUpdate();
});
// set the hook to load passwordfield on global func call
GlobalInfos.loadPasswordPage = (callback?: () => void): void => {
// try refreshing the token
token.refreshAPIToken((err) => {
if (err !== '') {
this.setState({password: true});
} else {
// call callback if request was successful
if (callback) {
callback();
}
}
}, true);
}; };
} }
@ -84,13 +45,18 @@ class App extends React.Component<{}, state> {
// set theme // set theme
GlobalInfos.enableDarkTheme(result.DarkMode); GlobalInfos.enableDarkTheme(result.DarkMode);
GlobalInfos.setVideoPaths(result.VideoPath, result.TVShowPath); GlobalInfos.setVideoPath(result.VideoPath);
this.setState({ this.setState({
mediacentername: result.MediacenterName generalSettingsLoaded: true,
passwordsupport: result.Password,
mediacentername: result.Mediacenter_name,
onapierror: false
}); });
// set tab title to received mediacenter name // set tab title to received mediacenter name
document.title = result.MediacenterName; document.title = result.Mediacenter_name;
}, error => {
this.setState({onapierror: true});
}); });
} }
@ -98,100 +64,63 @@ class App extends React.Component<{}, state> {
this.initialAPICall(); this.initialAPICall();
} }
render(): JSX.Element { render(): JSX.Element {
// add the main theme to the page body
document.body.className = GlobalInfos.getThemeStyle().backgroundcolor;
if (this.state.password === true) {
// render authentication page if auth is neccessary
return (
<AuthenticationPage
onSuccessLogin={(): void => {
this.setState({password: false});
// reinit general infos
this.initialAPICall();
}}
/>
);
} else if (this.state.password === false) {
return (
<Router>
<div className={style.app}>
{this.navBar()}
{this.routing()}
</div>
</Router>
);
} else {
return <>still loading...</>;
}
}
/**
* render the top navigation bar
*/
navBar(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
// add the main theme to the page body
document.body.className = themeStyle.backgroundcolor;
return ( return (
<div className={[style.navcontainer, themeStyle.backgroundcolor, themeStyle.textcolor, themeStyle.hrcolor].join(' ')}> <Router>
<div className={style.navbrand}>{this.state.mediacentername}</div> <div className={style.app}>
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/'} activeStyle={{opacity: '0.85'}}> <div className={[style.navcontainer, themeStyle.backgroundcolor, themeStyle.textcolor, themeStyle.hrcolor].join(' ')}>
Home <div className={style.navbrand}>{this.state.mediacentername}</div>
</NavLink> <NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/'} activeStyle={{opacity: '0.85'}}>Home</NavLink>
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/random'} activeStyle={{opacity: '0.85'}}> <NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/random'} activeStyle={{opacity: '0.85'}}>Random
Random Video Video</NavLink>
</NavLink>
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/categories'} activeStyle={{opacity: '0.85'}}> <NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/categories'} activeStyle={{opacity: '0.85'}}>Categories</NavLink>
Categories <NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/settings'} activeStyle={{opacity: '0.85'}}>Settings</NavLink>
</NavLink> </div>
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/tvshows'} activeStyle={{opacity: '0.85'}}> {this.routing()}
TV Shows </div>
</NavLink> {this.state.onapierror ? this.ApiError() : null}
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/settings'} activeStyle={{opacity: '0.85'}}> </Router>
Settings
</NavLink>
</div>
); );
} }
/**
* render the react router elements
*/
routing(): JSX.Element { routing(): JSX.Element {
return ( return (
<Switch> <Switch>
<Route path='/random'> <Route path="/random">
<RandomPage /> <RandomPage/>
</Route> </Route>
<Route path='/categories'> <Route path="/categories">
<CategoryPage /> <CategoryPage/>
</Route> </Route>
<Route path='/tvshows'> <Route path="/settings">
<TVShowPage /> <SettingsPage/>
</Route> </Route>
<Route path='/settings'> <Route exact path="/player/:id">
<SettingsPage /> <Player/>
</Route> </Route>
<Route exact path='/player/:id'> <Route exact path="/actors">
<Player /> <ActorOverviewPage/>
</Route> </Route>
<Route exact path='/tvplayer/:id'> <Route path="/actors/:id">
<TVPlayer /> <ActorPage/>
</Route> </Route>
<Route exact path='/actors'> <Route path="/">
<ActorOverviewPage /> <HomePage/>
</Route>
<Route path='/actors/:id'>
<ActorPage />
</Route>
<Route path='/'>
<HomePage />
</Route> </Route>
</Switch> </Switch>
); );
} }
ApiError(): JSX.Element {
// on api error show popup and retry and show again if failing..
return (<NoBackendConnectionPopup onHide={(): void => this.initialAPICall()}/>);
}
} }
export default App; export default App;

View File

@ -5,13 +5,13 @@ import React from 'react';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import {ActorType} from '../../types/VideoTypes'; import {ActorType} from '../../types/VideoTypes';
interface Props { interface props {
actor: ActorType; actor: ActorType;
onClick?: (actor: ActorType) => void; onClick?: (actor: ActorType) => void
} }
class ActorTile extends React.Component<Props> { class ActorTile extends React.Component<props> {
constructor(props: Props) { constructor(props: props) {
super(props); super(props);
this.state = {}; this.state = {};
@ -21,7 +21,12 @@ class ActorTile extends React.Component<Props> {
if (this.props.onClick) { if (this.props.onClick) {
return this.renderActorTile(this.props.onClick); return this.renderActorTile(this.props.onClick);
} else { } else {
return <Link to={{pathname: '/actors/' + this.props.actor.ActorId}}>{this.renderActorTile(() => {})}</Link>; return (
<Link to={{pathname: '/actors/' + this.props.actor.ActorId}}>
{this.renderActorTile(() => {
})}
</Link>
);
} }
} }
@ -29,19 +34,9 @@ class ActorTile extends React.Component<Props> {
return ( return (
<div className={style.actortile} onClick={(): void => customclickhandler(this.props.actor)}> <div className={style.actortile} onClick={(): void => customclickhandler(this.props.actor)}>
<div className={style.actortile_thumbnail}> <div className={style.actortile_thumbnail}>
{ {this.props.actor.Thumbnail === '' ? <FontAwesomeIcon style={{
this.props.actor.Thumbnail === '' ? ( lineHeight: '130px'
<FontAwesomeIcon }} icon={faUser} size='5x'/> : 'dfdf' /* todo render picture provided here! */}
style={{
lineHeight: '130px'
}}
icon={faUser}
size='5x'
/>
) : (
'dfdf'
) /* todo render picture provided here! */
}
</div> </div>
<div className={style.actortile_name}>{this.props.actor.Name}</div> <div className={style.actortile_name}>{this.props.actor.Name}</div>
</div> </div>

View File

@ -1,49 +0,0 @@
import {shallow} from 'enzyme';
import React from 'react';
import DynamicContentContainer from './DynamicContentContainer';
describe('<DynamicContentContainer/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<DynamicContentContainer data={[]} renderElement={(el) => (<></>)}/>);
wrapper.unmount();
});
it('inserts tiles correctly if enough available', () => {
const wrapper = shallow(<DynamicContentContainer data={[
{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
]} renderElement={(el) => (<a/>)}/>);
expect(wrapper.find('a')).toHaveLength(16);
});
it('inserts tiles correctly if not enough available', () => {
const wrapper = shallow(<DynamicContentContainer data={[
{}, {}, {}, {}
]} renderElement={(el) => (<a/>)}/>);
expect(wrapper.find('a')).toHaveLength(4);
});
it('no items available', () => {
const wrapper = shallow(<DynamicContentContainer data={[]} renderElement={(el) => (<a/>)}/>);
expect(wrapper.find('a')).toHaveLength(0);
expect(wrapper.find('.maincontent').text()).toBe('no items to show!');
});
it('test clean', function () {
const wrapper = shallow(<DynamicContentContainer data={[{}, {}, {}]} renderElement={(el) => (<a/>)}/>);
expect(wrapper.find('a')).toHaveLength(3);
wrapper.instance().clean();
expect(wrapper.find('a')).toHaveLength(0);
});
it('test update', function () {
const wrapper = shallow(<DynamicContentContainer data={[{}, {}, {}]} renderElement={(el) => (<a/>)}/>);
const func = jest.fn();
wrapper.instance().clean = func;
// perform component update
wrapper.setProps({data: [{}, {}]});
expect(func).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,107 +0,0 @@
import React from 'react';
import style from './DynamicContentContainer.module.css';
interface Props<T> {
renderElement: (elem: T) => JSX.Element;
data: T[];
initialLoadNr?: number;
}
interface state<T> {
loadeditems: T[];
}
/**
* A videocontainer storing lots of Preview elements
* includes scroll handling and loading of preview infos
*/
class DynamicContentContainer<T> extends React.Component<Props<T>, state<T>> {
// stores current index of loaded elements
loadindex: number = 0;
readonly InitialLoadNR = this.props.initialLoadNr
? this.props.initialLoadNr === -1
? this.props.data.length
: this.props.initialLoadNr
: 16;
constructor(props: Props<T>) {
super(props);
this.state = {
loadeditems: []
};
}
componentDidMount(): void {
document.addEventListener('scroll', this.trackScrolling);
this.loadPreviewBlock(this.InitialLoadNR);
}
componentDidUpdate(prevProps: Props<T>): void {
// when source props change force update!
if (prevProps.data.length !== this.props.data.length) {
this.clean();
this.loadPreviewBlock(this.InitialLoadNR);
}
}
/**
* clear all elements rendered...
*/
clean(): void {
this.loadindex = 0;
this.setState({loadeditems: []});
}
render(): JSX.Element {
return (
<div className={style.maincontent}>
{this.state.loadeditems.map((elem) => {
return this.props.renderElement(elem);
})}
{/*todo css for no items to show*/}
{this.state.loadeditems.length === 0 ? 'no items to show!' : null}
{this.props.children}
</div>
);
}
componentWillUnmount(): void {
// unbind scroll listener when unmounting component
document.removeEventListener('scroll', this.trackScrolling);
}
/**
* load previews to the container
* @param nr number of previews to load
*/
loadPreviewBlock(nr: number): void {
let ret = [];
for (let i = 0; i < nr; i++) {
// only add if not end
if (this.props.data.length > this.loadindex + i) {
ret.push(this.props.data[this.loadindex + i]);
}
}
this.setState({
loadeditems: [...this.state.loadeditems, ...ret]
});
this.loadindex += nr;
}
/**
* scroll event handler -> load new previews if on bottom
*/
trackScrolling = (): void => {
// comparison if current scroll position is on bottom --> 200 is bottom offset to trigger load
if (window.innerHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) {
this.loadPreviewBlock(8);
}
};
}
export default DynamicContentContainer;

View File

@ -1,12 +1,12 @@
import React from 'react'; import React from "react";
import style from '../Popups/AddActorPopup/AddActorPopup.module.css'; import style from "../Popups/AddActorPopup/AddActorPopup.module.css";
import {Button} from '../GPElements/Button'; import {Button} from "../GPElements/Button";
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilter, faTimes} from '@fortawesome/free-solid-svg-icons'; import {faFilter, faTimes} from "@fortawesome/free-solid-svg-icons";
import {addKeyHandler, removeKeyHandler} from '../../utils/ShortkeyHandler'; import {addKeyHandler, removeKeyHandler} from "../../utils/ShortkeyHandler";
interface Props { interface props {
onFilterChange: (filter: string) => void; onFilterChange: (filter: string) => void
} }
interface state { interface state {
@ -14,17 +14,18 @@ interface state {
filter: string; filter: string;
} }
class FilterButton extends React.Component<Props, state> { class FilterButton extends React.Component<props, state> {
// filterfield anchor, needed to focus after filter btn click // filterfield anchor, needed to focus after filter btn click
private filterfield: HTMLInputElement | null | undefined; private filterfield: HTMLInputElement | null | undefined;
constructor(props: Props) {
constructor(props: props) {
super(props); super(props);
this.state = { this.state = {
filtervisible: false, filtervisible: false,
filter: '' filter: ''
}; }
this.keypress = this.keypress.bind(this); this.keypress = this.keypress.bind(this);
this.enableFilterField = this.enableFilterField.bind(this); this.enableFilterField = this.enableFilterField.bind(this);
@ -42,57 +43,34 @@ class FilterButton extends React.Component<Props, state> {
if (this.state.filtervisible) { if (this.state.filtervisible) {
return ( return (
<> <>
<input <input className={'form-control mr-sm-2 ' + style.searchinput}
className={'form-control mr-sm-2 ' + style.searchinput} type='text' placeholder='Filter' value={this.state.filter}
type='text' onChange={(e): void => {
placeholder='Filter' this.props.onFilterChange(e.target.value);
value={this.state.filter} this.setState({filter: e.target.value});
onChange={(e): void => { }}
this.props.onFilterChange(e.target.value); ref={(input): void => {
this.setState({filter: e.target.value}); this.filterfield = input;
}} }}/>
ref={(input): void => { <Button title={<FontAwesomeIcon style={{
this.filterfield = input; verticalAlign: 'middle',
}} lineHeight: '130px'
/> }} icon={faTimes} size='1x'/>} color={{backgroundColor: 'red'}} onClick={(): void => {
<Button this.setState({filter: '', filtervisible: false});
title={ }}/>
<FontAwesomeIcon
style={{
verticalAlign: 'middle',
lineHeight: '130px'
}}
icon={faTimes}
size='1x'
/>
}
color={{backgroundColor: 'red'}}
onClick={(): void => {
this.setState({filter: '', filtervisible: false});
}}
/>
</> </>
); );
} else { } else {
return ( return (<Button
<Button title={<span>Filter <FontAwesomeIcon
title={ style={{
<span> verticalAlign: 'middle',
Filter{' '} lineHeight: '130px'
<FontAwesomeIcon }}
style={{ icon={faFilter}
verticalAlign: 'middle', size='1x'/></span>}
lineHeight: '130px' color={{backgroundColor: 'cornflowerblue', color: 'white'}}
}} onClick={this.enableFilterField}/>)
icon={faFilter}
size='1x'
/>
</span>
}
color={{backgroundColor: 'cornflowerblue', color: 'white'}}
onClick={this.enableFilterField}
/>
);
} }
} }
@ -118,4 +96,4 @@ class FilterButton extends React.Component<Props, state> {
} }
} }
export default FilterButton; export default FilterButton;

View File

@ -5,11 +5,11 @@ import {Spinner} from 'react-bootstrap';
import {IconDefinition} from '@fortawesome/fontawesome-common-types'; import {IconDefinition} from '@fortawesome/fontawesome-common-types';
interface props { interface props {
onClick?: () => void; onClick?: () => void
backColor: string; backColor: string
icon: IconDefinition; icon: IconDefinition
text: string | number; text: string | number
subtext: string | number; subtext: string | number
} }
/** /**
@ -18,35 +18,23 @@ interface props {
class InfoHeaderItem extends React.Component<props> { class InfoHeaderItem extends React.Component<props> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<div <div onClick={(): void => {
onClick={(): void => { // call clicklistener if defined
// call clicklistener if defined if (this.props.onClick != null) this.props.onClick();
if (this.props.onClick != null) { }} className={style.infoheaderitem} style={{backgroundColor: this.props.backColor}}>
this.props.onClick();
}
}}
className={style.infoheaderitem}
style={{backgroundColor: this.props.backColor}}>
<div className={style.icon}> <div className={style.icon}>
<FontAwesomeIcon <FontAwesomeIcon style={{
style={{ verticalAlign: 'middle',
verticalAlign: 'middle', lineHeight: '130px'
lineHeight: '130px' }} icon={this.props.icon} size='5x'/>
}}
icon={this.props.icon}
size='5x'
/>
</div> </div>
{this.props.text !== null && this.props.text !== undefined ? ( {this.props.text !== null && this.props.text !== undefined ?
<> <>
<div className={style.maintext}>{this.props.text}</div> <div className={style.maintext}>{this.props.text}</div>
<div className={style.subtext}>{this.props.subtext}</div> <div className={style.subtext}>{this.props.subtext}</div>
</> </>
) : ( : <span className={style.loadAnimation}><Spinner animation='border'/></span>
<span className={style.loadAnimation}> }
<Spinner animation='border' />
</span>
)}
</div> </div>
); );
} }

View File

@ -17,8 +17,10 @@ class PageTitle extends React.Component<props> {
<div className={style.pageheader + ' ' + themeStyle.backgroundcolor}> <div className={style.pageheader + ' ' + themeStyle.backgroundcolor}>
<span className={style.pageheadertitle + ' ' + themeStyle.textcolor}>{this.props.title}</span> <span className={style.pageheadertitle + ' ' + themeStyle.textcolor}>{this.props.title}</span>
<span className={style.pageheadersubtitle + ' ' + themeStyle.textcolor}>{this.props.subtitle}</span> <span className={style.pageheadersubtitle + ' ' + themeStyle.textcolor}>{this.props.subtitle}</span>
<>{this.props.children}</> <>
<Line /> {this.props.children}
</>
<Line/>
</div> </div>
); );
} }
@ -33,7 +35,7 @@ export class Line extends React.Component {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<> <>
<hr className={themeStyle.hrcolor} /> <hr className={themeStyle.hrcolor}/>
</> </>
); );
} }

View File

@ -40,7 +40,7 @@ describe('<AddActorPopup/>', function () {
it('simulate actortile click', function () { it('simulate actortile click', function () {
const func = jest.fn(); const func = jest.fn();
const wrapper = shallow(<AddActorPopup onHide={() => {func();}} movieId={1}/>); const wrapper = shallow(<AddActorPopup onHide={() => {func();}} movie_id={1}/>);
global.callAPIMock({result: 'success'}); global.callAPIMock({result: 'success'});

View File

@ -6,11 +6,11 @@ import {NewActorPopupContent} from '../NewActorPopup/NewActorPopup';
import {APINode, callAPI} from '../../../utils/Api'; import {APINode, callAPI} from '../../../utils/Api';
import {ActorType} from '../../../types/VideoTypes'; import {ActorType} from '../../../types/VideoTypes';
import {GeneralSuccess} from '../../../types/GeneralTypes'; import {GeneralSuccess} from '../../../types/GeneralTypes';
import FilterButton from '../../FilterButton/FilterButton'; import FilterButton from "../../FilterButton/FilterButton";
interface Props { interface props {
onHide: () => void; onHide: () => void;
movieId: number; movie_id: number;
} }
interface state { interface state {
@ -22,11 +22,11 @@ interface state {
/** /**
* Popup for Adding a new Actor to a Video * Popup for Adding a new Actor to a Video
*/ */
class AddActorPopup extends React.Component<Props, state> { class AddActorPopup extends React.Component<props, state> {
// filterfield anchor, needed to focus after filter btn click // filterfield anchor, needed to focus after filter btn click
private filterfield: HTMLInputElement | null | undefined; private filterfield: HTMLInputElement | null | undefined;
constructor(props: Props) { constructor(props: props) {
super(props); super(props);
this.state = { this.state = {
@ -48,19 +48,12 @@ class AddActorPopup extends React.Component<Props, state> {
return ( return (
<> <>
{/* todo render actor tiles here and add search field*/} {/* todo render actor tiles here and add search field*/}
<PopupBase <PopupBase title='Add new Actor to Video' onHide={this.props.onHide} banner={
title='Add new Actor to Video' <button
onHide={this.props.onHide} className={style.newactorbutton}
banner={ onClick={(): void => {
<button this.setState({contentDefault: false});
className={style.newactorbutton} }}>Create new Actor</button>} ParentSubmit={this.parentSubmit}>
onClick={(): void => {
this.setState({contentDefault: false});
}}>
Create new Actor
</button>
}
ParentSubmit={this.parentSubmit}>
{this.resolvePage()} {this.resolvePage()}
</PopupBase> </PopupBase>
</> </>
@ -72,18 +65,11 @@ class AddActorPopup extends React.Component<Props, state> {
* @returns {JSX.Element} * @returns {JSX.Element}
*/ */
resolvePage(): JSX.Element { resolvePage(): JSX.Element {
if (this.state.contentDefault) { if (this.state.contentDefault) return (this.getContent());
return this.getContent(); else return (<NewActorPopupContent onHide={(): void => {
} else { this.loadActors();
return ( this.setState({contentDefault: true});
<NewActorPopupContent }}/>);
onHide={(): void => {
this.loadActors();
this.setState({contentDefault: true});
}}
/>
);
}
} }
/** /**
@ -95,19 +81,15 @@ class AddActorPopup extends React.Component<Props, state> {
return ( return (
<> <>
<div className={style.searchbar}> <div className={style.searchbar}>
<FilterButton <FilterButton onFilterChange={(filter): void => {
onFilterChange={(filter): void => { this.setState({filter: filter})
this.setState({filter: filter}); }}/>
}}
/>
</div> </div>
{this.state.actors.filter(this.filterSearch).map((el) => ( {this.state.actors.filter(this.filterSearch).map((el) => (<ActorTile actor={el} onClick={this.tileClickHandler}/>))}
<ActorTile actor={el} onClick={this.tileClickHandler} />
))}
</> </>
); );
} else { } else {
return <div>somekind of loading</div>; return (<div>somekind of loading</div>);
} }
} }
@ -116,29 +98,25 @@ class AddActorPopup extends React.Component<Props, state> {
*/ */
tileClickHandler(actor: ActorType): void { tileClickHandler(actor: ActorType): void {
// fetch the available actors // fetch the available actors
callAPI<GeneralSuccess>( callAPI<GeneralSuccess>(APINode.Actor, {
APINode.Actor, action: 'addActorToVideo',
{ ActorId: actor.ActorId,
action: 'addActorToVideo', MovieId: this.props.movie_id
ActorId: actor.ActorId, }, result => {
MovieId: this.props.movieId if (result.result === 'success') {
}, // return back to player page
(result) => { this.props.onHide();
if (result.result === 'success') { } else {
// return back to player page console.error('an error occured while fetching actors: ' + result);
this.props.onHide();
} else {
console.error('an error occured while fetching actors: ' + result);
}
} }
); });
} }
/** /**
* load the actors from backend and set state * load the actors from backend and set state
*/ */
loadActors(): void { loadActors(): void {
callAPI<ActorType[]>(APINode.Actor, {action: 'getAllActors'}, (result) => { callAPI<ActorType[]>(APINode.Actor, {action: 'getAllActors'}, result => {
this.setState({actors: result}); this.setState({actors: result});
}); });
} }

View File

@ -3,10 +3,10 @@ import Tag from '../../Tag/Tag';
import PopupBase from '../PopupBase'; import PopupBase from '../PopupBase';
import {APINode, callAPI} from '../../../utils/Api'; import {APINode, callAPI} from '../../../utils/Api';
import {TagType} from '../../../types/VideoTypes'; import {TagType} from '../../../types/VideoTypes';
import FilterButton from '../../FilterButton/FilterButton'; import FilterButton from "../../FilterButton/FilterButton";
import styles from './AddTagPopup.module.css'; import styles from './AddTagPopup.module.css'
interface Props { interface props {
onHide: () => void; onHide: () => void;
submit: (tagId: number, tagName: string) => void; submit: (tagId: number, tagName: string) => void;
} }
@ -19,8 +19,8 @@ interface state {
/** /**
* component creates overlay to add a new tag to a video * component creates overlay to add a new tag to a video
*/ */
class AddTagPopup extends React.Component<Props, state> { class AddTagPopup extends React.Component<props, state> {
constructor(props: Props) { constructor(props: props) {
super(props); super(props);
this.state = {items: [], filter: ''}; this.state = {items: [], filter: ''};
@ -42,11 +42,13 @@ class AddTagPopup extends React.Component<Props, state> {
return ( return (
<PopupBase title='Add a Tag to this Video:' onHide={this.props.onHide} ParentSubmit={this.parentSubmit}> <PopupBase title='Add a Tag to this Video:' onHide={this.props.onHide} ParentSubmit={this.parentSubmit}>
<div className={styles.actionbar}> <div className={styles.actionbar}>
<FilterButton onFilterChange={(filter): void => this.setState({filter: filter})} /> <FilterButton onFilterChange={(filter): void => this.setState({filter: filter})}/>
</div> </div>
{this.state.items {this.state.items ?
? this.state.items.filter(this.tagFilter).map((i) => <Tag tagInfo={i} onclick={(): void => this.onItemClick(i)} />) this.state.items.filter(this.tagFilter).map((i) => (
: null} <Tag tagInfo={i}
onclick={(): void => this.onItemClick(i)}/>
)) : null}
</PopupBase> </PopupBase>
); );
} }

View File

@ -15,7 +15,7 @@ class NewActorPopup extends React.Component<NewActorPopupProps> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<PopupBase title='Add new Tag' onHide={this.props.onHide} height='200px' width='400px'> <PopupBase title='Add new Tag' onHide={this.props.onHide} height='200px' width='400px'>
<NewActorPopupContent onHide={this.props.onHide} /> <NewActorPopupContent onHide={this.props.onHide}/>
</PopupBase> </PopupBase>
); );
} }
@ -28,17 +28,10 @@ export class NewActorPopupContent extends React.Component<NewActorPopupProps> {
return ( return (
<> <>
<div> <div>
<input <input type='text' placeholder='Actor Name' onChange={(v): void => {
type='text' this.value = v.target.value;
placeholder='Actor Name' }}/></div>
onChange={(v): void => { <button className={style.savebtn} onClick={(): void => this.storeselection()}>Save</button>
this.value = v.target.value;
}}
/>
</div>
<button className={style.savebtn} onClick={(): void => this.storeselection()}>
Save
</button>
</> </>
); );
} }
@ -48,9 +41,7 @@ export class NewActorPopupContent extends React.Component<NewActorPopupProps> {
*/ */
storeselection(): void { storeselection(): void {
// check if user typed in name // check if user typed in name
if (this.value === '' || this.value === undefined) { if (this.value === '' || this.value === undefined) return;
return;
}
callAPI(APINode.Actor, {action: 'createActor', actorname: this.value}, (result: GeneralSuccess) => { callAPI(APINode.Actor, {action: 'createActor', actorname: this.value}, (result: GeneralSuccess) => {
if (result.result !== 'success') { if (result.result !== 'success') {

View File

@ -5,7 +5,7 @@ import {APINode, callAPI} from '../../../utils/Api';
import {GeneralSuccess} from '../../../types/GeneralTypes'; import {GeneralSuccess} from '../../../types/GeneralTypes';
interface props { interface props {
onHide: () => void; onHide: () => void
} }
/** /**
@ -16,24 +16,11 @@ class NewTagPopup extends React.Component<props> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<PopupBase <PopupBase title='Add new Tag' onHide={this.props.onHide} height='200px' width='400px' ParentSubmit={(): void => this.storeselection()}>
title='Add new Tag' <div><input type='text' placeholder='Tagname' onChange={(v): void => {
onHide={this.props.onHide} this.value = v.target.value;
height='200px' }}/></div>
width='400px' <button className={style.savebtn} onClick={(): void => this.storeselection()}>Save</button>
ParentSubmit={(): void => this.storeselection()}>
<div>
<input
type='text'
placeholder='Tagname'
onChange={(v): void => {
this.value = v.target.value;
}}
/>
</div>
<button className={style.savebtn} onClick={(): void => this.storeselection()}>
Save
</button>
</PopupBase> </PopupBase>
); );
} }

View File

@ -0,0 +1,29 @@
import {shallow} from 'enzyme';
import React from 'react';
import {NoBackendConnectionPopup} from './NoBackendConnectionPopup';
import {getBackendDomain} from '../../../utils/Api';
describe('<NoBackendConnectionPopup/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<NoBackendConnectionPopup onHide={() => {}}/>);
wrapper.unmount();
});
it('hides on refresh click', function () {
const func = jest.fn();
const wrapper = shallow(<NoBackendConnectionPopup onHide={func}/>);
expect(func).toBeCalledTimes(0);
wrapper.find('button').simulate('click');
expect(func).toBeCalledTimes(1);
});
it('simulate change of textfield', function () {
const wrapper = shallow(<NoBackendConnectionPopup onHide={() => {}}/>);
wrapper.find('input').simulate('change', {target: {value: 'testvalue'}});
expect(getBackendDomain()).toBe('testvalue');
});
});

View File

@ -0,0 +1,20 @@
import React from 'react';
import PopupBase from '../PopupBase';
import style from '../NewActorPopup/NewActorPopup.module.css';
import {setCustomBackendDomain} from '../../../utils/Api';
interface NBCProps {
onHide: (_: void) => void
}
export function NoBackendConnectionPopup(props: NBCProps): JSX.Element {
return (
<PopupBase title='No connection to backend API!' onHide={props.onHide} height='200px' width='600px'>
<div>
<input type='text' placeholder='http://192.168.0.2' onChange={(v): void => {
setCustomBackendDomain(v.target.value);
}}/></div>
<button className={style.savebtn} onClick={(): void => props.onHide()}>Refresh</button>
</PopupBase>
);
}

View File

@ -8,11 +8,18 @@ describe('<PopupBase/>', function () {
wrapper.unmount(); wrapper.unmount();
}); });
let events;
function mockKeyPress() {
events = [];
document.addEventListener = jest.fn((event, cb) => {
events[event] = cb;
});
}
it('simulate keypress', function () { it('simulate keypress', function () {
mockKeyPress(); mockKeyPress();
const func = jest.fn(); const func = jest.fn();
const events = mockKeyPress();
shallow(<PopupBase onHide={() => func()}/>); shallow(<PopupBase onHide={() => func()}/>);
// trigger the keypress event // trigger the keypress event
@ -22,7 +29,7 @@ describe('<PopupBase/>', function () {
}); });
it('test an Enter sumit', function () { it('test an Enter sumit', function () {
const events = mockKeyPress(); mockKeyPress();
const func = jest.fn(); const func = jest.fn();
shallow(<PopupBase ParentSubmit={() => func()}/>); shallow(<PopupBase ParentSubmit={() => func()}/>);

View File

@ -4,7 +4,7 @@ import {Line} from '../PageTitle/PageTitle';
import React, {RefObject} from 'react'; import React, {RefObject} from 'react';
import {addKeyHandler, removeKeyHandler} from '../../utils/ShortkeyHandler'; import {addKeyHandler, removeKeyHandler} from '../../utils/ShortkeyHandler';
interface Props { interface props {
width?: string; width?: string;
height?: string; height?: string;
banner?: JSX.Element; banner?: JSX.Element;
@ -16,11 +16,11 @@ interface Props {
/** /**
* wrapper class for generic types of popups * wrapper class for generic types of popups
*/ */
class PopupBase extends React.Component<Props> { class PopupBase extends React.Component<props> {
private wrapperRef: RefObject<HTMLDivElement>; private wrapperRef: RefObject<HTMLDivElement>;
private framedimensions: {minHeight: string | undefined; width: string | undefined; height: string | undefined}; private framedimensions: { minHeight: string | undefined; width: string | undefined; height: string | undefined };
constructor(props: Props) { constructor(props: props) {
super(props); super(props);
this.state = {items: []}; this.state = {items: []};
@ -32,9 +32,9 @@ class PopupBase extends React.Component<Props> {
// parse style props // parse style props
this.framedimensions = { this.framedimensions = {
width: this.props.width ? this.props.width : undefined, width: (this.props.width ? this.props.width : undefined),
height: this.props.height ? this.props.height : undefined, height: (this.props.height ? this.props.height : undefined),
minHeight: this.props.height ? this.props.height : undefined minHeight: (this.props.height ? this.props.height : undefined)
}; };
} }
@ -63,8 +63,10 @@ class PopupBase extends React.Component<Props> {
<div className={style.banner}>{this.props.banner}</div> <div className={style.banner}>{this.props.banner}</div>
</div> </div>
<Line /> <Line/>
<div className={style.content}>{this.props.children}</div> <div className={style.content}>
{this.props.children}
</div>
</div> </div>
); );
} }
@ -88,9 +90,7 @@ class PopupBase extends React.Component<Props> {
this.props.onHide(); this.props.onHide();
} else if (event.key === 'Enter') { } else if (event.key === 'Enter') {
// call a parentsubmit if defined // call a parentsubmit if defined
if (this.props.ParentSubmit) { if (this.props.ParentSubmit) this.props.ParentSubmit();
this.props.ParentSubmit();
}
} }
} }
@ -98,19 +98,15 @@ class PopupBase extends React.Component<Props> {
* make the element drag and droppable * make the element drag and droppable
*/ */
dragElement(): void { dragElement(): void {
let xOld = 0, let xOld = 0, yOld = 0;
yOld = 0;
const elmnt = this.wrapperRef.current; const elmnt = this.wrapperRef.current;
if (elmnt === null) { if (elmnt === null) return;
return; if (elmnt.firstChild === null) return;
}
if (elmnt.firstChild === null) {
return;
}
(elmnt.firstChild as HTMLDivElement).onmousedown = dragMouseDown; (elmnt.firstChild as HTMLDivElement).onmousedown = dragMouseDown;
function dragMouseDown(e: MouseEvent): void { function dragMouseDown(e: MouseEvent): void {
e.preventDefault(); e.preventDefault();
// get the mouse cursor position at startup: // get the mouse cursor position at startup:
@ -129,11 +125,9 @@ class PopupBase extends React.Component<Props> {
xOld = e.clientX; xOld = e.clientX;
yOld = e.clientY; yOld = e.clientY;
// set the element's new position: // set the element's new position:
if (elmnt === null) { if (elmnt === null) return;
return; elmnt.style.top = (elmnt.offsetTop - dy) + 'px';
} elmnt.style.left = (elmnt.offsetLeft - dx) + 'px';
elmnt.style.top = elmnt.offsetTop - dy + 'px';
elmnt.style.left = elmnt.offsetLeft - dx + 'px';
} }
function closeDragElement(): void { function closeDragElement(): void {

View File

@ -2,16 +2,17 @@ import React from 'react';
import PopupBase from '../PopupBase'; import PopupBase from '../PopupBase';
import {Button} from '../../GPElements/Button'; import {Button} from '../../GPElements/Button';
interface Props { interface props {
onHide: (_: void) => void; onHide: (_: void) => void;
submit: (_: void) => void; submit: (_: void) => void;
} }
export default function SubmitPopup(props: Props): JSX.Element { export default function SubmitPopup(props: props): JSX.Element {
return ( return (
<PopupBase title='Are you sure?' onHide={props.onHide} height='160px' width='300px'> <PopupBase title='Are you sure?' onHide={props.onHide} height='160px' width='300px'>
<Button title='Submit' color={{backgroundColor: 'green'}} onClick={(): void => props.submit()} /> <Button title='Submit' color={{backgroundColor: 'green'}} onClick={(): void => props.submit()}/>
<Button title='Cancel' color={{backgroundColor: 'red'}} onClick={(): void => props.onHide()} /> <Button title='Cancel' color={{backgroundColor: 'red'}} onClick={(): void => props.onHide()}/>
</PopupBase> </PopupBase>
); );
} }

View File

@ -3,18 +3,16 @@ import style from './Preview.module.css';
import {Spinner} from 'react-bootstrap'; import {Spinner} from 'react-bootstrap';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import GlobalInfos from '../../utils/GlobalInfos'; import GlobalInfos from '../../utils/GlobalInfos';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {APINode, callAPIPlain} from '../../utils/Api';
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
interface PreviewProps { interface PreviewProps {
name: string; name: string;
picLoader: (callback: (pic: string) => void) => void; movie_id: number;
linkPath?: string;
onClick?: () => void; onClick?: () => void;
} }
interface PreviewState { interface PreviewState {
picLoaded: boolean | null; previewpicture: string | null;
} }
/** /**
@ -22,61 +20,42 @@ interface PreviewState {
* floating side by side * floating side by side
*/ */
class Preview extends React.Component<PreviewProps, PreviewState> { class Preview extends React.Component<PreviewProps, PreviewState> {
// store the picture to display
pic?: string;
constructor(props: PreviewProps) { constructor(props: PreviewProps) {
super(props); super(props);
this.state = { this.state = {
picLoaded: null previewpicture: null
}; };
} }
componentDidMount(): void { componentDidMount(): void {
this.props.picLoader((result) => { callAPIPlain(APINode.Video, {action: 'readThumbnail', movieid: this.props.movie_id}, (result) => {
this.pic = result;
this.setState({ this.setState({
picLoaded: result !== '' previewpicture: result
}); });
}); });
} }
render(): JSX.Element { render(): JSX.Element {
if (this.props.linkPath !== undefined) {
return <Link to={this.props.linkPath}>{this.content()}</Link>;
} else {
return this.content();
}
}
content(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<div <Link to={'/player/' + this.props.movie_id} onClick={this.props.onClick}>
className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview} <div className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}>
onClick={this.props.onClick}> <div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div>
<div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div> <div className={style.previewpic}>
<div className={style.previewpic}> {this.state.previewpicture !== null ?
{this.state.picLoaded === false ? ( <img className={style.previewimage}
<FontAwesomeIcon src={this.state.previewpicture}
style={{ alt='Pic loading.'/> :
color: 'white', <span className={style.loadAnimation}><Spinner animation='border'/></span>}
marginTop: '55px'
}} </div>
icon={faPhotoVideo} <div className={style.previewbottom}>
size='5x'
/> </div>
) : this.state.picLoaded === null ? (
<span className={style.loadAnimation}>
<Spinner animation='border' />
</span>
) : (
<img className={style.previewimage} src={this.pic} alt='Pic loading.' />
)}
</div> </div>
<div className={style.previewbottom} /> </Link>
</div>
); );
} }
} }
@ -84,12 +63,15 @@ class Preview extends React.Component<PreviewProps, PreviewState> {
/** /**
* Component for a Tag-name tile (used in category page) * Component for a Tag-name tile (used in category page)
*/ */
export class TagPreview extends React.Component<{name: string}> { export class TagPreview extends React.Component<{ name: string }> {
render(): JSX.Element { render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<div className={style.videopreview + ' ' + style.tagpreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}> <div
<div className={style.tagpreviewtitle + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div> className={style.videopreview + ' ' + style.tagpreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}>
<div className={style.tagpreviewtitle + ' ' + themeStyle.lighttextcolor}>
{this.props.name}
</div>
</div> </div>
); );
} }

View File

@ -5,29 +5,37 @@ import Preview, {TagPreview} from './Preview';
describe('<Preview/>', function () { describe('<Preview/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
const wrapper = shallow(<Preview movieId={1} name='test' picLoader={callback => callback('')}/>); const wrapper = shallow(<Preview movie_id={1}/>);
wrapper.unmount(); wrapper.unmount();
}); });
it('picture rendered correctly', () => { it('picture rendered correctly', done => {
const func = jest.fn(); const mockSuccessResponse = 'testsrc';
const wrapper = shallow(<Preview movieId={1} name='test' picLoader={callback => { const mockJsonPromise = Promise.resolve(mockSuccessResponse);
func(); const mockFetchPromise = Promise.resolve({
callback('42'); text: () => mockJsonPromise
}}/>); });
global.fetch = jest.fn().mockImplementation(() => mockFetchPromise);
// expect picloader tobe called once const wrapper = shallow(<Preview name='test' movie_id={1}/>);
expect(func).toHaveBeenCalledTimes(1)
// now called 1 times
expect(global.fetch).toHaveBeenCalledTimes(1);
process.nextTick(() => {
// received picture should be rendered into wrapper
expect(wrapper.find('.previewimage').props().src).not.toBeNull();
// check if preview title renders correctly
expect(wrapper.find('.previewtitle').text()).toBe('test');
global.fetch.mockClear();
done();
});
// received picture should be rendered into wrapper
expect(wrapper.find('.previewimage').props().src).toBe('42');
// check if preview title renders correctly
expect(wrapper.find('.previewtitle').text()).toBe('test');
}); });
it('spinner loads correctly', function () { it('spinner loads correctly', function () {
// if callback is never called --> infinite spinner const wrapper = shallow(<Preview movie_id={1}/>);
const wrapper = shallow(<Preview movieId={1} name='test' picLoader={callback => {}}/>);
// expect load animation to be visible // expect load animation to be visible
expect(wrapper.find('.loadAnimation')).toHaveLength(1); expect(wrapper.find('.loadAnimation')).toHaveLength(1);

View File

@ -13,16 +13,11 @@ interface SideBarProps {
class SideBar extends React.Component<SideBarProps> { class SideBar extends React.Component<SideBarProps> {
render(): JSX.Element { render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
const classnn = const classnn = style.sideinfogeometry + ' ' + (this.props.hiddenFrame === undefined ? style.sideinfo + ' ' + themeStyle.secbackground : '');
style.sideinfogeometry +
' ' +
(this.props.hiddenFrame === undefined ? style.sideinfo + ' ' + themeStyle.secbackground : '');
return ( return (<div className={classnn} style={{width: this.props.width}}>
<div className={classnn} style={{width: this.props.width}}> {this.props.children}
{this.props.children} </div>);
</div>
);
} }
} }
@ -32,7 +27,9 @@ class SideBar extends React.Component<SideBarProps> {
export class SideBarTitle extends React.Component { export class SideBarTitle extends React.Component {
render(): JSX.Element { render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return <div className={style.sidebartitle + ' ' + themeStyle.subtextcolor}>{this.props.children}</div>; return (
<div className={style.sidebartitle + ' ' + themeStyle.subtextcolor}>{this.props.children}</div>
);
} }
} }
@ -43,9 +40,8 @@ export class SideBarItem extends React.Component {
render(): JSX.Element { render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<div className={style.sidebarinfo + ' ' + themeStyle.thirdbackground + ' ' + themeStyle.lighttextcolor}> <div
{this.props.children} className={style.sidebarinfo + ' ' + themeStyle.thirdbackground + ' ' + themeStyle.lighttextcolor}>{this.props.children}</div>
</div>
); );
} }
} }

View File

@ -5,8 +5,8 @@ import {Link} from 'react-router-dom';
import {TagType} from '../../types/VideoTypes'; import {TagType} from '../../types/VideoTypes';
interface props { interface props {
onclick?: (_: string) => void; onclick?: (_: string) => void
tagInfo: TagType; tagInfo: TagType
} }
/** /**
@ -17,15 +17,18 @@ class Tag extends React.Component<props> {
if (this.props.onclick) { if (this.props.onclick) {
return this.renderButton(); return this.renderButton();
} else { } else {
return <Link to={'/categories/' + this.props.tagInfo.TagId}>{this.renderButton()}</Link>; return (
<Link to={'/categories/' + this.props.tagInfo.TagId}>
{this.renderButton()}
</Link>
);
} }
} }
renderButton(): JSX.Element { renderButton(): JSX.Element {
return ( return (
<button className={styles.tagbtn} onClick={(): void => this.TagClick()} data-testid='Test-Tag'> <button className={styles.tagbtn} onClick={(): void => this.TagClick()}
{this.props.tagInfo.TagName} data-testid='Test-Tag'>{this.props.tagInfo.TagName}</button>
</button>
); );
} }

View File

@ -1,5 +1,4 @@
.maincontent { .maincontent {
float: left; float: left;
width: 70%; width: 70%;
flex: 1; }
}

View File

@ -7,4 +7,24 @@ describe('<VideoContainer/>', function () {
const wrapper = shallow(<VideoContainer data={[]}/>); const wrapper = shallow(<VideoContainer data={[]}/>);
wrapper.unmount(); wrapper.unmount();
}); });
it('inserts tiles correctly if enough available', () => {
const wrapper = shallow(<VideoContainer data={[
{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
]}/>);
expect(wrapper.find('Preview')).toHaveLength(16);
});
it('inserts tiles correctly if not enough available', () => {
const wrapper = shallow(<VideoContainer data={[
{}, {}, {}, {}
]}/>);
expect(wrapper.find('Preview')).toHaveLength(4);
});
it('no items available', () => {
const wrapper = shallow(<VideoContainer data={[]}/>);
expect(wrapper.find('Preview')).toHaveLength(0);
expect(wrapper.find('.maincontent').text()).toBe('no items to show!');
});
}); });

View File

@ -1,38 +1,114 @@
import React from 'react'; import React from 'react';
import Preview from '../Preview/Preview'; import Preview from '../Preview/Preview';
import style from './VideoContainer.module.css';
import {VideoTypes} from '../../types/ApiTypes'; import {VideoTypes} from '../../types/ApiTypes';
import DynamicContentContainer from '../DynamicContentContainer/DynamicContentContainer';
import {APINode, callAPIPlain} from '../../utils/Api';
interface Props { interface props {
data: VideoTypes.VideoUnloadedType[]; data: VideoTypes.VideoUnloadedType[];
children?: JSX.Element; onScrollPositionChange?: (scrollPos: number, loadedTiles: number) => void;
initialScrollPosition?: {scrollPos: number, loadedTiles: number};
} }
const VideoContainer = (props: Props): JSX.Element => { interface state {
return ( loadeditems: VideoTypes.VideoUnloadedType[];
<DynamicContentContainer selectionnr: number;
renderElement={(el): JSX.Element => ( }
<Preview
key={el.MovieId} /**
picLoader={(callback: (pic: string) => void): void => { * A videocontainer storing lots of Preview elements
callAPIPlain( * includes scroll handling and loading of preview infos
APINode.Video, */
{ class VideoContainer extends React.Component<props, state> {
action: 'readThumbnail', // stores current index of loaded elements
movieid: el.MovieId loadindex: number = 0;
},
(result) => callback(result) constructor(props: props) {
); super(props);
}}
name={el.MovieName} this.state = {
linkPath={'/player/' + el.MovieId} loadeditems: [],
/> selectionnr: 0
)} };
data={props.data}> }
{props.children}
</DynamicContentContainer> componentDidMount(): void {
); document.addEventListener('scroll', this.trackScrolling);
};
console.log(this.props.initialScrollPosition)
if(this.props.initialScrollPosition !== undefined){
this.loadPreviewBlock(this.props.initialScrollPosition.loadedTiles, () => {
if(this.props.initialScrollPosition !== undefined)
window.scrollTo(0, this.props.initialScrollPosition.scrollPos);
});
}else{
this.loadPreviewBlock(16);
}
}
render(): JSX.Element {
return (
<div className={style.maincontent}>
{this.state.loadeditems.map(elem => (
<Preview
key={elem.MovieId}
name={elem.MovieName}
movie_id={elem.MovieId}
onClick={(): void => {
if (this.props.onScrollPositionChange !== undefined)
this.props.onScrollPositionChange(window.pageYOffset - document.documentElement.clientHeight, this.loadindex)
}}/>
))}
{/*todo css for no items to show*/}
{this.state.loadeditems.length === 0 ?
'no items to show!' : null}
{this.props.children}
</div>
);
}
componentWillUnmount(): void {
this.setState({});
// unbind scroll listener when unmounting component
document.removeEventListener('scroll', this.trackScrolling);
}
/**
* load previews to the container
* @param nr number of previews to load
*/
loadPreviewBlock(nr: number, callback? : () => void): void {
console.log('loadpreviewblock called ...');
let ret = [];
for (let i = 0; i < nr; i++) {
// only add if not end
if (this.props.data.length > this.loadindex + i) {
ret.push(this.props.data[this.loadindex + i]);
}
}
this.setState({
loadeditems: [
...this.state.loadeditems,
...ret
]
}, callback);
this.loadindex += nr;
}
/**
* scroll event handler -> load new previews if on bottom
*/
trackScrolling = (): void => {
// comparison if current scroll position is on bottom --> 200 is bottom offset to trigger load
if (document.documentElement.clientHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) {
this.loadPreviewBlock(8);
if (this.props.onScrollPositionChange !== undefined)
this.props.onScrollPositionChange(document.documentElement.clientHeight + window.pageYOffset, this.loadindex)
}
};
}
export default VideoContainer; export default VideoContainer;

View File

@ -3,11 +3,11 @@ import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
// don't allow console logs within production env // don't allow console logs within production env
global.console.log = process.env.NODE_ENV !== 'development' ? (_: string | number | boolean): void => {} : global.console.log; global.console.log = process.env.NODE_ENV !== 'development' ? (s: string | number | boolean): void => {} : global.console.log;
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<App /> <App/>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')
); );

View File

@ -8,6 +8,20 @@ describe('<ActorOverviewPage/>', function () {
wrapper.unmount(); wrapper.unmount();
}); });
it('test inerstion of actor tiles', function () {
const wrapper = shallow(<ActorOverviewPage/>);
wrapper.setState({
actors: [{
thumbnail: '',
name: 'testname',
actor_id: 42
}]
});
expect(wrapper.find('ActorTile')).toHaveLength(1);
});
it('test newtagpopup visibility', function () { it('test newtagpopup visibility', function () {
const wrapper = shallow(<ActorOverviewPage/>); const wrapper = shallow(<ActorOverviewPage/>);

View File

@ -4,20 +4,20 @@ import {ActorType} from '../../types/VideoTypes';
import ActorTile from '../../elements/ActorTile/ActorTile'; import ActorTile from '../../elements/ActorTile/ActorTile';
import PageTitle from '../../elements/PageTitle/PageTitle'; import PageTitle from '../../elements/PageTitle/PageTitle';
import SideBar from '../../elements/SideBar/SideBar'; import SideBar from '../../elements/SideBar/SideBar';
// import style from './ActorOverviewPage.module.css'; import style from './ActorOverviewPage.module.css';
import {Button} from '../../elements/GPElements/Button'; import {Button} from '../../elements/GPElements/Button';
import NewActorPopup from '../../elements/Popups/NewActorPopup/NewActorPopup'; import NewActorPopup from '../../elements/Popups/NewActorPopup/NewActorPopup';
import DynamicContentContainer from '../../elements/DynamicContentContainer/DynamicContentContainer';
interface Props {} interface props {
}
interface state { interface state {
actors: ActorType[]; actors: ActorType[];
NActorPopupVisible: boolean; NActorPopupVisible: boolean
} }
class ActorOverviewPage extends React.Component<Props, state> { class ActorOverviewPage extends React.Component<props, state> {
constructor(props: Props) { constructor(props: props) {
super(props); super(props);
this.state = { this.state = {
@ -33,30 +33,24 @@ class ActorOverviewPage extends React.Component<Props, state> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<PageTitle title='Actors' subtitle={this.state.actors.length + ' Actors'} /> <PageTitle title='Actors' subtitle={this.state.actors.length + ' Actors'}/>
<SideBar> <SideBar>
<Button title='Add Actor' onClick={(): void => this.setState({NActorPopupVisible: true})} /> <Button title='Add Actor' onClick={(): void => this.setState({NActorPopupVisible: true})}/>
</SideBar> </SideBar>
<DynamicContentContainer <div className={style.container}>
renderElement={(el): JSX.Element => <ActorTile key={el.ActorId} actor={el} />} {this.state.actors.map((el) => (<ActorTile key={el.ActorId} actor={el}/>))}
data={this.state.actors} </div>
initialLoadNr={36} {this.state.NActorPopupVisible ?
/> <NewActorPopup onHide={(): void => {
this.setState({NActorPopupVisible: false});
{this.state.NActorPopupVisible ? ( this.fetchAvailableActors(); // refetch actors
<NewActorPopup }}/> : null}
onHide={(): void => {
this.setState({NActorPopupVisible: false});
this.fetchAvailableActors(); // refetch actors
}}
/>
) : null}
</> </>
); );
} }
fetchAvailableActors(): void { fetchAvailableActors(): void {
callAPI<ActorType[]>(APINode.Actor, {action: 'getAllActors'}, (result) => { callAPI<ActorType[]>(APINode.Actor, {action: 'getAllActors'}, result => {
this.setState({actors: result}); this.setState({actors: result});
}); });
} }

View File

@ -13,20 +13,22 @@ import {Button} from '../../elements/GPElements/Button';
import {ActorTypes, VideoTypes} from '../../types/ApiTypes'; import {ActorTypes, VideoTypes} from '../../types/ApiTypes';
interface state { interface state {
data: VideoTypes.VideoUnloadedType[]; data: VideoTypes.VideoUnloadedType[],
actor: ActorType; actor: ActorType
} }
/** /**
* empty default props with id in url * empty default props with id in url
*/ */
interface Props extends RouteComponentProps<{id: string}> {} interface props extends RouteComponentProps<{ id: string }> {
}
/** /**
* info page about a specific actor and a list of all its videos * info page about a specific actor and a list of all its videos
*/ */
export class ActorPage extends React.Component<Props, state> { export class ActorPage extends React.Component<props, state> {
constructor(props: Props) { constructor(props: props) {
super(props); super(props);
this.state = {data: [], actor: {ActorId: 0, Name: '', Thumbnail: ''}}; this.state = {data: [], actor: {ActorId: 0, Name: '', Thumbnail: ''}};
@ -38,17 +40,20 @@ export class ActorPage extends React.Component<Props, state> {
<PageTitle title={this.state.actor.Name} subtitle={this.state.data ? this.state.data.length + ' videos' : null}> <PageTitle title={this.state.actor.Name} subtitle={this.state.data ? this.state.data.length + ' videos' : null}>
<span className={style.overviewbutton}> <span className={style.overviewbutton}>
<Link to='/actors'> <Link to='/actors'>
<Button onClick={(): void => {}} title='Go to Actor overview' /> <Button onClick={(): void => {}} title='Go to Actor overview'/>
</Link> </Link>
</span> </span>
</PageTitle> </PageTitle>
<SideBar> <SideBar>
<div className={style.pic}> <div className={style.pic}>
<FontAwesomeIcon style={{color: 'white'}} icon={faUser} size='10x' /> <FontAwesomeIcon style={{color: 'white'}} icon={faUser} size='10x'/>
</div> </div>
<SideBarTitle>Attention: This is an early preview!</SideBarTitle> <SideBarTitle>Attention: This is an early preview!</SideBarTitle>
</SideBar> </SideBar>
<VideoContainer data={this.state.data} /> {this.state.data.length !== 0 ?
<VideoContainer
data={this.state.data}/> :
<div>No Data found!</div>}
</> </>
); );
} }
@ -61,19 +66,15 @@ export class ActorPage extends React.Component<Props, state> {
* request more actor info from backend * request more actor info from backend
*/ */
getActorInfo(): void { getActorInfo(): void {
callAPI( callAPI(APINode.Actor, {
APINode.Actor, action: 'getActorInfo',
{ ActorId: parseInt(this.props.match.params.id)
action: 'getActorInfo', }, (result: ActorTypes.videofetchresult) => {
ActorId: parseInt(this.props.match.params.id, 10) this.setState({
}, data: result.Videos ? result.Videos : [],
(result: ActorTypes.videofetchresult) => { actor: result.Info
this.setState({ });
data: result.Videos ? result.Videos : [], });
actor: result.Info
});
}
);
} }
} }

View File

@ -1,56 +0,0 @@
.main {
background-color: #00b3ff;
margin-left: calc(50% - 125px);
margin-top: 5%;
padding-bottom: 15px;
width: 250px;
text-align: center;
border-radius: 10px;
}
.loginText {
font-size: xx-large;
text-align: center;
margin-bottom: 15px;
font-weight: bolder;
}
.openmediacenterlabel {
margin-top: 5%;
text-align: center;
font-size: xxx-large;
font-weight: bold;
text-transform: capitalize;
color: white;
}
.input {
margin-left: 10px;
margin-right: 10px;
width: calc(100% - 20px);
background: transparent;
border-width: 0 0 1px 0;
color: #505050;
border-color: #505050;
text-align: center;
margin-bottom: 25px;
font-size: larger;
}
.submitbtn {
margin-top: 10px;
}
::placeholder {
color: #505050;
opacity: 1;
}
*:focus {
outline: none;
}
.input:focus {
color: black;
border-color: black;
}

View File

@ -1,55 +0,0 @@
import React from 'react';
import AuthenticationPage from './AuthenticationPage';
import {shallow} from 'enzyme';
import {token} from "../../utils/TokenHandler";
describe('<AuthenticationPage/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<AuthenticationPage submit={() => {
}}/>);
wrapper.unmount();
});
it('test button click', function () {
const func = jest.fn();
const wrapper = shallow(<AuthenticationPage onSuccessLogin={func}/>);
wrapper.instance().authenticate = jest.fn(() => {
wrapper.instance().props.onSuccessLogin()
});
wrapper.setState({pwdText: 'testpwd'});
wrapper.find('Button').simulate('click');
expect(func).toHaveBeenCalledTimes(1);
});
it('test fail authenticate', function () {
const events = mockKeyPress();
token.refreshAPIToken = jest.fn().mockImplementation((callback, force, pwd) => {
callback('there was an error')
});
const wrapper = shallow(<AuthenticationPage/>);
events.keyup({key: 'Enter'});
expect(wrapper.state().wrongPWDInfo).toBe(true);
});
it('test success authenticate', function () {
const events = mockKeyPress();
const func = jest.fn()
token.refreshAPIToken = jest.fn().mockImplementation((callback, force, pwd) => {
callback('')
});
const wrapper = shallow(<AuthenticationPage onSuccessLogin={func}/>);
events.keyup({key: 'Enter'});
expect(wrapper.state().wrongPWDInfo).toBe(false);
expect(func).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,110 +0,0 @@
import React from 'react';
import {Button} from '../../elements/GPElements/Button';
import style from './AuthenticationPage.module.css';
import {addKeyHandler, removeKeyHandler} from '../../utils/ShortkeyHandler';
import {token} from '../../utils/TokenHandler';
import {faTimes} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
interface state {
pwdText: string;
wrongPWDInfo: boolean;
}
interface Props {
onSuccessLogin: () => void;
}
class AuthenticationPage extends React.Component<Props, state> {
constructor(props: Props) {
super(props);
this.state = {
pwdText: '',
wrongPWDInfo: false
};
this.keypress = this.keypress.bind(this);
this.authenticate = this.authenticate.bind(this);
}
componentDidMount(): void {
addKeyHandler(this.keypress);
}
componentWillUnmount(): void {
removeKeyHandler(this.keypress);
}
render(): JSX.Element {
return (
<>
<div className={style.openmediacenterlabel}>OpenMediaCenter</div>
<div className={style.main}>
<div className={style.loginText}>Login</div>
<div>
<input
className={style.input}
placeholder='Password'
type='password'
onChange={(ch): void => this.setState({pwdText: ch.target.value})}
value={this.state.pwdText}
/>
{this.state.wrongPWDInfo ? (
<div>
<FontAwesomeIcon
style={{
color: 'red',
marginRight: '7px'
}}
icon={faTimes}
size='1x'
/>
wrong password!
</div>
) : null}
</div>
<div className={style.submitbtn}>
<Button title='Submit' onClick={this.authenticate} />
</div>
</div>
</>
);
}
/**
* request a new token and check if pwd was valid
*/
authenticate(): void {
token.refreshAPIToken(
(error) => {
if (error !== '') {
this.setState({wrongPWDInfo: true});
// set timeout to make the info auto-disappearing
setTimeout(() => {
this.setState({wrongPWDInfo: false});
}, 2000);
} else {
this.props.onSuccessLogin();
}
},
true,
this.state.pwdText
);
}
/**
* key event handling
* @param event keyevent
*/
keypress(event: KeyboardEvent): void {
// hide if escape is pressed
if (event.key === 'Enter') {
// call submit
this.authenticate();
}
}
}
export default AuthenticationPage;

View File

@ -12,10 +12,10 @@ class CategoryPage extends React.Component {
return ( return (
<Switch> <Switch>
<Route path='/categories/:id'> <Route path='/categories/:id'>
<CategoryViewWR /> <CategoryViewWR/>
</Route> </Route>
<Route path='/categories'> <Route path='/categories'>
<TagView /> <TagView/>
</Route> </Route>
</Switch> </Switch>
); );

View File

@ -11,7 +11,7 @@ import {DefaultTags, GeneralSuccess} from '../../types/GeneralTypes';
import {Button} from '../../elements/GPElements/Button'; import {Button} from '../../elements/GPElements/Button';
import SubmitPopup from '../../elements/Popups/SubmitPopup/SubmitPopup'; import SubmitPopup from '../../elements/Popups/SubmitPopup/SubmitPopup';
interface CategoryViewProps extends RouteComponentProps<{id: string}> {} interface CategoryViewProps extends RouteComponentProps<{ id: string }> {}
interface CategoryViewState { interface CategoryViewState {
loaded: boolean; loaded: boolean;
@ -34,51 +34,42 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
} }
componentDidMount(): void { componentDidMount(): void {
this.fetchVideoData(parseInt(this.props.match.params.id, 10)); this.fetchVideoData(parseInt(this.props.match.params.id));
} }
componentDidUpdate(prevProps: Readonly<CategoryViewProps>): void { componentDidUpdate(prevProps: Readonly<CategoryViewProps>, prevState: Readonly<CategoryViewState>): void {
// trigger video refresh if id changed // trigger video refresh if id changed
if (prevProps.match.params.id !== this.props.match.params.id) { if (prevProps.match.params.id !== this.props.match.params.id) {
this.reloadVideoData(); this.setState({loaded: false});
this.fetchVideoData(parseInt(this.props.match.params.id));
} }
} }
reloadVideoData(): void {
this.setState({loaded: false});
this.fetchVideoData(parseInt(this.props.match.params.id, 10));
}
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<PageTitle title='Categories' subtitle={this.videodata.length + ' Videos'} /> <PageTitle
title='Categories'
subtitle={this.videodata.length + ' Videos'}/>
<SideBar> <SideBar>
<SideBarTitle>Default Tags:</SideBarTitle> <SideBarTitle>Default Tags:</SideBarTitle>
<Tag tagInfo={DefaultTags.all} /> <Tag tagInfo={DefaultTags.all}/>
<Tag tagInfo={DefaultTags.fullhd} /> <Tag tagInfo={DefaultTags.fullhd}/>
<Tag tagInfo={DefaultTags.hd} /> <Tag tagInfo={DefaultTags.hd}/>
<Tag tagInfo={DefaultTags.lowq} /> <Tag tagInfo={DefaultTags.lowq}/>
<Line /> <Line/>
<Button <Button title='Delete Tag' onClick={(): void => {this.deleteTag(false);}} color={{backgroundColor: 'red'}}/>
title='Delete Tag'
onClick={(): void => {
this.deleteTag(false);
}}
color={{backgroundColor: 'red'}}
/>
</SideBar> </SideBar>
{this.state.loaded ? <VideoContainer data={this.videodata} /> : null} {this.state.loaded ?
<VideoContainer
data={this.videodata}/> : null}
<button <button data-testid='backbtn' className='btn btn-success'
data-testid='backbtn' onClick={(): void => {
className='btn btn-success' this.props.history.push('/categories');
onClick={(): void => { }}>Back to Categories
this.props.history.push('/categories');
}}>
Back to Categories
</button> </button>
{this.handlePopups()} {this.handlePopups()}
</> </>
@ -87,14 +78,9 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
private handlePopups(): JSX.Element { private handlePopups(): JSX.Element {
if (this.state.submitForceDelete) { if (this.state.submitForceDelete) {
return ( return (<SubmitPopup
<SubmitPopup onHide={(): void => this.setState({submitForceDelete: false})}
onHide={(): void => this.setState({submitForceDelete: false})} submit={(): void => {this.deleteTag(true);}}/>);
submit={(): void => {
this.deleteTag(true);
}}
/>
);
} else { } else {
return <></>; return <></>;
} }
@ -105,7 +91,7 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
* @param id tagid * @param id tagid
*/ */
private fetchVideoData(id: number): void { private fetchVideoData(id: number): void {
callAPI<VideoTypes.VideoUnloadedType[]>(APINode.Video, {action: 'getMovies', tag: id}, (result) => { callAPI<VideoTypes.VideoUnloadedType[]>(APINode.Video, {action: 'getMovies', tag: id}, result => {
this.videodata = result; this.videodata = result;
this.setState({loaded: true}); this.setState({loaded: true});
}); });
@ -115,23 +101,19 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
* delete the current tag * delete the current tag
*/ */
private deleteTag(force: boolean): void { private deleteTag(force: boolean): void {
callAPI<GeneralSuccess>( callAPI<GeneralSuccess>(APINode.Tags, {
APINode.Tags, action: 'deleteTag',
{ TagId: parseInt(this.props.match.params.id),
action: 'deleteTag', Force: force
TagId: parseInt(this.props.match.params.id, 10), }, result => {
Force: force console.log(result.result);
}, if (result.result === 'success') {
(result) => { this.props.history.push('/categories');
console.log(result.result); } else if (result.result === 'not empty tag') {
if (result.result === 'success') { // show submisison tag to ask if really delete
this.props.history.push('/categories'); this.setState({submitForceDelete: true});
} else if (result.result === 'not empty tag') {
// show submisison tag to ask if really delete
this.setState({submitForceDelete: true});
}
} }
); });
} }
} }

View File

@ -8,6 +8,13 @@ describe('<TagView/>', function () {
wrapper.unmount(); wrapper.unmount();
}); });
it('test Tag insertion', function () {
const wrapper = shallow(<TagView/>);
wrapper.setState({loadedtags: [{tag_name: 'test', tag_id: 42}]});
expect(wrapper.find('TagPreview')).toHaveLength(1);
});
it('test new tag popup', function () { it('test new tag popup', function () {
const wrapper = shallow(<TagView/>); const wrapper = shallow(<TagView/>);

View File

@ -1,5 +1,6 @@
import {TagType} from '../../types/VideoTypes'; import {TagType} from '../../types/VideoTypes';
import React from 'react'; import React from 'react';
import videocontainerstyle from '../../elements/VideoContainer/VideoContainer.module.css';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import {TagPreview} from '../../elements/Preview/Preview'; import {TagPreview} from '../../elements/Preview/Preview';
import {APINode, callAPI} from '../../utils/Api'; import {APINode, callAPI} from '../../utils/Api';
@ -8,17 +9,16 @@ import SideBar, {SideBarTitle} from '../../elements/SideBar/SideBar';
import Tag from '../../elements/Tag/Tag'; import Tag from '../../elements/Tag/Tag';
import {DefaultTags} from '../../types/GeneralTypes'; import {DefaultTags} from '../../types/GeneralTypes';
import NewTagPopup from '../../elements/Popups/NewTagPopup/NewTagPopup'; import NewTagPopup from '../../elements/Popups/NewTagPopup/NewTagPopup';
import DynamicContentContainer from '../../elements/DynamicContentContainer/DynamicContentContainer';
interface TagViewState { interface TagViewState {
loadedtags: TagType[]; loadedtags: TagType[];
popupvisible: boolean; popupvisible: boolean;
} }
interface Props {} interface props {}
class TagView extends React.Component<Props, TagViewState> { class TagView extends React.Component<props, TagViewState> {
constructor(props: Props) { constructor(props: props) {
super(props); super(props);
this.state = { this.state = {
@ -34,34 +34,31 @@ class TagView extends React.Component<Props, TagViewState> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<PageTitle title='Categories' subtitle={this.state.loadedtags.length + ' different Tags'} /> <PageTitle
title='Categories'
subtitle={this.state.loadedtags.length + ' different Tags'}/>
<SideBar> <SideBar>
<SideBarTitle>Default Tags:</SideBarTitle> <SideBarTitle>Default Tags:</SideBarTitle>
<Tag tagInfo={DefaultTags.all} /> <Tag tagInfo={DefaultTags.all}/>
<Tag tagInfo={DefaultTags.fullhd} /> <Tag tagInfo={DefaultTags.fullhd}/>
<Tag tagInfo={DefaultTags.hd} /> <Tag tagInfo={DefaultTags.hd}/>
<Tag tagInfo={DefaultTags.lowq} /> <Tag tagInfo={DefaultTags.lowq}/>
<Line /> <Line/>
<button <button data-testid='btnaddtag' className='btn btn-success' onClick={(): void => {
data-testid='btnaddtag' this.setState({popupvisible: true});
className='btn btn-success' }}>Add a new Tag!
onClick={(): void => {
this.setState({popupvisible: true});
}}>
Add a new Tag!
</button> </button>
</SideBar> </SideBar>
<DynamicContentContainer <div className={videocontainerstyle.maincontent}>
data={this.state.loadedtags} {this.state.loadedtags ?
renderElement={(m): JSX.Element => ( this.state.loadedtags.map((m) => (
<Link to={'/categories/' + m.TagId} key={m.TagId}> <Link to={'/categories/' + m.TagId} key={m.TagId}>
<TagPreview name={m.TagName} /> <TagPreview name={m.TagName}/></Link>
</Link> )) :
)} 'loading'}
initialLoadNr={20} </div>
/>
{this.handlePopups()} {this.handlePopups()}
</> </>
); );
@ -71,7 +68,7 @@ class TagView extends React.Component<Props, TagViewState> {
* load all available tags from db. * load all available tags from db.
*/ */
loadTags(): void { loadTags(): void {
callAPI<TagType[]>(APINode.Tags, {action: 'getAllTags'}, (result) => { callAPI<TagType[]>(APINode.Tags, {action: 'getAllTags'}, result => {
this.setState({loadedtags: result}); this.setState({loadedtags: result});
}); });
} }
@ -79,15 +76,13 @@ class TagView extends React.Component<Props, TagViewState> {
private handlePopups(): JSX.Element { private handlePopups(): JSX.Element {
if (this.state.popupvisible) { if (this.state.popupvisible) {
return ( return (
<NewTagPopup <NewTagPopup onHide={(): void => {
onHide={(): void => { this.setState({popupvisible: false});
this.setState({popupvisible: false}); this.loadTags();
this.loadTags(); }}/>
}}
/>
); );
} else { } else {
return <></>; return (<></>);
} }
} }
} }

View File

@ -10,6 +10,21 @@ describe('<HomePage/>', function () {
wrapper.unmount(); wrapper.unmount();
}); });
it('test data insertion', function () {
const wrapper = shallow(<HomePage/>);
expect(wrapper.find('VideoContainer')).toHaveLength(0);
wrapper.setState({
data: [
{}, {}
]
});
// there shoud be loaded the Videocontainer element into dom after fetching videos correctly
expect(wrapper.find('VideoContainer')).toHaveLength(1);
});
it('test title and nr insertions', function () { it('test title and nr insertions', function () {
const wrapper = shallow(<HomePage/>); const wrapper = shallow(<HomePage/>);

View File

@ -10,25 +10,26 @@ import {Route, Switch, withRouter} from 'react-router-dom';
import {RouteComponentProps} from 'react-router'; import {RouteComponentProps} from 'react-router';
import SearchHandling from './SearchHandling'; import SearchHandling from './SearchHandling';
import {VideoTypes} from '../../types/ApiTypes'; import {VideoTypes} from '../../types/ApiTypes';
import {DefaultTags} from '../../types/GeneralTypes'; import {DefaultTags} from "../../types/GeneralTypes";
interface Props extends RouteComponentProps {} interface props extends RouteComponentProps {
}
interface state { interface state {
sideinfo: VideoTypes.startDataType; sideinfo: VideoTypes.startDataType
subtitle: string; subtitle: string,
data: VideoTypes.VideoUnloadedType[]; data: VideoTypes.VideoUnloadedType[],
selectionnr: number; selectionnr: number
} }
/** /**
* The home page component showing on the initial pageload * The home page component showing on the initial pageload
*/ */
export class HomePage extends React.Component<Props, state> { export class HomePage extends React.Component<props, state> {
/** keyword variable needed temporary store search keyword */ /** keyword variable needed temporary store search keyword */
keyword = ''; keyword = '';
constructor(props: Props) { constructor(props: props) {
super(props); super(props);
this.state = { this.state = {
@ -38,7 +39,7 @@ export class HomePage extends React.Component<Props, state> {
HDNr: 0, HDNr: 0,
SDNr: 0, SDNr: 0,
DifferentTags: 0, DifferentTags: 0,
Tagged: 0 Tagged: 0,
}, },
subtitle: 'All Videos', subtitle: 'All Videos',
data: [], data: [],
@ -50,6 +51,8 @@ export class HomePage extends React.Component<Props, state> {
// initial get of all videos // initial get of all videos
this.fetchVideoData(DefaultTags.all.TagId); this.fetchVideoData(DefaultTags.all.TagId);
this.fetchStartData(); this.fetchStartData();
console.log(this.props)
} }
/** /**
@ -65,7 +68,7 @@ export class HomePage extends React.Component<Props, state> {
}); });
this.setState({ this.setState({
data: result, data: result,
selectionnr: result.length selectionnr: result.length,
}); });
}); });
} }
@ -79,86 +82,78 @@ export class HomePage extends React.Component<Props, state> {
}); });
} }
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<Switch> <Switch>
<Route path='/search/:name'> <Route path='/search/:name'>
<SearchHandling /> <SearchHandling/>
</Route> </Route>
<Route path='/'> <Route path='/'>
<PageTitle title='Home Page' subtitle={this.state.subtitle + ' - ' + this.state.selectionnr}> <PageTitle
<form title='Home Page'
className={'form-inline ' + style.searchform} subtitle={this.state.subtitle + ' - ' + this.state.selectionnr}>
onSubmit={(e): void => { <form className={'form-inline ' + style.searchform} onSubmit={(e): void => {
e.preventDefault(); e.preventDefault();
this.props.history.push('/search/' + this.keyword); this.props.history.push('/search/' + this.keyword);
}}> }}>
<input <input data-testid='searchtextfield' className='form-control mr-sm-2'
data-testid='searchtextfield' type='text' placeholder='Search'
className='form-control mr-sm-2' onChange={(e): void => {
type='text' this.keyword = e.target.value;
placeholder='Search' }}/>
onChange={(e): void => { <button data-testid='searchbtnsubmit' className='btn btn-success' type='submit'>Search
this.keyword = e.target.value;
}}
/>
<button data-testid='searchbtnsubmit' className='btn btn-success' type='submit'>
Search
</button> </button>
</form> </form>
</PageTitle> </PageTitle>
<SideBar> <SideBar>
<SideBarTitle>Infos:</SideBarTitle> <SideBarTitle>Infos:</SideBarTitle>
<Line /> <Line/>
<SideBarItem> <SideBarItem><b>{this.state.sideinfo.VideoNr}</b> Videos Total!</SideBarItem>
<b>{this.state.sideinfo.VideoNr}</b> Videos Total! <SideBarItem><b>{this.state.sideinfo.FullHdNr}</b> FULL-HD Videos!</SideBarItem>
</SideBarItem> <SideBarItem><b>{this.state.sideinfo.HDNr}</b> HD Videos!</SideBarItem>
<SideBarItem> <SideBarItem><b>{this.state.sideinfo.SDNr}</b> SD Videos!</SideBarItem>
<b>{this.state.sideinfo.FullHdNr}</b> FULL-HD Videos! <SideBarItem><b>{this.state.sideinfo.DifferentTags}</b> different Tags!</SideBarItem>
</SideBarItem> <Line/>
<SideBarItem>
<b>{this.state.sideinfo.HDNr}</b> HD Videos!
</SideBarItem>
<SideBarItem>
<b>{this.state.sideinfo.SDNr}</b> SD Videos!
</SideBarItem>
<SideBarItem>
<b>{this.state.sideinfo.DifferentTags}</b> different Tags!
</SideBarItem>
<Line />
<SideBarTitle>Default Tags:</SideBarTitle> <SideBarTitle>Default Tags:</SideBarTitle>
<Tag <Tag tagInfo={{TagName: 'All', TagId: DefaultTags.all.TagId}} onclick={(): void => {
tagInfo={{TagName: 'All', TagId: DefaultTags.all.TagId}} this.fetchVideoData(DefaultTags.all.TagId);
onclick={(): void => { this.setState({subtitle: `All Videos`});
this.fetchVideoData(DefaultTags.all.TagId); }}/>
this.setState({subtitle: 'All Videos'}); <Tag tagInfo={{TagName: 'Full Hd', TagId: DefaultTags.fullhd.TagId}} onclick={(): void => {
}} this.fetchVideoData(DefaultTags.fullhd.TagId);
/> this.setState({subtitle: `Full Hd Videos`});
<Tag }}/>
tagInfo={{TagName: 'Full Hd', TagId: DefaultTags.fullhd.TagId}} <Tag tagInfo={{TagName: 'Low Quality', TagId: DefaultTags.lowq.TagId}}
onclick={(): void => { onclick={(): void => {
this.fetchVideoData(DefaultTags.fullhd.TagId); this.fetchVideoData(DefaultTags.lowq.TagId);
this.setState({subtitle: 'Full Hd Videos'}); this.setState({subtitle: `Low Quality Videos`});
}} }}/>
/> <Tag tagInfo={{TagName: 'HD', TagId: DefaultTags.hd.TagId}} onclick={(): void => {
<Tag this.fetchVideoData(DefaultTags.hd.TagId);
tagInfo={{TagName: 'Low Quality', TagId: DefaultTags.lowq.TagId}} this.setState({subtitle: `HD Videos`});
onclick={(): void => { }}/>
this.fetchVideoData(DefaultTags.lowq.TagId);
this.setState({subtitle: 'Low Quality Videos'});
}}
/>
<Tag
tagInfo={{TagName: 'HD', TagId: DefaultTags.hd.TagId}}
onclick={(): void => {
this.fetchVideoData(DefaultTags.hd.TagId);
this.setState({subtitle: 'HD Videos'});
}}
/>
</SideBar> </SideBar>
<VideoContainer data={this.state.data} /> {this.state.data.length !== 0 ?
<div className={style.rightinfo} /> <VideoContainer
data={this.state.data}
onScrollPositionChange={(pos, loadedTiles): void => {
this.props.location.state = {pos: pos, loaded: loadedTiles};
console.log("history state update called...")
const {state} = this.props.location;
const stateCopy: { pos: number, loaded: number } = {...state as { pos: number, loaded: number }};
stateCopy.loaded = loadedTiles;
stateCopy.pos = pos;
this.props.history.replace({state: stateCopy});
console.log(this.props)
}}
initialScrollPosition={this.props.location.state !== null ? {scrollPos: (this.props.location.state as { pos: number, loaded: number }).pos, loadedTiles: (this.props.location.state as { pos: number, loaded: number }).loaded} : undefined}/> :
<div>No Data found!</div>}
<div className={style.rightinfo}>
</div>
</Route> </Route>
</Switch> </Switch>
</> </>

View File

@ -11,14 +11,14 @@ interface params {
name: string; name: string;
} }
interface Props extends RouteComponentProps<params> {} interface props extends RouteComponentProps<params> {}
interface state { interface state {
data: VideoTypes.VideoUnloadedType[]; data: VideoTypes.VideoUnloadedType[];
} }
export class SearchHandling extends React.Component<Props, state> { export class SearchHandling extends React.Component<props, state> {
constructor(props: Props) { constructor(props: props) {
super(props); super(props);
this.state = { this.state = {
@ -33,8 +33,8 @@ export class SearchHandling extends React.Component<Props, state> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<PageTitle title='Search' subtitle={this.props.match.params.name + ': ' + this.state.data.length} /> <PageTitle title='Search' subtitle={this.props.match.params.name + ': ' + this.state.data.length}/>
<SideBar hiddenFrame /> <SideBar hiddenFrame/>
{this.getVideoData()} {this.getVideoData()}
</> </>
); );
@ -45,9 +45,11 @@ export class SearchHandling extends React.Component<Props, state> {
*/ */
getVideoData(): JSX.Element { getVideoData(): JSX.Element {
if (this.state.data.length !== 0) { if (this.state.data.length !== 0) {
return <VideoContainer data={this.state.data} />; return (
<VideoContainer data={this.state.data}/>
);
} else { } else {
return <div>No Data found!</div>; return (<div>No Data found!</div>);
} }
} }

View File

@ -13,42 +13,59 @@ import {faPlusCircle} from '@fortawesome/free-solid-svg-icons';
import AddActorPopup from '../../elements/Popups/AddActorPopup/AddActorPopup'; import AddActorPopup from '../../elements/Popups/AddActorPopup/AddActorPopup';
import ActorTile from '../../elements/ActorTile/ActorTile'; import ActorTile from '../../elements/ActorTile/ActorTile';
import {withRouter} from 'react-router-dom'; import {withRouter} from 'react-router-dom';
import {APINode, callAPI} from '../../utils/Api'; import {APINode, callAPI, getBackendDomain} from '../../utils/Api';
import {RouteComponentProps} from 'react-router'; import {RouteComponentProps} from 'react-router';
import {DefaultPlyrOptions, GeneralSuccess} from '../../types/GeneralTypes'; import {GeneralSuccess} from '../../types/GeneralTypes';
import {ActorType, TagType} from '../../types/VideoTypes'; import {ActorType, TagType} from '../../types/VideoTypes';
import PlyrJS from 'plyr'; import PlyrJS from 'plyr';
import {Button} from '../../elements/GPElements/Button'; import {Button} from '../../elements/GPElements/Button';
import {VideoTypes} from '../../types/ApiTypes'; import {VideoTypes} from '../../types/ApiTypes';
import GlobalInfos from '../../utils/GlobalInfos'; import GlobalInfos from "../../utils/GlobalInfos";
interface Props extends RouteComponentProps<{id: string}> {} interface myprops extends RouteComponentProps<{ id: string }> {}
interface mystate { interface mystate {
sources?: PlyrJS.SourceInfo; sources?: PlyrJS.SourceInfo,
movieId: number; movie_id: number,
movieName: string; movie_name: string,
likes: number; likes: number,
quality: number; quality: number,
length: number; length: number,
tags: TagType[]; tags: TagType[],
suggesttag: TagType[]; suggesttag: TagType[],
popupvisible: boolean; popupvisible: boolean,
actorpopupvisible: boolean; actorpopupvisible: boolean,
actors: ActorType[]; actors: ActorType[]
} }
/** /**
* Player page loads when a video is selected to play and handles the video view * Player page loads when a video is selected to play and handles the video view
* and actions such as tag adding and liking * and actions such as tag adding and liking
*/ */
export class Player extends React.Component<Props, mystate> { export class Player extends React.Component<myprops, mystate> {
constructor(props: Props) { options: PlyrJS.Options = {
controls: [
'play-large', // The large play button in the center
'play', // Play/pause playback
'progress', // The progress bar and scrubber for playback and buffering
'current-time', // The current time of playback
'duration', // The full duration of the media
'mute', // Toggle mute
'volume', // Volume control
'captions', // Toggle captions
'settings', // Settings menu
'airplay', // Airplay (currently Safari only)
'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
'fullscreen' // Toggle fullscreen
]
};
constructor(props: myprops) {
super(props); super(props);
this.state = { this.state = {
movieId: -1, movie_id: -1,
movieName: '', movie_name: '',
likes: 0, likes: 0,
quality: 0, quality: 0,
length: 0, length: 0,
@ -70,37 +87,27 @@ export class Player extends React.Component<Props, mystate> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<div id='videocontainer'> <div id='videocontainer'>
<PageTitle title='Watch' subtitle={this.state.movieName} /> <PageTitle
title='Watch'
subtitle={this.state.movie_name}/>
{this.assembleSideBar()} {this.assembleSideBar()}
<div className={style.videowrapper}> <div className={style.videowrapper}>
{/* video component is added here */} {/* video component is added here */}
{this.state.sources ? ( {this.state.sources ? <Plyr
<Plyr style={plyrstyle} source={this.state.sources} options={DefaultPlyrOptions} /> style={plyrstyle}
) : ( source={this.state.sources}
<div>not loaded yet</div> options={this.options}/> :
)} <div>not loaded yet</div>}
<div className={style.videoactions}> <div className={style.videoactions}>
<Button onClick={(): void => this.likebtn()} title='Like this Video!' color={{backgroundColor: 'green'}} /> <Button onClick={(): void => this.likebtn()} title='Like this Video!' color={{backgroundColor: 'green'}}/>
<Button <Button onClick={(): void => this.setState({popupvisible: true})} title='Give this Video a Tag' color={{backgroundColor: '#3574fe'}}/>
onClick={(): void => this.setState({popupvisible: true})} <Button title='Delete Video' onClick={(): void => {this.deleteVideo();}} color={{backgroundColor: 'red'}}/>
title='Give this Video a Tag'
color={{backgroundColor: '#3574fe'}}
/>
<Button
title='Delete Video'
onClick={(): void => {
this.deleteVideo();
}}
color={{backgroundColor: 'red'}}
/>
</div> </div>
{this.assembleActorTiles()} {this.assembleActorTiles()}
</div> </div>
<button className={style.closebutton} onClick={(): void => this.closebtn()}> <button className={style.closebutton} onClick={(): void => this.closebtn()}>Close</button>
Close
</button>
{ {
// handle the popovers switched on and off according to state changes // handle the popovers switched on and off according to state changes
this.handlePopOvers() this.handlePopOvers()
@ -116,26 +123,18 @@ export class Player extends React.Component<Props, mystate> {
return ( return (
<SideBar> <SideBar>
<SideBarTitle>Infos:</SideBarTitle> <SideBarTitle>Infos:</SideBarTitle>
<Line /> <Line/>
<SideBarItem> <SideBarItem><b>{this.state.likes}</b> Likes!</SideBarItem>
<b>{this.state.likes}</b> Likes! {this.state.quality !== 0 ?
</SideBarItem> <SideBarItem><b>{this.state.quality}p</b> Quality!</SideBarItem> : null}
{this.state.quality !== 0 ? ( {this.state.length !== 0 ?
<SideBarItem> <SideBarItem><b>{Math.round(this.state.length / 60)}</b> Minutes of length!</SideBarItem> : null}
<b>{this.state.quality}p</b> Quality! <Line/>
</SideBarItem>
) : null}
{this.state.length !== 0 ? (
<SideBarItem>
<b>{Math.round(this.state.length / 60)}</b> Minutes of length!
</SideBarItem>
) : null}
<Line />
<SideBarTitle>Tags:</SideBarTitle> <SideBarTitle>Tags:</SideBarTitle>
{this.state.tags.map((m: TagType) => ( {this.state.tags.map((m: TagType) => (
<Tag key={m.TagId} tagInfo={m} /> <Tag key={m.TagId} tagInfo={m}/>
))} ))}
<Line /> <Line/>
<SideBarTitle>Tag Quickadd:</SideBarTitle> <SideBarTitle>Tag Quickadd:</SideBarTitle>
{this.state.suggesttag.map((m: TagType) => ( {this.state.suggesttag.map((m: TagType) => (
<Tag <Tag
@ -143,8 +142,7 @@ export class Player extends React.Component<Props, mystate> {
key={m.TagName} key={m.TagName}
onclick={(): void => { onclick={(): void => {
this.quickAddTag(m.TagId, m.TagName); this.quickAddTag(m.TagId, m.TagName);
}} }}/>
/>
))} ))}
</SideBar> </SideBar>
); );
@ -156,20 +154,18 @@ export class Player extends React.Component<Props, mystate> {
private assembleActorTiles(): JSX.Element { private assembleActorTiles(): JSX.Element {
return ( return (
<div className={style.actorcontainer}> <div className={style.actorcontainer}>
{this.state.actors ? this.state.actors.map((actr: ActorType) => <ActorTile key={actr.ActorId} actor={actr} />) : <></>} {this.state.actors ?
<div this.state.actors.map((actr: ActorType) => (
className={style.actorAddTile} <ActorTile key={actr.ActorId} actor={actr}/>
onClick={(): void => { )) : <></>
this.addActor(); }
}}> <div className={style.actorAddTile} onClick={(): void => {
this.addActor();
}}>
<div className={style.actorAddTile_thumbnail}> <div className={style.actorAddTile_thumbnail}>
<FontAwesomeIcon <FontAwesomeIcon style={{
style={{ lineHeight: '130px'
lineHeight: '130px' }} icon={faPlusCircle} size='5x'/>
}}
icon={faPlusCircle}
size='5x'
/>
</div> </div>
<div className={style.actorAddTile_name}>Add Actor</div> <div className={style.actorAddTile_name}>Add Actor</div>
</div> </div>
@ -177,6 +173,7 @@ export class Player extends React.Component<Props, mystate> {
); );
} }
/** /**
* handle the popovers generated according to state changes * handle the popovers generated according to state changes
* @returns {JSX.Element} * @returns {JSX.Element}
@ -184,18 +181,18 @@ export class Player extends React.Component<Props, mystate> {
handlePopOvers(): JSX.Element { handlePopOvers(): JSX.Element {
return ( return (
<> <>
{this.state.popupvisible ? ( {
<AddTagPopup onHide={(): void => this.setState({popupvisible: false})} submit={this.quickAddTag} /> this.state.popupvisible ?
) : null} <AddTagPopup onHide={(): void => this.setState({popupvisible: false})}
{this.state.actorpopupvisible ? ( submit={this.quickAddTag}/> : null
<AddActorPopup }
onHide={(): void => { {
this.state.actorpopupvisible ?
<AddActorPopup onHide={(): void => {
this.refetchActors(); this.refetchActors();
this.setState({actorpopupvisible: false}); this.setState({actorpopupvisible: false});
}} }} movie_id={this.state.movie_id}/> : null
movieId={this.state.movieId} }
/>
) : null}
</> </>
); );
} }
@ -206,95 +203,82 @@ export class Player extends React.Component<Props, mystate> {
* @param tagName name of tag to add * @param tagName name of tag to add
*/ */
quickAddTag(tagId: number, tagName: string): void { quickAddTag(tagId: number, tagName: string): void {
callAPI( callAPI(APINode.Tags, {
APINode.Tags, action: 'addTag',
{ TagId: tagId,
action: 'addTag', MovieId: parseInt(this.props.match.params.id)
TagId: tagId, }, (result: GeneralSuccess) => {
MovieId: parseInt(this.props.match.params.id, 10) if (result.result !== 'success') {
}, console.error('error occured while writing to db -- todo error handling');
(result: GeneralSuccess) => { console.error(result.result);
if (result.result !== 'success') { } else {
console.error('error occured while writing to db -- todo error handling'); // check if tag has already been added
console.error(result.result); const tagIndex = this.state.tags.map(function (e: TagType) {
} else { return e.TagName;
// check if tag has already been added }).indexOf(tagName);
const tagIndex = this.state.tags
.map(function (e: TagType) {
return e.TagName;
})
.indexOf(tagName);
// only add tag if it isn't already there // only add tag if it isn't already there
if (tagIndex === -1) { if (tagIndex === -1) {
// update tags if successful // update tags if successful
let array = [...this.state.suggesttag]; // make a separate copy of the array (because of setState) let array = [...this.state.suggesttag]; // make a separate copy of the array (because of setState)
const quickaddindex = this.state.suggesttag const quickaddindex = this.state.suggesttag.map(function (e: TagType) {
.map(function (e: TagType) { return e.TagId;
return e.TagId; }).indexOf(tagId);
})
.indexOf(tagId);
// check if tag is available in quickadds // check if tag is available in quickadds
if (quickaddindex !== -1) { if (quickaddindex !== -1) {
array.splice(quickaddindex, 1); array.splice(quickaddindex, 1);
this.setState({ this.setState({
tags: [...this.state.tags, {TagName: tagName, TagId: tagId}], tags: [...this.state.tags, {TagName: tagName, TagId: tagId}],
suggesttag: array suggesttag: array
}); });
} else { } else {
this.setState({ this.setState({
tags: [...this.state.tags, {TagName: tagName, TagId: tagId}] tags: [...this.state.tags, {TagName: tagName, TagId: tagId}]
}); });
}
} }
} }
} }
); });
} }
/** /**
* fetch all the required infos of a video from backend * fetch all the required infos of a video from backend
*/ */
fetchMovieData(): void { fetchMovieData(): void {
callAPI( callAPI(APINode.Video, {action: 'loadVideo', MovieId: parseInt(this.props.match.params.id)}, (result: VideoTypes.loadVideoType) => {
APINode.Video, console.log(result)
{action: 'loadVideo', MovieId: parseInt(this.props.match.params.id, 10)}, this.setState({
(result: VideoTypes.loadVideoType) => { sources: {
this.setState({ type: 'video',
sources: { sources: [
type: 'video', {
sources: [ src: getBackendDomain() + GlobalInfos.getVideoPath() + result.MovieUrl,
{ type: 'video/mp4',
src: size: 1080
(process.env.REACT_APP_CUST_BACK_DOMAIN ? process.env.REACT_APP_CUST_BACK_DOMAIN : '') + }
GlobalInfos.getVideoPath() + ],
result.MovieUrl, poster: result.Poster
type: 'video/mp4', },
size: 1080 movie_id: result.MovieId,
} movie_name: result.MovieName,
], likes: result.Likes,
poster: result.Poster quality: result.Quality,
}, length: result.Length,
movieId: result.MovieId, tags: result.Tags,
movieName: result.MovieName, suggesttag: result.SuggestedTag,
likes: result.Likes, actors: result.Actors
quality: result.Quality, });
length: result.Length, });
tags: result.Tags,
suggesttag: result.SuggestedTag,
actors: result.Actors
});
}
);
} }
/** /**
* click handler for the like btn * click handler for the like btn
*/ */
likebtn(): void { likebtn(): void {
callAPI(APINode.Video, {action: 'addLike', MovieId: parseInt(this.props.match.params.id, 10)}, (result: GeneralSuccess) => { callAPI(APINode.Video, {action: 'addLike', MovieId: parseInt(this.props.match.params.id)}, (result: GeneralSuccess) => {
if (result.result === 'success') { if (result.result === 'success') {
// likes +1 --> avoid reload of all data // likes +1 --> avoid reload of all data
this.setState({likes: this.state.likes + 1}); this.setState({likes: this.state.likes + 1});
@ -317,19 +301,15 @@ export class Player extends React.Component<Props, mystate> {
* delete the current video and return to last page * delete the current video and return to last page
*/ */
deleteVideo(): void { deleteVideo(): void {
callAPI( callAPI(APINode.Video, {action: 'deleteVideo', MovieId: parseInt(this.props.match.params.id)}, (result: GeneralSuccess) => {
APINode.Video, if (result.result === 'success') {
{action: 'deleteVideo', MovieId: parseInt(this.props.match.params.id, 10)}, // return to last element if successful
(result: GeneralSuccess) => { this.props.history.goBack();
if (result.result === 'success') { } else {
// return to last element if successful console.error('an error occured while liking');
this.props.history.goBack(); console.error(result);
} else {
console.error('an error occured while liking');
console.error(result);
}
} }
); });
} }
/** /**
@ -343,14 +323,11 @@ export class Player extends React.Component<Props, mystate> {
* fetch the available video actors again * fetch the available video actors again
*/ */
refetchActors(): void { refetchActors(): void {
callAPI<ActorType[]>( callAPI<ActorType[]>(APINode.Actor, {action: 'getActorsOfVideo', MovieId: parseInt(this.props.match.params.id)}, result => {
APINode.Actor, this.setState({actors: result});
{action: 'getActorsOfVideo', MovieId: parseInt(this.props.match.params.id, 10)}, });
(result) => {
this.setState({actors: result});
}
);
} }
} }
export default withRouter(Player); export default withRouter(Player);

View File

@ -23,8 +23,6 @@ interface GetRandomMoviesType {
* Randompage shuffles random viedeopreviews and provides a shuffle btn * Randompage shuffles random viedeopreviews and provides a shuffle btn
*/ */
class RandomPage extends React.Component<{}, state> { class RandomPage extends React.Component<{}, state> {
readonly LoadNR = 3;
constructor(props: {}) { constructor(props: {}) {
super(props); super(props);
@ -39,7 +37,7 @@ class RandomPage extends React.Component<{}, state> {
componentDidMount(): void { componentDidMount(): void {
addKeyHandler(this.keypress); addKeyHandler(this.keypress);
this.loadShuffledvideos(this.LoadNR); this.loadShuffledvideos(4);
} }
componentWillUnmount(): void { componentWillUnmount(): void {
@ -49,26 +47,26 @@ class RandomPage extends React.Component<{}, state> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<div> <div>
<PageTitle title='Random Videos' subtitle='4pc' /> <PageTitle title='Random Videos'
subtitle='4pc'/>
<SideBar> <SideBar>
<SideBarTitle>Visible Tags:</SideBarTitle> <SideBarTitle>Visible Tags:</SideBarTitle>
{this.state.tags.map((m) => ( {this.state.tags.map((m) => (
<Tag key={m.TagId} tagInfo={m} /> <Tag key={m.TagId} tagInfo={m}/>
))} ))}
</SideBar> </SideBar>
{this.state.videos.length !== 0 ? ( {this.state.videos.length !== 0 ?
<VideoContainer data={this.state.videos}> <VideoContainer
data={this.state.videos}>
<div className={style.Shufflebutton}> <div className={style.Shufflebutton}>
<button onClick={(): void => this.shuffleclick()} className={style.btnshuffle}> <button onClick={(): void => this.shuffleclick()} className={style.btnshuffle}>Shuffle</button>
Shuffle
</button>
</div> </div>
</VideoContainer> </VideoContainer>
) : ( :
<div>No Data found!</div> <div>No Data found!</div>}
)}
</div> </div>
); );
} }
@ -77,7 +75,7 @@ class RandomPage extends React.Component<{}, state> {
* click handler for shuffle btn * click handler for shuffle btn
*/ */
shuffleclick(): void { shuffleclick(): void {
this.loadShuffledvideos(this.LoadNR); this.loadShuffledvideos(4);
} }
/** /**
@ -85,8 +83,8 @@ class RandomPage extends React.Component<{}, state> {
* @param nr number of videos to load * @param nr number of videos to load
*/ */
loadShuffledvideos(nr: number): void { loadShuffledvideos(nr: number): void {
callAPI<GetRandomMoviesType>(APINode.Video, {action: 'getRandomMovies', number: nr}, (result) => { callAPI<GetRandomMoviesType>(APINode.Video, {action: 'getRandomMovies', number: nr}, result => {
console.log(result); console.log(result)
this.setState({videos: []}); // needed to trigger rerender of main videoview this.setState({videos: []}); // needed to trigger rerender of main videoview
this.setState({ this.setState({
videos: result.Videos, videos: result.Videos,

View File

@ -6,37 +6,42 @@ import InfoHeaderItem from '../../elements/InfoHeaderItem/InfoHeaderItem';
import {faArchive, faBalanceScaleLeft, faRulerVertical} from '@fortawesome/free-solid-svg-icons'; import {faArchive, faBalanceScaleLeft, faRulerVertical} from '@fortawesome/free-solid-svg-icons';
import {faAddressCard} from '@fortawesome/free-regular-svg-icons'; import {faAddressCard} from '@fortawesome/free-regular-svg-icons';
import {version} from '../../../package.json'; import {version} from '../../../package.json';
import {APINode, callAPI} from '../../utils/Api'; import {APINode, callAPI, setCustomBackendDomain} from '../../utils/Api';
import {SettingsTypes} from '../../types/ApiTypes'; import {SettingsTypes} from '../../types/ApiTypes';
import {GeneralSuccess} from '../../types/GeneralTypes'; import {GeneralSuccess} from '../../types/GeneralTypes';
interface state { interface state {
generalSettings: SettingsTypes.loadGeneralSettingsType; customapi: boolean
apipath: string
generalSettings: SettingsTypes.loadGeneralSettingsType
} }
interface Props {} interface props {
}
/** /**
* Component for Generalsettings tag on Settingspage * Component for Generalsettings tag on Settingspage
* handles general settings of mediacenter which concerns to all pages * handles general settings of mediacenter which concerns to all pages
*/ */
class GeneralSettings extends React.Component<Props, state> { class GeneralSettings extends React.Component<props, state> {
constructor(props: Props) { constructor(props: props) {
super(props); super(props);
this.state = { this.state = {
customapi: false,
apipath: '',
generalSettings: { generalSettings: {
DarkMode: true, DarkMode: true,
DBSize: 0, DBSize: 0,
DifferentTags: 0, DifferentTags: 0,
EpisodePath: '', EpisodePath: "",
MediacenterName: '', MediacenterName: "",
Password: '', Password: "",
PasswordEnabled: false, PasswordEnabled: false,
TagsAdded: 0, TagsAdded: 0,
TMDBGrabbing: false, TMDBGrabbing: false,
VideoNr: 0, VideoNr: 0,
VideoPath: '' VideoPath: ""
} }
}; };
} }
@ -50,73 +55,79 @@ class GeneralSettings extends React.Component<Props, state> {
return ( return (
<> <>
<div className={style.infoheader}> <div className={style.infoheader}>
<InfoHeaderItem <InfoHeaderItem backColor='lightblue'
backColor='lightblue' text={this.state.generalSettings.VideoNr}
text={this.state.generalSettings.VideoNr} subtext='Videos in Gravity'
subtext='Videos in Gravity' icon={faArchive}/>
icon={faArchive} <InfoHeaderItem backColor='yellow'
/> text={this.state.generalSettings.DBSize + ' MB'}
<InfoHeaderItem subtext='Database size'
backColor='yellow' icon={faRulerVertical}/>
text={this.state.generalSettings.DBSize + ' MB'} <InfoHeaderItem backColor='green'
subtext='Database size' text={this.state.generalSettings.DifferentTags}
icon={faRulerVertical} subtext='different Tags'
/> icon={faAddressCard}/>
<InfoHeaderItem <InfoHeaderItem backColor='orange'
backColor='green' text={this.state.generalSettings.TagsAdded}
text={this.state.generalSettings.DifferentTags} subtext='tags added'
subtext='different Tags' icon={faBalanceScaleLeft}/>
icon={faAddressCard}
/>
<InfoHeaderItem
backColor='orange'
text={this.state.generalSettings.TagsAdded}
subtext='tags added'
icon={faBalanceScaleLeft}
/>
</div> </div>
<div className={style.GeneralForm + ' ' + themeStyle.subtextcolor}> <div className={style.GeneralForm + ' ' + themeStyle.subtextcolor}>
<Form <Form data-testid='mainformsettings' onSubmit={(e): void => {
data-testid='mainformsettings' e.preventDefault();
onSubmit={(e): void => { this.saveSettings();
e.preventDefault(); }}>
this.saveSettings();
}}>
<Form.Row> <Form.Row>
<Form.Group as={Col} data-testid='videpathform'> <Form.Group as={Col} data-testid='videpathform'>
<Form.Label>Video Path</Form.Label> <Form.Label>Video Path</Form.Label>
<Form.Control <Form.Control type='text' placeholder='/var/www/html/video'
type='text' value={this.state.generalSettings.VideoPath}
placeholder='/var/www/html/video' onChange={(ee): void => this.setState({
value={this.state.generalSettings.VideoPath} generalSettings: {
onChange={(ee): void => ...this.state.generalSettings,
this.setState({ VideoPath: ee.target.value
generalSettings: { }
...this.state.generalSettings, })}/>
VideoPath: ee.target.value
}
})
}
/>
</Form.Group> </Form.Group>
<Form.Group as={Col} data-testid='tvshowpath'> <Form.Group as={Col} data-testid='tvshowpath'>
<Form.Label>TV Show Path</Form.Label> <Form.Label>TV Show Path</Form.Label>
<Form.Control <Form.Control type='text' placeholder='/var/www/html/tvshow'
type='text' value={this.state.generalSettings.EpisodePath}
placeholder='/var/www/html/tvshow' onChange={(e): void => this.setState({
value={this.state.generalSettings.EpisodePath} generalSettings: {
onChange={(e): void => ...this.state.generalSettings,
this.setState({ EpisodePath: e.target.value
generalSettings: { }
...this.state.generalSettings, })}/>
EpisodePath: e.target.value
}
})
}
/>
</Form.Group> </Form.Group>
</Form.Row> </Form.Row>
<Form.Check
type='switch'
id='custom-switch-api'
label='Use custom API url'
checked={this.state.customapi}
onChange={(): void => {
if (this.state.customapi) {
setCustomBackendDomain('');
}
this.setState({customapi: !this.state.customapi});
}}
/>
{this.state.customapi ?
<Form.Group className={style.customapiform} data-testid='apipath'>
<Form.Label>API Backend url</Form.Label>
<Form.Control type='text' placeholder='https://127.0.0.1'
value={this.state.apipath}
onChange={(e): void => {
this.setState({apipath: e.target.value});
setCustomBackendDomain(e.target.value);
}}/>
</Form.Group> : null}
<Form.Check <Form.Check
type='switch' type='switch'
id='custom-switch' id='custom-switch'
@ -133,24 +144,19 @@ class GeneralSettings extends React.Component<Props, state> {
}} }}
/> />
{this.state.generalSettings.PasswordEnabled ? ( {this.state.generalSettings.PasswordEnabled ?
<Form.Group data-testid='passwordfield'> <Form.Group data-testid='passwordfield'>
<Form.Label>Password</Form.Label> <Form.Label>Password</Form.Label>
<Form.Control <Form.Control type='password' placeholder='**********'
type='password' value={this.state.generalSettings.Password}
placeholder='**********' onChange={(e): void => this.setState({
value={this.state.generalSettings.Password} generalSettings: {
onChange={(e): void => ...this.state.generalSettings,
this.setState({ Password: e.target.value
generalSettings: { }
...this.state.generalSettings, })}/>
Password: e.target.value </Form.Group> : null
} }
})
}
/>
</Form.Group>
) : null}
<Form.Check <Form.Check
type='switch' type='switch'
@ -176,24 +182,21 @@ class GeneralSettings extends React.Component<Props, state> {
checked={GlobalInfos.isDarkTheme()} checked={GlobalInfos.isDarkTheme()}
onChange={(): void => { onChange={(): void => {
GlobalInfos.enableDarkTheme(!GlobalInfos.isDarkTheme()); GlobalInfos.enableDarkTheme(!GlobalInfos.isDarkTheme());
this.forceUpdate();
// todo initiate rerender
}} }}
/> />
<Form.Group className={style.mediacenternameform} data-testid='nameform'> <Form.Group className={style.mediacenternameform} data-testid='nameform'>
<Form.Label>The name of the Mediacenter</Form.Label> <Form.Label>The name of the Mediacenter</Form.Label>
<Form.Control <Form.Control type='text' placeholder='Mediacentername'
type='text' value={this.state.generalSettings.MediacenterName}
placeholder='Mediacentername' onChange={(e): void => this.setState({
value={this.state.generalSettings.MediacenterName} generalSettings: {
onChange={(e): void => ...this.state.generalSettings,
this.setState({ MediacenterName: e.target.value
generalSettings: { }
...this.state.generalSettings, })}/>
MediacenterName: e.target.value
}
})
}
/>
</Form.Group> </Form.Group>
<Button variant='primary' type='submit'> <Button variant='primary' type='submit'>
@ -201,7 +204,9 @@ class GeneralSettings extends React.Component<Props, state> {
</Button> </Button>
</Form> </Form>
</div> </div>
<div className={style.footer}>Version: {version}</div> <div className={style.footer}>
Version: {version}
</div>
</> </>
); );
} }
@ -220,27 +225,23 @@ class GeneralSettings extends React.Component<Props, state> {
*/ */
saveSettings(): void { saveSettings(): void {
let settings = this.state.generalSettings; let settings = this.state.generalSettings;
if (!this.state.generalSettings.PasswordEnabled) { if(!this.state.generalSettings.PasswordEnabled){
settings.Password = '-1'; settings.Password = '-1';
} }
settings.DarkMode = GlobalInfos.isDarkTheme(); settings.DarkMode = GlobalInfos.isDarkTheme()
callAPI( callAPI(APINode.Settings, {
APINode.Settings, action: 'saveGeneralSettings',
{ Settings: settings
action: 'saveGeneralSettings', }, (result: GeneralSuccess) => {
Settings: settings if (result.result) {
}, console.log('successfully saved settings');
(result: GeneralSuccess) => { // todo 2020-07-10: popup success
if (result.result) { } else {
console.log('successfully saved settings'); console.log('failed to save settings');
// todo 2020-07-10: popup success // todo 2020-07-10: popup error
} else {
console.log('failed to save settings');
// todo 2020-07-10: popup error
}
} }
); });
} }
} }

View File

@ -1,7 +1,7 @@
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import MovieSettings from './MovieSettings'; import MovieSettings from './MovieSettings';
import {callAPI} from '../../utils/Api'; import {callAPI} from "../../utils/Api";
describe('<MovieSettings/>', function () { describe('<MovieSettings/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
@ -23,55 +23,106 @@ describe('<MovieSettings/>', function () {
}); });
it('test simulate reindex', function () { it('test simulate reindex', function () {
callAPIMock({success: true}) global.fetch = global.prepareFetchApi({success: true});
const wrapper = shallow(<MovieSettings/>); const wrapper = shallow(<MovieSettings/>);
wrapper.find('button').findWhere(e => e.text() === 'Reindex Movie' && e.type() === 'button').simulate('click'); wrapper.find('button').findWhere(e => e.text() === 'Reindex Movie' && e.type() === 'button').simulate('click');
// initial send of reindex request to server // initial send of reindex request to server
expect(callAPI).toBeCalledTimes(1); expect(global.fetch).toBeCalledTimes(1);
}); });
it('test simulate tvshow reindex', function () { it('test failing reindex start', done => {
callAPIMock({success: true}) global.fetch = global.prepareFetchApi({success: false});
const wrapper = shallow(<MovieSettings/>); const wrapper = shallow(<MovieSettings/>);
wrapper.find('button').findWhere(e => e.text() === 'TVShow Reindex' && e.type() === 'button').simulate('click'); wrapper.find('button').findWhere(e => e.text() === 'Reindex Movie' && e.type() === 'button').simulate('click');
// initial send of reindex request to server
expect(global.fetch).toBeCalledTimes(1);
process.nextTick(() => {
// reindex already running --> so disable startbdn
expect(wrapper.state()).toMatchObject({startbtnDisabled: true});
global.fetch.mockClear();
done();
});
});
it('content available received and in state', () => {
const wrapper = shallow(<MovieSettings/>);
callAPIMock({
ContentAvailable: true,
Messages: ['firstline', 'secondline']
})
wrapper.instance().updateStatus();
expect(wrapper.state()).toMatchObject({
text: [
'firstline',
'secondline'
]
});
});
it('test reindex with no content available', () => {
callAPIMock({
Messages: [],
ContentAvailable: false
})
global.clearInterval = jest.fn();
const wrapper = shallow(<MovieSettings/>);
wrapper.instance().updateStatus();
// expect the refresh interval to be cleared
expect(global.clearInterval).toBeCalledTimes(1);
// expect startbtn to be reenabled
expect(wrapper.state()).toMatchObject({startbtnDisabled: false});
});
it('test simulate gravity cleanup', () => {
// global.fetch = global.prepareFetchApi('mmi');
callAPIMock({})
const wrapper = shallow(<MovieSettings/>);
wrapper.instance().setState = jest.fn();
wrapper.find('button').findWhere(e => e.text() === 'Cleanup Gravity' && e.type() === 'button').simulate('click');
// initial send of reindex request to server // initial send of reindex request to server
expect(callAPI).toBeCalledTimes(1); expect(callAPI).toBeCalledTimes(1);
expect(wrapper.instance().setState).toBeCalledTimes(1);
}); });
it('test handlemessage ', function () { it('expect insertion before existing ones', function () {
const wrapper = shallow(<MovieSettings/>);
const func = jest.fn((str) => {})
wrapper.instance().appendLog = func
wrapper.instance().handleMessage('{"Action":"message", "Message":"testmsg"}')
expect(func).toHaveBeenCalledTimes(1);
expect(func).toHaveBeenLastCalledWith('testmsg')
wrapper.setState({startbtnDisabled: false});
// expect button to get disabled!
wrapper.instance().handleMessage('{"Action":"reindexAction", "Event":"start"}');
expect(wrapper.state().startbtnDisabled).toBeTruthy()
// expect button to get enabled
wrapper.instance().handleMessage('{"Action":"reindexAction", "Event":"stop"}');
expect(wrapper.state().startbtnDisabled).not.toBeTruthy()
});
it('test appendlog', function () {
const wrapper = shallow(<MovieSettings/>); const wrapper = shallow(<MovieSettings/>);
wrapper.instance().appendLog("testmsg"); callAPIMock({
expect(wrapper.state().text).toHaveLength(1) ContentAvailable: true,
expect(wrapper.state().text[0]).toBe('testmsg') Messages: ['test']
})
wrapper.instance().appendLog("testmsg2"); wrapper.instance().updateStatus();
expect(wrapper.state().text).toHaveLength(2)
expect(wrapper.state().text[0]).toBe('testmsg2') expect(wrapper.state()).toMatchObject({
expect(wrapper.state().text[1]).toBe('testmsg') text: ['test']
});
// expect an untouched state if we try to add an empty string...
callAPIMock({
ContentAvailable: true,
Messages: ['']
})
wrapper.instance().updateStatus();
expect(wrapper.state()).toMatchObject({
text: ['', 'test']
});
}); });
}); });

View File

@ -2,32 +2,23 @@ import React from 'react';
import style from './MovieSettings.module.css'; import style from './MovieSettings.module.css';
import {APINode, callAPI} from '../../utils/Api'; import {APINode, callAPI} from '../../utils/Api';
import {GeneralSuccess} from '../../types/GeneralTypes'; import {GeneralSuccess} from '../../types/GeneralTypes';
import {SettingsTypes} from '../../types/ApiTypes';
interface state { interface state {
text: string[]; text: string[]
startbtnDisabled: boolean; startbtnDisabled: boolean
} }
interface Props {} interface props {}
interface MessageBase {
Action: string;
}
interface TextMessage extends MessageBase {
Message: string;
}
interface ReindexEvent extends MessageBase {
Event: string;
}
/** /**
* Component for MovieSettings on Settingspage * Component for MovieSettings on Settingspage
* handles settings concerning to movies in general * handles settings concerning to movies in general
*/ */
class MovieSettings extends React.Component<Props, state> { class MovieSettings extends React.Component<props, state> {
constructor(props: Props) { myinterval: number = -1;
constructor(props: props) {
super(props); super(props);
this.state = { this.state = {
@ -37,92 +28,27 @@ class MovieSettings extends React.Component<Props, state> {
} }
componentDidMount(): void { componentDidMount(): void {
// expectingMessage is set to true this.myinterval = window.setInterval(this.updateStatus, 1000);
this.dial();
} }
dial(): void { componentWillUnmount(): void {
console.log('trying to connect...'); if (this.myinterval !== -1)
// check which ws protocol we need clearInterval(this.myinterval);
const wsProt = window.location.protocol === 'http:' ? 'ws' : 'wss';
const conn = new WebSocket(`${wsProt}://${window.location.host}/subscribe`);
conn.addEventListener('close', (ev) => {
this.appendLog(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`, true);
if (ev.code !== 1001) {
this.appendLog('Reconnecting in 1s', true);
setTimeout((): void => this.dial(), 1000);
}
});
conn.addEventListener('open', (_ev) => {
console.info('websocket connected');
});
// This is where we handle messages received.
conn.addEventListener('message', (ev) => {
if (typeof ev.data !== 'string') {
console.error('unexpected message type', typeof ev.data);
return;
}
this.handleMessage(ev.data);
});
}
handleMessage(message: string): void {
const obj: MessageBase = JSON.parse(message);
if (obj.Action === 'message') {
const msg = obj as TextMessage;
this.appendLog(msg.Message);
} else if (obj.Action === 'reindexAction') {
const msg = obj as ReindexEvent;
if (msg.Event === 'start') {
this.setState({startbtnDisabled: true});
} else if (msg.Event === 'stop') {
this.setState({startbtnDisabled: false});
}
} else {
console.error('unexpected response from server: ' + message);
}
}
// appendLog appends the passed text to messageLog.
appendLog(text: string, error?: boolean): void {
this.setState({
// insert a string for each line
text: [text, ...this.state.text]
});
if (error) {
console.log('heyy err');
}
} }
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<button <button disabled={this.state.startbtnDisabled}
disabled={this.state.startbtnDisabled} className='btn btn-success'
className='btn btn-success' onClick={(): void => {this.startReindex();}}>Reindex Movie
onClick={(): void => {
this.startReindex();
}}>
Reindex Movie
</button> </button>
<button <button className='btn btn-warning'
className='btn btn-warning' onClick={(): void => {this.cleanupGravity();}}>Cleanup Gravity
onClick={(): void => {
this.startTVShowReindex();
}}>
TVShow Reindex
</button> </button>
<div className={style.indextextarea}> <div className={style.indextextarea}>{this.state.text.map(m => (
{this.state.text.map((m) => ( <div key={m} className='textarea-element'>{m}</div>
<div key={m} className='textarea-element'> ))}</div>
{m}
</div>
))}
</div>
</> </>
); );
} }
@ -131,34 +57,54 @@ class MovieSettings extends React.Component<Props, state> {
* starts the reindex process of the videos in the specified folder * starts the reindex process of the videos in the specified folder
*/ */
startReindex(): void { startReindex(): void {
this.setState({text: []});
// clear output text before start // clear output text before start
this.setState({text: [], startbtnDisabled: true});
callAPI(APINode.Settings, {action: 'startReindex'}, (result: GeneralSuccess): void => { callAPI(APINode.Settings, {action: 'startReindex'}, (result: GeneralSuccess): void => {
console.log(result);
if (result.result === 'success') { if (result.result === 'success') {
console.log('started successfully'); console.log('started successfully');
} else {
console.log('error, reindex already running');
this.setState({startbtnDisabled: true});
} }
}); });
if (this.myinterval !== -1) {
clearInterval(this.myinterval);
}
this.myinterval = window.setInterval(this.updateStatus, 1000);
} }
/**
* This interval function reloads the current status of reindexing from backend
*/
updateStatus = (): void => {
callAPI(APINode.Settings, {action: 'getStatusMessage'}, (result: SettingsTypes.getStatusMessageType) => {
this.setState({
// insert a string for each line
text: [...result.Messages, ...this.state.text]
});
// todo 2020-07-4: scroll to bottom of div here
if (!result.ContentAvailable) {
// clear refresh interval if no content available
clearInterval(this.myinterval);
this.setState({startbtnDisabled: false});
}
});
};
/** /**
* send request to cleanup db gravity * send request to cleanup db gravity
*/ */
cleanupGravity(): void { cleanupGravity(): void {
callAPI(APINode.Settings, {action: 'cleanupGravity'}, () => { callAPI(APINode.Settings, {action: 'cleanupGravity'}, (result) => {
this.setState({ this.setState({
text: ['successfully cleaned up gravity!'] text: ['successfully cleaned up gravity!']
}); });
}); });
} }
private startTVShowReindex(): void {
this.setState({text: []});
callAPI(APINode.Settings, {action: 'startTVShowReindex'}, (result: GeneralSuccess): void => {
if (result.result === 'success') {
console.log('started successfully');
}
});
}
} }
export default MovieSettings; export default MovieSettings;

View File

@ -28,17 +28,17 @@ class SettingsPage extends React.Component {
</div> </div>
<div className={style.SettingsContent}> <div className={style.SettingsContent}>
<Switch> <Switch>
<Route path='/settings/general'> <Route path="/settings/general">
<GeneralSettings /> <GeneralSettings/>
</Route> </Route>
<Route path='/settings/movies'> <Route path="/settings/movies">
<MovieSettings /> <MovieSettings/>
</Route> </Route>
<Route path='/settings/tv'> <Route path="/settings/tv">
<span /> <span/>
</Route> </Route>
<Route path='/settings'> <Route path="/settings">
<Redirect to='/settings/general' /> <Redirect to='/settings/general'/>
</Route> </Route>
</Switch> </Switch>
</div> </div>

View File

@ -1,43 +0,0 @@
import {shallow} from 'enzyme';
import React from 'react';
import {EpisodePage, EpisodeTile} from './EpisodePage';
describe('<EpisodePage/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<EpisodePage history={{}} location={{}} match={{params: {id: 42}}}/>);
wrapper.unmount();
});
it('content showing when loaded', function () {
const wrapper = shallow(<EpisodePage history={{}} location={{}} match={{params: {id: 42}}}/>);
expect(wrapper.find('DynamicContentContainer')).toHaveLength(0)
wrapper.setState({loaded: true});
expect(wrapper.find('DynamicContentContainer')).toHaveLength(1)
});
});
describe('<EpisodeTile/>', () => {
it('renders without crashing', function () {
const wrapper = shallow(<EpisodeTile episode={{
ID: 0,
Name: 'testname',
Season: 0,
Episode: 0
}}/>);
wrapper.unmount();
});
it('renders text', function () {
const wrapper = shallow(<EpisodeTile episode={{
ID: 0,
Name: 'testname',
Season: 0,
Episode: 0
}}/>);
expect(wrapper.findWhere(e => e.text() === 'testname')).toHaveLength(1)
});
})

View File

@ -1,104 +0,0 @@
import * as React from 'react';
import {RouteComponentProps} from 'react-router';
import {withRouter} from 'react-router-dom';
import {APINode, callAPI} from '../../utils/Api';
import {Link} from 'react-router-dom';
import DynamicContentContainer from '../../elements/DynamicContentContainer/DynamicContentContainer';
import tileStyle from './EpisodeTile.module.css';
import GlobalInfos from '../../utils/GlobalInfos';
import {faPlay} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import PageTitle, {Line} from '../../elements/PageTitle/PageTitle';
import SideBar, {SideBarItem, SideBarTitle} from '../../elements/SideBar/SideBar';
interface Props extends RouteComponentProps<{id: string}> {}
interface State {
loaded: boolean;
error: number;
Showname: string;
}
interface Episode {
ID: number;
Name: string;
Season: number;
Episode: number;
}
interface EpisodeData {
error: number;
episodes: Episode[];
ShowName: string;
}
export class EpisodePage extends React.Component<Props, State> {
episodes: Episode[] = [];
state = {
loaded: false,
error: 0,
Showname: ''
};
componentDidMount(): void {
callAPI(APINode.TVShow, {action: 'getEpisodes', ShowID: parseInt(this.props.match.params.id, 10)}, (data: EpisodeData) => {
if (data.error !== 0) {
this.setState({error: data.error, loaded: true});
} else {
this.episodes = data.episodes;
this.setState({loaded: true, Showname: data.ShowName});
}
});
}
render(): JSX.Element {
// check if content is loaded
if (!this.state.loaded) {
return <>loading...</>;
}
// check if there is an error to display
if (this.state.error !== 0) {
return <>Error code: {this.state.error}</>;
}
return (
<>
<PageTitle title={this.state.Showname} subtitle={this.episodes.length + ' Episodes'} />
<SideBar>
<SideBarTitle>Infos:</SideBarTitle>
<Line />
<SideBarItem>
<b>{this.episodes.length}</b> Episodes Total!
</SideBarItem>
</SideBar>
<DynamicContentContainer
renderElement={(el): JSX.Element => <EpisodeTile key={el.ID} episode={el} />}
data={this.episodes}
initialLoadNr={-1}
/>
</>
);
}
}
export const EpisodeTile = (props: {episode: Episode}): JSX.Element => {
const themestyle = GlobalInfos.getThemeStyle();
return (
<Link to={'/tvplayer/' + props.episode.ID}>
<div className={tileStyle.tile + ' ' + themestyle.secbackground + ' ' + themestyle.textcolor}>
<FontAwesomeIcon
style={{
marginRight: '10px'
}}
icon={faPlay}
size='1x'
/>
Season: {props.episode.Season} Episode: {props.episode.Episode} {props.episode.Name}
</div>
</Link>
);
};
export default withRouter(EpisodePage);

View File

@ -1,15 +0,0 @@
.tile {
margin: 15px;
padding-top: 15px;
padding-bottom: 15px;
width: 50%;
padding-left: 15px;
}
.tile:hover {
opacity: 0.7;
}
.tile:hover svg {
color: dodgerblue;
}

View File

@ -1,10 +0,0 @@
import {shallow} from 'enzyme';
import {TVPlayer} from './TVPlayer';
import React from 'react';
describe('<TVPlayer/>', () => {
it('renders without crashing', function () {
const wrapper = shallow(<TVPlayer match={{params: {id: 42}}}/>);
wrapper.unmount();
});
})

View File

@ -1,97 +0,0 @@
import * as React from 'react';
import {RouteComponentProps} from 'react-router';
import {withRouter} from 'react-router-dom';
import PageTitle from '../../elements/PageTitle/PageTitle';
import style from '../Player/Player.module.css';
import {Plyr} from 'plyr-react';
import plyrstyle from 'plyr-react/dist/plyr.css';
import {DefaultPlyrOptions} from '../../types/GeneralTypes';
import {APINode, callAPI} from '../../utils/Api';
import GlobalInfos from '../../utils/GlobalInfos';
import PlyrJS from 'plyr';
interface Props extends RouteComponentProps<{id: string}> {}
interface State {
loaded: boolean;
}
interface EpisodeData {
Name: string;
Season: number;
Episode: number;
TVShowID: number;
Path: string;
}
export class TVPlayer extends React.Component<Props, State> {
state = {
loaded: false
};
data: EpisodeData | null = null;
componentDidMount(): void {
this.loadVideo();
}
loadVideo(): void {
callAPI(
APINode.TVShow,
{
action: 'loadEpisode',
ID: parseInt(this.props.match.params.id, 10)
},
(data: EpisodeData) => {
console.log(data);
this.data = data;
this.setState({loaded: true});
}
);
}
assemblePlyrObject(): JSX.Element {
if (this.state.loaded && this.data !== null) {
const sources: PlyrJS.SourceInfo = {
type: 'video',
sources: [
{
src:
(process.env.REACT_APP_CUST_BACK_DOMAIN ? process.env.REACT_APP_CUST_BACK_DOMAIN : '') +
GlobalInfos.getTVShowPath() +
this.data.Path,
type: 'video/mp4',
size: 1080
}
],
poster: ''
};
return <Plyr style={plyrstyle} source={sources} options={DefaultPlyrOptions} />;
} else {
return <div>not loaded yet</div>;
}
}
render(): JSX.Element {
return (
<div id='videocontainer'>
<PageTitle title='Watch' subtitle='todo' />
<div className={style.videowrapper}>
{/* video component is added here */}
{this.assemblePlyrObject()}
</div>
<button className={style.closebutton} onClick={(): void => this.closebtn()}>
Close
</button>
</div>
);
}
private closebtn(): void {
this.props.history.goBack();
}
}
export default withRouter(TVPlayer);

View File

@ -1,10 +0,0 @@
import {shallow} from 'enzyme';
import {TVShowPage} from './TVShowPage';
import React from 'react';
describe('<TVShowPage/>', () => {
it('renders without crashing', function () {
const wrapper = shallow(<TVShowPage />);
wrapper.unmount();
});
})

View File

@ -1,83 +0,0 @@
import React from 'react';
import Preview from '../../elements/Preview/Preview';
import {APINode, callAPI, callAPIPlain} from '../../utils/Api';
import {TVShow} from '../../types/ApiTypes';
import DynamicContentContainer from '../../elements/DynamicContentContainer/DynamicContentContainer';
import {Route, Switch, useRouteMatch} from 'react-router-dom';
import EpisodePage from './EpisodePage';
import PageTitle, {Line} from '../../elements/PageTitle/PageTitle';
import SideBar, {SideBarItem, SideBarTitle} from '../../elements/SideBar/SideBar';
interface State {
loading: boolean;
}
interface Props {}
export class TVShowPage extends React.Component<Props, State> {
state = {
loading: true
};
data: TVShow.TVshowType[] = [];
componentDidMount(): void {
callAPI(APINode.TVShow, {action: 'getTVShows'}, (resp: TVShow.TVshowType[]) => {
this.data = resp;
this.setState({loading: false});
});
}
render(): JSX.Element {
return (
<>
<PageTitle title='TV Shows' subtitle='' />
<SideBar>
<SideBarTitle>Infos:</SideBarTitle>
<Line />
<SideBarItem>
<b>{this.data.length}</b> TV-Shows Total!
</SideBarItem>
</SideBar>
<div>
<DynamicContentContainer
renderElement={(elem): JSX.Element => (
<Preview
key={elem.Id}
name={elem.Name}
picLoader={(callback: (pic: string) => void): void => {
callAPIPlain(
APINode.TVShow,
{
action: 'readThumbnail',
Id: elem.Id
},
(result) => callback(result)
);
}}
linkPath={'/tvshows/' + elem.Id}
/>
)}
data={this.state.loading ? [] : this.data}
initialLoadNr={20}
/>
</div>
</>
);
}
}
export default function (): JSX.Element {
let match = useRouteMatch();
return (
<Switch>
<Route path={`${match.path}/:id`}>
<EpisodePage />
</Route>
<Route path={match.path}>
<TVShowPage />
</Route>
</Switch>
);
}

View File

@ -6,8 +6,7 @@ import '@testing-library/jest-dom/extend-expect';
import {configure} from 'enzyme'; import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'; import Adapter from 'enzyme-adapter-react-16';
import {CookieTokenStore} from "./utils/TokenStore/CookieTokenStore"; import GlobalInfos from './utils/GlobalInfos';
import {token} from "./utils/TokenHandler";
configure({adapter: new Adapter()}); configure({adapter: new Adapter()});
@ -38,14 +37,12 @@ global.prepareFailingFetchApi = () => {
global.callAPIMock = (resonse) => { global.callAPIMock = (resonse) => {
const helpers = require('./utils/Api'); const helpers = require('./utils/Api');
helpers.callAPI = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);}); helpers.callAPI = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);});
helpers.callApiUnsafe = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);});
}; };
// code to run before each test // code to run before each test
global.beforeEach(() => { global.beforeEach(() => {
// empty fetch response implementation for each test // empty fetch response implementation for each test
global.fetch = prepareFetchApi({}); global.fetch = prepareFetchApi({});
token.init(new CookieTokenStore());
// todo with callAPIMock // todo with callAPIMock
}); });
@ -54,12 +51,3 @@ global.afterEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
}); });
global.mockKeyPress = () => {
let events = [];
document.addEventListener = jest.fn((event, cb) => {
events[event] = cb;
});
return events;
}

View File

@ -2,16 +2,16 @@ import {ActorType, TagType} from './VideoTypes';
export namespace VideoTypes { export namespace VideoTypes {
export interface loadVideoType { export interface loadVideoType {
MovieUrl: string; MovieUrl: string
Poster: string; Poster: string
MovieId: number; MovieId: number
MovieName: string; MovieName: string
Likes: number; Likes: number
Quality: number; Quality: number
Length: number; Length: number
Tags: TagType[]; Tags: TagType[]
SuggestedTag: TagType[]; SuggestedTag: TagType[]
Actors: ActorType[]; Actors: ActorType[]
} }
export interface startDataType { export interface startDataType {
@ -25,7 +25,7 @@ export namespace VideoTypes {
export interface VideoUnloadedType { export interface VideoUnloadedType {
MovieId: number; MovieId: number;
MovieName: string; MovieName: string
} }
} }
@ -33,24 +33,23 @@ export namespace SettingsTypes {
export interface initialApiCallData { export interface initialApiCallData {
DarkMode: boolean; DarkMode: boolean;
Password: boolean; Password: boolean;
MediacenterName: string; Mediacenter_name: string;
VideoPath: string; VideoPath: string;
TVShowPath: string;
} }
export interface loadGeneralSettingsType { export interface loadGeneralSettingsType {
VideoPath: string; VideoPath: string,
EpisodePath: string; EpisodePath: string,
MediacenterName: string; MediacenterName: string,
Password: string; Password: string,
PasswordEnabled: boolean; PasswordEnabled: boolean,
TMDBGrabbing: boolean; TMDBGrabbing: boolean,
DarkMode: boolean; DarkMode: boolean,
VideoNr: number; VideoNr: number,
DBSize: number; DBSize: number,
DifferentTags: number; DifferentTags: number,
TagsAdded: number; TagsAdded: number
} }
export interface getStatusMessageType { export interface getStatusMessageType {
@ -59,16 +58,6 @@ export namespace SettingsTypes {
} }
} }
export namespace TVShow {
/**
* result of actor fetch
*/
export interface TVshowType {
Id: number;
Name: string;
}
}
export namespace ActorTypes { export namespace ActorTypes {
/** /**
* result of actor fetch * result of actor fetch

View File

@ -1,12 +1,11 @@
import {TagType} from './VideoTypes'; import {TagType} from './VideoTypes';
import PlyrJS from 'plyr';
export interface GeneralSuccess { export interface GeneralSuccess {
result: string; result: string
} }
interface TagarrayType { interface TagarrayType {
[_: string]: TagType; [_: string]: TagType
} }
export const DefaultTags: TagarrayType = { export const DefaultTags: TagarrayType = {
@ -15,20 +14,3 @@ export const DefaultTags: TagarrayType = {
lowq: {TagId: 3, TagName: 'lowquality'}, lowq: {TagId: 3, TagName: 'lowquality'},
hd: {TagId: 4, TagName: 'hd'} hd: {TagId: 4, TagName: 'hd'}
}; };
export const DefaultPlyrOptions: PlyrJS.Options = {
controls: [
'play-large', // The large play button in the center
'play', // Play/pause playback
'progress', // The progress bar and scrubber for playback and buffering
'current-time', // The current time of playback
'duration', // The full duration of the media
'mute', // Toggle mute
'volume', // Volume control
'captions', // Toggle captions
'settings', // Settings menu
'airplay', // Airplay (currently Safari only)
'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
'fullscreen' // Toggle fullscreen
]
};

View File

@ -2,8 +2,8 @@
* type accepted by Tag component * type accepted by Tag component
*/ */
export interface TagType { export interface TagType {
TagName: string; TagName: string
TagId: number; TagId: number
} }
export interface ActorType { export interface ActorType {

View File

@ -1,15 +1,193 @@
import GlobalInfos from './GlobalInfos'; let customBackendURL: string;
import {token} from './TokenHandler';
const APIPREFIX: string = '/api/'; /**
* get the domain of the api backend
* @return string domain of backend http://x.x.x.x/bla
*/
export function getBackendDomain(): string {
let userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf(' electron/') > -1) {
// Electron-specific code - force a custom backendurl
return (customBackendURL);
} else {
// use custom only if defined
if (customBackendURL) {
return (customBackendURL);
} else {
return (window.location.origin);
}
}
}
/**
* set a custom backend domain
* @param domain a url in format [http://x.x.x.x/somanode]
*/
export function setCustomBackendDomain(domain: string): void {
customBackendURL = domain;
}
/**
* a helper function to get the api path
*/
function getAPIDomain(): string {
return getBackendDomain() + '/api/';
}
/** /**
* interface how an api request should look like * interface how an api request should look like
*/ */
interface ApiBaseRequest { interface ApiBaseRequest {
action: string | number; action: string | number,
[_: string]: string | number | boolean | object; [_: string]: string | number | boolean | object
}
// store api token - empty if not set
let apiToken = ''
// a callback que to be called after api token refresh
let callQue: (() => void)[] = []
// flag to check wheter a api refresh is currently pending
let refreshInProcess = false;
// store the expire seconds of token
let expireSeconds = -1;
interface APIToken {
access_token: string;
expires_in: number;
scope: string;
token_type: string;
}
/**
* refresh the api token or use that one in cookie if still valid
* @param callback to be called after successful refresh
*/
export function refreshAPIToken(callback: () => void): void {
callQue.push(callback);
// check if already is a token refresh is in process
if (refreshInProcess) {
// if yes return
return;
} else {
// if not set flat
refreshInProcess = true;
}
// check if a cookie with token is available
const token = getTokenCookie();
if (token !== null) {
// check if token is at least valid for the next minute
if (token.expire > (new Date().getTime() / 1000) + 60) {
apiToken = token.token;
expireSeconds = token.expire;
callback();
console.log("token still valid...")
callFuncQue();
return;
}
}
const formData = new FormData();
formData.append("grant_type", "client_credentials");
formData.append("client_id", "openmediacenter");
formData.append("client_secret", 'openmediacenter');
formData.append("scope", 'all');
fetch(getBackendDomain() + '/token', {method: 'POST', body: formData})
.then((response) => response.json()
.then((result: APIToken) => {
console.log(result)
// set api token
apiToken = result.access_token;
// set expire time
expireSeconds = (new Date().getTime() / 1000) + result.expires_in;
setTokenCookie(apiToken, expireSeconds);
// call all handlers and release flag
callFuncQue();
}));
}
/**
* call all qued callbacks
*/
function callFuncQue(): void {
// call all pending handlers
callQue.map(func => {
return func();
})
// reset pending que
callQue = []
// release flag to be able to start new refresh
refreshInProcess = false;
}
/**
* set the cookie for the currently gotten token
* @param token token string
* @param validSec second time when the token will be invalid
*/
function setTokenCookie(token: string, validSec: number): void {
let d = new Date();
d.setTime(validSec * 1000);
console.log("token set" + d.toUTCString())
let expires = "expires=" + d.toUTCString();
document.cookie = "token=" + token + ";" + expires + ";path=/";
document.cookie = "token_expire=" + validSec + ";" + expires + ";path=/";
}
/**
* get all required cookies for the token
*/
function getTokenCookie(): { token: string, expire: number } | null {
const token = decodeCookie('token');
const expireInString = decodeCookie('token_expire');
const expireIn = parseInt(expireInString, 10) | 0;
if (expireIn !== 0 && token !== '') {
return {token: token, expire: expireIn};
} else {
return null
}
}
/**
* decode a simple cookie with key specified
* @param key cookie key
*/
function decodeCookie(key: string): string {
let name = key + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
/**
* check if api token is valid -- if not request new one
* when finished call callback
* @param callback function to be called afterwards
*/
function checkAPITokenValid(callback: () => void): void {
// check if token is valid and set
if (apiToken === '' || expireSeconds <= new Date().getTime() / 1000) {
refreshAPIToken(() => {
callback()
})
} else {
callback()
}
} }
/** /**
@ -19,69 +197,28 @@ interface ApiBaseRequest {
* @param callback the callback with json reply from backend * @param callback the callback with json reply from backend
* @param errorcallback a optional callback if an error occured * @param errorcallback a optional callback if an error occured
*/ */
export function callAPI<T>( export function callAPI<T>(apinode: APINode,
apinode: APINode, fd: ApiBaseRequest,
fd: ApiBaseRequest, callback: (_: T) => void,
callback: (_: T) => void, errorcallback: (_: string) => void = (_: string): void => {
errorcallback: (_: string) => void = (_: string): void => {} }): void {
): void { checkAPITokenValid(() => {
token.checkAPITokenValid((mytoken) => { fetch(getAPIDomain() + apinode, {
fetch(APIPREFIX + apinode, { method: 'POST', body: JSON.stringify(fd), headers: new Headers({
method: 'POST',
body: JSON.stringify(fd),
headers: new Headers({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: 'Bearer ' + mytoken 'Authorization': 'Bearer ' + apiToken,
}) }),
}) }).then((response) => {
.then((response) => {
if (response.status === 200) {
// success
response.json().then((result: T) => {
callback(result);
});
} else if (response.status === 400) {
// Bad Request --> invalid token
console.log('loading Password page.');
// load password page
if (GlobalInfos.loadPasswordPage) {
GlobalInfos.loadPasswordPage(() => {
callAPI(apinode, fd, callback, errorcallback);
});
}
} else {
console.log('Error: ' + response.statusText);
}
})
.catch((reason) => errorcallback(reason));
});
}
/**
* make a public unsafe api call (without token) -- use as rare as possible only for initialization (eg. check if pwd is neccessary)
* @param apinode
* @param fd
* @param callback
* @param errorcallback
*/
export function callApiUnsafe<T>(
apinode: APINode,
fd: ApiBaseRequest,
callback: (_: T) => void,
errorcallback?: (_: string) => void
): void {
fetch(APIPREFIX + apinode, {method: 'POST', body: JSON.stringify(fd)})
.then((response) => {
if (response.status !== 200) { if (response.status !== 200) {
console.log('Error: ' + response.statusText); console.log('Error: ' + response.statusText);
// todo place error popup here // todo place error popup here
} else { } else {
response.json().then((result: T) => { response.json().then((result: T) => {
callback(result); callback(result);
}); })
} }
}) }).catch(reason => errorcallback(reason));
.catch((reason) => (errorcallback ? errorcallback(reason) : {})); })
} }
/** /**
@ -91,31 +228,26 @@ export function callApiUnsafe<T>(
* @param callback the callback with PLAIN text reply from backend * @param callback the callback with PLAIN text reply from backend
*/ */
export function callAPIPlain(apinode: APINode, fd: ApiBaseRequest, callback: (_: string) => void): void { export function callAPIPlain(apinode: APINode, fd: ApiBaseRequest, callback: (_: string) => void): void {
token.checkAPITokenValid((mytoken) => { checkAPITokenValid(() => {
fetch(APIPREFIX + apinode, { fetch(getAPIDomain() + apinode, {
method: 'POST', method: 'POST', body: JSON.stringify(fd), headers: new Headers({
body: JSON.stringify(fd),
headers: new Headers({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: 'Bearer ' + mytoken 'Authorization': 'Bearer ' + apiToken,
}) })
}).then((response) => })
response.text().then((result) => { .then((response) => response.text()
callback(result); .then((result) => {
}) callback(result);
); }));
}); });
} }
/** /**
* API nodes definitions * API nodes definitions
*/ */
// eslint-disable-next-line no-shadow
export enum APINode { export enum APINode {
Settings = 'settings', Settings = 'settings',
Tags = 'tags', Tags = 'tags',
Actor = 'actor', Actor = 'actor',
Video = 'video', Video = 'video'
TVShow = 'tvshow'
} }

View File

@ -7,8 +7,7 @@ import lighttheme from '../AppLightTheme.module.css';
*/ */
class StaticInfos { class StaticInfos {
private darktheme: boolean = true; private darktheme: boolean = true;
private videopath: string = ''; private videopath: string = ""
private tvshowpath: string = '';
/** /**
* check if the current theme is the dark theme * check if the current theme is the dark theme
@ -16,7 +15,7 @@ class StaticInfos {
*/ */
isDarkTheme(): boolean { isDarkTheme(): boolean {
return this.darktheme; return this.darktheme;
} };
/** /**
* setter to enable or disable the dark or light theme * setter to enable or disable the dark or light theme
@ -24,33 +23,22 @@ class StaticInfos {
*/ */
enableDarkTheme(enable = true): void { enableDarkTheme(enable = true): void {
this.darktheme = enable; this.darktheme = enable;
// trigger onThemeChange handlers
this.handlers.map((func) => {
func();
});
} }
/** /**
* get the currently selected theme stylesheet * get the currently selected theme stylesheet
* @returns {*} the style object of the current active theme * @returns {*} the style object of the current active theme
*/ */
getThemeStyle(): {[_: string]: string} { getThemeStyle(): { [_: string]: string } {
return this.isDarkTheme() ? darktheme : lighttheme; return this.isDarkTheme() ? darktheme : lighttheme;
} }
handlers: (() => void)[] = [];
onThemeChange(func: () => void): void {
this.handlers.push(func);
}
/** /**
* set the current videopath * set the current videopath
* @param vidpath videopath with beginning and ending slash * @param vidpath videopath with beginning and ending slash
*/ */
setVideoPaths(vidpath: string, tvshowpath: string): void { setVideoPath(vidpath: string): void {
this.videopath = vidpath; this.videopath = vidpath;
this.tvshowpath = tvshowpath;
} }
/** /**
@ -59,18 +47,7 @@ class StaticInfos {
getVideoPath(): string { getVideoPath(): string {
return this.videopath; return this.videopath;
} }
/**
* return the current tvshow path
*/
getTVShowPath(): string {
return this.tvshowpath;
}
/**
* load the Password page manually
*/
loadPasswordPage: ((callback?: () => void) => void) | undefined = undefined;
} }
export default new StaticInfos(); const GlobalInfos = new StaticInfos();
export default GlobalInfos;

View File

@ -1,135 +0,0 @@
import {TokenStore} from './TokenStore/TokenStore';
export namespace token {
// store api token - empty if not set
let apiToken = '';
// a callback que to be called after api token refresh
let callQue: ((error: string) => void)[] = [];
// flag to check wheter a api refresh is currently pending
let refreshInProcess = false;
// store the expire seconds of token
let expireSeconds = -1;
let tokenStore: TokenStore;
let APiHost: string = '/';
export function init(ts: TokenStore, apiHost?: string): void {
tokenStore = ts;
if (apiHost) {
APiHost = apiHost;
}
}
/**
* refresh the api token or use that one in cookie if still valid
* @param callback to be called after successful refresh
* @param password
* @param force
*/
export function refreshAPIToken(callback: (error: string) => void, force?: boolean, password?: string): void {
callQue.push(callback);
// check if already is a token refresh is in process
if (refreshInProcess) {
// if yes return
return;
} else {
// if not set flat
refreshInProcess = true;
}
if (apiTokenValid() && !force) {
console.log('token still valid...');
callFuncQue('');
return;
}
const formData = new FormData();
formData.append('grant_type', 'client_credentials');
formData.append('client_id', 'openmediacenter');
formData.append('client_secret', password ? password : 'openmediacenter');
formData.append('scope', 'all');
interface APIToken {
error?: string;
// eslint-disable-next-line camelcase
access_token: string; // no camel case allowed because of backendlib
// eslint-disable-next-line camelcase
expires_in: number; // no camel case allowed because of backendlib
scope: string;
// eslint-disable-next-line camelcase
token_type: string; // no camel case allowed because of backendlib
}
console.log(APiHost);
fetch(APiHost + 'token', {method: 'POST', body: formData})
.then((response) =>
response.json().then((result: APIToken) => {
if (result.error) {
callFuncQue(result.error);
return;
}
// set api token
apiToken = result.access_token;
// set expire time
expireSeconds = new Date().getTime() / 1000 + result.expires_in;
// setTokenCookie(apiToken, expireSeconds);
tokenStore.setToken({accessToken: apiToken, expireTime: expireSeconds, tokenType: '', scope: ''});
// call all handlers and release flag
callFuncQue('');
})
)
.catch((e) => {
callback(e);
});
}
export function apiTokenValid(): boolean {
// check if a cookie with token is available
// const token = getTokenCookie();
const tmptoken = tokenStore.loadToken();
if (tmptoken !== null) {
// check if token is at least valid for the next minute
if (tmptoken.expireTime > new Date().getTime() / 1000 + 60) {
apiToken = tmptoken.accessToken;
expireSeconds = tmptoken.expireTime;
return true;
}
}
return false;
}
/**
* call all qued callbacks
*/
function callFuncQue(error: string): void {
// call all pending handlers
callQue.map((func) => {
return func(error);
});
// reset pending que
callQue = [];
// release flag to be able to start new refresh
refreshInProcess = false;
}
/**
* check if api token is valid -- if not request new one
* when finished call callback
* @param callback function to be called afterwards
*/
export function checkAPITokenValid(callback: (mytoken: string) => void): void {
// check if token is valid and set
if (apiToken === '' || expireSeconds <= new Date().getTime() / 1000) {
console.log('token not valid...');
refreshAPIToken(() => {
callback(apiToken);
});
} else {
callback(apiToken);
}
}
}

View File

@ -1,48 +0,0 @@
import {Token, TokenStore} from './TokenStore';
export class CookieTokenStore extends TokenStore {
loadToken(): Token | null {
const token = this.decodeCookie('token');
const expireInString = this.decodeCookie('token_expire');
const expireIn = parseInt(expireInString, 10);
if (expireIn !== 0 && token !== '') {
return {accessToken: token, expireTime: expireIn, scope: '', tokenType: ''};
} else {
return null;
}
}
/**
* set the cookie for the currently gotten token
* @param token the token to set
*/
setToken(token: Token): void {
let d = new Date();
d.setTime(token.expireTime * 1000);
console.log('token set' + d.toUTCString());
let expires = 'expires=' + d.toUTCString();
document.cookie = 'token=' + token.accessToken + ';' + expires + ';path=/';
document.cookie = 'token_expire=' + token.expireTime + ';' + expires + ';path=/';
}
/**
* decode a simple cookie with key specified
* @param key cookie key
*/
decodeCookie(key: string): string {
let name = key + '=';
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return '';
}
}

View File

@ -1,11 +0,0 @@
export interface Token {
accessToken: string;
expireTime: number; // second time when token will be invalidated
scope: string;
tokenType: string;
}
export abstract class TokenStore {
abstract loadToken(): Token | null;
abstract setToken(token: Token): void;
}

199
yarn.lock
View File

@ -1977,20 +1977,6 @@
dependencies: dependencies:
"@types/yargs-parser" "*" "@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.17.0.tgz#6f856eca4e6a52ce9cf127dfd349096ad936aa2d"
integrity sha512-/fKFDcoHg8oNan39IKFOb5WmV7oWhQe1K6CDaAVfJaNWEhmfqlA24g+u1lqU5bMH7zuNasfMId4LaYWC5ijRLw==
dependencies:
"@typescript-eslint/experimental-utils" "4.17.0"
"@typescript-eslint/scope-manager" "4.17.0"
debug "^4.1.1"
functional-red-black-tree "^1.0.1"
lodash "^4.17.15"
regexpp "^3.0.0"
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/eslint-plugin@^4.5.0": "@typescript-eslint/eslint-plugin@^4.5.0":
version "4.16.1" version "4.16.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651"
@ -2017,18 +2003,6 @@
eslint-scope "^5.0.0" eslint-scope "^5.0.0"
eslint-utils "^2.0.0" eslint-utils "^2.0.0"
"@typescript-eslint/experimental-utils@4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.17.0.tgz#762c44aaa1a6a3c05b6d63a8648fb89b89f84c80"
integrity sha512-ZR2NIUbnIBj+LGqCFGQ9yk2EBQrpVVFOh9/Kd0Lm6gLpSAcCuLLe5lUCibKGCqyH9HPwYC0GIJce2O1i8VYmWA==
dependencies:
"@types/json-schema" "^7.0.3"
"@typescript-eslint/scope-manager" "4.17.0"
"@typescript-eslint/types" "4.17.0"
"@typescript-eslint/typescript-estree" "4.17.0"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
"@typescript-eslint/experimental-utils@^3.10.1": "@typescript-eslint/experimental-utils@^3.10.1":
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686"
@ -2040,16 +2014,6 @@
eslint-scope "^5.0.0" eslint-scope "^5.0.0"
eslint-utils "^2.0.0" eslint-utils "^2.0.0"
"@typescript-eslint/parser@^4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.17.0.tgz#141b647ffc72ebebcbf9b0fe6087f65b706d3215"
integrity sha512-KYdksiZQ0N1t+6qpnl6JeK9ycCFprS9xBAiIrw4gSphqONt8wydBw4BXJi3C11ywZmyHulvMaLjWsxDjUSDwAw==
dependencies:
"@typescript-eslint/scope-manager" "4.17.0"
"@typescript-eslint/types" "4.17.0"
"@typescript-eslint/typescript-estree" "4.17.0"
debug "^4.1.1"
"@typescript-eslint/parser@^4.5.0": "@typescript-eslint/parser@^4.5.0":
version "4.16.1" version "4.16.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.16.1.tgz#3bbd3234dd3c5b882b2bcd9899bc30e1e1586d2a" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.16.1.tgz#3bbd3234dd3c5b882b2bcd9899bc30e1e1586d2a"
@ -2068,14 +2032,6 @@
"@typescript-eslint/types" "4.16.1" "@typescript-eslint/types" "4.16.1"
"@typescript-eslint/visitor-keys" "4.16.1" "@typescript-eslint/visitor-keys" "4.16.1"
"@typescript-eslint/scope-manager@4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz#f4edf94eff3b52a863180f7f89581bf963e3d37d"
integrity sha512-OJ+CeTliuW+UZ9qgULrnGpPQ1bhrZNFpfT/Bc0pzNeyZwMik7/ykJ0JHnQ7krHanFN9wcnPK89pwn84cRUmYjw==
dependencies:
"@typescript-eslint/types" "4.17.0"
"@typescript-eslint/visitor-keys" "4.17.0"
"@typescript-eslint/types@3.10.1": "@typescript-eslint/types@3.10.1":
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727"
@ -2086,11 +2042,6 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.16.1.tgz#5ba2d3e38b1a67420d2487519e193163054d9c15" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.16.1.tgz#5ba2d3e38b1a67420d2487519e193163054d9c15"
integrity sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA== integrity sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA==
"@typescript-eslint/types@4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.17.0.tgz#f57d8fc7f31b348db946498a43050083d25f40ad"
integrity sha512-RN5z8qYpJ+kXwnLlyzZkiJwfW2AY458Bf8WqllkondQIcN2ZxQowAToGSd9BlAUZDB5Ea8I6mqL2quGYCLT+2g==
"@typescript-eslint/typescript-estree@3.10.1": "@typescript-eslint/typescript-estree@3.10.1":
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853"
@ -2118,19 +2069,6 @@
semver "^7.3.2" semver "^7.3.2"
tsutils "^3.17.1" tsutils "^3.17.1"
"@typescript-eslint/typescript-estree@4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz#b835d152804f0972b80dbda92477f9070a72ded1"
integrity sha512-lRhSFIZKUEPPWpWfwuZBH9trYIEJSI0vYsrxbvVvNyIUDoKWaklOAelsSkeh3E2VBSZiNe9BZ4E5tYBZbUczVQ==
dependencies:
"@typescript-eslint/types" "4.17.0"
"@typescript-eslint/visitor-keys" "4.17.0"
debug "^4.1.1"
globby "^11.0.1"
is-glob "^4.0.1"
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/visitor-keys@3.10.1": "@typescript-eslint/visitor-keys@3.10.1":
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931"
@ -2146,14 +2084,6 @@
"@typescript-eslint/types" "4.16.1" "@typescript-eslint/types" "4.16.1"
eslint-visitor-keys "^2.0.0" eslint-visitor-keys "^2.0.0"
"@typescript-eslint/visitor-keys@4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz#9c304cfd20287c14a31d573195a709111849b14d"
integrity sha512-WfuMN8mm5SSqXuAr9NM+fItJ0SVVphobWYkWOwQ1odsfC014Vdxk/92t4JwS1Q6fCA/ABfCKpa3AVtpUKTNKGQ==
dependencies:
"@typescript-eslint/types" "4.17.0"
eslint-visitor-keys "^2.0.0"
"@webassemblyjs/ast@1.9.0": "@webassemblyjs/ast@1.9.0":
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
@ -2500,11 +2430,6 @@ argparse@^1.0.7:
dependencies: dependencies:
sprintf-js "~1.0.2" sprintf-js "~1.0.2"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-query@^4.2.2: aria-query@^4.2.2:
version "4.2.2" version "4.2.2"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
@ -4794,11 +4719,6 @@ escodegen@^1.14.1:
optionalDependencies: optionalDependencies:
source-map "~0.6.1" source-map "~0.6.1"
eslint-config-prettier@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz#4ef1eaf97afe5176e6a75ddfb57c335121abc5a6"
integrity sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw==
eslint-config-react-app@^6.0.0: eslint-config-react-app@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz#ccff9fc8e36b322902844cbd79197982be355a0e" resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz#ccff9fc8e36b322902844cbd79197982be355a0e"
@ -4806,13 +4726,6 @@ eslint-config-react-app@^6.0.0:
dependencies: dependencies:
confusing-browser-globals "^1.0.10" confusing-browser-globals "^1.0.10"
eslint-formatter-gitlab@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/eslint-formatter-gitlab/-/eslint-formatter-gitlab-2.2.0.tgz#e91bf9e22d04ae54561bf84788ed509c32a26e01"
integrity sha512-iLQB4Fp8CFAB2PiHVGMx58Zukzx5ZiNZa8MUKWzEkYwohkv6ZoutMi1JQWG89x1I+26EsvleTMoF2QkZco/XKA==
dependencies:
js-yaml "^4.0.0"
eslint-import-resolver-node@^0.3.4: eslint-import-resolver-node@^0.3.4:
version "0.3.4" version "0.3.4"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
@ -4829,14 +4742,6 @@ eslint-module-utils@^2.6.0:
debug "^2.6.9" debug "^2.6.9"
pkg-dir "^2.0.0" pkg-dir "^2.0.0"
eslint-plugin-eslint-comments@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz#9e1cd7b4413526abb313933071d7aba05ca12ffa"
integrity sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==
dependencies:
escape-string-regexp "^1.0.5"
ignore "^5.0.5"
eslint-plugin-flowtype@^5.2.0: eslint-plugin-flowtype@^5.2.0:
version "5.3.1" version "5.3.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.3.1.tgz#df6227e28c61d967b825c1327a27818bbb2ad325" resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.3.1.tgz#df6227e28c61d967b825c1327a27818bbb2ad325"
@ -4871,13 +4776,6 @@ eslint-plugin-jest@^24.1.0:
dependencies: dependencies:
"@typescript-eslint/experimental-utils" "^4.0.1" "@typescript-eslint/experimental-utils" "^4.0.1"
eslint-plugin-jest@^24.3.1:
version "24.3.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.3.1.tgz#c8df037847b83397940bef7fbc2cc168ab466bcc"
integrity sha512-RQt59rfMSHyvedImT72iaf8JcvCcR4P7Uq499dALtjY8mrCjbwWrFi1UceG4sid2wVIeDi+0tjxXZ8CZEVO7Zw==
dependencies:
"@typescript-eslint/experimental-utils" "^4.0.1"
eslint-plugin-jsx-a11y@^6.3.1: eslint-plugin-jsx-a11y@^6.3.1:
version "6.4.1" version "6.4.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd"
@ -4895,19 +4793,12 @@ eslint-plugin-jsx-a11y@^6.3.1:
jsx-ast-utils "^3.1.0" jsx-ast-utils "^3.1.0"
language-tags "^1.0.5" language-tags "^1.0.5"
eslint-plugin-prettier@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz#7079cfa2497078905011e6f82e8dd8453d1371b7"
integrity sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==
dependencies:
prettier-linter-helpers "^1.0.0"
eslint-plugin-react-hooks@^4.2.0: eslint-plugin-react-hooks@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556"
integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==
eslint-plugin-react@^7.21.5, eslint-plugin-react@^7.22.0: eslint-plugin-react@^7.21.5:
version "7.22.0" version "7.22.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.22.0.tgz#3d1c542d1d3169c45421c1215d9470e341707269" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.22.0.tgz#3d1c542d1d3169c45421c1215d9470e341707269"
integrity sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA== integrity sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA==
@ -5018,49 +4909,6 @@ eslint@^7.11.0:
text-table "^0.2.0" text-table "^0.2.0"
v8-compile-cache "^2.0.3" v8-compile-cache "^2.0.3"
eslint@^7.22.0:
version "7.22.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.22.0.tgz#07ecc61052fec63661a2cab6bd507127c07adc6f"
integrity sha512-3VawOtjSJUQiiqac8MQc+w457iGLfuNGLFn8JmF051tTKbh5/x/0vlcEj8OgDCaw7Ysa2Jn8paGshV7x2abKXg==
dependencies:
"@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.0"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.0.1"
doctrine "^3.0.0"
enquirer "^2.3.5"
eslint-scope "^5.1.1"
eslint-utils "^2.1.0"
eslint-visitor-keys "^2.0.0"
espree "^7.3.1"
esquery "^1.4.0"
esutils "^2.0.2"
file-entry-cache "^6.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^5.0.0"
globals "^13.6.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash "^4.17.21"
minimatch "^3.0.4"
natural-compare "^1.4.0"
optionator "^0.9.1"
progress "^2.0.0"
regexpp "^3.1.0"
semver "^7.2.1"
strip-ansi "^6.0.0"
strip-json-comments "^3.1.0"
table "^6.0.4"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^7.3.0, espree@^7.3.1: espree@^7.3.0, espree@^7.3.1:
version "7.3.1" version "7.3.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
@ -5299,11 +5147,6 @@ fast-deep-equal@^3.1.1:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-diff@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-glob@^3.1.1: fast-glob@^3.1.1:
version "3.2.5" version "3.2.5"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661"
@ -5733,13 +5576,6 @@ globals@^12.1.0:
dependencies: dependencies:
type-fest "^0.8.1" type-fest "^0.8.1"
globals@^13.6.0:
version "13.6.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.6.0.tgz#d77138e53738567bb96a3916ff6f6b487af20ef7"
integrity sha512-YFKCX0SiPg7l5oKYCJ2zZGxcXprVXHcSnVuvzrT3oSENQonVLqM5pf9fN5dLGZGyCjhw8TN8Btwe/jKnZ0pjvQ==
dependencies:
type-fest "^0.20.2"
globby@11.0.1: globby@11.0.1:
version "11.0.1" version "11.0.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357"
@ -6155,7 +5991,7 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.0.5, ignore@^5.1.4: ignore@^5.1.4:
version "5.1.8" version "5.1.8"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
@ -7177,13 +7013,6 @@ js-yaml@^3.13.1:
argparse "^1.0.7" argparse "^1.0.7"
esprima "^4.0.0" esprima "^4.0.0"
js-yaml@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
dependencies:
argparse "^2.0.1"
jsbn@~0.1.0: jsbn@~0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@ -7534,7 +7363,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.5: "lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -9373,23 +9202,6 @@ prepend-http@^1.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prettier-config@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prettier-config/-/prettier-config-1.0.0.tgz#f8eed3916369b81678acaa33dc0298147d1958c8"
integrity sha1-+O7TkWNpuBZ4rKoz3AKYFH0ZWMg=
prettier-linter-helpers@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
dependencies:
fast-diff "^1.1.2"
prettier@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
pretty-bytes@^5.3.0: pretty-bytes@^5.3.0:
version "5.6.0" version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@ -11468,11 +11280,6 @@ type-fest@^0.11.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
type-fest@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
type-fest@^0.3.1: type-fest@^0.3.1:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1"