Compare commits

..

1 Commits

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

View File

@ -1,295 +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: {
File: true,
FileList: true,
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",
"@typescript-eslint/no-shadow": "warn",
// 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:
@ -25,16 +29,12 @@ Minimize_Frontend:
- node_modules/ - node_modules/
Build_Backend: Build_Backend:
image: luki42/go-ffmpeg:latest image: golang:latest
stage: build_backend stage: build
script: script:
- cd apiGo - cd apiGo
- go build -v -tags sharedffmpeg -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:
@ -62,29 +61,16 @@ Backend_Tests:
stage: test stage: test
script: script:
- cd apiGo - cd apiGo
- go install github.com/jstemmer/go-junit-report@v0.9.1 - 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
@ -93,9 +79,11 @@ Debian_Server:
- vers=$(grep -Po '"version":.*?[^\\]",' package.json | grep -Po '[0-9]+\.[0-9]+\.[0-9]+') # parse the version out of package .json - vers=$(grep -Po '"version":.*?[^\\]",' package.json | grep -Po '[0-9]+\.[0-9]+\.[0-9]+') # parse the version out of package .json
- cd deb - cd deb
- mkdir -p "./OpenMediaCenter/var/www/openmediacenter/videos/" - mkdir -p "./OpenMediaCenter/var/www/openmediacenter/videos/"
- mkdir -p "./OpenMediaCenter/tmp/"
- mkdir -p "./OpenMediaCenter/usr/bin/" - mkdir -p "./OpenMediaCenter/usr/bin/"
- cp -r ../build/* ./OpenMediaCenter/var/www/openmediacenter/ - cp -r ../build/* ./OpenMediaCenter/var/www/openmediacenter/
- cp ../apiGo/openmediacenter ./OpenMediaCenter/usr/bin/ - cp ../apiGo/openmediacenter ./OpenMediaCenter/usr/bin/
- cp ../database.sql ./OpenMediaCenter/tmp/openmediacenter.sql
- 'echo "Version: ${vers}" >> ./OpenMediaCenter/DEBIAN/control' - 'echo "Version: ${vers}" >> ./OpenMediaCenter/DEBIAN/control'
- chmod -R 0775 * - chmod -R 0775 *
- dpkg-deb --build OpenMediaCenter - dpkg-deb --build OpenMediaCenter
@ -108,54 +96,21 @@ Debian_Server:
- Minimize_Frontend - Minimize_Frontend
- Build_Backend - Build_Backend
.Test_Server_Common: Test_Server:
stage: deploy stage: deploy
image: luki42/ssh:latest image: luki42/alpineopenssh:latest
needs: needs:
- Frontend_Tests - Frontend_Tests
- Backend_Tests - Backend_Tests
- Debian_Server - Debian_Server
only:
- 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/
- ssh root@192.168.0.42 "DEBIAN_FRONTEND=noninteractive apt-get --reinstall -y -qq install /tmp/OpenMediaCenter-*.deb && rm /tmp/OpenMediaCenter-*.deb" - ssh root@192.168.0.42 "DEBIAN_FRONTEND=noninteractive apt-get --reinstall -y -qq install /tmp/OpenMediaCenter-*.deb && rm /tmp/OpenMediaCenter-*.deb"
allow_failure: true allow_failure: true
Test_Server_CD:
extends: .Test_Server_Common
only:
refs:
- master
Test_Server_MANUAL:
extends: .Test_Server_Common
when: manual
.Test_Server_2_Common:
stage: deploy
image: luki42/ssh:latest
needs:
- Frontend_Tests
- Backend_Tests
- Debian_Server
script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY_2" | ssh-add -
- mkdir -p ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- scp deb/OpenMediaCenter-*.deb root@192.168.0.44:/tmp/
- ssh root@192.168.0.44 "DEBIAN_FRONTEND=noninteractive apt-get --reinstall -y -qq install /tmp/OpenMediaCenter-*.deb && rm /tmp/OpenMediaCenter-*.deb"
allow_failure: true
Test_Server_2_CD:
extends: .Test_Server_2_Common
only:
refs:
- master
Test_Server_2_MANUAL:
extends: .Test_Server_2_Common
when: manual

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`
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.
Now you could optionally check if the service is up and running: `systemctl status OpenMediaCenter`
## 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

@ -1,10 +0,0 @@
package api
func AddHandlers() {
addVideoHandlers()
addSettingsHandlers()
addTagHandlers()
addActorsHandlers()
addTvshowHandlers()
addUploadHandler()
}

View File

@ -2,97 +2,42 @@ package api
import ( import (
"fmt" "fmt"
"openmediacenter/apiGo/api/api"
"openmediacenter/apiGo/api/types" "openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
) )
func addActorsHandlers() { func AddActorsHandlers() {
saveActorsToDB() saveActorsToDB()
getActorsFromDB() getActorsFromDB()
} }
func getActorsFromDB() { func getActorsFromDB() {
/** AddHandler("getAllActors", ActorNode, nil, func() []byte {
* @api {post} /api/actor [getAllActors] query := "SELECT actor_id, name, thumbnail FROM actors"
* @apiDescription Get all available Actors return jsonify(readActorsFromResultset(database.Query(query)))
* @apiName getAllActors
* @apiGroup Actor
*
* @apiSuccess {Object[]} . Array of Actors available
* @apiSuccess {uint32} .ActorId Actor Id
* @apiSuccess {string} .Name Actor Name
* @apiSuccess {string} .Thumbnail Portrait Thumbnail
*/
api.AddHandler("getAllActors", api.ActorNode, api.PermUser, func(context api.Context) {
query := "SELECT actor_id, name, thumbnail FROM actors ORDER BY name ASC"
context.Json(readActorsFromResultset(database.Query(query)))
}) })
/** var gaov struct {
* @api {post} /api/actor [getActorsOfVideo]
* @apiDescription Get all actors playing in one video
* @apiName getActorsOfVideo
* @apiGroup Actor
*
* @apiParam {int} MovieId ID of video
*
* @apiSuccess {Object[]} . Array of Actors available
* @apiSuccess {uint32} .ActorId Actor Id
* @apiSuccess {string} .Name Actor Name
* @apiSuccess {string} .Thumbnail Portrait Thumbnail
*/
api.AddHandler("getActorsOfVideo", api.ActorNode, api.PermUser, func(context api.Context) {
var args struct {
MovieId int MovieId int
} }
err := api.DecodeRequest(context.GetRequest(), &args) AddHandler("getActorsOfVideo", ActorNode, &gaov, func() []byte {
if err != nil {
context.Text("failed to decode request")
return
}
query := fmt.Sprintf(`SELECT a.actor_id, name, thumbnail FROM actors_videos query := fmt.Sprintf(`SELECT a.actor_id, name, thumbnail FROM actors_videos
JOIN actors a on actors_videos.actor_id = a.actor_id JOIN actors a on actors_videos.actor_id = a.actor_id
WHERE actors_videos.video_id=%d`, args.MovieId) WHERE actors_videos.video_id=%d`, gaov.MovieId)
context.Json(readActorsFromResultset(database.Query(query))) return jsonify(readActorsFromResultset(database.Query(query)))
}) })
/** var gai struct {
* @api {post} /api/actor [getActorInfo]
* @apiDescription Get all infos for an actor
* @apiName getActorInfo
* @apiGroup Actor
*
* @apiParam {int} ActorId ID of Actor
*
* @apiSuccess {VideoUnloadedType[]} Videos Array of Videos this actor plays in
* @apiSuccess {uint32} Videos.MovieId Video Id
* @apiSuccess {string} Videos.MovieName Video Name
*
* @apiSuccess {Info} Info Infos about the actor
* @apiSuccess {uint32} Info.ActorId Actor Id
* @apiSuccess {string} Info.Name Actor Name
* @apiSuccess {string} Info.Thumbnail Actor Thumbnail
*/
api.AddHandler("getActorInfo", api.ActorNode, api.PermUser, func(context api.Context) {
var args struct {
ActorId int ActorId int
} }
AddHandler("getActorInfo", ActorNode, &gai, func() []byte {
err := api.DecodeRequest(context.GetRequest(), &args)
if err != nil {
context.Error("unable to decode request")
return
}
query := fmt.Sprintf(`SELECT movie_id, movie_name FROM actors_videos query := fmt.Sprintf(`SELECT movie_id, movie_name FROM actors_videos
JOIN videos v on v.movie_id = actors_videos.video_id JOIN videos v on v.movie_id = actors_videos.video_id
WHERE actors_videos.actor_id=%d`, args.ActorId) WHERE actors_videos.actor_id=%d`, gai.ActorId)
videos := readVideosFromResultset(database.Query(query)) videos := readVideosFromResultset(database.Query(query))
query = fmt.Sprintf("SELECT actor_id, name, thumbnail FROM actors WHERE actor_id=%d", args.ActorId) query = fmt.Sprintf("SELECT actor_id, name, thumbnail FROM actors WHERE actor_id=%d", gai.ActorId)
actor := readActorsFromResultset(database.Query(query))[0] actor := readActorsFromResultset(database.Query(query))[0]
var result = struct { var result = struct {
@ -103,55 +48,25 @@ func getActorsFromDB() {
Info: actor, Info: actor,
} }
context.Json(result) return jsonify(result)
}) })
} }
func saveActorsToDB() { func saveActorsToDB() {
/** var ca struct {
* @api {post} /api/video [createActor]
* @apiDescription Create a new Actor
* @apiName createActor
* @apiGroup Actor
*
* @apiParam {string} ActorName Name of new Actor
*
* @apiSuccess {string} result 'success' if successfully or Error message if not
*/
api.AddHandler("createActor", api.ActorNode, api.PermUser, func(context api.Context) {
var args struct {
ActorName string ActorName string
} }
api.DecodeRequest(context.GetRequest(), &args) AddHandler("createActor", ActorNode, &ca, func() []byte {
query := "INSERT IGNORE INTO actors (name) VALUES (?)" query := "INSERT IGNORE INTO actors (name) VALUES (?)"
// todo bit ugly return database.SuccessQuery(query, ca.ActorName)
context.Text(string(database.SuccessQuery(query, args.ActorName)))
}) })
/** var aatv struct {
* @api {post} /api/video [addActorToVideo]
* @apiDescription Add Actor to Video
* @apiName addActorToVideo
* @apiGroup Actor
*
* @apiParam {int} ActorId Id of Actor
* @apiParam {int} MovieId Id of Movie to add to
*
* @apiSuccess {string} result 'success' if successfully or Error message if not
*/
api.AddHandler("addActorToVideo", api.ActorNode, api.PermUser, func(context api.Context) {
var args struct {
ActorId int ActorId int
MovieId int MovieId int
} }
err := api.DecodeRequest(context.GetRequest(), &args) AddHandler("addActorToVideo", ActorNode, &aatv, func() []byte {
if err != nil { query := fmt.Sprintf("INSERT IGNORE INTO actors_videos (actor_id, video_id) VALUES (%d,%d)", aatv.ActorId, aatv.MovieId)
context.Error("unable to decode request") return database.SuccessQuery(query)
return
}
query := fmt.Sprintf("INSERT IGNORE INTO actors_videos (actor_id, video_id) VALUES (%d,%d)", args.ActorId, args.MovieId)
context.Text(string(database.SuccessQuery(query)))
}) })
} }

105
apiGo/api/ApiBase.go Normal file
View File

@ -0,0 +1,105 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"openmediacenter/apiGo/api/oauth"
)
const APIPREFIX = "/api"
const (
VideoNode = iota
TagNode = iota
SettingsNode = iota
ActorNode = iota
)
type actionStruct struct {
Action string
}
type Handler struct {
action string
handler func() []byte
arguments interface{}
apiNode int
}
var handlers []Handler
func AddHandler(action string, apiNode int, n interface{}, h func() []byte) {
// append new handler to the handlers
handlers = append(handlers, Handler{action, h, n, apiNode})
}
func ServerInit(port uint16) {
http.Handle(APIPREFIX+"/video", oauth.ValidateToken(videoHandler))
http.Handle(APIPREFIX+"/tags", oauth.ValidateToken(tagHandler))
http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler))
http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(actorHandler))
// initialize oauth service and add corresponding auth routes
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 {
for i := range handlers {
if handlers[i].action == action && handlers[i].apiNode == apiNode {
// call the handler and return
if handlers[i].arguments != nil {
// decode the arguments to the corresponding arguments object
err := json.Unmarshal([]byte(requestBody), &handlers[i].arguments)
if err != nil {
fmt.Printf("failed to decode arguments of action %s :: %s\n", action, requestBody)
}
}
return handlers[i].handler()
}
}
fmt.Printf("no handler found for Action: %d/%s\n", apiNode, action)
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) {
// only allow post requests
if req.Method != "POST" {
return
}
buf := new(bytes.Buffer)
buf.ReadFrom(req.Body)
body := buf.String()
var t actionStruct
err := json.Unmarshal([]byte(body), &t)
if err != nil {
fmt.Println("failed to read action from request! :: " + body)
}
rw.Write(handleAPICall(t.Action, body, node))
}

66
apiGo/api/ApiBase_test.go Normal file
View File

@ -0,0 +1,66 @@
package api
import (
"testing"
)
func cleanUp() {
handlers = nil
}
func TestAddHandler(t *testing.T) {
cleanUp()
AddHandler("test", ActorNode, nil, func() []byte {
return nil
})
if len(handlers) != 1 {
t.Errorf("Handler insertion failed, got: %d handlers, want: %d.", len(handlers), 1)
}
}
func TestCallOfHandler(t *testing.T) {
cleanUp()
i := 0
AddHandler("test", ActorNode, nil, func() []byte {
i++
return nil
})
// simulate the call of the api
handleAPICall("test", "", ActorNode)
if i != 1 {
t.Errorf("Unexpected number of Lambda calls : %d/1", i)
}
}
func TestDecodingOfArguments(t *testing.T) {
cleanUp()
var myvar struct {
Test string
TestInt int
}
AddHandler("test", ActorNode, &myvar, func() []byte {
return nil
})
// simulate the call of the api
handleAPICall("test", `{"Test":"myString","TestInt":42}`, ActorNode)
if myvar.TestInt != 42 || myvar.Test != "myString" {
t.Errorf("Wrong parsing of argument parameters : %d/42 - %s/myString", myvar.TestInt, myvar.Test)
}
}
func TestNoHandlerCovers(t *testing.T) {
cleanUp()
ret := handleAPICall("test", "", ActorNode)
if ret != nil {
t.Error("Expect nil return within unhandled api action")
}
}

View File

@ -1,69 +0,0 @@
package api
import (
"fmt"
"io"
"openmediacenter/apiGo/api/api"
"openmediacenter/apiGo/database"
"openmediacenter/apiGo/videoparser"
"os"
)
func addUploadHandler() {
api.AddHandler("fileupload", api.VideoNode, api.PermUser, func(ctx api.Context) {
// get path where to store videos to
mSettings, PathPrefix, _ := database.GetSettings()
req := ctx.GetRequest()
mr, err := req.MultipartReader()
if err != nil {
ctx.Errorf("incorrect request!")
return
}
videoparser.InitDeps(&mSettings)
for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
// only allow valid extensions
if !videoparser.ValidVideoSuffix(part.FileName()) {
continue
}
vidpath := PathPrefix + mSettings.VideoPath + part.FileName()
dst, err := os.OpenFile(vidpath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
ctx.Error("error opening file")
return
}
fmt.Printf("Uploading file %s\n", part.FileName())
// so now loop through every appended file and upload
buffer := make([]byte, 100000)
for {
cBytes, err := part.Read(buffer)
if cBytes > 0 {
dst.Write(buffer[0:cBytes])
}
if err == io.EOF {
fmt.Printf("Finished uploading file %s\n", part.FileName())
go videoparser.ProcessVideo(part.FileName())
break
}
}
_ = dst.Close()
}
ctx.Json(struct {
Message string
}{Message: "finished all files"})
})
}

View File

@ -2,6 +2,7 @@ package api
import ( import (
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"openmediacenter/apiGo/api/types" "openmediacenter/apiGo/api/types"
) )
@ -13,8 +14,7 @@ func readVideosFromResultset(rows *sql.Rows) []types.VideoUnloadedType {
var vid types.VideoUnloadedType var vid types.VideoUnloadedType
err := rows.Scan(&vid.MovieId, &vid.MovieName) err := rows.Scan(&vid.MovieId, &vid.MovieName)
if err != nil { if err != nil {
fmt.Println(err.Error()) panic(err.Error()) // proper error handling instead of panic in your app
return nil
} }
result = append(result, vid) result = append(result, vid)
} }
@ -31,7 +31,7 @@ func readTagsFromResultset(rows *sql.Rows) []types.Tag {
var tag types.Tag var tag types.Tag
err := rows.Scan(&tag.TagId, &tag.TagName) err := rows.Scan(&tag.TagId, &tag.TagName)
if err != nil { if err != nil {
panic(err.Error()) // proper Error handling instead of panic in your app panic(err.Error()) // proper error handling instead of panic in your app
} }
result = append(result, tag) result = append(result, tag)
} }
@ -52,7 +52,7 @@ func readActorsFromResultset(rows *sql.Rows) []types.Actor {
actor.Thumbnail = string(thumbnail) actor.Thumbnail = string(thumbnail)
} }
if err != nil { if err != nil {
panic(err.Error()) // proper Error handling instead of panic in your app panic(err.Error()) // proper error handling instead of panic in your app
} }
result = append(result, actor) result = append(result, actor)
} }
@ -61,18 +61,11 @@ func readActorsFromResultset(rows *sql.Rows) []types.Actor {
return result return result
} }
// ID - Name : pay attention to the order! func jsonify(v interface{}) []byte {
func readTVshowsFromResultset(rows *sql.Rows) []types.TVShow { // jsonify results
result := []types.TVShow{} str, err := json.Marshal(v)
for rows.Next() {
var vid types.TVShow
err := rows.Scan(&vid.Id, &vid.Name)
if err != nil { if err != nil {
panic(err.Error()) // proper Error handling instead of panic in your app fmt.Println("Error while Jsonifying return object: " + err.Error())
} }
result = append(result, vid) return str
}
rows.Close()
return result
} }

View File

@ -1,125 +1,66 @@
package api package api
import ( import (
"openmediacenter/apiGo/api/api" "encoding/json"
"fmt"
"openmediacenter/apiGo/api/types" "openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/config"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"openmediacenter/apiGo/database/settings"
"openmediacenter/apiGo/videoparser" "openmediacenter/apiGo/videoparser"
"regexp"
"strings"
) )
func addSettingsHandlers() { func AddSettingsHandlers() {
saveSettingsToDB() saveSettingsToDB()
getSettingsFromDB() getSettingsFromDB()
reIndexHandling() reIndexHandling()
} }
func getSettingsFromDB() { func getSettingsFromDB() {
/** AddHandler("loadInitialData", SettingsNode, nil, func() []byte {
* @api {post} /api/settings [loadGeneralSettings] query := "SELECT DarkMode, password, mediacenter_name, video_path from settings"
* @apiDescription Get the settings object
* @apiName loadGeneralSettings
* @apiGroup Settings
*
* @apiSuccess {Object} Settings Settings object
* @apiSuccess {string} Settings.VideoPath webserver path to the videos
* @apiSuccess {string} Settings.EpisodePath webserver path to the tvshows
* @apiSuccess {string} Settings.MediacenterName overall name of the mediacenter
* @apiSuccess {string} Settings.Password new server password (-1 if no password set)
* @apiSuccess {bool} Settings.TMDBGrabbing TMDB grabbing support to grab tag info and thumbnails
* @apiSuccess {bool} Settings.DarkMode Darkmode enabled?
* @apiSuccess {Object} Sizes Sizes object
* @apiSuccess {uint32} Sizes.VideoNr total number of videos
* @apiSuccess {float32} Sizes.DBSize total size of database
* @apiSuccess {uint32} Sizes.DifferentTags number of different tags available
* @apiSuccess {uint32} Sizes.TagsAdded number of different tags added to videos
*/
api.AddHandler("loadGeneralSettings", api.SettingsNode, api.PermUser, func(context api.Context) {
result, _, sizes := database.GetSettings()
var ret = struct { type InitialDataType struct {
Settings *types.SettingsType DarkMode int
Sizes *types.SettingsSizeType Pasword int
}{ Mediacenter_name string
Settings: &result, VideoPath string
Sizes: &sizes,
} }
context.Json(ret)
})
/** result := InitialDataType{}
* @api {post} /api/settings [loadInitialData]
* @apiDescription load startdata to display on homepage err := database.QueryRow(query).Scan(&result.DarkMode, &result.Pasword, &result.Mediacenter_name, &result.VideoPath)
* @apiName loadInitialData if err != nil {
* @apiGroup Settings fmt.Println("error while parsing db data: " + err.Error())
* }
* @apiSuccess {string} VideoPath webserver path to the videos
* @apiSuccess {string} EpisodePath webserver path to the tvshows
* @apiSuccess {string} MediacenterName overall name of the mediacenter
* @apiSuccess {string} Pasword new server password (-1 if no password set)
* @apiSuccess {bool} DarkMode Darkmode enabled?
* @apiSuccess {bool} TVShowEnabled is are TVShows enabled
*/
api.AddHandler("loadInitialData", api.SettingsNode, api.PermUser, func(context api.Context) {
sett := settings.LoadSettings()
type InitialDataTypeResponse struct { type InitialDataTypeResponse struct {
DarkMode bool DarkMode bool
Pasword bool Pasword bool
MediacenterName string Mediacenter_name string
VideoPath string VideoPath string
TVShowPath string
TVShowEnabled bool
FullDeleteEnabled bool
} }
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}")
videoUrl := regexMatchUrl.FindString(sett.VideoPath)
tvshowurl := regexMatchUrl.FindString(sett.TVShowPath)
serverVideoPath := strings.TrimPrefix(sett.VideoPath, videoUrl)
serverTVShowPath := strings.TrimPrefix(sett.TVShowPath, tvshowurl)
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,
TVShowEnabled: !config.GetConfig().Features.DisableTVSupport,
FullDeleteEnabled: config.GetConfig().Features.FullyDeletableVideos,
} }
context.Json(res) str, _ := json.Marshal(res)
return str
})
AddHandler("loadGeneralSettings", SettingsNode, nil, func() []byte {
result := database.GetSettings()
return jsonify(result)
}) })
} }
func saveSettingsToDB() { func saveSettingsToDB() {
/** var sgs struct {
* @api {post} /api/settings [saveGeneralSettings] Settings types.SettingsType
* @apiDescription Save the global settings provided
* @apiName saveGeneralSettings
* @apiGroup Settings
*
* @apiParam {string} VideoPath webserver path to the videos
* @apiParam {string} EpisodePath webserver path to the tvshows
* @apiParam {string} MediacenterName overall name of the mediacenter
* @apiParam {string} Password new server password (-1 if no password set)
* @apiParam {bool} TMDBGrabbing TMDB grabbing support to grab tag info and thumbnails
* @apiParam {bool} DarkMode Darkmode enabled?
*
* @apiSuccess {string} result 'success' if successfully or Error message if not
*/
api.AddHandler("saveGeneralSettings", api.SettingsNode, api.PermUser, func(context api.Context) {
var args types.SettingsType
err := api.DecodeRequest(context.GetRequest(), &args)
if err != nil {
context.Error("unable to decode arguments")
return
} }
AddHandler("saveGeneralSettings", SettingsNode, &sgs, func() []byte {
query := ` query := `
UPDATE settings SET UPDATE settings SET
video_path=?, video_path=?,
@ -129,48 +70,25 @@ func saveSettingsToDB() {
TMDB_grabbing=?, TMDB_grabbing=?,
DarkMode=? DarkMode=?
WHERE 1` WHERE 1`
// todo avoid conversion return database.SuccessQuery(query,
context.Text(string(database.SuccessQuery(query, sgs.Settings.VideoPath, sgs.Settings.EpisodePath, sgs.Settings.Password,
args.VideoPath, args.EpisodePath, args.Password, sgs.Settings.MediacenterName, sgs.Settings.TMDBGrabbing, sgs.Settings.DarkMode)
args.MediacenterName, args.TMDBGrabbing, args.DarkMode)))
}) })
} }
// methods for handling reindexing and cleanup of db gravity // methods for handling reindexing and cleanup of db gravity
func reIndexHandling() { func reIndexHandling() {
/** AddHandler("startReindex", SettingsNode, nil, func() []byte {
* @api {post} /api/settings [startReindex]
* @apiDescription Start Database video reindex Job
* @apiName startReindex
* @apiGroup Settings
*
* @apiSuccess {string} result 'success' if successfully or Error message if not
*/
api.AddHandler("startReindex", api.SettingsNode, api.PermUser, func(context api.Context) {
videoparser.StartReindex() videoparser.StartReindex()
context.Text(string(database.ManualSuccessResponse(nil))) return database.ManualSuccessResponse(nil)
}) })
/** AddHandler("cleanupGravity", SettingsNode, nil, func() []byte {
* @api {post} /api/settings [startTVShowReindex]
* @apiDescription Start Database TVShow reindex job
* @apiName startTVShowReindex
* @apiGroup Settings
*
* @apiSuccess {string} result 'success' if successfully or Error message if not
*/
api.AddHandler("startTVShowReindex", api.SettingsNode, api.PermUser, func(context api.Context) {
videoparser.StartTVShowReindex()
context.Text(string(database.ManualSuccessResponse(nil)))
})
/**
* @api {post} /api/settings [cleanupGravity]
* @apiDescription Start Database cleanup job
* @apiName cleanupGravity
* @apiGroup Settings
*/
api.AddHandler("cleanupGravity", api.SettingsNode, api.PermUser, func(context api.Context) {
videoparser.StartCleanup() videoparser.StartCleanup()
return nil
})
AddHandler("getStatusMessage", SettingsNode, nil, func() []byte {
return jsonify(videoparser.GetStatusMessage())
}) })
} }

View File

@ -1,166 +0,0 @@
package api
import (
"fmt"
"openmediacenter/apiGo/api/api"
"openmediacenter/apiGo/config"
"openmediacenter/apiGo/database"
)
func addTvshowHandlers() {
// do not add handlers if tvshows not enabled
if config.GetConfig().Features.DisableTVSupport {
return
}
/**
* @api {post} /api/tvshow [getTVShows]
* @apiDescription get all available tv shows
* @apiName getTVShows
* @apiGroup TVshow
*
* @apiSuccess {Object[]} .
* @apiSuccess {uint32} .Id tvshow id
* @apiSuccess {string} .Name tvshow name
*/
api.AddHandler("getTVShows", api.TVShowNode, api.PermUser, func(context api.Context) {
query := "SELECT id, name FROM tvshow"
rows := database.Query(query)
context.Json(readTVshowsFromResultset(rows))
})
/**
* @api {post} /api/tvshow [getEpisodes]
* @apiDescription get all Episodes of a TVShow
* @apiName getEpisodes
* @apiGroup TVshow
*
* @apiParam {uint32} ShowID id of tvshow to get episodes from
*
* @apiSuccess {Object[]} .
* @apiSuccess {uint32} .ID episode id
* @apiSuccess {string} .Name episode name
* @apiSuccess {uint8} .Season Season number
* @apiSuccess {uint8} .Episode Episode number
*/
api.AddHandler("getEpisodes", api.TVShowNode, api.PermUser, func(context api.Context) {
var args struct {
ShowID uint32
}
err := api.DecodeRequest(context.GetRequest(), &args)
if err != nil {
context.Text("unable to decode request")
return
}
query := fmt.Sprintf("SELECT id, name, season, episode FROM tvshow_episodes WHERE tvshow_id=%d", args.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)
}
context.Json(episodes)
})
/**
* @api {post} /api/tvshow [loadEpisode]
* @apiDescription load all info of episode
* @apiName loadEpisode
* @apiGroup TVshow
*
* @apiParam {uint32} ID id of episode
*
* @apiSuccess {uint32} TVShowID episode id
* @apiSuccess {string} Name episode name
* @apiSuccess {uint8} Season Season number
* @apiSuccess {uint8} Episode Episode number
* @apiSuccess {string} Path webserver path of video file
*/
api.AddHandler("loadEpisode", api.TVShowNode, api.PermUser, func(context api.Context) {
var args struct {
ID uint32
}
err := api.DecodeRequest(context.GetRequest(), &args)
if err != nil {
context.Text("unable to decode argument")
return
}
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`, args.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())
context.Error(err.Error())
return
}
ret.Path = foldername + "/" + filename
context.Json(ret)
})
/**
* @api {post} /api/tvshow [readThumbnail]
* @apiDescription Load Thubnail of specific episode
* @apiName readThumbnail
* @apiGroup TVshow
*
* @apiParam {int} Id id of episode to load thumbnail
*
* @apiSuccess {string} . Base64 encoded Thubnail
*/
api.AddHandler("readThumbnail", api.TVShowNode, api.PermUser, func(context api.Context) {
var args struct {
Id int
}
err := api.DecodeRequest(context.GetRequest(), &args)
if err != nil {
context.Text("unable to decode request")
return
}
var pic []byte
query := fmt.Sprintf("SELECT thumbnail FROM tvshow WHERE id=%d", args.Id)
err = database.QueryRow(query).Scan(&pic)
if err != nil {
fmt.Printf("the thumbnail of movie id %d couldn't be found", args.Id)
return
}
context.Text(string(pic))
})
}

View File

@ -2,135 +2,73 @@ package api
import ( import (
"fmt" "fmt"
"openmediacenter/apiGo/api/api"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"regexp" "regexp"
) )
func addTagHandlers() { func AddTagHandlers() {
getFromDB() getFromDB()
addToDB() addToDB()
deleteFromDB() deleteFromDB()
} }
func deleteFromDB() { func deleteFromDB() {
/** var dT struct {
* @api {post} /api/tags [deleteTag]
* @apiDescription Start Database video reindex Job
* @apiName deleteTag
* @apiGroup Tags
*
* @apiParam {bool} [Force] force delete tag with its constraints
* @apiParam {int} TagId id of tag to delete
*
* @apiSuccess {string} result 'success' if successfully or Error message if not
*/
api.AddHandler("deleteTag", api.TagNode, api.PermUser, func(context api.Context) {
var args struct {
TagId int TagId int
Force bool Force bool
} }
err := api.DecodeRequest(context.GetRequest(), &args) AddHandler("deleteTag", TagNode, &dT, func() []byte {
if err != nil {
context.Text("unable to decode request")
return
}
// delete key constraints first // delete key constraints first
if args.Force { if dT.Force {
query := fmt.Sprintf("DELETE FROM video_tags WHERE tag_id=%d", args.TagId) query := fmt.Sprintf("DELETE FROM video_tags WHERE tag_id=%d", dT.TagId)
err := database.Edit(query) err := database.Edit(query)
// respond only if result not successful // respond only if result not successful
if err != nil { if err != nil {
context.Text(string(database.ManualSuccessResponse(err))) return database.ManualSuccessResponse(err)
return
} }
} }
query := fmt.Sprintf("DELETE FROM tags WHERE tag_id=%d", args.TagId) query := fmt.Sprintf("DELETE FROM tags WHERE tag_id=%d", dT.TagId)
err = database.Edit(query) err := database.Edit(query)
if err == nil { if err == nil {
// return if successful // return if successful
context.Text(string(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()) {
context.Text(string(database.ManualSuccessResponse(fmt.Errorf("not empty tag")))) return []byte(`{"result":"not empty tag"}`)
} else { } else {
context.Text(string(database.ManualSuccessResponse(err))) return database.ManualSuccessResponse(err)
} }
} }
}) })
} }
func getFromDB() { func getFromDB() {
/** AddHandler("getAllTags", TagNode, nil, func() []byte {
* @api {post} /api/tags [getAllTags] query := "SELECT tag_id,tag_name from tags"
* @apiDescription get all available Tags return jsonify(readTagsFromResultset(database.Query(query)))
* @apiName getAllTags
* @apiGroup Tags
*
* @apiSuccess {Object[]} array of tag objects
* @apiSuccess {uint32} TagId
* @apiSuccess {string} TagName name of the Tag
*/
api.AddHandler("getAllTags", api.TagNode, api.PermUser, func(context api.Context) {
query := "SELECT tag_id,tag_name from tags ORDER BY tag_name ASC"
context.Json(readTagsFromResultset(database.Query(query)))
}) })
} }
func addToDB() { func addToDB() {
/** var ct struct {
* @api {post} /api/tags [createTag]
* @apiDescription create a new tag
* @apiName createTag
* @apiGroup Tags
*
* @apiParam {string} TagName name of the tag
*
* @apiSuccess {string} result 'success' if successfully or Error message if not
*/
api.AddHandler("createTag", api.TagNode, api.PermUser, func(context api.Context) {
var args struct {
TagName string TagName string
} }
err := api.DecodeRequest(context.GetRequest(), &args) AddHandler("createTag", TagNode, &ct, func() []byte {
if err != nil {
context.Text("unable to decode request")
return
}
query := "INSERT IGNORE INTO tags (tag_name) VALUES (?)" query := "INSERT IGNORE INTO tags (tag_name) VALUES (?)"
context.Text(string(database.SuccessQuery(query, args.TagName))) return database.SuccessQuery(query, ct.TagName)
}) })
/** var at struct {
* @api {post} /api/tags [addTag]
* @apiDescription Add new tag to video
* @apiName addTag
* @apiGroup Tags
*
* @apiParam {int} TagId Tag id to add to video
* @apiParam {int} MovieId Video Id of video to add tag to
*
* @apiSuccess {string} result 'success' if successfully or Error message if not
*/
api.AddHandler("addTag", api.TagNode, api.PermUser, func(context api.Context) {
var args struct {
MovieId int MovieId int
TagId int TagId int
} }
err := api.DecodeRequest(context.GetRequest(), &args) AddHandler("addTag", TagNode, &at, func() []byte {
if err != nil {
context.Text("unable to decode request")
return
}
query := "INSERT IGNORE INTO video_tags(tag_id, video_id) VALUES (?,?)" query := "INSERT IGNORE INTO video_tags(tag_id, video_id) VALUES (?,?)"
context.Text(string(database.SuccessQuery(query, args.TagId, args.MovieId))) return database.SuccessQuery(query, at.TagId, at.MovieId)
}) })
} }

View File

@ -4,196 +4,69 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url" "net/url"
"openmediacenter/apiGo/api/api"
"openmediacenter/apiGo/api/types" "openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/config"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"os"
"strconv" "strconv"
"strings"
) )
func addVideoHandlers() { func AddVideoHandlers() {
getVideoHandlers() getVideoHandlers()
loadVideosHandlers() loadVideosHandlers()
addToVideoHandlers() addToVideoHandlers()
} }
func getVideoHandlers() { func getVideoHandlers() {
/** var mrq struct {
* @api {post} /api/video [getMovies] Tag int
* @apiDescription Request available Videos
* @apiName GetMovies
* @apiGroup video
*
* @apiParam {int} [Tag=1] id of VideoTag to get videos (1=all)
*
* @apiSuccess {Object[]} Videos List of Videos
* @apiSuccess {number} Videos.MovieId Id of Video
* @apiSuccess {String} Videos.MovieName Name of video
* @apiSuccess {String} TagName Name of the Tag returned
*/
api.AddHandler("getMovies", api.VideoNode, api.PermUser, func(context api.Context) {
var args struct {
Tag uint32
Sort uint8
} }
err := api.DecodeRequest(context.GetRequest(), &args) AddHandler("getMovies", VideoNode, &mrq, func() []byte {
if err != nil {
context.Text("unable to decode request")
return
}
const (
date = iota
likes = iota
random = iota
names = iota
length = iota
)
// if wrong number passed no sorting is performed
var SortClause = ""
switch args.Sort {
case date:
SortClause = "ORDER BY create_date DESC, movie_name"
break
case likes:
SortClause = "ORDER BY likes DESC"
break
case random:
SortClause = "ORDER BY RAND()"
break
case names:
SortClause = "ORDER BY movie_name"
break
case length:
SortClause = "ORDER BY length DESC"
break
}
var query string var query string
// 1 is the id of the ALL tag // 1 is the id of the ALL tag
if args.Tag != 1 { if mrq.Tag != 1 {
query = fmt.Sprintf(`SELECT movie_id,movie_name,previewratio,t.tag_name FROM videos query = fmt.Sprintf(`SELECT movie_id,movie_name FROM videos
INNER JOIN video_tags vt on videos.movie_id = vt.video_id INNER JOIN video_tags vt on videos.movie_id = vt.video_id
INNER JOIN tags t on vt.tag_id = t.tag_id INNER JOIN tags t on vt.tag_id = t.tag_id
WHERE t.tag_id = %d %s`, args.Tag, SortClause) WHERE t.tag_id = '%d'
ORDER BY likes DESC, create_date, movie_name`, mrq.Tag)
} else { } else {
query = fmt.Sprintf("SELECT movie_id,movie_name,previewratio, (SELECT 'All' as tag_name) FROM videos %s", SortClause) query = "SELECT movie_id,movie_name FROM videos ORDER BY create_date DESC, movie_name"
} }
var result struct { result := readVideosFromResultset(database.Query(query))
Videos []types.VideoUnloadedType // jsonify results
TagName string str, _ := json.Marshal(result)
} return str
rows := database.Query(query)
vids := []types.VideoUnloadedType{}
var name string
for rows.Next() {
var vid types.VideoUnloadedType
err := rows.Scan(&vid.MovieId, &vid.MovieName, &vid.Ratio, &name)
if err != nil {
return
}
vids = append(vids, vid)
}
if rows.Close() != nil {
return
}
// if the tag id doesn't exist the query won't return a name
if name == "" {
return
}
result.Videos = vids
result.TagName = name
context.Json(result)
}) })
/** var rtn struct {
* @api {post} /api/video [readThumbnail]
* @apiDescription Load Thubnail of specific Video
* @apiName readThumbnail
* @apiGroup video
*
* @apiParam {int} Movieid id of video to load thumbnail
*
* @apiSuccess {string} . Base64 encoded Thubnail
*/
api.AddHandler("readThumbnail", api.VideoNode, api.PermUser, func(context api.Context) {
var args struct {
Movieid int Movieid int
} }
err := api.DecodeRequest(context.GetRequest(), &args) AddHandler("readThumbnail", VideoNode, &rtn, func() []byte {
if err != nil {
context.Text("unable to decode request")
return
}
var pic []byte var pic []byte
query := fmt.Sprintf("SELECT thumbnail FROM videos WHERE movie_id=%d", args.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 {
fmt.Printf("the thumbnail of movie id %d couldn't be found", args.Movieid) fmt.Printf("the thumbnail of movie id %d couldn't be found", rtn.Movieid)
return return nil
} }
context.Text(string(pic)) return pic
}) })
/** var grm struct {
* @api {post} /api/video [getRandomMovies]
* @apiDescription Load random videos
* @apiName getRandomMovies
* @apiGroup video
*
* @apiParam {int} Number number of random videos to load
*
* @apiSuccess {Object[]} Tags Array of tags occuring in selection
* @apiSuccess {string} Tags.TagName Tagname
* @apiSuccess {uint32} Tags.TagId Tag ID
*
* @apiSuccess {Object[]} Videos Array of the videos
* @apiSuccess {string} Videos.MovieName Video Name
* @apiSuccess {int} Videos.MovieId Video ID
*/
api.AddHandler("getRandomMovies", api.VideoNode, api.PermUser, func(context api.Context) {
var args struct {
Number int Number int
TagFilter []uint32
} }
if api.DecodeRequest(context.GetRequest(), &args) != nil { AddHandler("getRandomMovies", VideoNode, &grm, func() []byte {
context.Text("unable to decode request")
return
}
var result struct { var result struct {
Tags []types.Tag Tags []types.Tag
Videos []types.VideoUnloadedType Videos []types.VideoUnloadedType
} }
whereclause := "WHERE 1" query := fmt.Sprintf("SELECT movie_id,movie_name FROM videos ORDER BY RAND() LIMIT %d", grm.Number)
if len(args.TagFilter) > 0 {
d, _ := json.Marshal(args.TagFilter)
vals := strings.Trim(string(d), "[]")
whereclause = fmt.Sprintf("WHERE tag_id IN (%s)", vals)
}
query := fmt.Sprintf(`
SELECT video_tags.video_id,v.movie_name FROM video_tags join videos v on v.movie_id = video_tags.video_id
%s
group by video_id
ORDER BY RAND()
LIMIT %d`, whereclause, args.Number)
result.Videos = readVideosFromResultset(database.Query(query)) result.Videos = readVideosFromResultset(database.Query(query))
if len(result.Videos) > 0 {
var ids string var ids string
for i := range result.Videos { for i := range result.Videos {
ids += "video_tags.video_id=" + strconv.Itoa(result.Videos[i].MovieId) ids += "video_tags.video_id=" + strconv.Itoa(result.Videos[i].MovieId)
@ -210,105 +83,55 @@ SELECT video_tags.video_id,v.movie_name FROM video_tags join videos v on v.movie
GROUP BY t.tag_id`, ids) GROUP BY t.tag_id`, ids)
rows := database.Query(query) rows := database.Query(query)
if rows != nil {
for rows.Next() { for rows.Next() {
var tag types.Tag var tag types.Tag
err := rows.Scan(&tag.TagName, &tag.TagId) err := rows.Scan(&tag.TagName, &tag.TagId)
if err != nil { if err != nil {
panic(err.Error()) // proper Error handling instead of panic in your app panic(err.Error()) // proper error handling instead of panic in your app
} }
// append to final array // append to final array
result.Tags = append(result.Tags, tag) result.Tags = append(result.Tags, tag)
} }
}
} else {
result.Tags = []types.Tag{}
}
context.Json(result) // jsonify results
str, _ := json.Marshal(result)
return str
}) })
/** var gsk struct {
* @api {post} /api/video [getSearchKeyWord]
* @apiDescription Get videos for search keyword
* @apiName getSearchKeyWord
* @apiGroup video
*
* @apiParam {string} KeyWord Keyword to search for
*
* @apiSuccess {Object[]} . List of Videos
* @apiSuccess {number} .MovieId Id of Video
* @apiSuccess {String} .MovieName Name of video
*/
api.AddHandler("getSearchKeyWord", api.VideoNode, api.PermUser, func(context api.Context) {
var args struct {
KeyWord string KeyWord string
} }
if api.DecodeRequest(context.GetRequest(), &args) != nil { AddHandler("getSearchKeyWord", VideoNode, &gsk, func() []byte {
context.Text("unable to decode request")
return
}
query := fmt.Sprintf(`SELECT movie_id,movie_name FROM videos query := fmt.Sprintf(`SELECT movie_id,movie_name FROM videos
WHERE movie_name LIKE '%%%s%%' WHERE movie_name LIKE '%%%s%%'
ORDER BY likes DESC, create_date DESC, movie_name`, args.KeyWord) ORDER BY likes DESC, create_date DESC, movie_name`, gsk.KeyWord)
context.Json(readVideosFromResultset(database.Query(query)))
result := readVideosFromResultset(database.Query(query))
// jsonify results
str, _ := json.Marshal(result)
return str
}) })
} }
// function to handle stuff for loading specific videos and startdata // function to handle stuff for loading specific videos and startdata
func loadVideosHandlers() { func loadVideosHandlers() {
/** var lv struct {
* @api {post} /api/video [loadVideo]
* @apiDescription Load all data for a specific video
* @apiName loadVideo
* @apiGroup video
*
* @apiParam {int} MovieId ID of video
*
* @apiSuccess {string} MovieName Videoname
* @apiSuccess {uint32} MovieId Video ID
* @apiSuccess {string} MovieUrl Url to video file
* @apiSuccess {string} Poster Base64 encoded Poster
* @apiSuccess {uint64} Likes Number of likes
* @apiSuccess {uint16} Quality Video FrameWidth
* @apiSuccess {uint16} Length Video Length in seconds
*
*
* @apiSuccess {Object[]} Tags Array of tags of video
* @apiSuccess {string} Tags.TagName Tagname
* @apiSuccess {uint32} Tags.TagId Tag ID
*
* @apiSuccess {Object[]} SuggestedTag Array of tags for quick add suggestions
* @apiSuccess {string} SuggestedTag.TagName Tagname
* @apiSuccess {uint32} SuggestedTag.TagId Tag ID
*
* @apiSuccess {Object[]} Actors Array of Actors playing in this video
* @apiSuccess {uint32} Actors.ActorId Actor Id
* @apiSuccess {string} Actors.Name Actor Name
* @apiSuccess {string} Actors.Thumbnail Portrait Thumbnail
*/
api.AddHandler("loadVideo", api.VideoNode, api.PermUser, func(context api.Context) {
var args struct {
MovieId int MovieId int
} }
if api.DecodeRequest(context.GetRequest(), &args) != nil { AddHandler("loadVideo", VideoNode, &lv, func() []byte {
context.Text("unable to decode request") query := fmt.Sprintf(`SELECT movie_name,movie_url,movie_id,thumbnail,poster,likes,quality,length
return FROM videos WHERE movie_id=%d`, lv.MovieId)
}
query := fmt.Sprintf(`SELECT movie_name,movie_url,movie_id,thumbnail,poster,likes,quality,length,release_date
FROM videos WHERE movie_id=%d`, args.MovieId)
var res types.FullVideoType var res types.FullVideoType
var poster []byte var poster []byte
var thumbnail []byte var thumbnail []byte
err := database.QueryRow(query).Scan(&res.MovieName, &res.MovieUrl, &res.MovieId, &thumbnail, &poster, &res.Likes, &res.Quality, &res.Length, &res.ReleaseDate) err := database.QueryRow(query).Scan(&res.MovieName, &res.MovieUrl, &res.MovieId, &thumbnail, &poster, &res.Likes, &res.Quality, &res.Length)
if err != nil { if err != nil {
fmt.Printf("Error getting full data list of videoid - %d", args.MovieId) fmt.Printf("error getting full data list of videoid - %d", lv.MovieId)
fmt.Println(err.Error()) fmt.Println(err.Error())
return return nil
} }
// we ned to urlencode the movieurl // we ned to urlencode the movieurl
@ -326,7 +149,7 @@ func loadVideosHandlers() {
query = fmt.Sprintf(`SELECT t.tag_id, t.tag_name FROM video_tags query = fmt.Sprintf(`SELECT t.tag_id, t.tag_name FROM video_tags
INNER JOIN tags t on video_tags.tag_id = t.tag_id INNER JOIN tags t on video_tags.tag_id = t.tag_id
WHERE video_tags.video_id=%d WHERE video_tags.video_id=%d
GROUP BY t.tag_id`, args.MovieId) GROUP BY t.tag_id`, lv.MovieId)
res.Tags = readTagsFromResultset(database.Query(query)) res.Tags = readTagsFromResultset(database.Query(query))
@ -335,34 +158,23 @@ func loadVideosHandlers() {
SELECT video_tags.tag_id FROM video_tags SELECT video_tags.tag_id FROM video_tags
WHERE video_id=%d) WHERE video_id=%d)
ORDER BY rand() ORDER BY rand()
LIMIT 5`, args.MovieId) LIMIT 5`, lv.MovieId)
res.SuggestedTag = readTagsFromResultset(database.Query(query)) res.SuggestedTag = readTagsFromResultset(database.Query(query))
// query the actors corresponding to video // query the actors corresponding to video
query = fmt.Sprintf(`SELECT a.actor_id, name, thumbnail FROM actors_videos query = fmt.Sprintf(`SELECT a.actor_id, name, thumbnail FROM actors_videos
JOIN actors a on actors_videos.actor_id = a.actor_id JOIN actors a on actors_videos.actor_id = a.actor_id
WHERE actors_videos.video_id=%d`, args.MovieId) WHERE actors_videos.video_id=%d`, lv.MovieId)
res.Actors = readActorsFromResultset(database.Query(query)) res.Actors = readActorsFromResultset(database.Query(query))
context.Json(res) // jsonify results
str, _ := json.Marshal(res)
return str
}) })
/** AddHandler("getStartData", VideoNode, nil, func() []byte {
* @api {post} /api/video [getStartData]
* @apiDescription Get general video informations at start
* @apiName getStartData
* @apiGroup video
*
* @apiSuccess {uint32} VideoNr Total nr of videos
* @apiSuccess {uint32} FullHdNr number of FullHD videos
* @apiSuccess {uint32} HDNr number of HD videos
* @apiSuccess {uint32} SDNr number of SD videos
* @apiSuccess {uint32} DifferentTags number of different Tags available
* @apiSuccess {uint32} Tagged number of different Tags assigned
*/
api.AddHandler("getStartData", api.VideoNode, api.PermUser, func(context api.Context) {
var result types.StartData var result types.StartData
// query settings and infotile values // query settings and infotile values
query := ` query := `
@ -396,90 +208,26 @@ func loadVideosHandlers() {
_ = database.QueryRow(query).Scan(&result.VideoNr, &result.Tagged, &result.HDNr, &result.FullHdNr, &result.SDNr, &result.DifferentTags) _ = database.QueryRow(query).Scan(&result.VideoNr, &result.Tagged, &result.HDNr, &result.FullHdNr, &result.SDNr, &result.DifferentTags)
context.Json(result) // jsonify results
str, _ := json.Marshal(result)
return str
}) })
} }
func addToVideoHandlers() { func addToVideoHandlers() {
/** var al struct {
* @api {post} /api/video [addLike]
* @apiDescription Add a like to a video
* @apiName addLike
* @apiGroup video
*
* @apiParam {int} MovieId ID of video
*
* @apiSuccess {string} result 'success' if successfully or Error message if not
*/
api.AddHandler("addLike", api.VideoNode, api.PermUser, func(context api.Context) {
var args struct {
MovieId int MovieId int
} }
if api.DecodeRequest(context.GetRequest(), &args) != nil { AddHandler("addLike", VideoNode, &al, func() []byte {
context.Text("unable to decode request") query := fmt.Sprintf("update videos set likes = likes + 1 where movie_id = %d", al.MovieId)
return return database.SuccessQuery(query)
}
query := fmt.Sprintf("update videos set likes = likes + 1 where movie_id = %d", args.MovieId)
context.Text(string(database.SuccessQuery(query)))
}) })
/** var dv struct {
* @api {post} /api/video [deleteVideo]
* @apiDescription Delete a specific video from database
* @apiName deleteVideo
* @apiGroup video
*
* @apiParam {int} MovieId ID of video
* @apiParam {bool} FullyDelete Delete video from disk?
*
* @apiSuccess {string} result 'success' if successfully or Error message if not
*/
api.AddHandler("deleteVideo", api.VideoNode, api.PermUser, func(context api.Context) {
var args struct {
MovieId int MovieId int
FullyDelete bool
} }
if api.DecodeRequest(context.GetRequest(), &args) != nil { AddHandler("deleteVideo", VideoNode, &dv, func() []byte {
context.Text("unable to decode request") query := fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", dv.MovieId)
return return database.SuccessQuery(query)
}
// delete tag constraints
query := fmt.Sprintf("DELETE FROM video_tags WHERE video_id=%d", args.MovieId)
err := database.Edit(query)
// delete actor constraints
query = fmt.Sprintf("DELETE FROM actors_videos WHERE video_id=%d", args.MovieId)
err = database.Edit(query)
// respond only if result not successful
if err != nil {
context.Text(string(database.ManualSuccessResponse(err)))
}
// only allow deletion of video if cli flag is set, independent of passed api arg
if config.GetConfig().Features.FullyDeletableVideos && args.FullyDelete {
// get physical path of video to delete
query = fmt.Sprintf("SELECT movie_url FROM videos WHERE movie_id=%d", args.MovieId)
var vidpath string
err := database.QueryRow(query).Scan(&vidpath)
if err != nil {
context.Text(string(database.ManualSuccessResponse(err)))
}
sett, videoprefix, _ := database.GetSettings()
assembledPath := videoprefix + sett.VideoPath + vidpath
err = os.Remove(assembledPath)
if err != nil {
fmt.Printf("unable to delete file: %s -- %s\n", assembledPath, err.Error())
context.Text(string(database.ManualSuccessResponse(err)))
}
}
// delete video row from db
query = fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", args.MovieId)
context.Text(string(database.SuccessQuery(query)))
}) })
} }

View File

@ -1,64 +0,0 @@
package api
import (
"fmt"
"net/http"
"openmediacenter/apiGo/database/settings"
)
const (
VideoNode = "video"
TagNode = "tags"
SettingsNode = "settings"
ActorNode = "actor"
TVShowNode = "tv"
LoginNode = "login"
)
func AddHandler(action string, apiNode string, perm Perm, handler func(ctx Context)) {
http.Handle(fmt.Sprintf("/api/%s/%s", apiNode, action), http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
srvPwd := settings.GetPassword()
if srvPwd == nil {
// no password set
ctx := &apicontext{writer: writer, responseWritten: false, request: request, userid: -1, permid: PermUnauthorized}
callHandler(ctx, handler, writer)
} else {
tokenheader := request.Header.Get("Token")
id := -1
permid := PermUnauthorized
// check token if token provided
if tokenheader != "" {
id, permid = TokenValid(request.Header.Get("Token"))
}
ctx := &apicontext{writer: writer, responseWritten: false, request: request, userid: id, permid: permid}
// check if rights are sufficient to perform the action
if permid <= perm {
callHandler(ctx, handler, writer)
} else {
ctx.Error("insufficient permissions")
}
}
}))
}
func callHandler(ctx *apicontext, handler func(ctx Context), writer http.ResponseWriter) {
handler(ctx)
if !ctx.responseWritten {
// none of the response functions called so send default response
ctx.Error("Unknown server Error occured")
writer.WriteHeader(501)
}
}
func ServerInit(port uint16) error {
// initialize auth service and add corresponding auth routes
InitOAuth()
fmt.Printf("OpenMediacenter server up and running on port %d\n", port)
return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
}

View File

@ -1,100 +0,0 @@
package api
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"openmediacenter/apiGo/database"
"strconv"
"time"
)
type Perm uint8
const (
PermAdmin Perm = iota
PermUser
PermUnauthorized
)
func (p Perm) String() string {
return [...]string{"PermAdmin", "PermUser", "PermUnauthorized"}[p]
}
const SignKey = "89013f1753a6890c6090b09e3c23ff43"
const TokenExpireHours = 8760
type Token struct {
Token string
ExpiresAt int64
}
func TokenValid(token string) (int, Perm) {
t, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(SignKey), nil
})
if err != nil {
return -1, PermUnauthorized
}
claims := t.Claims.(*jwt.StandardClaims)
id, err := strconv.Atoi(claims.Issuer)
permid, err := strconv.Atoi(claims.Subject)
if err != nil {
return -1, PermUnauthorized
}
return id, Perm(permid)
}
func InitOAuth() {
AddHandler("login", LoginNode, PermUnauthorized, func(ctx Context) {
var t struct {
Password string
}
if DecodeRequest(ctx.GetRequest(), &t) != nil {
fmt.Println("Error accured while decoding Testrequest!!")
}
// empty check
if t.Password == "" {
ctx.Error("empty password")
return
}
// generate Argon2 Hash of passed pwd
HashPassword(t.Password)
// todo use hashed password
var password string
err := database.QueryRow("SELECT password FROM settings WHERE 1").Scan(&password)
if err != nil || t.Password != password {
ctx.Error("unauthorized")
return
}
expires := time.Now().Add(time.Hour * TokenExpireHours).Unix()
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
Issuer: strconv.Itoa(int(0)),
Subject: strconv.Itoa(int(PermUser)),
ExpiresAt: expires,
})
token, err := claims.SignedString([]byte(SignKey))
if err != nil {
fmt.Println(err.Error())
ctx.Error("failed to generate authorization token")
return
}
type ResponseType struct {
Token Token
}
ctx.Json(Token{
Token: token,
ExpiresAt: expires,
})
})
}

View File

@ -1,65 +0,0 @@
package api
import (
"fmt"
"net/http"
)
type Context interface {
Json(t interface{})
Text(msg string)
Error(msg string)
Errorf(msg string, args ...interface{})
GetRequest() *http.Request
GetWriter() http.ResponseWriter
UserID() int
PermID() Perm
}
type apicontext struct {
writer http.ResponseWriter
request *http.Request
responseWritten bool
userid int
permid Perm
}
func (r *apicontext) GetRequest() *http.Request {
return r.request
}
func (r *apicontext) UserID() int {
return r.userid
}
func (r *apicontext) GetWriter() http.ResponseWriter {
return r.writer
}
func (r *apicontext) Json(t interface{}) {
r.writer.Write(Jsonify(t))
r.responseWritten = true
}
func (r *apicontext) Text(msg string) {
r.writer.Write([]byte(msg))
r.responseWritten = true
}
func (r *apicontext) Error(msg string) {
type Error struct {
Message string
}
r.writer.WriteHeader(500)
r.writer.Write(Jsonify(Error{Message: msg}))
r.responseWritten = true
}
func (r *apicontext) Errorf(msg string, args ...interface{}) {
r.Error(fmt.Sprintf(msg, &args))
}
func (r *apicontext) PermID() Perm {
return r.permid
}

View File

@ -1,13 +0,0 @@
package api
import (
"encoding/hex"
"golang.org/x/crypto/argon2"
)
func HashPassword(pwd string) *string {
// todo generate random salt
hash := argon2.IDKey([]byte(pwd), []byte(SignKey), 3, 64*1024, 2, 32)
hexx := hex.EncodeToString(hash)
return &hexx
}

View File

@ -1,11 +0,0 @@
package api
import "testing"
func TestHashlength(t *testing.T) {
h := HashPassword("test")
if len(*h) != 64 {
t.Errorf("Invalid hash length: %d", len(*h))
}
}

View File

@ -1,31 +0,0 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func Jsonify(v interface{}) []byte {
// Jsonify results
str, err := json.Marshal(v)
if err != nil {
fmt.Println("Error while Jsonifying return object: " + err.Error())
}
return str
}
// DecodeRequest decodes the request
func DecodeRequest(request *http.Request, arg interface{}) error {
buf := new(bytes.Buffer)
buf.ReadFrom(request.Body)
body := buf.String()
err := json.Unmarshal([]byte(body), &arg)
if err != nil {
fmt.Println("JSON decode Error" + err.Error())
}
return err
}

View File

@ -1,22 +0,0 @@
package api
import "testing"
func TestJsonify(t *testing.T) {
var obj = struct {
ID uint32
Str string
Boo bool
}{
ID: 42,
Str: "teststr",
Boo: true,
}
res := Jsonify(obj)
exp := `{"ID":42,"Str":"teststr","Boo":true}`
if string(res) != exp {
t.Errorf("Invalid json response: %s !== %s", string(res), exp)
}
}

67
apiGo/api/oauth/Oauth.go Normal file
View File

@ -0,0 +1,67 @@
package oauth
import (
"gopkg.in/oauth2.v3/errors"
"gopkg.in/oauth2.v3/manage"
"gopkg.in/oauth2.v3/models"
"gopkg.in/oauth2.v3/server"
"gopkg.in/oauth2.v3/store"
"log"
"net/http"
)
var srv *server.Server
func InitOAuth() {
manager := manage.NewDefaultManager()
// token store
manager.MustTokenStorage(store.NewMemoryTokenStore())
clientStore := store.NewClientStore()
// todo we need to check here if a password is enabled in db -- when yes set it here!
clientStore.Set("openmediacenter", &models.Client{
ID: "openmediacenter",
Secret: "openmediacenter",
Domain: "http://localhost:8081",
})
manager.MapClientStorage(clientStore)
srv = server.NewServer(server.NewConfig(), manager)
srv.SetClientInfoHandler(server.ClientFormHandler)
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
log.Println("Internal Error:", err.Error())
return
})
srv.SetResponseErrorHandler(func(re *errors.Response) {
log.Println("Response Error:", re.Error.Error())
})
http.HandleFunc("/authorize", func(w http.ResponseWriter, r *http.Request) {
err := srv.HandleAuthorizeRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
})
http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
err := srv.HandleTokenRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
}
func ValidateToken(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_, err := srv.ValidationBearerToken(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
f.ServeHTTP(w, r)
}
}

View File

@ -3,18 +3,16 @@ package types
type VideoUnloadedType struct { type VideoUnloadedType struct {
MovieId int MovieId int
MovieName string MovieName string
Ratio float32
} }
type FullVideoType struct { type FullVideoType struct {
MovieName string MovieName string
MovieId uint32 MovieId int
MovieUrl string MovieUrl string
Poster string Poster string
ReleaseDate *string Likes int
Likes uint64 Quality int
Quality uint16 Length int
Length uint16
Tags []Tag Tags []Tag
SuggestedTag []Tag SuggestedTag []Tag
Actors []Actor Actors []Actor
@ -22,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 {
@ -48,16 +46,11 @@ type SettingsType struct {
PasswordEnabled bool PasswordEnabled bool
TMDBGrabbing bool TMDBGrabbing bool
DarkMode bool DarkMode bool
}
type SettingsSizeType struct { VideoNr int
VideoNr uint32
DBSize float32 DBSize float32
DifferentTags uint32 DifferentTags int
TagsAdded uint32 TagsAdded int
}
type TVShow struct { PathPrefix string
Id uint32
Name string
} }

View File

@ -1,206 +0,0 @@
package config
import (
"errors"
"flag"
"fmt"
"github.com/pelletier/go-toml/v2"
"os"
)
type DatabaseT struct {
DBName string
DBPassword string
DBUser string
DBPort uint16
DBHost string
}
type FeaturesT struct {
DisableTVSupport bool
FullyDeletableVideos bool
}
type GeneralT struct {
VerboseLogging bool
ReindexPrefix string
}
type FileConfT struct {
Database DatabaseT
General GeneralT
Features FeaturesT
}
func defaultConfig() *FileConfT {
return &FileConfT{
Database: DatabaseT{
DBName: "mediacenter",
DBPassword: "mediapassword",
DBUser: "mediacenteruser",
DBPort: 3306,
DBHost: "127.0.0.1",
},
General: GeneralT{
VerboseLogging: false,
ReindexPrefix: "/var/www/openmediacenter",
},
Features: FeaturesT{
DisableTVSupport: false,
FullyDeletableVideos: false,
},
}
}
var liveConf FileConfT
func Init() {
cfgname := "openmediacenter.cfg"
cfgpath := "/etc/"
// load config from disk
dat, err := os.ReadFile(cfgpath + cfgname)
if err != nil {
// handle error if not exists or no sufficient read access
if _, ok := err.(*os.PathError); ok {
// check if config exists on local dir
dat, err = os.ReadFile(cfgname)
if err != nil {
generateNewConfig(cfgpath, cfgname)
} else {
// ok decode local config
decodeConfig(&dat)
}
} else {
// other error
fmt.Println(err.Error())
}
} else {
decodeConfig(&dat)
}
handleCommandLineArguments()
}
func generateNewConfig(cfgpath string, cfgname string) {
// config really doesn't exist!
fmt.Printf("config not existing -- generating new empty config at %s%s\n", cfgpath, cfgname)
// generate new default config
obj, _ := toml.Marshal(defaultConfig())
liveConf = *defaultConfig()
err := os.WriteFile(cfgpath+cfgname, obj, 777)
if err != nil {
if errors.Is(err, os.ErrPermission) {
// permisson denied to create file try to create at current dir
err = os.WriteFile(cfgname, obj, 777)
if err != nil {
fmt.Println("failed to create default config file!")
} else {
fmt.Println("config file created at .")
}
} else {
fmt.Println(err.Error())
}
}
}
func decodeConfig(bin *[]byte) {
config := FileConfT{}
err := toml.Unmarshal(*bin, &config)
if err != nil {
fmt.Println(err)
liveConf = *defaultConfig()
} else {
fmt.Println("Successfully loaded config file!")
liveConf = config
}
}
// handleCommandLineArguments let cli args override the config
func handleCommandLineArguments() {
// get a defaultconfig obj to set defaults
dconf := defaultConfig()
const (
DBHost = "DBHost"
DBPort = "DBPort"
DBUser = "DBUser"
DBPassword = "DBPassword"
DBName = "DBName"
Verbose = "v"
ReindexPrefix = "ReindexPrefix"
DisableTVSupport = "DisableTVSupport"
FullyDeletableVideos = "FullyDeletableVideos"
)
dbhostPtr := flag.String(DBHost, dconf.Database.DBHost, "database host name")
dbPortPtr := flag.Int(DBPort, int(dconf.Database.DBPort), "database port")
dbUserPtr := flag.String(DBUser, dconf.Database.DBUser, "database username")
dbPassPtr := flag.String(DBPassword, dconf.Database.DBPassword, "database username")
dbNamePtr := flag.String(DBName, dconf.Database.DBName, "database name")
verbosePtr := flag.Bool(Verbose, dconf.General.VerboseLogging, "Verbose log output")
pathPrefix := flag.String(ReindexPrefix, dconf.General.ReindexPrefix, "Prefix path for videos to reindex")
disableTVShowSupport := flag.Bool(DisableTVSupport, dconf.Features.DisableTVSupport, "Disable the TVShow support and pages")
videosFullyDeletable := flag.Bool(FullyDeletableVideos, dconf.Features.FullyDeletableVideos, "Allow deletion from harddisk")
flag.Parse()
if isFlagPassed(DBHost) {
liveConf.Database.DBHost = *dbhostPtr
}
if isFlagPassed(DBPort) {
liveConf.Database.DBPort = uint16(*dbPortPtr)
}
if isFlagPassed(DBName) {
liveConf.Database.DBName = *dbNamePtr
}
if isFlagPassed(DBUser) {
liveConf.Database.DBUser = *dbUserPtr
}
if isFlagPassed(DBPassword) {
liveConf.Database.DBPassword = *dbPassPtr
}
if isFlagPassed(Verbose) {
liveConf.General.VerboseLogging = *verbosePtr
}
if isFlagPassed(ReindexPrefix) {
liveConf.General.ReindexPrefix = *pathPrefix
}
if isFlagPassed(DisableTVSupport) {
liveConf.Features.DisableTVSupport = *disableTVShowSupport
}
if isFlagPassed(FullyDeletableVideos) {
liveConf.Features.FullyDeletableVideos = *videosFullyDeletable
}
}
// isFlagPassed check whether a flag was passed
func isFlagPassed(name string) bool {
found := false
flag.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
return found
}
func GetConfig() *FileConfT {
return &liveConf
}

View File

@ -1,9 +0,0 @@
package config
import "testing"
func TestSaveLoadConfig(t *testing.T) {
generateNewConfig("", "openmediacenter.cfg")
Init()
}

View File

@ -2,24 +2,26 @@ package database
import ( import (
"database/sql" "database/sql"
"embed"
"fmt" "fmt"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/pressly/goose/v3"
"log"
"openmediacenter/apiGo/api/types" "openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/config"
"os"
) )
var db *sql.DB var db *sql.DB
var DBName string var DBName string
//go:embed migrations/*.sql // store the command line parameter for Videoprefix
var embedMigrations embed.FS var SettingsVideoPrefix = ""
func InitDB() error { type DatabaseConfig struct {
dbconf := config.GetConfig().Database DBHost string
DBPort int
DBUser string
DBPassword string
DBName string
}
func InitDB(dbconf *DatabaseConfig) {
DBName = dbconf.DBName DBName = dbconf.DBName
// Open up our database connection. // Open up our database connection.
@ -28,32 +30,15 @@ func InitDB() error {
// if there is an error opening the connection, handle it // if there is an error opening the connection, handle it
if err != nil { if err != nil {
return fmt.Errorf("Error while connecting to database! - %s\n", err.Error()) fmt.Printf("Error while connecting to database! - %s\n", err.Error())
} }
if db != nil { if db != nil {
ping := db.Ping() ping := db.Ping()
if ping != nil { if ping != nil {
return fmt.Errorf("Error while connecting to database! - %s\n", ping.Error()) fmt.Printf("Error while connecting to database! - %s\n", ping.Error())
} }
} }
fmt.Println("Running Database migrations!")
// perform database migrations
goose.SetBaseFS(embedMigrations)
goose.SetLogger(log.New(os.Stdout, "", 0))
// set mysql dialect
err = goose.SetDialect("mysql")
if err != nil {
return err
}
if err := goose.Up(db, "migrations"); err != nil {
return err
}
return nil
} }
func Query(query string, args ...interface{}) *sql.Rows { func Query(query string, args ...interface{}) *sql.Rows {
@ -105,7 +90,9 @@ func Close() {
db.Close() db.Close()
} }
func GetSettings() (result types.SettingsType, PathPrefix string, sizes types.SettingsSizeType) { func GetSettings() types.SettingsType {
var result types.SettingsType
// query settings and infotile values // query settings and infotile values
query := fmt.Sprintf(` query := fmt.Sprintf(`
SELECT ( SELECT (
@ -133,7 +120,7 @@ func GetSettings() (result types.SettingsType, PathPrefix string, sizes types.Se
var DarkMode int var DarkMode int
var TMDBGrabbing int var TMDBGrabbing int
err := QueryRow(query).Scan(&sizes.VideoNr, &sizes.DBSize, &sizes.DifferentTags, &sizes.TagsAdded, err := QueryRow(query).Scan(&result.VideoNr, &result.DBSize, &result.DifferentTags, &result.TagsAdded,
&result.VideoPath, &result.EpisodePath, &result.Password, &result.MediacenterName, &TMDBGrabbing, &DarkMode) &result.VideoPath, &result.EpisodePath, &result.Password, &result.MediacenterName, &TMDBGrabbing, &DarkMode)
if err != nil { if err != nil {
@ -143,6 +130,7 @@ func GetSettings() (result types.SettingsType, PathPrefix string, sizes types.Se
result.TMDBGrabbing = TMDBGrabbing != 0 result.TMDBGrabbing = TMDBGrabbing != 0
result.PasswordEnabled = result.Password != "-1" result.PasswordEnabled = result.Password != "-1"
result.DarkMode = DarkMode != 0 result.DarkMode = DarkMode != 0
PathPrefix = config.GetConfig().General.ReindexPrefix result.PathPrefix = SettingsVideoPrefix
return
return result
} }

View File

@ -1,122 +0,0 @@
-- +goose Up
-- +goose StatementBegin
create table if not exists actors
(
actor_id int auto_increment
primary key,
name varchar(50) null,
thumbnail mediumblob null
)
comment 'informations about different actors';
-- +goose StatementEnd
-- +goose StatementBegin
create table if not exists settings
(
video_path varchar(255) null,
episode_path varchar(255) null,
password varchar(32) default '-1' null,
mediacenter_name varchar(32) default 'OpenMediaCenter' null,
TMDB_grabbing tinyint null,
DarkMode tinyint default 0 null
);
-- +goose StatementEnd
-- +goose StatementBegin
create table if not exists tags
(
tag_id int auto_increment
primary key,
tag_name varchar(50) null
);
-- +goose StatementEnd
-- +goose StatementBegin
create table if not exists tvshow
(
name varchar(100) null,
thumbnail mediumblob null,
id int auto_increment
primary key,
foldername varchar(100) null
);
-- +goose StatementEnd
-- +goose StatementBegin
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)
);
-- +goose StatementEnd
-- +goose StatementBegin
create table if not exists videos
(
movie_id int auto_increment
primary key,
movie_name varchar(200) null,
movie_url varchar(250) null,
thumbnail mediumblob null,
poster mediumblob null,
likes int default 0 null,
quality int default 0 null,
length int default 0 null comment 'in seconds',
create_date datetime default current_timestamp() null
);
-- +goose StatementEnd
-- +goose StatementBegin
create table if not exists actors_videos
(
actor_id int null,
video_id int null,
constraint actors_videos_actors_id_fk
foreign key (actor_id) references actors (actor_id),
constraint actors_videos_videos_movie_id_fk
foreign key (video_id) references videos (movie_id)
);
-- +goose StatementEnd
-- +goose StatementBegin
create index if not exists actors_videos_actor_id_index
on actors_videos (actor_id);
-- +goose StatementEnd
-- +goose StatementBegin
create index if not exists actors_videos_video_id_index
on actors_videos (video_id);
-- +goose StatementEnd
-- +goose StatementBegin
create table if not exists video_tags
(
tag_id int null,
video_id int null,
constraint video_tags_tags_tag_id_fk
foreign key (tag_id) references tags (tag_id),
constraint video_tags_videos_movie_id_fk
foreign key (video_id) references videos (movie_id)
on delete cascade
);
-- +goose StatementEnd
-- +goose StatementBegin
INSERT IGNORE INTO tags (tag_id, tag_name)
VALUES (2, 'fullhd');
-- +goose StatementEnd
-- +goose StatementBegin
INSERT IGNORE INTO tags (tag_id, tag_name)
VALUES (3, 'lowquality');
-- +goose StatementEnd
-- +goose StatementBegin
INSERT IGNORE INTO tags (tag_id, tag_name)
VALUES (4, 'hd');
-- +goose StatementEnd
-- +goose StatementBegin
INSERT IGNORE INTO settings (video_path, episode_path, password, mediacenter_name)
VALUES ('./videos/', './tvshows/', -1, 'OpenMediaCenter');
-- +goose StatementEnd
-- +goose Down

View File

@ -1,12 +0,0 @@
-- +goose Up
-- +goose StatementBegin
alter table videos
add release_date date null;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
alter table videos
drop release_date;
-- +goose StatementEnd

View File

@ -1,11 +0,0 @@
-- +goose Up
-- +goose StatementBegin
alter table videos
add previewratio FLOAT default -1.0 null;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
alter table videos
drop previewratio;
-- +goose StatementEnd

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,12 +3,7 @@ module openmediacenter/apiGo
go 1.16 go 1.16
require ( require (
github.com/3d0c/gmf v0.0.0-20210925211039-e278e6e53b16 github.com/go-session/session v3.1.2+incompatible
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/go-sql-driver/mysql v1.5.0
github.com/fsnotify/fsnotify v1.5.4 gopkg.in/oauth2.v3 v3.12.0
github.com/go-sql-driver/mysql v1.6.0
github.com/pelletier/go-toml/v2 v2.0.0-beta.3
github.com/pressly/goose/v3 v3.1.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
nhooyr.io/websocket v1.8.7
) )

View File

@ -1,104 +1,111 @@
github.com/3d0c/gmf v0.0.0-20210925211039-e278e6e53b16 h1:LX3XWmS88yKgWJcMXb8vusphpDBe9+6LTI9FyHeFFWQ= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/3d0c/gmf v0.0.0-20210925211039-e278e6e53b16/go.mod h1:0QMRcUq2JsDECeAq7bj4h79k7XbhtTsrPUQf6G7qfPs= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ClickHouse/clickhouse-go v1.4.5/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
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/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 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/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pelletier/go-toml/v2 v2.0.0-beta.3 h1:PNCTU4naEJ8mKal97P3A2qDU74QRQGlv4FXiL1XDqi4=
github.com/pelletier/go-toml/v2 v2.0.0-beta.3/go.mod h1:aNseLYu/uKskg0zpr/kbr2z8yGuWtotWf/0BpGIAL2Y=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pressly/goose/v3 v3.1.0 h1:V2Ulfm2XL9GtYNmrPUNFHieimf6diwADyMObnuuR2Mc= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/pressly/goose/v3 v3.1.0/go.mod h1:tYsY0oL0yd48jg15POIZfOZiu66mqWpfDd/nJ28KWyU= 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/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
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/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU= github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0 h1:QnyrPZZvPmR0AtJCxxfCtI1qN+fYpKTKJ/5opWmZ34k=
github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/tidwall/buntdb v1.1.0 h1:H6LzK59KiNjf1nHVPFrYj4Qnl8d8YLBsYamdL8N+Bao=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/tidwall/buntdb v1.1.0/go.mod h1:Y39xhcDW10WlyYXeLgGftXVbjtM0QP+/kpz8xl9cbzE=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/tidwall/gjson v1.3.2 h1:+7p3qQFaH3fOMXAJSrdZwGKcOO/lYdGS0HqGhPqDdTI=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo=
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/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
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/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/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/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
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/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
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/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/net v0.0.0-20180724234803-3673e40ba225/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-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/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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/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/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.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 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=

View File

@ -1,18 +0,0 @@
package housekeeping
import "fmt"
func RunHouseKeepingTasks() {
fmt.Println("Runnint houskeeping tasks!")
fmt.Println("Deduplicating Tags")
deduplicateTags()
fmt.Println("Deduplicating Tags assigned to videos")
deduplicateVideoTags()
fmt.Println("Fix missing video metadata like ratio")
fixMissingMetadata()
fmt.Println("Finished housekeeping")
}

View File

@ -1,5 +0,0 @@
package housekeeping
func fixMissingMetadata() {
// todo
}

View File

@ -1,86 +0,0 @@
package housekeeping
import (
"fmt"
"openmediacenter/apiGo/database"
)
func deduplicateTags() {
// find all duplicate tags
// gives first occurence of duplicate
query := `
SELECT
tag_name
FROM
tags
GROUP BY tag_name
HAVING COUNT(tag_name) > 1`
rows := database.Query(query)
duplicates := []string{}
if rows != nil {
for rows.Next() {
var id string
err := rows.Scan(&id)
if err != nil {
panic(err.Error()) // proper Error handling instead of panic in your app
}
duplicates = append(duplicates, id)
}
} else {
// nothing to do
return
}
fmt.Print("deleting duplicate tag ids: ")
fmt.Println(duplicates)
for _, el := range duplicates {
query := fmt.Sprintf("SELECT tag_id FROM tags WHERE tag_name='%s'", el)
rows := database.Query(query)
ids := []uint32{}
for rows.Next() {
var id uint32
err := rows.Scan(&id)
if err != nil {
panic(err.Error()) // proper Error handling instead of panic in your app
}
ids = append(ids, id)
}
// id to copy other data to
mainid := ids[0]
// ids to copy from
copyids := ids[1:]
fmt.Printf("Migrating %s\n", el)
migrateTags(mainid, copyids)
}
}
func migrateTags(destid uint32, sourcids []uint32) {
querytempl := `
UPDATE video_tags
SET
tag_id = %d
WHERE
tag_id = %d`
for _, id := range sourcids {
err := database.Edit(fmt.Sprintf(querytempl, destid, id))
if err != nil {
fmt.Printf("failed to set id from %d to %d\n", id, destid)
return
}
fmt.Printf("Merged %d into %d\n", id, destid)
// now lets delete this tag
query := fmt.Sprintf(`DELETE FROM tags WHERE tag_id=%d`, id)
err = database.Edit(query)
if err != nil {
fmt.Printf("failed to delete Tag %d", id)
return
}
}
}

View File

@ -1,37 +0,0 @@
package housekeeping
import (
"fmt"
"openmediacenter/apiGo/database"
)
func deduplicateVideoTags() {
// gives first occurence of duplicate
query := `
SELECT
tag_id, video_id, count(tag_id)
FROM
video_tags
GROUP BY tag_id, video_id
HAVING COUNT(tag_id) > 1`
rows := database.Query(query)
if rows != nil {
for rows.Next() {
var tagid uint32
var vidid uint32
var nr uint32
err := rows.Scan(&tagid, &vidid, &nr)
if err != nil {
panic(err.Error()) // proper Error handling instead of panic in your app
}
// now lets delete this tag
query := fmt.Sprintf(`DELETE FROM video_tags WHERE tag_id=%d AND video_id=%d LIMIT %d`, tagid, vidid, nr-1)
err = database.Edit(query)
if err != nil {
fmt.Printf("failed to delete Tag %d + vid %d", tagid, vidid)
return
}
}
}
}

View File

@ -4,60 +4,50 @@ import (
"flag" "flag"
"fmt" "fmt"
"openmediacenter/apiGo/api" "openmediacenter/apiGo/api"
api2 "openmediacenter/apiGo/api/api"
"openmediacenter/apiGo/config"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"openmediacenter/apiGo/housekeeping"
"openmediacenter/apiGo/static"
"openmediacenter/apiGo/videoparser"
"os"
"os/signal"
) )
func main() { func main() {
fmt.Println("init OpenMediaCenter server") fmt.Println("init OpenMediaCenter server")
const port uint16 = 8081
errc := make(chan error, 1)
housekPTr := flag.Bool("HouseKeeping", false, "Run housekeeping tasks")
config.Init()
db, verbose, pathPrefix := handleCommandLineArguments()
// todo some verbosity logger or sth // todo some verbosity logger or sth
fmt.Printf("Use verbose output: %t\n", config.GetConfig().General.VerboseLogging)
fmt.Printf("Videopath prefix: %s\n", config.GetConfig().General.ReindexPrefix)
err := database.InitDB() fmt.Printf("Use verbose output: %t\n", verbose)
if err != nil { fmt.Printf("Videopath prefix: %s\n", *pathPrefix)
errc <- err
} // set pathprefix in database settings object
database.SettingsVideoPrefix = *pathPrefix
database.InitDB(db)
defer database.Close() defer database.Close()
// check if we should run the housekeeping tasks api.AddVideoHandlers()
if *housekPTr { api.AddSettingsHandlers()
housekeeping.RunHouseKeepingTasks() api.AddTagHandlers()
return api.AddActorsHandlers()
}
api.AddHandlers() api.ServerInit(8081)
}
videoparser.SetupSettingsWebsocket()
videoparser.InitFileWatcher() func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) {
dbhostPtr := flag.String("DBHost", "127.0.0.1", "database host name")
// add the static files dbPortPtr := flag.Int("DBPort", 3306, "database port")
static.ServeStaticFiles() dbUserPtr := flag.String("DBUser", "mediacenteruser", "database username")
dbPassPtr := flag.String("DBPassword", "mediapassword", "database username")
// init api dbNamePtr := flag.String("DBName", "mediacenter", "database name")
go func() {
errc <- api2.ServerInit(port) verbosePtr := flag.Bool("v", true, "Verbose log output")
}()
pathPrefix := flag.String("ReindexPrefix", "/var/www/openmediacenter", "Prefix path for videos to reindex")
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt) flag.Parse()
select {
case err := <-errc: return &database.DatabaseConfig{
fmt.Printf("failed to serve: %v\n", err) DBHost: *dbhostPtr,
case sig := <-sigs: DBPort: *dbPortPtr,
fmt.Printf("terminating server: %v\n", sig) DBUser: *dbUserPtr,
} DBPassword: *dbPassPtr,
DBName: *dbNamePtr,
}, *verbosePtr, pathPrefix
} }

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,58 +0,0 @@
package videoparser
import (
"github.com/fsnotify/fsnotify"
"log"
"openmediacenter/apiGo/config"
"openmediacenter/apiGo/database"
"strings"
)
func InitFileWatcher() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal("NewWatcher failed: ", err)
}
mSettings, _, _ := database.GetSettings()
vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath
epsfolder := config.GetConfig().General.ReindexPrefix + mSettings.EpisodePath
defer watcher.Close()
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
// start new reindex
// (may be optimized by checking here if added file is video
// and start reindex just for one file)
if strings.Contains(event.Name, vidFolder) {
StartReindex()
} else if strings.Contains(event.Name, epsfolder) {
StartTVShowReindex()
} else {
log.Printf("Event in wrong folder: %s %s\n", event.Name, event.Op)
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
err = watcher.Add(vidFolder)
if err != nil {
log.Println("Adding of file watcher failed: ", err)
}
err = watcher.Add(epsfolder)
if err != nil {
log.Println("Adding of file watcher failed: ", err)
}
}

View File

@ -1,31 +0,0 @@
package videoparser
import (
"encoding/json"
)
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)
}

View File

@ -0,0 +1,294 @@
package videoparser
import (
"database/sql"
"encoding/base64"
"encoding/json"
"fmt"
"openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/database"
"openmediacenter/apiGo/videoparser/tmdb"
"os/exec"
"regexp"
"strconv"
)
var mSettings types.SettingsType
var mExtDepsAvailable *ExtDependencySupport
// default Tag ids
const (
FullHd = 2
Hd = 4
LowQuality = 3
)
type ExtDependencySupport struct {
FFMpeg bool
MediaInfo bool
}
type VideoAttributes struct {
Duration float32
FileSize uint
Width uint
}
func ReIndexVideos(path []string, sett types.SettingsType) {
mSettings = sett
// check if the extern dependencies are available
mExtDepsAvailable = checkExtDependencySupport()
fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg)
fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo)
for _, s := range path {
processVideo(s)
}
AppendMessageBuffer("reindex finished successfully!")
contentAvailable = false
fmt.Println("Reindexing finished!")
}
func processVideo(fileNameOrig string) {
fmt.Printf("Processing %s video-", fileNameOrig)
// match the file extension
r, _ := regexp.Compile(`\.[a-zA-Z0-9]+$`)
fileName := r.ReplaceAllString(fileNameOrig, "")
year, fileName := matchYear(fileName)
// now we should look if this video already exists in db
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
func addVideo(videoName string, fileName string, year int) {
var ppic *string
var poster *string
var tmdbData *tmdb.VideoTMDB
var err error
// initialize defaults
vidAtr := &VideoAttributes{
Duration: 0,
FileSize: 0,
Width: 0,
}
if mExtDepsAvailable.FFMpeg {
ppic, err = parseFFmpegPic(fileName)
if err != nil {
fmt.Printf("FFmpeg error occured: %s\n", err.Error())
} else {
fmt.Println("successfully extracted thumbnail!!")
}
}
if mExtDepsAvailable.MediaInfo {
atr := getVideoAttributes(fileName)
if atr != nil {
vidAtr = atr
}
}
// if TMDB grabbing is enabled serach in api for video...
if mSettings.TMDBGrabbing {
tmdbData = tmdb.SearchVideo(videoName, year)
if tmdbData != nil {
// reassign parsed pic as poster
poster = ppic
// and tmdb pic as thumbnail
ppic = &tmdbData.Thumbnail
}
}
query := `INSERT INTO videos(movie_name,movie_url,poster,thumbnail,quality,length) VALUES (?,?,?,?,?,?)`
err, insertId := database.Insert(query, videoName, fileName, poster, ppic, vidAtr.Width, vidAtr.Duration)
if err != nil {
fmt.Printf("Failed to insert video into db: %s\n", err.Error())
return
}
// add default tags
if vidAtr.Width != 0 {
insertSizeTag(vidAtr.Width, uint(insertId))
}
// add tmdb tags
if mSettings.TMDBGrabbing && tmdbData != nil {
insertTMDBTags(tmdbData.GenreIds, insertId)
}
AppendMessageBuffer(fmt.Sprintf("%s - added!", videoName))
}
func matchYear(fileName string) (int, string) {
r, _ := regexp.Compile(`\([0-9]{4}?\)`)
years := r.FindAllString(fileName, -1)
if len(years) == 0 {
return -1, fileName
}
year, err := strconv.Atoi(years[len(years)-1])
if err != nil {
return -1, fileName
}
// cut out year from 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
func insertSizeTag(width uint, videoId uint) {
var tagType uint
if width >= 1080 {
tagType = FullHd
} else if width >= 720 {
tagType = Hd
} else {
tagType = LowQuality
}
query := fmt.Sprintf("INSERT INTO video_tags(video_id,tag_id) VALUES (%d,%d)", videoId, tagType)
err := database.Edit(query)
if err != nil {
fmt.Printf("Eror occured while adding default Tag: %s\n", err.Error())
}
}
// insert id array of tmdb geners to database
func insertTMDBTags(ids []int, videoId int64) {
genres := tmdb.GetGenres()
for _, id := range ids {
var idGenre *tmdb.TMDBGenre
for _, genre := range *genres {
if genre.Id == id {
idGenre = &genre
break
}
}
// skip tag if name couldn't be found
if idGenre == nil {
continue
}
// now we check if the tag we want to add already exists
tagId := createTagToDB(idGenre.Name)
// now we add the tag
query := fmt.Sprintf("INSERT INTO video_tags(video_id,tag_id) VALUES (%d,%d)", videoId, tagId)
_ = database.Edit(query)
}
}
// returns id of tag or creates it if not existing
func createTagToDB(tagName string) int64 {
query := fmt.Sprintf("SELECT tag_id FROM tags WHERE tag_name = %s", tagName)
var id int64
err := database.QueryRow(query).Scan(&id)
if err == sql.ErrNoRows {
// tag doesn't exist -- add it
query = fmt.Sprintf("INSERT INTO tags (tag_name) VALUES (%s)", tagName)
err, id = database.Insert(query)
}
return id
}

View File

@ -1,143 +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 filename 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 filename string
err := rows.Scan(&filename)
if err != nil {
fmt.Println(err.Error())
}
dbepisodes = append(dbepisodes, filename)
}
// 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 {
if b == nil || len(b) == 0 {
return a
}
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
err := rows.Scan(&show)
if err != nil {
continue
}
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

@ -1,246 +0,0 @@
package videoparser
import (
"database/sql"
"fmt"
"openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/config"
"openmediacenter/apiGo/database"
"openmediacenter/apiGo/videoparser/thumbnail"
"openmediacenter/apiGo/videoparser/tmdb"
"regexp"
"strconv"
"strings"
)
var mSettings *types.SettingsType
// default Tag ids
const (
FullHd = 2
Hd = 4
LowQuality = 3
)
type VideoAttributes struct {
Duration float32
FileSize uint
Width uint
}
func InitDeps(sett *types.SettingsType) {
mSettings = sett
}
func ReIndexVideos(path []string) {
// filter out those urls which are already existing in db
nonExisting := filterExisting(path)
fmt.Printf("There are %d videos not existing in db.\n", len(*nonExisting))
for _, s := range *nonExisting {
ProcessVideo(s)
}
AppendMessage("reindex finished successfully!")
SendEvent("stop")
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) {
fmt.Printf("Processing %s video\n", fileNameOrig)
// match the file extension
r := regexp.MustCompile(`\.[a-zA-Z0-9]+$`)
fileName := r.ReplaceAllString(fileNameOrig, "")
// match the year and cut year from name
year, fileName := matchYear(fileName)
fmt.Printf("The Video %s doesn't exist! Adding it to database.\n", fileName)
addVideo(fileName, fileNameOrig, year)
}
// add a video to the database
func addVideo(videoName string, fileName string, year int) {
var ppic *string
var tmdbData *tmdb.VideoTMDB
var err error
var insertid int64
vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath
// if TMDB grabbing is enabled serach in api for video...
if mSettings.TMDBGrabbing {
tmdbData = tmdb.SearchVideo(videoName, year)
}
// parse pic from 4min frame
ppic, vinfo, ffmpegErr := thumbnail.Parse(vidFolder+fileName, 240)
if ffmpegErr == nil {
if mSettings.TMDBGrabbing && tmdbData != nil {
// inesert fixed pic ratio what we get from tmdb
previewRatio := 2 / 3
query := `INSERT INTO videos(movie_name,movie_url,poster,thumbnail,quality,previewratio,length,release_date) VALUES (?,?,?,?,?,?,?,?)`
err, insertid = database.Insert(query, videoName, fileName, ppic, tmdbData.Thumbnail, vinfo.Width, previewRatio, vinfo.Length, tmdbData.ReleaseDate)
} else {
previewRatio := float32(vinfo.Height) / float32(vinfo.Width)
// insert without tmdb info
query := `INSERT INTO videos(movie_name,movie_url,poster,thumbnail,quality,previewratio,length) VALUES (?,?,?,?,?,?,?)`
err, insertid = database.Insert(query, videoName, fileName, ppic, ppic, vinfo.Width, previewRatio, vinfo.Length)
}
} else {
fmt.Printf("FFmpeg error occured: %s\n", ffmpegErr.Error())
if mSettings.TMDBGrabbing && tmdbData != nil {
query := `INSERT INTO videos(movie_name,movie_url,thumbnail,release_date) VALUES (?,?,?,?)`
err, insertid = database.Insert(query, videoName, fileName, tmdbData.Thumbnail, tmdbData.ReleaseDate)
} else {
query := `INSERT INTO videos(movie_name,movie_url) VALUES (?,?)`
err, insertid = database.Insert(query, videoName, fileName)
}
}
if err != nil {
fmt.Printf("Failed to insert video into db: %s\n", err.Error())
return
}
if ffmpegErr == nil {
// add default tags
if vinfo.Width != 0 {
insertSizeTag(uint(vinfo.Width), uint(insertid))
}
}
// add tmdb tags
if mSettings.TMDBGrabbing && tmdbData != nil {
insertTMDBTags(tmdbData.GenreIds, insertid)
}
AppendMessage(fmt.Sprintf("%s - added!", videoName))
}
func matchYear(fileName string) (int, string) {
r := regexp.MustCompile(`\([0-9]{4}?\)`)
years := r.FindAllString(fileName, -1)
if len(years) == 0 {
return -1, fileName
}
yearStr := years[len(years)-1]
// get last year occurance and cut first and last char
year, err := strconv.Atoi(yearStr[1 : len(yearStr)-1])
if err != nil {
return -1, fileName
}
// cut out year from filename
return year, r.ReplaceAllString(fileName, "")
}
// insert the default size tags to corresponding video
func insertSizeTag(width uint, videoId uint) {
var tagType uint
if width >= 1080 {
tagType = FullHd
} else if width >= 720 {
tagType = Hd
} else {
tagType = LowQuality
}
query := fmt.Sprintf("INSERT INTO video_tags(video_id,tag_id) VALUES (%d,%d)", videoId, tagType)
err := database.Edit(query)
if err != nil {
fmt.Printf("Eror occured while adding default Tag: %s\n", err.Error())
}
}
// insert id array of tmdb geners to database
func insertTMDBTags(ids []int, videoId int64) {
genres := tmdb.GetGenres()
for _, id := range ids {
var idGenre *tmdb.TMDBGenre
for _, genre := range *genres {
if genre.Id == id {
idGenre = &genre
break
}
}
// skip tag if name couldn't be found
if idGenre == nil {
continue
}
// now we check if the tag we want to add already exists
tagId := createTagToDB(idGenre.Name)
if tagId != -1 {
// now we add the tag
query := fmt.Sprintf("INSERT INTO video_tags(video_id,tag_id) VALUES (%d,%d)", videoId, tagId)
_ = database.Edit(query)
}
}
}
// returns id of tag or creates it if not existing
func createTagToDB(tagName string) int64 {
query := "SELECT tag_id FROM tags WHERE tag_name = ?"
var id int64 = -1
err := database.QueryRow(query, tagName).Scan(&id)
if err == sql.ErrNoRows {
// tag doesn't exist -- add it
query = "INSERT INTO tags (tag_name) VALUES (?)"
err, id = database.Insert(query, tagName)
if err != nil {
fmt.Println(err.Error())
}
} else if err != nil {
if err != nil {
fmt.Println(err.Error())
}
}
return id
}

View File

@ -2,136 +2,67 @@ package videoparser
import ( import (
"fmt" "fmt"
"io/ioutil"
"openmediacenter/apiGo/config"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"os" "os"
"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 getVideoTypes() []string {
return []string{".mp4", ".mov", ".mkv", ".flv", ".avi", ".mpeg", ".m4v"}
}
func ValidVideoSuffix(filename string) bool {
validExts := getVideoTypes()
for _, validExt := range validExts {
if strings.HasSuffix(filename, validExt) {
return true
}
}
return false
}
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
vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath mSettings.VideoPath = mSettings.PathPrefix + mSettings.VideoPath
// check if path even exists // check if path even exists
if _, err := os.Stat(vidFolder); 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", vidFolder))
SendEvent("stop")
return false
}
filelist, err := ioutil.ReadDir(vidFolder)
if err != nil {
fmt.Println(err.Error())
return false return false
} }
var files []string var files []string
for _, file := range filelist { err := filepath.Walk(mSettings.VideoPath, func(path string, info os.FileInfo, err error) error {
if !file.IsDir() && ValidVideoSuffix(file.Name()) { if err != nil {
files = append(files, file.Name()) fmt.Println(err.Error())
} return err
} }
if !info.IsDir() && strings.HasSuffix(info.Name(), ".mp4") {
files = append(files, info.Name())
}
return nil
})
if err != nil {
fmt.Println(err.Error())
}
// start reindex process // start reindex process
AppendMessage("Starting Reindexing!") AppendMessageBuffer("Starting Reindexing!")
InitDeps(&mSettings) go ReIndexVideos(files, mSettings)
go ReIndexVideos(files)
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, PathPrefix, _ := database.GetSettings()
// add the path prefix to videopath
mSettings.EpisodePath = 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 ValidVideoSuffix(epfile.Name()) {
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

@ -1,22 +0,0 @@
package thumbnail
import (
"encoding/base64"
"fmt"
)
type VidInfo struct {
Width uint32
Height uint32
Length uint64
FrameRate float32
Size int64
}
func EncodeBase64(data *[]byte, mimetype string) *string {
strEncPic := base64.StdEncoding.EncodeToString(*data)
backpic64 := fmt.Sprintf("data:%s;base64,%s", mimetype, strEncPic)
return &backpic64
}

View File

@ -1,198 +0,0 @@
// +build sharedffmpeg
package thumbnail
import (
"fmt"
"github.com/3d0c/gmf"
"io"
"log"
"os"
)
func Parse(filename string, time uint64) (*string, *VidInfo, error) {
dta, inf, err := decodePic(filename, "mjpeg", time)
if err == nil && dta != nil {
// base64 encode picture
enc := EncodeBase64(dta, "image/jpeg")
return enc, inf, nil
} else {
return nil, nil, err
}
}
func decodePic(srcFileName string, encodeExtension string, time uint64) (pic *[]byte, info *VidInfo, err error) {
var swsctx *gmf.SwsCtx
gmf.LogSetLevel(gmf.AV_LOG_PANIC)
stat, err := os.Stat(srcFileName)
if err != nil {
// file seems to not even exist
return nil, nil, err
}
fileSize := stat.Size()
inputCtx, err := gmf.NewInputCtx(srcFileName)
if err != nil {
log.Printf("Error creating context - %s\n", err)
return nil, nil, err
}
defer inputCtx.Free()
srcVideoStream, err := inputCtx.GetBestStream(gmf.AVMEDIA_TYPE_VIDEO)
if err != nil {
log.Printf("No video stream found in '%s'\n", srcFileName)
return nil, nil, err
}
encodeCodec, err := gmf.FindEncoder(encodeExtension)
if err != nil {
log.Printf("%s\n", err)
return nil, nil, err
}
cc := gmf.NewCodecCtx(encodeCodec)
defer gmf.Release(cc)
cc.SetTimeBase(gmf.AVR{Num: 1, Den: 1})
cc.SetPixFmt(gmf.AV_PIX_FMT_YUVJ444P).SetWidth(srcVideoStream.CodecPar().Width()).SetHeight(srcVideoStream.CodecPar().Height())
if encodeCodec.IsExperimental() {
cc.SetStrictCompliance(gmf.FF_COMPLIANCE_EXPERIMENTAL)
}
if err := cc.Open(nil); err != nil {
log.Println(err)
return nil, nil, err
}
defer cc.Free()
err = inputCtx.SeekFrameAt(int64(time), srcVideoStream.Index())
if err != nil {
log.Printf("Error while seeking file: %s\n", err.Error())
return nil, nil, err
}
// find encodeCodec to decode video
decodeCodec, err := gmf.FindDecoder(srcVideoStream.CodecPar().CodecId())
if err != nil {
fmt.Println(err)
return nil, nil, err
}
icc := gmf.NewCodecCtx(decodeCodec)
defer gmf.Release(icc)
// copy stream parameters in codeccontext
err = srcVideoStream.CodecPar().ToContext(icc)
if err != nil {
fmt.Println(err.Error())
}
// convert source pix_fmt into AV_PIX_FMT_RGBA
if swsctx, err = gmf.NewSwsCtx(icc.Width(), icc.Height(), icc.PixFmt(), cc.Width(), cc.Height(), cc.PixFmt(), gmf.SWS_BICUBIC); err != nil {
panic(err)
}
defer swsctx.Free()
frameRate := float32(srcVideoStream.GetRFrameRate().AVR().Num) / float32(srcVideoStream.GetRFrameRate().AVR().Den)
inf := VidInfo{
Width: uint32(icc.Width()),
Height: uint32(icc.Height()),
FrameRate: frameRate,
Length: uint64(inputCtx.Duration()),
Size: fileSize,
}
info = &inf
var (
pkt *gmf.Packet
frames []*gmf.Frame
drain int = -1
frameCount int = 0
)
for {
if drain >= 0 {
break
}
pkt, err = inputCtx.GetNextPacket()
if err != nil && err != io.EOF {
if pkt != nil {
pkt.Free()
}
log.Printf("error getting next packet - %s", err)
break
} else if err != nil && pkt == nil {
drain = 0
}
if pkt != nil && pkt.StreamIndex() != srcVideoStream.Index() {
continue
}
frames, err = srcVideoStream.CodecCtx().Decode(pkt)
if err != nil {
log.Printf("Fatal error during decoding - %s\n", err)
break
}
// Decode() method doesn't treat EAGAIN and EOF as errors
// it returns empty frames slice instead. Countinue until
// input EOF or frames received.
if len(frames) == 0 && drain < 0 {
continue
}
if frames, err = gmf.DefaultRescaler(swsctx, frames); err != nil {
panic(err)
}
packets, err := cc.Encode(frames, drain)
if len(packets) == 0 {
continue
}
if err != nil {
continue
}
picdata := packets[0].Data()
pic = &picdata
// cleanup here
for _, p := range packets {
p.Free()
}
for i := range frames {
frames[i].Free()
frameCount++
}
if pkt != nil {
pkt.Free()
pkt = nil
}
// we only want to encode first picture then exit
break
}
for i := 0; i < inputCtx.StreamsCnt(); i++ {
st, err := inputCtx.GetStream(i)
if err == nil && st != nil {
st.Free()
}
}
icc.Free()
srcVideoStream.Free()
return
}

View File

@ -1,130 +0,0 @@
// +build !sharedffmpeg
package thumbnail
import (
"encoding/json"
"fmt"
"os/exec"
"strconv"
)
type ExtDependencySupport struct {
FFMpeg bool
MediaInfo bool
}
func Parse(filename string, time uint64) (*string, *VidInfo, error) {
// check if the extern dependencies are available
mExtDepsAvailable := checkExtDependencySupport()
fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg)
fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo)
var pic *string = nil
var info *VidInfo = nil
if mExtDepsAvailable.FFMpeg {
p, err := parseFFmpegPic(filename, time)
if err != nil {
return nil, nil, err
}
pic = EncodeBase64(p, "image/jpeg")
}
if mExtDepsAvailable.MediaInfo {
i, err := getVideoAttributes(filename)
if err != nil {
return nil, nil, err
}
info = i
}
return pic, info, nil
}
// check if a specific system command is available
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// ext dependency support check
func checkExtDependencySupport() *ExtDependencySupport {
var extDepsAvailable ExtDependencySupport
extDepsAvailable.FFMpeg = commandExists("ffmpeg")
extDepsAvailable.MediaInfo = commandExists("mediainfo")
return &extDepsAvailable
}
func secToString(time uint64) string {
return fmt.Sprintf("%02d:%02d:%02d", time/3600, (time/60)%60, time%60)
}
// parse the thumbail picture from video file
func parseFFmpegPic(path string, time uint64) (*[]byte, error) {
app := "ffmpeg"
cmd := exec.Command(app,
"-hide_banner",
"-loglevel", "panic",
"-ss", secToString(time),
"-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
}
return &stdout, nil
}
func getVideoAttributes(path string) (*VidInfo, error) {
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
Height string
}
}
}
err = json.Unmarshal(stdout, &t)
if err != nil {
return nil, err
}
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)
height, err := strconv.Atoi(t.Media.Track[1].Height)
if err != nil {
return nil, err
}
ret := VidInfo{
Length: uint64(duration),
Size: int64(filesize),
Width: uint32(width),
Height: uint32(height),
FrameRate: 0,
}
return &ret, nil
}

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
) )
@ -18,47 +17,24 @@ type VideoTMDB struct {
Thumbnail string Thumbnail string
Overview string Overview string
Title string Title string
ReleaseDate string
GenreIds []int
}
type TVShowTMDB struct {
Thumbnail string
Overview string
GenreIds []int 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 {
@ -67,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
@ -88,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
@ -112,70 +82,22 @@ func SearchVideo(MovieName string, year int) *VideoTMDB {
// continue label // continue label
cont: cont:
var thumbnail = "" thumbnail := fetchPoster(tmdbVid)
if tmdbVid.PosterPath != nil {
pic := fetchPoster(*tmdbVid.PosterPath)
if pic != nil {
thumbnail = *pic
}
}
result := VideoTMDB{ result := VideoTMDB{
Thumbnail: thumbnail, Thumbnail: *thumbnail,
Overview: tmdbVid.Overview, Overview: tmdbVid.Overview,
Title: tmdbVid.Title, Title: tmdbVid.Title,
ReleaseDate: tmdbVid.ReleaseDate, GenreIds: tmdbVid.Genre_ids,
GenreIds: tmdbVid.GenreIds,
} }
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,
}
if t.Results[0].PosterPath != nil {
pic := fetchPoster(*t.Results[0].PosterPath)
if pic != nil {
res.Thumbnail = *pic
}
}
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
@ -194,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
@ -207,14 +129,10 @@ func fetchGenres() *[]TMDBGenre {
return nil return nil
} }
type RespType struct { var t []TMDBGenre
Genres []TMDBGenre
}
var t RespType
err = json.Unmarshal(body, &t) err = json.Unmarshal(body, &t)
return &t.Genres return &t
} }
func GetGenres() *[]TMDBGenre { func GetGenres() *[]TMDBGenre {

View File

@ -11,8 +11,8 @@ 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
); );
@ -24,29 +24,6 @@ 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
@ -54,11 +31,11 @@ create table if not exists videos
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,
quality int default 0 null, create_date datetime default CURRENT_TIMESTAMP null,
length int default 0 null comment 'in seconds', quality int null,
create_date datetime default current_timestamp() null length int null comment 'in seconds',
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

@ -1,5 +1,5 @@
Package: OpenMediaCenter Package: OpenMediaCenter
Depends: nginx, mariadb-server, libffmpeg-ocaml Depends: nginx, mariadb-server, mediainfo
Section: web Section: web
Priority: optional Priority: optional
Architecture: all Architecture: all

View File

@ -6,6 +6,10 @@ ln -sf /etc/nginx/sites-available/OpenMediaCenter.conf /etc/nginx/sites-enabled/
mysql -uroot -pPASS -e "CREATE DATABASE IF NOT EXISTS mediacenter;" mysql -uroot -pPASS -e "CREATE DATABASE IF NOT EXISTS mediacenter;"
mysql -uroot -pPASS -e "CREATE USER IF NOT EXISTS 'mediacenteruser'@'localhost' IDENTIFIED BY 'mediapassword';" mysql -uroot -pPASS -e "CREATE USER IF NOT EXISTS 'mediacenteruser'@'localhost' IDENTIFIED BY 'mediapassword';"
mysql -uroot -pPASS -e "GRANT ALL PRIVILEGES ON mediacenter . * TO 'mediacenteruser'@'localhost';" mysql -uroot -pPASS -e "GRANT ALL PRIVILEGES ON mediacenter . * TO 'mediacenteruser'@'localhost';"
mysql -u mediacenteruser -pmediapassword mediacenter < /tmp/openmediacenter.sql
# removed unused sql style file
rm /tmp/openmediacenter.sql
# correct user rights # correct user rights
chown -R www-data:www-data /var/www/openmediacenter chown -R www-data:www-data /var/www/openmediacenter
@ -14,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

@ -13,15 +13,7 @@ server {
try_files $uri /index.html; try_files $uri /index.html;
} }
location ~* ^/(api) { location ~* ^/(api/|token) {
client_max_body_size 10G;
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",
@ -12,21 +13,19 @@
"@fortawesome/free-regular-svg-icons": "^5.15.1", "@fortawesome/free-regular-svg-icons": "^5.15.1",
"@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "^0.1.13", "@fortawesome/react-fontawesome": "^0.1.13",
"bootstrap": "^5.0.2", "bootstrap": "^4.5.3",
"plyr-react": "^3.0.7", "plyr-react": "^3.0.7",
"react": "^17.0.1", "react": "^17.0.1",
"react-bootstrap": "^1.4.0", "react-bootstrap": "^1.4.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-router": "^5.2.0", "react-router": "^5.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"typescript": "^4.3.5" "typescript": "^4.1.3"
}, },
"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/",
"apidoc": "apidoc --single -i apiGo/ -o doc/"
}, },
"jest": { "jest": {
"collectCoverageFrom": [ "collectCoverageFrom": [
@ -40,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%",
@ -53,37 +69,18 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^12.0.0", "@testing-library/react": "^11.2.2",
"@testing-library/user-event": "^13.2.1", "@testing-library/user-event": "^12.6.0",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.19",
"@types/node": "^16.4.7", "@types/node": "^14.14.31",
"@types/react": "^17.0.15", "@types/react": "^17.0.2",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.1",
"@types/react-router": "5.1.16", "@types/react-router": "5.1.12",
"@types/react-router-dom": "^5.1.8", "@types/react-router-dom": "^5.1.6",
"@typescript-eslint/eslint-plugin": "^4.28.5",
"@typescript-eslint/parser": "^4.28.5",
"apidoc": "^0.28.1",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5", "enzyme-adapter-react-16": "^1.15.5",
"eslint": "^7.31.0",
"eslint-config-prettier": "^8.1.0",
"eslint-formatter-gitlab": "^2.2.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-jest": "^24.4.0",
"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.3.2",
"prettier-config": "^1.0.0",
"react-scripts": "4.0.3" "react-scripts": "4.0.3"
},
"apidoc": {
"name": "OpenMediaCenter",
"description": "API Documentation of OpenMediaCenter",
"title": "OpenMediaCenter Doc",
"sampleUrl": null
} }
} }

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

@ -12,7 +12,6 @@
margin-left: 20px; margin-left: 20px;
opacity: 0.6; opacity: 0.6;
text-transform: capitalize; text-transform: capitalize;
text-decoration: none;
} }
.navitem:hover { .navitem:hover {

View File

@ -1,8 +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";
import {LoginContext} from './utils/context/LoginContext';
describe('<App/>', function () { describe('<App/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
@ -12,20 +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 render of password page', function () { it('test initial fetch from api', done => {
const wrapper = shallow(<App/>); global.fetch = global.prepareFetchApi({
wrapper.setState({password: true}); generalSettingsLoaded: true,
passwordsupport: true,
mediacentername: 'testname'
});
expect(wrapper.find('AuthenticationPage')).toHaveLength(1); const wrapper = shallow(<App/>);
const func = jest.fn();
wrapper.instance().setState = func;
expect(global.fetch).toBeCalledTimes(1);
process.nextTick(() => {
expect(func).toBeCalledTimes(1);
global.fetch.mockClear();
done();
});
}); });
}); });

View File

@ -1,4 +1,4 @@
import React, {useContext} from 'react'; import React from 'react';
import HomePage from './pages/HomePage/HomePage'; import HomePage from './pages/HomePage/HomePage';
import RandomPage from './pages/RandomPage/RandomPage'; import RandomPage from './pages/RandomPage/RandomPage';
import GlobalInfos from './utils/GlobalInfos'; import GlobalInfos from './utils/GlobalInfos';
@ -9,19 +9,20 @@ 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 {NoBackendConnectionPopup} from './elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup';
import {NavLink, Route, Switch, useRouteMatch} 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 AuthenticationPage from './pages/AuthenticationPage/AuthenticationPage'; import {SettingsTypes} from './types/ApiTypes';
import TVShowPage from './pages/TVShowPage/TVShowPage';
import TVPlayer from './pages/TVShowPage/TVPlayer';
import {LoginContextProvider} from './utils/context/LoginContextProvider';
import {FeatureContext} from './utils/context/FeatureContext';
interface state { interface state {
generalSettingsLoaded: boolean;
passwordsupport: boolean;
mediacentername: string; mediacentername: string;
onapierror: boolean;
} }
/** /**
@ -30,133 +31,96 @@ interface state {
class App extends React.Component<{}, state> { class App extends React.Component<{}, state> {
constructor(props: {}) { constructor(props: {}) {
super(props); super(props);
this.state = { this.state = {
mediacentername: 'OpenMediaCenter' generalSettingsLoaded: false,
passwordsupport: false,
mediacentername: 'OpenMediaCenter',
onapierror: false
}; };
}
// force an update on theme change initialAPICall(): void {
GlobalInfos.onThemeChange(() => { // this is the first api call so if it fails we know there is no connection to backend
this.forceUpdate(); callAPI(APINode.Settings, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => {
// set theme
GlobalInfos.enableDarkTheme(result.DarkMode);
GlobalInfos.setVideoPath(result.VideoPath);
this.setState({
generalSettingsLoaded: true,
passwordsupport: result.Password,
mediacentername: result.Mediacenter_name,
onapierror: false
});
// set tab title to received mediacenter name
document.title = result.Mediacenter_name;
}, error => {
this.setState({onapierror: true});
}); });
} }
componentDidMount(): void {
this.initialAPICall();
}
render(): JSX.Element { render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle();
// add the main theme to the page body // add the main theme to the page body
document.body.className = GlobalInfos.getThemeStyle().backgroundcolor; document.body.className = themeStyle.backgroundcolor;
return ( return (
<LoginContextProvider> <Router>
<Switch> <div className={style.app}>
<Route path='/login'> <div className={[style.navcontainer, themeStyle.backgroundcolor, themeStyle.textcolor, themeStyle.hrcolor].join(' ')}>
<AuthenticationPage /> <div className={style.navbrand}>{this.state.mediacentername}</div>
</Route> <NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/'} activeStyle={{opacity: '0.85'}}>Home</NavLink>
<Route path='/media'> <NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/random'} activeStyle={{opacity: '0.85'}}>Random
{this.navBar()} Video</NavLink>
<MyRouter />
</Route> <NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/categories'} activeStyle={{opacity: '0.85'}}>Categories</NavLink>
</Switch> <NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/settings'} activeStyle={{opacity: '0.85'}}>Settings</NavLink>
</LoginContextProvider> </div>
{this.routing()}
</div>
{this.state.onapierror ? this.ApiError() : null}
</Router>
); );
} }
static contextType = FeatureContext; routing(): JSX.Element {
/**
* render the top navigation bar
*/
navBar(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<div className={[style.navcontainer, themeStyle.backgroundcolor, themeStyle.textcolor, themeStyle.hrcolor].join(' ')}> <Switch>
<div className={style.navbrand}>{this.state.mediacentername}</div> <Route path="/random">
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/media'} activeStyle={{opacity: '0.85'}}> <RandomPage/>
Home </Route>
</NavLink> <Route path="/categories">
<NavLink <CategoryPage/>
className={[style.navitem, themeStyle.navitem].join(' ')} </Route>
to={'/media/random'} <Route path="/settings">
activeStyle={{opacity: '0.85'}}> <SettingsPage/>
Random Video </Route>
</NavLink> <Route exact path="/player/:id">
<Player/>
<NavLink </Route>
className={[style.navitem, themeStyle.navitem].join(' ')} <Route exact path="/actors">
to={'/media/categories'} <ActorOverviewPage/>
activeStyle={{opacity: '0.85'}}> </Route>
Categories <Route path="/actors/:id">
</NavLink> <ActorPage/>
</Route>
<NavLink <Route path="/">
className={[style.navitem, themeStyle.navitem].join(' ')} <HomePage/>
to={'/media/actors'} </Route>
activeStyle={{opacity: '0.85'}}> </Switch>
Actors
</NavLink>
{this.context.TVShowEnabled ? (
<NavLink
className={[style.navitem, themeStyle.navitem].join(' ')}
to={'/media/tvshows'}
activeStyle={{opacity: '0.85'}}>
TV Shows
</NavLink>
) : null}
<NavLink
className={[style.navitem, themeStyle.navitem].join(' ')}
to={'/media/settings'}
activeStyle={{opacity: '0.85'}}>
Settings
</NavLink>
</div>
); );
} }
ApiError(): JSX.Element {
// on api error show popup and retry and show again if failing..
return (<NoBackendConnectionPopup onHide={(): void => this.initialAPICall()}/>);
}
} }
const MyRouter = (): JSX.Element => {
const match = useRouteMatch();
const features = useContext(FeatureContext);
return (
<Switch>
<Route exact path={`${match.url}/random`}>
<RandomPage />
</Route>
<Route path={`${match.url}/categories`}>
<CategoryPage />
</Route>
<Route path={`${match.url}/settings`}>
<SettingsPage />
</Route>
<Route exact path={`${match.url}/player/:id`}>
<Player />
</Route>
<Route exact path={`${match.url}/actors`}>
<ActorOverviewPage />
</Route>
<Route exact path={`${match.url}/actors/:id`}>
<ActorPage />
</Route>
{features.TVShowEnabled ? (
<Route path={`${match.url}/tvshows`}>
<TVShowPage />
</Route>
) : null}
{features.TVShowEnabled ? (
<Route exact path={`${match.url}/tvplayer/:id`}>
<TVPlayer />
</Route>
) : null}
<Route path={`${match.url}/`}>
<HomePage />
</Route>
</Switch>
);
};
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: '/media/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 === '' ? (
<FontAwesomeIcon
style={{
lineHeight: '130px' lineHeight: '130px'
}} }} icon={faUser} size='5x'/> : 'dfdf' /* todo render picture provided here! */}
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,30 +0,0 @@
.dropArea {
border: 2px dashed #ccc;
border-radius: 20px;
width: 480px;
font-family: sans-serif;
padding: 20px;
}
.dropArea:hover {
cursor: pointer;
}
.dropArea.highlight {
border-color: purple;
}
.myForm {
margin-bottom: 10px;
}
.progresswrapper {
width: 100%;
height: 5px;
margin-top: 3px;
}
.finished {
margin-top: 10px;
text-align: center;
}

View File

@ -1,108 +0,0 @@
import style from './DropZone.module.css';
import React, {useState} from 'react';
import {cookie} from '../../utils/context/Cookie';
import GlobalInfos from '../../utils/GlobalInfos';
export const DropZone = (): JSX.Element => {
const [ondrag, setDrag] = useState(0);
const [percent, setpercent] = useState(0.0);
const [finished, setfinished] = useState<string | null>(null);
const theme = GlobalInfos.getThemeStyle();
const uploadFile = (f: FileList): void => {
const xhr = new XMLHttpRequest(); // create XMLHttpRequest
const data = new FormData(); // create formData object
for (let i = 0; i < f.length; i++) {
const file = f.item(i);
if (file) {
data.append('file' + i, file);
}
}
xhr.onload = function (): void {
console.log(this.responseText); // whatever the server returns
const resp = JSON.parse(this.responseText);
if (resp.Message === 'finished all files') {
setfinished('');
} else {
setfinished(resp.Message);
}
setTimeout(() => {
setpercent(0);
setfinished(null);
}, 2000);
};
xhr.upload.onprogress = function (e): void {
console.log(e.loaded / e.total);
setpercent((e.loaded * 100.0) / e.total);
};
xhr.open('post', '/api/video/fileupload'); // open connection
xhr.setRequestHeader('Accept', 'multipart/form-data');
const tkn = cookie.Load();
if (tkn) {
xhr.setRequestHeader('Token', tkn.Token);
}
xhr.send(data); // send data
};
return (
<div
className={style.dropArea + (ondrag > 0 ? ' ' + style.highlight : '') + ' ' + theme.secbackground}
onDragEnter={(e): void => {
e.preventDefault();
e.stopPropagation();
setDrag(ondrag + 1);
}}
onDragLeave={(e): void => {
e.preventDefault();
e.stopPropagation();
setDrag(ondrag - 1);
}}
onDragOver={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
onDrop={(e): void => {
setDrag(0);
e.preventDefault();
e.stopPropagation();
uploadFile(e.dataTransfer.files);
}}
onClick={(): void => {
let input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.onchange = function (): void {
if (input.files) {
uploadFile(input.files);
}
};
input.click();
}}>
<div className={style.myForm}>
<p>To upload new Videos darg and drop them here or click to select some...</p>
<div className={style.progresswrapper}>
<div style={{width: percent + '%', backgroundColor: 'green', height: 5}} />
</div>
{finished !== null ? (
finished === '' ? (
<div className={style.finished}>Finished uploading</div>
) : (
<div className={style.finished}>Upload failed: {finished}</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,113 +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 (
// diff the two arrays
this.props.data
.filter((x) => !prevProps.data.includes(x))
.concat(prevProps.data.filter((x) => !this.props.data.includes(x))).length !== 0
) {
this.clean((): void => {
this.loadPreviewBlock(this.InitialLoadNR);
});
}
}
/**
* clear all elements rendered...
*/
clean(callback: () => void): void {
this.loadindex = 0;
this.setState({loadeditems: []}, callback);
}
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'
placeholder='Filter'
value={this.state.filter}
onChange={(e): void => { onChange={(e): void => {
this.props.onFilterChange(e.target.value); this.props.onFilterChange(e.target.value);
this.setState({filter: e.target.value}); this.setState({filter: e.target.value});
}} }}
ref={(input): void => { ref={(input): void => {
this.filterfield = input; this.filterfield = input;
}} }}/>
/> <Button title={<FontAwesomeIcon style={{
<Button
title={
<FontAwesomeIcon
style={{
verticalAlign: 'middle', verticalAlign: 'middle',
lineHeight: '130px' lineHeight: '130px'
}} }} icon={faTimes} size='1x'/>} color={{backgroundColor: 'red'}} onClick={(): void => {
icon={faTimes}
size='1x'
/>
}
color={{backgroundColor: 'red'}}
onClick={(): void => {
this.setState({filter: '', filtervisible: false}); this.setState({filter: '', filtervisible: false});
}} }}/>
/>
</> </>
); );
} else { } else {
return ( return (<Button
<Button title={<span>Filter <FontAwesomeIcon
title={
<span>
Filter{' '}
<FontAwesomeIcon
style={{ style={{
verticalAlign: 'middle', verticalAlign: 'middle',
lineHeight: '130px' lineHeight: '130px'
}} }}
icon={faFilter} icon={faFilter}
size='1x' size='1x'/></span>}
/>
</span>
}
color={{backgroundColor: 'cornflowerblue', color: 'white'}} color={{backgroundColor: 'cornflowerblue', color: 'white'}}
onClick={this.enableFilterField} onClick={this.enableFilterField}/>)
/>
);
} }
} }

View File

@ -2,10 +2,7 @@
background-color: green; background-color: green;
border-radius: 5px; border-radius: 5px;
border-width: 0; border-width: 0;
color: white;
margin-right: 15px; margin-right: 15px;
padding: 6px; padding: 6px;
} }
.button:hover{
opacity: 0.7;
}

View File

@ -1,6 +1,14 @@
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import {Button, IconButton} from './Button'; import {Button} from './Button';
function prepareFetchApi(response) {
const mockJsonPromise = Promise.resolve(response);
const mockFetchPromise = Promise.resolve({
json: () => mockJsonPromise
});
return (jest.fn().mockImplementation(() => mockFetchPromise));
}
describe('<Button/>', function () { describe('<Button/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
@ -22,24 +30,3 @@ describe('<Button/>', function () {
expect(func).toHaveBeenCalledTimes(1); expect(func).toHaveBeenCalledTimes(1);
}); });
}); });
describe('<IconButton/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<IconButton onClick={() => {}} title='test'/>);
wrapper.unmount();
});
it('renders title', function () {
const wrapper = shallow(<IconButton onClick={() => {}} title='test1'/>);
expect(wrapper.text()).toBe('<FontAwesomeIcon />test1');
});
it('test onclick handling', () => {
const func = jest.fn();
const wrapper = shallow(<IconButton onClick={func} title='test1'/>);
wrapper.find('button').simulate('click');
expect(func).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,8 +1,5 @@
import React from 'react'; import React from 'react';
import style from './Button.module.css'; import style from './Button.module.css';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {IconDefinition} from '@fortawesome/fontawesome-common-types';
import GlobalInfos from '../../utils/GlobalInfos';
interface ButtonProps { interface ButtonProps {
title: string | JSX.Element; title: string | JSX.Element;
@ -11,30 +8,9 @@ interface ButtonProps {
} }
export function Button(props: ButtonProps): JSX.Element { export function Button(props: ButtonProps): JSX.Element {
const theme = GlobalInfos.getThemeStyle();
return ( return (
<button className={style.button + ' ' + theme.textcolor} style={props.color} onClick={props.onClick}> <button className={style.button} style={props.color} onClick={props.onClick}>
{props.title} {props.title}
</button> </button>
); );
} }
interface IconButtonProps {
title: string | JSX.Element;
onClick?: () => void;
icon: IconDefinition;
}
export function IconButton(props: IconButtonProps): JSX.Element {
const theme = GlobalInfos.getThemeStyle();
return (
<button className={style.button + ' ' + theme.textcolor} style={{backgroundColor: '#00000000'}} onClick={props.onClick}>
<span style={{fontSize: 12}}>
<FontAwesomeIcon className={theme.textcolor} icon={props.icon} size='2x' />
</span>
<span style={{marginLeft: 10}}>{props.title}</span>
</button>
);
}

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) { if (this.props.onClick != null) this.props.onClick();
this.props.onClick(); }} className={style.infoheaderitem} style={{backgroundColor: this.props.backColor}}>
}
}}
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'
onHide={this.props.onHide}
banner={
<button <button
className={style.newactorbutton} className={style.newactorbutton}
onClick={(): void => { onClick={(): void => {
this.setState({contentDefault: false}); this.setState({contentDefault: false});
}}> }}>Create new Actor</button>} ParentSubmit={this.parentSubmit}>
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 {
return (
<NewActorPopupContent
onHide={(): void => {
this.loadActors(); this.loadActors();
this.setState({contentDefault: true}); 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', action: 'addActorToVideo',
ActorId: actor.ActorId, ActorId: actor.ActorId,
MovieId: this.props.movieId MovieId: this.props.movie_id
}, }, result => {
(result) => {
if (result.result === 'success') { if (result.result === 'success') {
// return back to player page // return back to player page
this.props.onHide(); this.props.onHide();
} else { } else {
console.error('an error occured while fetching actors: ' + result); 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

@ -1,51 +0,0 @@
import {shallow} from 'enzyme';
import React from 'react';
import {ButtonPopup} from './ButtonPopup';
import exp from "constants";
describe('<ButtonPopup/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<ButtonPopup/>);
wrapper.unmount();
});
it('renders two buttons', function () {
const wrapper = shallow(<ButtonPopup/>);
expect(wrapper.find('Button')).toHaveLength(2);
});
it('renders three buttons if alternative defined', function () {
const wrapper = shallow(<ButtonPopup AlternativeButtonTitle='alt'/>);
expect(wrapper.find('Button')).toHaveLength(3);
});
it('test click handlings', function () {
const althandler = jest.fn();
const denyhandler = jest.fn();
const submithandler = jest.fn();
const wrapper = shallow(<ButtonPopup DenyButtonTitle='deny' onDeny={denyhandler} SubmitButtonTitle='submit'
onSubmit={submithandler} AlternativeButtonTitle='alt'
onAlternativeButton={althandler}/>);
wrapper.find('Button').findWhere(e => e.props().title === "deny").simulate('click');
expect(denyhandler).toHaveBeenCalledTimes(1);
wrapper.find('Button').findWhere(e => e.props().title === "alt").simulate('click');
expect(althandler).toHaveBeenCalledTimes(1);
wrapper.find('Button').findWhere(e => e.props().title === "submit").simulate('click');
expect(submithandler).toHaveBeenCalledTimes(1);
});
it('test Parentsubmit and parenthide callbacks', function () {
const ondeny = jest.fn();
const onsubmit = jest.fn();
const wrapper = shallow(<ButtonPopup DenyButtonTitle='deny' SubmitButtonTitle='submit' onDeny={ondeny} onSubmit={onsubmit} AlternativeButtonTitle='alt'/>);
wrapper.find('PopupBase').props().onHide();
expect(ondeny).toHaveBeenCalledTimes(1);
wrapper.find('PopupBase').props().ParentSubmit();
expect(onsubmit).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,58 +0,0 @@
import React from 'react';
import PopupBase from '../PopupBase';
import {Button} from '../../GPElements/Button';
/**
* Delete Video popup
* can only be rendered once!
* @constructor
*/
export const ButtonPopup = (props: {
onSubmit: () => void;
onDeny: () => void;
onAlternativeButton?: () => void;
SubmitButtonTitle: string;
DenyButtonTitle: string;
AlternativeButtonTitle?: string;
Title: string;
}): JSX.Element => {
return (
<>
<PopupBase
title={props.Title}
onHide={(): void => props.onDeny()}
height='200px'
width='400px'
ParentSubmit={(): void => {
props.onSubmit();
}}>
<Button
onClick={(): void => {
props.onSubmit();
}}
title={props.SubmitButtonTitle}
/>
{props.AlternativeButtonTitle ? (
<Button
color={{backgroundColor: 'darkorange'}}
onClick={(): void => {
props.onAlternativeButton ? props.onAlternativeButton() : null;
}}
title={props.AlternativeButtonTitle}
/>
) : (
<></>
)}
<Button
color={{backgroundColor: 'red'}}
onClick={(): void => {
props.onDeny();
}}
title={props.DenyButtonTitle}
/>
</PopupBase>
</>
);
};

View File

@ -25,7 +25,7 @@ describe('<NewActorPopupContent/>', () => {
const wrapper = shallow(<NewActorPopupContent onHide={() => {func();}}/>); const wrapper = shallow(<NewActorPopupContent onHide={() => {func();}}/>);
// manually set typed in actorname // manually set typed in actorname
wrapper.instance().nameValue = 'testactorname'; wrapper.instance().value = 'testactorname';
global.fetch = prepareFetchApi({}); global.fetch = prepareFetchApi({});
@ -55,6 +55,6 @@ describe('<NewActorPopupContent/>', () => {
wrapper.find('input').simulate('change', {target: {value: 'testinput'}}); wrapper.find('input').simulate('change', {target: {value: 'testinput'}});
expect(wrapper.instance().nameValue).toBe('testinput'); expect(wrapper.instance().value).toBe('testinput');
}); });
}); });

View File

@ -15,30 +15,23 @@ 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>
); );
} }
} }
export class NewActorPopupContent extends React.Component<NewActorPopupProps> { export class NewActorPopupContent extends React.Component<NewActorPopupProps> {
nameValue: string | undefined; value: string | undefined;
render(): JSX.Element { render(): JSX.Element {
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.nameValue = v.target.value;
}}
/>
</div>
<button className={style.savebtn} onClick={(): void => this.storeselection()}>
Save
</button>
</> </>
); );
} }
@ -48,11 +41,9 @@ 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.nameValue === '' || this.nameValue === undefined) { if (this.value === '' || this.value === undefined) return;
return;
}
callAPI(APINode.Actor, {action: 'createActor', ActorName: this.nameValue}, (result: GeneralSuccess) => { callAPI(APINode.Actor, {action: 'createActor', actorname: this.value}, (result: GeneralSuccess) => {
if (result.result !== 'success') { if (result.result !== 'success') {
console.log('error occured while writing to db -- todo error handling'); console.log('error occured while writing to db -- todo error handling');
console.log(result.result); console.log(result.result);

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}
height='200px'
width='400px'
ParentSubmit={(): void => this.storeselection()}>
<div>
<input
type='text'
placeholder='Tagname'
onChange={(v): void => {
this.value = v.target.value; this.value = v.target.value;
}} }}/></div>
/> <button className={style.savebtn} onClick={(): void => this.storeselection()}>Save</button>
</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

@ -2,6 +2,7 @@
font-size: smaller; font-size: smaller;
font-weight: bold; font-weight: bold;
height: 20px; height: 20px;
max-width: 266px;
text-align: center; text-align: center;
} }
@ -19,6 +20,13 @@
vertical-align: middle; vertical-align: middle;
} }
.previewimage {
max-height: 400px;
max-width: 410px;
min-height: 150px;
min-width: 266px;
}
.previewbottom { .previewbottom {
height: 20px; height: 20px;
} }

View File

@ -3,19 +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;
aspectRatio?: number;
} }
interface PreviewState { interface PreviewState {
picLoaded: boolean | null; previewpicture: string | null;
} }
/** /**
@ -23,98 +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;
static readonly DefMinWidth = 266;
static readonly DefMaxWidth = 410;
static readonly DefMinHeight = 150;
static readonly DefMaxHeight = 400;
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 != null) {
return <Link to={this.props.linkPath}>{this.content()}</Link>;
} else {
return this.content();
}
}
content(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
const ratio = this.props.aspectRatio;
let dimstyle = null;
// check if aspect ratio is passed
if (ratio != null) {
// if ratio is <1 we need to calc height
if (ratio < 1) {
const height = Preview.DefMaxWidth * ratio;
dimstyle = {height: height, width: Preview.DefMaxWidth};
} else {
const width = Preview.DefMaxHeight * ratio;
dimstyle = {width: width, height: Preview.DefMaxHeight};
}
}
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 <div className={style.previewpic}>
style={{maxWidth: dimstyle !== null ? dimstyle.width : Preview.DefMaxWidth}} {this.state.previewpicture !== null ?
className={style.previewtitle + ' ' + themeStyle.lighttextcolor}> <img className={style.previewimage}
{this.props.name} src={this.state.previewpicture}
alt='Pic loading.'/> :
<span className={style.loadAnimation}><Spinner animation='border'/></span>}
</div> </div>
<div style={dimstyle !== null ? dimstyle : undefined} className={style.previewpic}> <div className={style.previewbottom}>
{this.state.picLoaded === false ? (
<FontAwesomeIcon
style={{
color: 'white',
marginTop: '55px'
}}
icon={faPhotoVideo}
size='5x'
/>
) : this.state.picLoaded === null ? (
<span className={style.loadAnimation}>
<Spinner animation='border' />
</span>
) : (
<img
style={
dimstyle !== null
? dimstyle
: {
minWidth: Preview.DefMinWidth,
maxWidth: Preview.DefMaxWidth,
minHeight: Preview.DefMinHeight,
maxHeight: Preview.DefMaxHeight
}
}
src={this.pic}
alt='Pic loading.'
/>
)}
</div> </div>
<div className={style.previewbottom} />
</div> </div>
</Link>
); );
} }
} }
@ -122,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 // received picture should be rendered into wrapper
expect(wrapper.find('img').props().src).toBe('42'); expect(wrapper.find('.previewimage').props().src).not.toBeNull();
// check if preview title renders correctly // check if preview title renders correctly
expect(wrapper.find('.previewtitle').text()).toBe('test'); expect(wrapper.find('.previewtitle').text()).toBe('test');
global.fetch.mockClear();
done();
});
}); });
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={'/media/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,39 +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 {
loadeditems: VideoTypes.VideoUnloadedType[];
selectionnr: number;
}
/**
* A videocontainer storing lots of Preview elements
* includes scroll handling and loading of preview infos
*/
class VideoContainer extends React.Component<props, state> {
// stores current index of loaded elements
loadindex: number = 0;
constructor(props: props) {
super(props);
this.state = {
loadeditems: [],
selectionnr: 0
};
}
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 ( return (
<DynamicContentContainer <div className={style.maincontent}>
renderElement={(el): JSX.Element => ( {this.state.loadeditems.map(elem => (
<Preview <Preview
key={el.MovieId} key={elem.MovieId}
aspectRatio={el.Ratio > 0 ? el.Ratio : undefined} name={elem.MovieName}
picLoader={(callback: (pic: string) => void): void => { movie_id={elem.MovieId}
callAPIPlain( onClick={(): void => {
APINode.Video, if (this.props.onScrollPositionChange !== undefined)
{ this.props.onScrollPositionChange(window.pageYOffset - document.documentElement.clientHeight, this.loadindex)
action: 'readThumbnail', }}/>
Movieid: el.MovieId ))}
}, {/*todo css for no items to show*/}
(result) => callback(result) {this.state.loadeditems.length === 0 ?
'no items to show!' : null}
{this.props.children}
</div>
); );
}} }
name={el.MovieName}
linkPath={'/media/player/' + el.MovieId} componentWillUnmount(): void {
/> this.setState({});
)} // unbind scroll listener when unmounting component
data={props.data}> document.removeEventListener('scroll', this.trackScrolling);
{props.children} }
</DynamicContentContainer>
); /**
}; * 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

@ -1,19 +1,13 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
import {BrowserRouter} from 'react-router-dom';
import {FeatureContextProvider} from './utils/context/FeatureContext';
// 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>
<BrowserRouter> <App/>
<FeatureContextProvider>
<App />
</FeatureContextProvider>
</BrowserRouter>
</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.state.NActorPopupVisible ? (
<NewActorPopup
onHide={(): void => {
this.setState({NActorPopupVisible: false}); this.setState({NActorPopupVisible: false});
this.fetchAvailableActors(); // refetch actors this.fetchAvailableActors(); // refetch actors
}} }}/> : null}
/>
) : 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});
}); });
} }

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